From bf8b37442d5557d056d131057a8213ba24334c6e Mon Sep 17 00:00:00 2001 From: Daniel Weck Date: Sun, 5 May 2019 19:45:23 +0100 Subject: [PATCH] feat: Localization, English + French (#223) --- packages/ace-cli/package.json | 2 +- packages/ace-cli/src/index.js | 5 +- packages/ace-core/package.json | 4 +- .../ace-core/src/checker/checker-chromium.js | 50 ++++- packages/ace-core/src/checker/checker-epub.js | 28 +-- packages/ace-core/src/checker/checker.js | 4 +- packages/ace-core/src/core/ace.js | 15 +- packages/ace-core/src/l10n/locales/en.json | 37 +++ packages/ace-core/src/l10n/locales/fr.json | 37 +++ packages/ace-core/src/l10n/localize.js | 16 ++ packages/ace-core/src/scripts/ace-axe.js | 25 ++- packages/ace-http/package.json | 2 +- packages/ace-http/src/index.js | 11 +- packages/ace-localize/package.json | 26 +++ packages/ace-localize/src/localize.js | 88 ++++++++ packages/ace-report-axe/package.json | 3 +- packages/ace-report-axe/src/index.js | 211 +++++++++++------- .../ace-report-axe/src/l10n/locales/en.json | 57 +++++ .../ace-report-axe/src/l10n/locales/fr.json | 57 +++++ packages/ace-report-axe/src/l10n/localize.js | 16 ++ packages/ace-report/package.json | 3 +- .../ace-report/src/generate-html-report.js | 50 ++++- packages/ace-report/src/l10n/locales/en.json | 72 ++++++ packages/ace-report/src/l10n/locales/fr.json | 72 ++++++ packages/ace-report/src/l10n/localize.js | 16 ++ packages/ace-report/src/report-builders.js | 23 +- .../ace-report/src/report-template.handlebars | 195 +++++++++------- packages/ace-report/src/report.js | 10 +- packages/puppeteer-utils/package.json | 2 +- packages/puppeteer-utils/src/index.js | 12 + .../__tests__/__snapshots__/cli.test.js.snap | 2 + tests/__tests__/axe-rules.test.js | 2 + tests/__tests__/epub-rules.test.js | 3 + tests/__tests__/regression.test.js | 2 + tests/__tests__/report_files.test.js | 2 + tests/__tests__/report_json.test.js | 2 + tests/__tests__/unzip.test.js | 2 + tests/data/report/report.html | 2 - tests/runAceJS.js | 2 + website/content/docs/cli.md | 1 + website/content/docs/http-api.md | 1 + website/content/rules/html.md | 2 +- yarn.lock | 19 ++ 43 files changed, 963 insertions(+), 228 deletions(-) create mode 100644 packages/ace-core/src/l10n/locales/en.json create mode 100644 packages/ace-core/src/l10n/locales/fr.json create mode 100644 packages/ace-core/src/l10n/localize.js create mode 100644 packages/ace-localize/package.json create mode 100644 packages/ace-localize/src/localize.js create mode 100644 packages/ace-report-axe/src/l10n/locales/en.json create mode 100644 packages/ace-report-axe/src/l10n/locales/fr.json create mode 100644 packages/ace-report-axe/src/l10n/localize.js create mode 100644 packages/ace-report/src/l10n/locales/en.json create mode 100644 packages/ace-report/src/l10n/locales/fr.json create mode 100644 packages/ace-report/src/l10n/localize.js diff --git a/packages/ace-cli/package.json b/packages/ace-cli/package.json index c6ca2a6b..07798a68 100644 --- a/packages/ace-cli/package.json +++ b/packages/ace-cli/package.json @@ -1,6 +1,6 @@ { "name": "@daisy/ace-cli", - "version": "1.0.3", + "version": "1.0.4", "description": "Ace by DAISY, an Accessibility Checker for EPUB", "author": { "name": "DAISY developers", diff --git a/packages/ace-cli/src/index.js b/packages/ace-cli/src/index.js index 3a739f7c..efbc3053 100755 --- a/packages/ace-cli/src/index.js +++ b/packages/ace-cli/src/index.js @@ -32,6 +32,7 @@ const cli = meow({ -V, --verbose display verbose output -s, --silent do not display any output + -l, --lang language code for localized messages (e.g. "fr"), default is "en" Examples $ ace -o out ~/Documents/book.epub `, @@ -46,9 +47,10 @@ version: pkg.version t: 'tempdir', v: 'version', V: 'verbose', + l: 'lang', }, boolean: ['force', 'verbose', 'silent', 'subdir'], - string: ['outdir', 'tempdir'], + string: ['outdir', 'tempdir', 'lang'], }); function sleep(ms) { @@ -102,6 +104,7 @@ ${overrides.map(file => ` - ${file}`).join('\n')} verbose: cli.flags.verbose, silent: cli.flags.silent, jobId: '', + lang: cli.flags.lang, }) .then((jobData) => { var reportJson = jobData[1]; diff --git a/packages/ace-core/package.json b/packages/ace-core/package.json index 57c5730a..1c7d4f2b 100644 --- a/packages/ace-core/package.json +++ b/packages/ace-core/package.json @@ -1,6 +1,6 @@ { "name": "@daisy/ace-core", - "version": "1.0.3", + "version": "1.0.4", "description": "Core library for Ace", "author": { "name": "DAISY developers", @@ -18,6 +18,8 @@ "license": "MIT", "main": "lib/index.js", "dependencies": { + "@daisy/ace-localize": "^1.0.0", + "@daisy/ace-logger": "^1.0.1", "@daisy/ace-meta": "^1.0.3", "@daisy/ace-report": "^1.0.1", "@daisy/ace-report-axe": "^1.0.1", diff --git a/packages/ace-core/src/checker/checker-chromium.js b/packages/ace-core/src/checker/checker-chromium.js index 63050d34..ab912a23 100644 --- a/packages/ace-core/src/checker/checker-chromium.js +++ b/packages/ace-core/src/checker/checker-chromium.js @@ -12,6 +12,8 @@ const winston = require('winston'); const axe2ace = require('@daisy/ace-report-axe'); const utils = require('@daisy/puppeteer-utils'); +const { getRawResourcesForCurrentLanguage } = require('../l10n/localize').localizer; + tmp.setGracefulCleanup(); const scripts = [ @@ -23,7 +25,7 @@ const scripts = [ require.resolve('../scripts/ace-extraction.js'), ]; -async function checkSingle(spineItem, epub, browser) { +async function checkSingle(spineItem, epub, browser, lang) { winston.verbose(`- Processing ${spineItem.relpath}`); try { let url = spineItem.url; @@ -42,6 +44,46 @@ async function checkSingle(spineItem, epub, browser) { const page = await browser.newPage(); await page.goto(url); + + let localePath = ""; + try { + winston.info(`- Axe locale: [${lang}]`); + + // https://github.com/dequelabs/axe-core#localization + // https://github.com/dequelabs/axe-core/tree/develop/locales + + if (lang && lang !== "en" && lang.indexOf("en-") !== 0) { // default English built into Axe source code + localePath = path.resolve(require.resolve('axe-core'), `../locales/${lang}.json`); + if (fs.existsSync(localePath)) { + const localeStr = fs.readFileSync(localePath, { encoding: "utf8" }); + const localeScript = `window.__axeLocale__=${localeStr};`; + await utils.addScriptContents([localeScript], page); + } else { + winston.info(`- Axe locale missing? [${lang}] => ${localePath}`); + } + } + + let localizedScript = ""; + const rawJson = getRawResourcesForCurrentLanguage(); + + ["axecheck", "axerule"].forEach((checkOrRule) => { + const checkOrRuleKeys = Object.keys(rawJson[checkOrRule]); + for (const checkOrRuleKey of checkOrRuleKeys) { + const msgs = Object.keys(rawJson[checkOrRule][checkOrRuleKey]); + for (const msg of msgs) { + const k = `__aceLocalize__${checkOrRule}_${checkOrRuleKey}_${msg}`; + localizedScript += `window['${k}']="${rawJson[checkOrRule][checkOrRuleKey][msg]}";\n`; + } + } + }); + await utils.addScriptContents([localizedScript], page); + + } catch (err) { + console.log(err); + winston.verbose(err); + winston.info(`- Axe locale problem? [${lang}] => ${localePath}`); + } + await utils.addScripts(scripts, page); const results = await page.evaluate(() => new Promise((resolve, reject) => { @@ -57,7 +99,7 @@ async function checkSingle(spineItem, epub, browser) { await page.close(); // Post-process results - results.assertions = (results.axe != null) ? axe2ace.axe2ace(spineItem, results.axe) : []; + results.assertions = (results.axe != null) ? axe2ace.axe2ace(spineItem, results.axe, lang) : []; delete results.axe; winston.info(`- ${spineItem.relpath}: ${ (results.assertions && results.assertions.assertions.length > 0) @@ -97,14 +139,14 @@ async function checkSingle(spineItem, epub, browser) { } } -module.exports.check = async (epub) => { +module.exports.check = async (epub, lang) => { const args = []; if (os.platform() !== 'win32' && os.platform() !== 'darwin') { args.push('--no-sandbox') } const browser = await puppeteer.launch({ args }); winston.info('Checking documents...'); - return pMap(epub.contentDocs, doc => checkSingle(doc, epub, browser), { concurrency: 4 }) + return pMap(epub.contentDocs, doc => checkSingle(doc, epub, browser, lang), { concurrency: 4 }) .then(async (results) => { await browser.close(); return results; diff --git a/packages/ace-core/src/checker/checker-epub.js b/packages/ace-core/src/checker/checker-epub.js index 654a312b..eba04a89 100644 --- a/packages/ace-core/src/checker/checker-epub.js +++ b/packages/ace-core/src/checker/checker-epub.js @@ -3,6 +3,8 @@ const builders = require('@daisy/ace-report').builders; const winston = require('winston'); +const { localize } = require('../l10n/localize').localizer; + const ASSERTED_BY = 'Ace'; const MODE = 'automatic'; const KB_BASE = 'http://kb.daisy.org/publishing/'; @@ -42,11 +44,11 @@ function newMetadataAssertion(name, impact = 'serious') { return newViolation({ impact, title: `metadata-${name.toLowerCase().replace(':', '-')}`, - testDesc: `Ensures a '${name}' metadata is present`, - resDesc: `Add a '${name}' metadata property to the Package Document`, + testDesc: localize("checkepub.metadataviolation.testdesc", { name, interpolation: { escapeValue: false } }), + resDesc: localize("checkepub.metadataviolation.resdesc", { name, interpolation: { escapeValue: false } }), kbPath: 'docs/metadata/schema-org.html', - kbTitle: 'Schema.org Accessibility Metadata', - ruleDesc: `Publications must declare the '${name}' metadata` + kbTitle: localize("checkepub.metadataviolation.kbtitle"), + ruleDesc: localize("checkepub.metadataviolation.ruledesc", { name, interpolation: { escapeValue: false } }) }); } @@ -70,11 +72,11 @@ function checkTitle(assertions, epub) { if (title === '') { assertions.withAssertions(newViolation({ title: 'epub-title', - testDesc: 'Ensures the EPUB has a title', - resDesc: 'Add a \'dc:title\' metadata property to the Package Document', + testDesc: localize("checkepub.titleviolation.testdesc"), + resDesc: localize("checkepub.titleviolation.resdesc"), kbPath: '', - kbTitle: 'EPUB Title', - ruleDesc: 'Publications must have a title', + kbTitle: localize("checkepub.titleviolation.kbtitle"), + ruleDesc: localize("checkepub.titleviolation.ruledesc") })); } } @@ -85,11 +87,11 @@ function checkPageSource(assertion, epub) { || epub.metadata['dc:source'].toString() === '')) { assertion.withAssertions(newViolation({ title: 'epub-pagesource', - testDesc: 'Ensures the source of page breaks is identified', - resDesc: 'Add a \'dc:source\' metadata property to the Package Document', + testDesc: localize("checkepub.pagesourceviolation.testdesc"), + resDesc: localize("checkepub.pagesourceviolation.resdesc"), kbPath: 'docs/navigation/pagelist.html', - kbTitle: 'Page Navigation', - ruleDesc: 'Publications with page breaks must declare the \'dc:source\' metadata', + kbTitle: localize("checkepub.pagesourceviolation.kbtitle"), + ruleDesc: localize("checkepub.pagesourceviolation.ruledesc") })); } } @@ -121,7 +123,7 @@ function check(epub, report) { hasBindings: epub.hasBindings, hasSVGContentDocuments: epub.hasSVGContentDocuments, }); - + winston.info(`- ${epub.packageDoc.src}: ${ (builtAssertions.assertions && builtAssertions.assertions.length > 0) ? builtAssertions.assertions.length diff --git a/packages/ace-core/src/checker/checker.js b/packages/ace-core/src/checker/checker.js index b4febd68..ce297081 100644 --- a/packages/ace-core/src/checker/checker.js +++ b/packages/ace-core/src/checker/checker.js @@ -23,8 +23,8 @@ function consolidate(results, report) { return report; } -module.exports.check = function check(epub, report) { +module.exports.check = function check(epub, report,lang) { return epubChecker.check(epub, report) - .then(() => htmlChecker.check(epub)) + .then(() => htmlChecker.check(epub, lang)) .then(results => consolidate(results, report)); }; diff --git a/packages/ace-core/src/core/ace.js b/packages/ace-core/src/core/ace.js index c6f1aaf1..983db885 100644 --- a/packages/ace-core/src/core/ace.js +++ b/packages/ace-core/src/core/ace.js @@ -10,10 +10,21 @@ const pkg = require('@daisy/ace-meta/package'); const EPUB = require('@daisy/epub-utils').EPUB; const Report = require('@daisy/ace-report').Report; const checker = require('../checker/checker.js'); +const { setCurrentLanguage } = require('../l10n/localize').localizer; + +const logger = require('@daisy/ace-logger'); tmp.setGracefulCleanup(); module.exports = function ace(epubPath, options) { + if (options.lang) { + setCurrentLanguage(options.lang); + } + + if (options.initLogger) { + logger.initLogger({ verbose: options.verbose, silent: options.silent }); + } + return new Promise((resolve, reject) => { // the jobid option just gets returned in the resolve/reject // so the calling function can track which job finished @@ -56,9 +67,9 @@ module.exports = function ace(epubPath, options) { epub.extract() .then(() => epub.parse()) // initialize the report - .then(() => new Report(epub, options.outdir)) + .then(() => new Report(epub, options.outdir, options.lang)) // Check each Content Doc - .then(report => checker.check(epub, report)) + .then(report => checker.check(epub, report, options.lang)) // Process the Results .then((report) => { if (options.outdir === undefined) { diff --git a/packages/ace-core/src/l10n/locales/en.json b/packages/ace-core/src/l10n/locales/en.json new file mode 100644 index 00000000..93b8aa0b --- /dev/null +++ b/packages/ace-core/src/l10n/locales/en.json @@ -0,0 +1,37 @@ +{ + "checkepub": { + "metadataviolation": { + "testdesc": "Ensures a '{{name}}' metadata is present", + "resdesc": "Add a '{{name}}' metadata property to the Package Document", + "ruledesc": "Publications must declare the '{{name}}' metadata", + "kbtitle": "Schema.org Accessibility Metadata" + }, + "titleviolation": { + "testdesc": "Ensures the EPUB has a title", + "resdesc": "Add a 'dc:title' metadata property to the Package Document", + "ruledesc": "Publications must have a title", + "kbtitle": "EPUB Title" + }, + "pagesourceviolation": { + "testdesc": "Ensures the source of page breaks is identified", + "resdesc": "Add a 'dc:source' metadata property to the Package Document", + "ruledesc": "Publications with page breaks must declare the 'dc:source' metadata", + "kbtitle": "Page Navigation" + } + }, + "axecheck": { + "matching-aria-role": { + "pass": "Element has an ARIA role matching its epub:type", + "fail": "Element has no ARIA role matching its epub:type" + } + }, + "axerule": { + "pagebreak-label": { + "desc": "Ensure page markers have an accessible label" + }, + "epub-type-has-matching-role": { + "help": "ARIA role should be used in addition to epub:type", + "desc": "Ensure the element has an ARIA role matching its epub:type" + } + } +} diff --git a/packages/ace-core/src/l10n/locales/fr.json b/packages/ace-core/src/l10n/locales/fr.json new file mode 100644 index 00000000..3d68bee8 --- /dev/null +++ b/packages/ace-core/src/l10n/locales/fr.json @@ -0,0 +1,37 @@ +{ + "checkepub": { + "metadataviolation": { + "testdesc": "Vérifie que la métadonnée '{{name}}' est présente", + "resdesc": "Ajouter la métadonnée '{{name}}' au Document de Package", + "ruledesc": "Toute publication doit déclarer la métadonnée '{{name}}'", + "kbtitle": "Métadonnées d'accessibilité Schema.org" + }, + "titleviolation": { + "testdesc": "Vérifie que le EPUB a un titre", + "resdesc": "Ajouter la métadonnée 'dc:title' au Document de Package", + "ruledesc": "Une publication doit avoir un titre", + "kbtitle": "Titre de l’EPUB" + }, + "pagesourceviolation": { + "testdesc": "Vérifie que la source des sauts de page est identifiée", + "resdesc": "Ajouter la métadonnée 'dc:source' au Document de Package", + "ruledesc": "Une publication avec des sauts de page doit déclarer la métadonnée 'dc:source'", + "kbtitle": "Navigation par page" + } + }, + "axecheck": { + "matching-aria-role": { + "pass": "L’élément a un rôle ARIA correspondant à son epub:type", + "fail": "L’élément n’a pas de rôle ARIA correspondant à son epub:type" + } + }, + "axerule": { + "pagebreak-label": { + "desc": "Vérifie que les sauts de page ont un label accessible" + }, + "epub-type-has-matching-role": { + "help": "Un rôle ARIA devrait être spécifié en plus de l’epub:type", + "desc": "Vérifie qu’un élément a un rôle ARIA correspondant à son epub:type" + } + } +} diff --git a/packages/ace-core/src/l10n/localize.js b/packages/ace-core/src/l10n/localize.js new file mode 100644 index 00000000..0569bc78 --- /dev/null +++ b/packages/ace-core/src/l10n/localize.js @@ -0,0 +1,16 @@ +const { newLocalizer } = require('@daisy/ace-localize'); + +const enJson = require("./locales/en.json"); +const frJson = require("./locales/fr.json"); + +export const localizer = newLocalizer({ + en: { + name: "English", + default: true, + translation: enJson, + }, + fr: { + name: "Français", + translation: frJson, + }, +}); diff --git a/packages/ace-core/src/scripts/ace-axe.js b/packages/ace-core/src/scripts/ace-axe.js index 87bfe11b..5d1da4db 100644 --- a/packages/ace-core/src/scripts/ace-axe.js +++ b/packages/ace-core/src/scripts/ace-axe.js @@ -8,6 +8,10 @@ daisy.epub = daisy.epub || {}; daisy.epub.createCFI = function(elem) { + if (elem && elem.parentNode && elem.parentNode.nodeType && elem.parentNode.nodeType !== 1) { + return "/"; // HTML root element (not "/2"!) + } + var cfi_ = undefined; var currentElement = elem; @@ -77,13 +81,14 @@ daisy.ace.run = function(done) { jsonItem.targetCFI.push(cfi); } } else { - throw "WTF?!"; + throw "Not ARRAY?!"; } } } }; window.axe.configure({ + locale: window.__axeLocale__, // configured from host bootstrapper page (checker-chromium) can be undefined checks: [ { id: "matching-aria-role", @@ -149,12 +154,14 @@ daisy.ace.run = function(done) { impact: 'minor', messages: { pass: function anonymous(it) { - var out = 'Element has an ARIA role matching its epub:type'; - return out; + // configured from host bootstrapper page (checker-chromium) + const k = "__aceLocalize__axecheck_matching-aria-role_pass"; + return window[k] || k; }, fail: function anonymous(it) { - var out = 'Element has no ARIA role matching its epub:type'; - return out; + // configured from host bootstrapper page (checker-chromium) + const k = "__aceLocalize__axecheck_matching-aria-role_fail"; + return window[k] || k; } } } @@ -172,7 +179,8 @@ daisy.ace.run = function(done) { }, any: ['aria-label', 'non-empty-title'], metadata: { - description: "Ensure page markers have an accessible label", + // configured from host bootstrapper page (checker-chromium) + description: (() => { const k = "__aceLocalize__axerule_pagebreak-label_desc"; return window[k] || k; })() }, tags: ['cat.epub'] }, @@ -184,8 +192,9 @@ daisy.ace.run = function(done) { }, any: ['matching-aria-role'], metadata: { - help: "ARIA role should be used in addition to epub:type", - description: "Ensure the element has an ARIA role matching its epub:type", + // configured from host bootstrapper page (checker-chromium) + help: (() => { const k = "__aceLocalize__axerule_epub-type-has-matching-role_help"; return window[k] || k; })(), + description: (() => { const k = "__aceLocalize__axerule_epub-type-has-matching-role_desc"; return window[k] || k; })() }, tags: ['best-practice'] }, diff --git a/packages/ace-http/package.json b/packages/ace-http/package.json index 9a7e8912..e1a04703 100644 --- a/packages/ace-http/package.json +++ b/packages/ace-http/package.json @@ -1,6 +1,6 @@ { "name": "@daisy/ace-http", - "version": "1.0.1", + "version": "1.0.2", "description": "HTTP API for Ace", "author": { "name": "DAISY developers", diff --git a/packages/ace-http/src/index.js b/packages/ace-http/src/index.js index 6631c64a..cf4aa158 100644 --- a/packages/ace-http/src/index.js +++ b/packages/ace-http/src/index.js @@ -39,6 +39,7 @@ const cli = meow({ -V, --verbose display verbose output -s, --silent do not display any output + -l, --lang language code for localized messages (e.g. "fr"), default is "en" Examples $ ace-http -p 3000 `, @@ -51,10 +52,11 @@ version: pkg.version v: 'version', V: 'verbose', H: 'host', - p: 'port' + p: 'port', + l: 'lang', }, boolean: ['verbose', 'silent'], - string: ['host', 'port'], + string: ['host', 'port', 'lang'], }); function run() { @@ -117,7 +119,8 @@ function postJob(req, res, next) { internal: { "id": jobid, "outputDir": tmp.dirSync({ unsafeCleanup: true }).name, - "epubPath": req.file.path + "epubPath": req.file.path, + "lang": cli.flags.lang, } }; newJob(jobdata); @@ -156,7 +159,7 @@ function newJob(jobdata) { joblist.push(jobdata); // execute the job with Ace - ace(jobdata.internal.epubPath, {'jobid': jobdata.internal.id, 'outdir': jobdata.internal.outputDir}) + ace(jobdata.internal.epubPath, {'jobid': jobdata.internal.id, 'outdir': jobdata.internal.outputDir, 'lang': jobdata.internal.lang}) .then((jobData) => { var jobId = jobData[0]; var idx = joblist.findIndex(job => job.internal.id === jobId); diff --git a/packages/ace-localize/package.json b/packages/ace-localize/package.json new file mode 100644 index 00000000..498f140f --- /dev/null +++ b/packages/ace-localize/package.json @@ -0,0 +1,26 @@ +{ + "name": "@daisy/ace-localize", + "version": "1.0.0", + "description": "Localization utilities for Ace", + "author": { + "name": "DAISY developers", + "organization": "DAISY Consortium", + "url": "http://www.daisy.org/" + }, + "repository": { + "type": "git", + "url": "https://github.com/daisy/ace" + }, + "bugs": { + "url": "https://github.com/daisy/ace/issues" + }, + "license": "MIT", + "main": "lib/localize.js", + "dependencies": { + "i18next": "^15.1.0", + "winston": "^2.4.0" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/ace-localize/src/localize.js b/packages/ace-localize/src/localize.js new file mode 100644 index 00000000..ed2dc7f8 --- /dev/null +++ b/packages/ace-localize/src/localize.js @@ -0,0 +1,88 @@ +const i18n = require('i18next'); +const winston = require('winston'); + +// EXAMPLE `resources` initialization parameter: +// { +// en: { +// name: "English", +// default: true, +// translation: { hello: "Hello" }, +// }, +// fr: { +// name: "Français", +// translation: { hello: "Bonjour" }, +// }, +// } +export function newLocalizer(resources) { + + const LANGUAGE_KEYS = Object.keys(resources); + const DEFAULT_LANGUAGE = LANGUAGE_KEYS.find((lang) => { + return resources[lang].default; + }); + var _currentLanguage = DEFAULT_LANGUAGE; + + const i18nextInstance = i18n.createInstance(); + // https://www.i18next.com/overview/configuration-options + i18nextInstance.init({ + debug: false, + resources: resources, + // lng: undefined, + fallbackLng: DEFAULT_LANGUAGE, + // whitelist: LANGUAGE_KEYS, + // nonExplicitWhitelist: true, + // load: "all", + // preload: LANGUAGE_KEYS, + // lowerCaseLng: false, + saveMissing: true, + missingKeyHandler: (lng, ns, key, fallbackValue, updateMissing, options) => { + if (!options || !options.ignoreMissingKey) { + winston.info('i18next missingKey (ACE REPORT AXE): ' + key); + } + return key; + }, + }); + + return { + LANGUAGES: resources, + + getDefaultLanguage: function() { + return DEFAULT_LANGUAGE; + }, + + getCurrentLanguage: function() { + return _currentLanguage; + }, + setCurrentLanguage: function(language) { + + for (const lang of LANGUAGE_KEYS) { + if (language === lang) { + _currentLanguage = language; + return; + } + } + // fallback + _currentLanguage = DEFAULT_LANGUAGE; + }, + + localize: function(msg, options) { + const opts = options || {}; + + if (i18nextInstance.language !== _currentLanguage) { + i18nextInstance.changeLanguage(_currentLanguage); + } + + return i18nextInstance.t(msg, opts); + }, + + getRawResources: function() { + return resources; + }, + + getRawResourcesForCurrentLanguage: function() { + if (resources[_currentLanguage]) { + return resources[_currentLanguage].translation; + } + return resources[DEFAULT_LANGUAGE].translation; + }, + }; +} diff --git a/packages/ace-report-axe/package.json b/packages/ace-report-axe/package.json index 264eb0db..3ac155a4 100644 --- a/packages/ace-report-axe/package.json +++ b/packages/ace-report-axe/package.json @@ -1,6 +1,6 @@ { "name": "@daisy/ace-report-axe", - "version": "1.0.2", + "version": "1.0.3", "description": "Ace report adapter for aXe", "author": { "name": "DAISY developers", @@ -18,6 +18,7 @@ "license": "MIT", "main": "lib/index.js", "dependencies": { + "@daisy/ace-localize": "^1.0.0", "@daisy/ace-report": "^1.0.1", "fs-extra": "^6.0.1", "winston": "^2.4.0" diff --git a/packages/ace-report-axe/src/index.js b/packages/ace-report-axe/src/index.js index 0331bbec..cd828f6d 100644 --- a/packages/ace-report-axe/src/index.js +++ b/packages/ace-report-axe/src/index.js @@ -5,82 +5,92 @@ const builders = require('@daisy/ace-report').builders; const winston = require('winston'); -// axe test IDs mapped to our KB pages -const kbMap = { - 'baseUrl': 'http://kb.daisy.org/publishing/', - 'map': { - 'accesskeys': {url: 'docs/html/accesskeys.html', title: 'Access Keys'}, - 'area-alt': {url: 'docs/html/maps.html', title: 'Image Maps'}, - 'aria-allowed-attr': {url: 'docs/script/aria.html', title: 'ARIA'}, - 'aria-hidden-body': {url: 'docs/script/aria.html', title: 'ARIA'}, - 'aria-required-attr': {url: 'docs/script/aria.html', title: 'ARIA'}, - 'aria-required-children': {url: 'docs/script/aria.html', title: 'ARIA'}, - 'aria-required-parent': {url: 'docs/script/aria.html', title: 'ARIA'}, - 'aria-roles': {url: 'docs/script/aria.html', title: 'ARIA'}, - 'aria-valid-attr-value': {url: 'docs/script/aria.html', title: 'ARIA'}, - 'aria-valid-attr': {url: 'docs/script/aria.html', title: 'ARIA'}, - 'button-name': {url: 'docs/html/forms.html', title: 'Forms'}, - 'checkboxgroup': {url: 'docs/html/forms.html', title: 'Forms'}, - 'color-contrast': {url: 'docs/css/color.html', title: 'Color'}, - 'definition-list': {url: 'docs/html/lists.html', title: 'Lists'}, - 'dlitem': {url: 'docs/html/lists.html', title: 'Lists'}, - 'document-title': {url: 'docs/html/title.html', title: 'Page Title'}, - 'duplicate-id': {url: 'docs/html/ids.html', title: 'Identifiers'}, - 'empty-heading': {url: 'docs/html/headings.html', title: 'Headings'}, - 'epub-type-has-matching-role': {url: 'docs/html/roles.html', title: 'ARIA role'}, - 'frame-title-unique': {url: 'docs/html/iframes.html', title: 'Inline Frames'}, - 'frame-title': {url: 'docs/html/iframes.html', title: 'Inline Frames'}, - 'heading-order': {url: 'docs/html/headings.html', title: 'Headings'}, - 'href-no-hash': {url: 'docs/html/links.html', title: 'Links'}, - 'html-has-lang': {url: 'docs/html/lang.html', title: 'Language'}, - 'html-lang-valid': {url: 'docs/html/lang.html', title: 'Language'}, - 'image-alt': {url: 'docs/html/images.html', title: 'Images'}, - 'image-redundant-alt': {url: 'docs/html/images.html', title: 'Images'}, - 'input-image-alt': {url: 'docs/html/images.html', title: 'Images'}, - 'label-title-only': {url: 'docs/html/forms.html', title: 'Forms'}, - 'label': {url: 'docs/html/forms.html', title: 'Forms'}, - 'layout-table': {url: 'docs/html/tables.html', title: 'Tables'}, - 'link-in-text-block': {url: 'docs/html/links.html', title: 'Links'}, - 'link-name': {url: 'docs/html/links.html', title: 'Links'}, - 'list': {url: 'docs/html/lists.html', title: 'Lists'}, - 'listitem': {url: 'docs/html/lists.html', title: 'Lists'}, - 'meta-refresh': {url: 'docs/html/meta.html', title: 'Meta'}, - 'meta-viewport-large': {url: 'docs/html/meta.html', title: 'Meta'}, - 'meta-viewport': {url: 'docs/html/meta.html', title: 'Meta'}, - 'object-alt': {url: 'docs/html/object.html', title: 'Object'}, - 'p-as-heading': {url: 'docs/html/headings.html', title: 'Headings'}, - 'pagebreak-label': {url: 'docs/navigation/pagelist.html', 'title': 'Page Breaks'}, - 'radiogroup': {url: 'docs/html/forms.html', title: 'Forms'}, - 'scope-attr-valid': {url: 'docs/html/tables.html', title: 'Tables'}, - 'server-side-image-map': {url: 'docs/html/maps.html', title: 'Image Maps'}, - 'table-duplicate-name': {url: 'docs/html/tables.html', title: 'Tables'}, - 'table-fake-caption': {url: 'docs/html/tables.html', title: 'Tables'}, - 'td-has-header': {url: 'docs/html/tables.html', title: 'Tables'}, - 'td-headers-attr': {url: 'docs/html/tables.html', title: 'Tables'}, - 'th-has-data-cells': {url: 'docs/html/tables.html', title: 'Tables'}, - 'valid-lang': {url: 'docs/html/lang.html', title: 'Language'}, - 'video-caption': {url: 'docs/html/video.html', title: 'Video'}, - 'video-description': {url: 'docs/html/video.html', title: 'Video'}, - } -}; +const { localize, setCurrentLanguage } = require('./l10n/localize').localizer; // each report is content doc level -function axe2ace(spineItem, axeResults) { +function axe2ace(spineItem, axeResults, lang) { winston.verbose(`Converting aXe results to ace for ${spineItem.relpath}`); + if (lang) { + setCurrentLanguage(lang); + } + + // axe test IDs mapped to our KB pages + const kbMap = { + 'baseUrl': 'http://kb.daisy.org/publishing/', + 'map': { + 'accesskeys': {url: 'docs/html/accesskeys.html', title: localize("kb.accesskeys")}, + 'area-alt': {url: 'docs/html/maps.html', title: localize("kb.area-alt")}, + 'aria-allowed-attr': {url: 'docs/script/aria.html', title: localize("kb.aria-allowed-attr")}, + 'aria-hidden-body': {url: 'docs/script/aria.html', title: localize("kb.aria-hidden-body")}, + 'aria-required-attr': {url: 'docs/script/aria.html', title: localize("kb.aria-required-attr")}, + 'aria-required-children': {url: 'docs/script/aria.html', title: localize("kb.aria-required-children")}, + 'aria-required-parent': {url: 'docs/script/aria.html', title: localize("kb.aria-required-parent")}, + 'aria-roles': {url: 'docs/script/aria.html', title: localize("kb.aria-roles")}, + 'aria-valid-attr-value': {url: 'docs/script/aria.html', title: localize("kb.aria-valid-attr-value")}, + 'aria-valid-attr': {url: 'docs/script/aria.html', title: localize("kb.aria-valid-attr")}, + 'button-name': {url: 'docs/html/forms.html', title: localize("kb.button-name")}, + 'checkboxgroup': {url: 'docs/html/forms.html', title: localize("kb.checkboxgroup")}, + 'color-contrast': {url: 'docs/css/color.html', title: localize("kb.color-contrast")}, + 'definition-list': {url: 'docs/html/lists.html', title: localize("kb.definition-list")}, + 'dlitem': {url: 'docs/html/lists.html', title: localize("kb.dlitem")}, + 'document-title': {url: 'docs/html/title.html', title: localize("kb.document-title")}, + 'duplicate-id': {url: 'docs/html/ids.html', title: localize("kb.duplicate-id")}, + 'empty-heading': {url: 'docs/html/headings.html', title: localize("kb.empty-heading")}, + 'epub-type-has-matching-role': {url: 'docs/html/roles.html', title: localize("kb.epub-type-has-matching-role")}, + 'frame-title-unique': {url: 'docs/html/iframes.html', title: localize("kb.frame-title-unique")}, + 'frame-title': {url: 'docs/html/iframes.html', title: localize("kb.frame-title")}, + 'heading-order': {url: 'docs/html/headings.html', title: localize("kb.heading-order")}, + 'href-no-hash': {url: 'docs/html/links.html', title: localize("kb.href-no-hash")}, + 'html-has-lang': {url: 'docs/html/lang.html', title: localize("kb.html-has-lang")}, + 'html-lang-valid': {url: 'docs/html/lang.html', title: localize("kb.html-lang-valid")}, + 'image-alt': {url: 'docs/html/images.html', title: localize("kb.image-alt")}, + 'image-redundant-alt': {url: 'docs/html/images.html', title: localize("kb.image-redundant-alt")}, + 'input-image-alt': {url: 'docs/html/images.html', title: localize("kb.input-image-alt")}, + 'label-title-only': {url: 'docs/html/forms.html', title: localize("kb.label-title-only")}, + 'label': {url: 'docs/html/forms.html', title: localize("kb.label")}, + 'layout-table': {url: 'docs/html/tables.html', title: localize("kb.layout-table")}, + 'link-in-text-block': {url: 'docs/html/links.html', title: localize("kb.link-in-text-block")}, + 'link-name': {url: 'docs/html/links.html', title: localize("kb.link-name")}, + 'list': {url: 'docs/html/lists.html', title: localize("kb.list")}, + 'listitem': {url: 'docs/html/lists.html', title: localize("kb.listitem")}, + 'meta-refresh': {url: 'docs/html/meta.html', title: localize("kb.meta-refresh")}, + 'meta-viewport-large': {url: 'docs/html/meta.html', title: localize("kb.meta-viewport-large")}, + 'meta-viewport': {url: 'docs/html/meta.html', title: localize("kb.meta-viewport")}, + 'object-alt': {url: 'docs/html/object.html', title: localize("kb.object-alt")}, + 'p-as-heading': {url: 'docs/html/headings.html', title: localize("kb.p-as-heading")}, + 'pagebreak-label': {url: 'docs/navigation/pagelist.html', title: localize("kb.pagebreak-label")}, + 'radiogroup': {url: 'docs/html/forms.html', title: localize("kb.radiogroup")}, + 'scope-attr-valid': {url: 'docs/html/tables.html', title: localize("kb.scope-attr-valid")}, + 'server-side-image-map': {url: 'docs/html/maps.html', title: localize("kb.server-side-image-map")}, + 'table-duplicate-name': {url: 'docs/html/tables.html', title: localize("kb.table-duplicate-name")}, + 'table-fake-caption': {url: 'docs/html/tables.html', title: localize("kb.table-fake-caption")}, + 'td-has-header': {url: 'docs/html/tables.html', title: localize("kb.td-has-header")}, + 'td-headers-attr': {url: 'docs/html/tables.html', title: localize("kb.td-headers-attr")}, + 'th-has-data-cells': {url: 'docs/html/tables.html', title: localize("kb.th-has-data-cells")}, + 'valid-lang': {url: 'docs/html/lang.html', title: localize("kb.valid-lang")}, + 'video-caption': {url: 'docs/html/video.html', title: localize("kb.video-caption")}, + 'video-description': {url: 'docs/html/video.html', title: localize("kb.video-description")}, + } + }; + // the content doc-level assertion const assertion = new builders.AssertionBuilder() .withSubAssertions() .withTestSubject(spineItem.relpath, spineItem.title); // process axe's individual checks for a single content document axeResults.violations.forEach((violation) => { + const kbURL = (kbMap.map.hasOwnProperty(violation.id)) ? kbMap.baseUrl + kbMap.map[violation.id].url : kbMap.baseUrl; - const kbTitle = (kbMap.map.hasOwnProperty(violation.id)) + let kbTitle = (kbMap.map.hasOwnProperty(violation.id)) ? kbMap.map[violation.id].title - : 'Unknown'; - if (kbTitle == 'Unknown') winston.verbose(`Couldn’t find KB key for rule '${violation.id}'`) + : '??'; + if (kbTitle == '??') { + winston.verbose(`Couldn’t find KB key for rule '${violation.id}'`); + kbTitle = localize("nokb"); + } const test = new builders.TestBuilder() .withImpact(violation.impact) .withTitle(violation.id) @@ -88,21 +98,72 @@ function axe2ace(spineItem, axeResults) { .withHelp(kbURL, kbTitle, violation.help) .withRulesetTags(violation.tags) .build(); - violation.nodes.forEach(node => assertion.withAssertions( - new builders.AssertionBuilder() - .withAssertedBy('aXe') - .withMode('automatic') - .withTest(test) - .withResult( - new builders.ResultBuilder('fail') - .withDescription(node.failureSummary) - .withPointer(node.target, node.targetCFI) - .withHTML(node.html) - .build()) - .build())); + + violation.nodes.forEach((node) => { + + let description = node.failureSummary; + + // https://github.com/dequelabs/axe-core/blob/v3.2.2/lib/core/reporters/helpers/failure-summary.js + // // https://github.com/dequelabs/axe-core/blob/v3.2.2/lib/misc/none-failure-summary.json#L4 + // description = description.replace("Fix all of the following:", ""); + // // https://github.com/dequelabs/axe-core/blob/v3.2.2/locales/fr.json#L664 + // description = description.replace("Corriger tous les éléments suivants :", ""); + // // https://github.com/dequelabs/axe-core/blob/v3.2.2/lib/misc/any-failure-summary.json#L4 + // description = description.replace("Fix any of the following:", ""); + // // https://github.com/dequelabs/axe-core/blob/v3.2.2/locales/fr.json#L656 + // description = description.replace("Corriger l’un des éléments suivants :", ""); + + // regexp for the above + description = description.replace(/[^:]+:/, ""); + + description = description.trim(); + + let target = node.target; + let targetCFI = node.targetCFI; + let html = node.html; + + const allAnyArrayItems = []; + if (node.any) { + node.any.forEach((anyItem) => { + allAnyArrayItems.push(anyItem); + }); + } + if (node.all) { + node.all.forEach((allItem) => { + allAnyArrayItems.push(allItem); + }); + } + allAnyArrayItems.forEach((item) => { + if (item.relatedNodes) { + item.relatedNodes.forEach((relatedNode) => { + if (relatedNode.html && relatedNode.target && relatedNode.target.length && relatedNode.targetCFI && relatedNode.targetCFI.length) { + html += " "; + html += relatedNode.html; + + target.push(relatedNode.target[0]); + targetCFI.push(relatedNode.targetCFI[0]); + } + }); + } + }); + + assertion.withAssertions( + new builders.AssertionBuilder() + .withAssertedBy('aXe') + .withMode('automatic') + .withTest(test) + .withResult( + new builders.ResultBuilder('fail') + .withDescription(description) + .withPointer(target, targetCFI) + .withHTML(html) + .build()) + .build()); }); + }); - return assertion.build(); + const ass = assertion.build(); + return ass; } module.exports.axe2ace = axe2ace; diff --git a/packages/ace-report-axe/src/l10n/locales/en.json b/packages/ace-report-axe/src/l10n/locales/en.json new file mode 100644 index 00000000..5ac80c5e --- /dev/null +++ b/packages/ace-report-axe/src/l10n/locales/en.json @@ -0,0 +1,57 @@ +{ + "kb": { + "accesskeys": "Access Keys", + "area-alt": "Image Maps", + "aria-allowed-attr": "ARIA", + "aria-hidden-body": "ARIA", + "aria-required-attr": "ARIA", + "aria-required-children": "ARIA", + "aria-required-parent": "ARIA", + "aria-roles": "ARIA", + "aria-valid-attr-value": "ARIA", + "aria-valid-attr": "ARIA", + "button-name": "Forms", + "checkboxgroup": "Forms", + "color-contrast": "Color", + "definition-list": "Lists", + "dlitem": "Lists", + "document-title": "Page Title", + "duplicate-id": "Identifiers", + "empty-heading": "Headings", + "epub-type-has-matching-role": "ARIA role", + "frame-title-unique": "Inline Frames", + "frame-title": "Inline Frames", + "heading-order": "Headings", + "href-no-hash": "Links", + "html-has-lang": "Language", + "html-lang-valid": "Language", + "image-alt": "Images", + "image-redundant-alt": "Images", + "input-image-alt": "Images", + "label-title-only": "Forms", + "label": "Forms", + "layout-table": "Tables", + "link-in-text-block": "Links", + "link-name": "Links", + "list": "Lists", + "listitem": "Lists", + "meta-refresh": "Meta", + "meta-viewport-large": "Meta", + "meta-viewport": "Meta", + "object-alt": "Object", + "p-as-heading": "Headings", + "pagebreak-label": "Page Breaks", + "radiogroup": "Forms", + "scope-attr-valid": "Tables", + "server-side-image-map": "Image Maps", + "table-duplicate-name": "Tables", + "table-fake-caption": "Tables", + "td-has-header": "Tables", + "td-headers-attr": "Tables", + "th-has-data-cells": "Tables", + "valid-lang": "Language", + "video-caption": "Video", + "video-description": "Video" + }, + "nokb": "(no specific entry)" +} diff --git a/packages/ace-report-axe/src/l10n/locales/fr.json b/packages/ace-report-axe/src/l10n/locales/fr.json new file mode 100644 index 00000000..7ff99a10 --- /dev/null +++ b/packages/ace-report-axe/src/l10n/locales/fr.json @@ -0,0 +1,57 @@ +{ + "kb": { + "accesskeys": "Raccourcis Clavier", + "area-alt": "Cartes Imagées", + "aria-allowed-attr": "ARIA", + "aria-hidden-body": "ARIA", + "aria-required-attr": "ARIA", + "aria-required-children": "ARIA", + "aria-required-parent": "ARIA", + "aria-roles": "ARIA", + "aria-valid-attr-value": "ARIA", + "aria-valid-attr": "ARIA", + "button-name": "Formulaires", + "checkboxgroup": "Formulaires", + "color-contrast": "Couleur", + "definition-list": "Listes", + "dlitem": "Listes", + "document-title": "Titre de Page", + "duplicate-id": "Identifiants", + "empty-heading": "Titres", + "epub-type-has-matching-role": "Rôle ARIA", + "frame-title-unique": "Éléments iframes", + "frame-title": "Éléments iframes", + "heading-order": "Titres", + "href-no-hash": "Liens", + "html-has-lang": "Langue", + "html-lang-valid": "Langue", + "image-alt": "Images", + "image-redundant-alt": "Images", + "input-image-alt": "Images", + "label-title-only": "Formulaires", + "label": "Formulaires", + "layout-table": "Tableaux", + "link-in-text-block": "Liens", + "link-name": "Liens", + "list": "Listes", + "listitem": "Listes", + "meta-refresh": "Métadonnées", + "meta-viewport-large": "Métadonnées", + "meta-viewport": "Métadonnées", + "object-alt": "Objet", + "p-as-heading": "Titres", + "pagebreak-label": "Sauts de Page", + "radiogroup": "Formulaires", + "scope-attr-valid": "Tableaux", + "server-side-image-map": "Cartes Imagées", + "table-duplicate-name": "Tableaux", + "table-fake-caption": "Tableaux", + "td-has-header": "Tableaux", + "td-headers-attr": "Tableaux", + "th-has-data-cells": "Tableaux", + "valid-lang": "Langue", + "video-caption": "Vidéo", + "video-description": "Vidéo" + }, + "nokb": "(pas d'entrée spécifique)" +} diff --git a/packages/ace-report-axe/src/l10n/localize.js b/packages/ace-report-axe/src/l10n/localize.js new file mode 100644 index 00000000..0569bc78 --- /dev/null +++ b/packages/ace-report-axe/src/l10n/localize.js @@ -0,0 +1,16 @@ +const { newLocalizer } = require('@daisy/ace-localize'); + +const enJson = require("./locales/en.json"); +const frJson = require("./locales/fr.json"); + +export const localizer = newLocalizer({ + en: { + name: "English", + default: true, + translation: enJson, + }, + fr: { + name: "Français", + translation: frJson, + }, +}); diff --git a/packages/ace-report/package.json b/packages/ace-report/package.json index 302f1a52..18849280 100644 --- a/packages/ace-report/package.json +++ b/packages/ace-report/package.json @@ -1,6 +1,6 @@ { "name": "@daisy/ace-report", - "version": "1.0.2", + "version": "1.0.3", "description": "Reporting utilities for Ace", "author": { "name": "DAISY developers", @@ -19,6 +19,7 @@ "main": "lib/index.js", "dependencies": { "@daisy/ace-config": "^1.0.0", + "@daisy/ace-localize": "^1.0.0", "@daisy/ace-meta": "^1.0.3", "escape-html": "^1.0.3", "fs-extra": "^6.0.1", diff --git a/packages/ace-report/src/generate-html-report.js b/packages/ace-report/src/generate-html-report.js index 23ef5cd4..ab785d86 100644 --- a/packages/ace-report/src/generate-html-report.js +++ b/packages/ace-report/src/generate-html-report.js @@ -4,6 +4,9 @@ const escape = require('escape-html'); const handlebars = require('handlebars'); const fs = require('fs'); const path = require('path'); +const winston = require('winston'); + +const { localize, getCurrentLanguage } = require('./l10n/localize').localizer; // generate the html report and return it as a string module.exports = function generateHtmlReport(reportData) { @@ -16,8 +19,8 @@ module.exports = function generateHtmlReport(reportData) { 'wcag2a': 'WCAG 2.0 A', 'wcag2aa': 'WCAG 2.0 AA', 'EPUB': 'EPUB', - 'best-practice': 'Best Practice', - 'other': 'Other' + 'best-practice': localize("bestpractice"), + 'other': localize("other") }; // return 5 data cells for each ruleset: critical, serious, moderate, minor, total @@ -35,12 +38,16 @@ module.exports = function generateHtmlReport(reportData) { handlebars.registerHelper('violationFilter', function(rule, options) { var filterOptions = ""; violationFilters[rule].forEach(function(value) { + // winston.info("######## " + value); + const valueDisplay = localize(value, {ignoreMissingKey: true}); // only handles "serious", "moderate", etc. so can be missingKey, such as "EPUB/package.opf", "color-contrast", "metadata-schema-accessibilitysummary" etc. (in which case => fallback to key string) // use nicer labels for ruleset options if (rule == "ruleset") { filterOptions += ""; } else { - filterOptions += ""; + filterOptions += ""; } }); return new handlebars.SafeString(filterOptions); @@ -54,22 +61,21 @@ module.exports = function generateHtmlReport(reportData) { handlebars.registerHelper('violationRows', function(options) { var htmlStr = ''; flatListOfViolations.forEach(function(violation) { + const valueDisplay = localize(violation['impact'], {ignoreMissingKey: false}); htmlStr += ` - ${violation['impact']} + ${valueDisplay} ${rulesetTagLabels[violation['applicableRulesetTag']]} ${violation['rule']}

${violation['engine']} - \"${violation['fileTitle']}\"

${violation['location']}`; + \"${violation['fileTitle']}\"

${violation['location']}`; if (violation.html) { - htmlStr +=`

Snippet:${violation.html.trim()}
`; + htmlStr +=`

${localize("snippet")}${violation.html.trim()}
`; } htmlStr += ""; var desc = violation["desc"]; - desc = desc.replace("Fix all of the following:", ""); - desc = desc.replace("Fix any of the following:", ""); var detailsArr = desc.split("\n"); var listStr = ''; @@ -81,7 +87,7 @@ module.exports = function generateHtmlReport(reportData) { htmlStr += `
    ${listStr}
-

Learn more about ${violation['kbtitle']}

+

${localize("learnmoreabout") + " " + violation['kbtitle']}

`; htmlStr += ""; @@ -101,6 +107,30 @@ module.exports = function generateHtmlReport(reportData) { } }); + handlebars.registerHelper('generatedBy', function(options) { + + return new handlebars.SafeString(localize("generatedby", { + v1: (reportData['earl:assertedBy']) ? reportData['earl:assertedBy']['doap:name'] : "doap:name?", + v2: (reportData['earl:assertedBy'] && reportData['earl:assertedBy']['doap:release']) ? reportData['earl:assertedBy']['doap:release']['doap:revision'] : "doap:release?", + v3: reportData['dct:date'], + interpolation: { escapeValue: false }})); + + // if (reportData['earl:assertedBy'].hasOwnProperty('doap:name') && + // reportData['earl:assertedBy'].hasOwnProperty('doap:release') && + // reportData['earl:assertedBy']['doap:release'].hasOwnProperty('doap:revision')) { + // } + // else { + // return new handlebars.SafeString(''); + // } + }); + handlebars.registerHelper('localize', function(key, options) { + if (key === "__language__") { + return getCurrentLanguage(); + } + const valueDisplay = localize(key, {ignoreMissingKey: false}); + return new handlebars.SafeString(valueDisplay); + }); + const content = fs.readFileSync(path.join(__dirname, "./report-template.handlebars")).toString(); var template = handlebars.compile(content); var result = template(reportData); @@ -189,7 +219,7 @@ function createFlatListOfViolations(violations) { var obj = { "file": filename, - "filetitle": filetitle, + "fileTitle": filetitle, "engine": item["earl:assertedBy"], "kburl": item["earl:test"]["help"]["url"], "kbtitle": item["earl:test"]["help"]["dct:title"], diff --git a/packages/ace-report/src/l10n/locales/en.json b/packages/ace-report/src/l10n/locales/en.json new file mode 100644 index 00000000..c083c6c3 --- /dev/null +++ b/packages/ace-report/src/l10n/locales/en.json @@ -0,0 +1,72 @@ +{ + "bestpractice": "Best Practice", + "other": "Other", + "snippet": "Snippet:", + "learnmoreabout": "Learn more about:", + "missingheading": "Missing heading: h{{i}}", + "ace-description": "DAISY Accessibility Checker for EPUB", + "report-title": "Ace Report", + "report-desc": "Report on automated accessibility checks for EPUB", + "via": "via", + "doctitle": "EPUB Accessibility Report by DAISY Ace", + "enablejavascript": "Please enable javascript for best performance.", + "doctopheading": "EPUB Accessibility Report", + "generatedby": "Generated by {{v1}} ({{v2}}) on {{v3}}", + "title": "Title:", + "violations": "Violations", + "metadata": "Metadata", + "outlines": "Outlines", + "images": "Images", + "image": "Image", + "critical": "Critical", + "serious": "Serious", + "moderate": "Moderate", + "minor": "Minor", + "total": "Total", + "summaryviolations": "Summary of violations", + "allviolations": "All violations", + "summaryviolationscaption": "Violation count, by ruleset and severity.", + "allviolationscaption": "Violations in the EPUB, with references to severity, guidelines and specific location of problem.", + "goto": "Go to:", + "impact": "Impact", + "all": "All", + "ruleset": "Ruleset", + "rule": "Rule", + "file": "File", + "resetfilters": "Reset filters", + "na": "N/A", + "location": "Location", + "role": "Role", + "details": "Details", + "topsection": "Top of section", + "pagenav": "Page navigation", + "allmetadata": "All Metadata", + "a11ymetadata": "Accessibility Metadata", + "a11ymetadatapresent": "The following accessibility metadata is present:", + "a11ymetadatanotfound": "No accessibility metadata was found.", + "a11ymetadatamissing": "The following accessibility metadata is missing:", + "pubmetadatacaption": "Publication metadata.", + "name": "Name", + "value": "Value", + "tocoutline": "TOC Outline", + "headsoutline": "Headings Outline", + "htmloutline": "HTML Outline", + "noimages": "No images found in this publication.", + "imagescaption": "Images in the EPUB, with their description", + "attribute": "attribute", + "dataTable_lengthMenu": "Display _MENU_ records per page", + "dataTable_search": "Search:", + "dataTable_info": "Showing: _START_ - _END_ / _TOTAL_", + "dataTable_infoEmpty": "Showing: 0 - 0 / 0", + "dataTable_emptyTable": "No data available in table", + "dataTable_infoFiltered": "(filtered from _MAX_ total entries)", + "dataTable_loadingRecords": "Loading...", + "dataTable_processing": "Processing...", + "dataTable_zeroRecords": "No matching records found", + "dataTable_paginateFirst": "First", + "dataTable_paginateLast": "Last", + "dataTable_paginateNext": "Next", + "dataTable_paginatePrevious": "Previous", + "dataTable_ariaSortAscending": ": activate to sort column ascending", + "dataTable_ariaSortDescending": ": activate to sort column descending" +} diff --git a/packages/ace-report/src/l10n/locales/fr.json b/packages/ace-report/src/l10n/locales/fr.json new file mode 100644 index 00000000..af6e1aa3 --- /dev/null +++ b/packages/ace-report/src/l10n/locales/fr.json @@ -0,0 +1,72 @@ +{ + "bestpractice": "Bonne Pratique", + "other": "Autre", + "snippet": "Extrait:", + "learnmoreabout": "En savoir plus:", + "missingheading": "Titre manquant: h{{i}}", + "ace-description": "Vérificateur DAISY de l’accessibilité EPUB", + "report-title": "Rapport Ace", + "report-desc": "Rapport de vérification automatique de l'accessibilité EPUB", + "via": "via", + "doctitle": "Rapport d'accessibilité EPUB par DAISY Ace", + "enablejavascript": "Activer javascript pour une exécution plus performante", + "doctopheading": "Rapport d'accessibilité EPUB", + "generatedby": "Généré par {{v1}} ({{v2}}) le {{v3}}", + "title": "Titre:", + "violations": "Violations", + "metadata": "Métadonnées", + "outlines": "Structures", + "images": "Images", + "image": "Image", + "critical": "Critique", + "serious": "Sérieux", + "moderate": "Modéré", + "minor": "Mineur", + "total": "Total", + "summaryviolations": "Résumé des violations", + "allviolations": "Toutes les violations", + "summaryviolationscaption": "Nombre de violations, par règle et sévérité.", + "allviolationscaption": "Violations dans l'EPUB, avec références à la sévérité, recommendations et références spécifiques au problème.", + "goto": "Aller à:", + "impact": "Impact", + "all": "Tout", + "ruleset": "Catégorie de règles", + "rule": "Règle", + "file": "Fichier", + "resetfilters": "Réinitialiser les filtres", + "na": "N/D", + "location": "Référence", + "role": "Rôle", + "details": "Détails", + "topsection": "Haut de section", + "pagenav": "Navigation par page", + "allmetadata": "Toutes les Métadonnées", + "a11ymetadata": "Métadonnées d'accessibilité", + "a11ymetadatapresent": "Les métadonnées d'accessibilité suivantes sont présentes:", + "a11ymetadatanotfound": "Aucune métadonnée d'accessibilité.", + "a11ymetadatamissing": "Les métadonnées d'accessibilité suivantes sont absentes:", + "pubmetadatacaption": "Métadonnées de Publication.", + "name": "Nom", + "value": "Valeur", + "tocoutline": "Table des Matières EPUB", + "headsoutline": "Structure des titres", + "htmloutline": "Structure HTML", + "noimages": "Pas d'images dans cette publication.", + "imagescaption": "Images dans l'EPUB, avec leurs descriptions", + "attribute": "attribut", + "dataTable_lengthMenu": "Afficher _MENU_ entrées par page", + "dataTable_search": "Chercher:", + "dataTable_info": "Affiche: _START_ - _END_ / _TOTAL_", + "dataTable_infoEmpty": "Affiche: 0 - 0 / 0", + "dataTable_emptyTable": "Pas de données dans le tableau", + "dataTable_infoFiltered": "(filtrage de _MAX_ entrées au total)", + "dataTable_loadingRecords": "Chargement...", + "dataTable_processing": "En cours...", + "dataTable_zeroRecords": "Aucune entrée trouvée", + "dataTable_paginateFirst": "Premier", + "dataTable_paginateLast": "Dernier", + "dataTable_paginateNext": "Suivant", + "dataTable_paginatePrevious": "Précédent", + "dataTable_ariaSortAscending": ": activer pour trier par colonne ascendante", + "dataTable_ariaSortDescending": ": activer pour trier par colonne descendante" +} diff --git a/packages/ace-report/src/l10n/localize.js b/packages/ace-report/src/l10n/localize.js new file mode 100644 index 00000000..0569bc78 --- /dev/null +++ b/packages/ace-report/src/l10n/localize.js @@ -0,0 +1,16 @@ +const { newLocalizer } = require('@daisy/ace-localize'); + +const enJson = require("./locales/en.json"); +const frJson = require("./locales/fr.json"); + +export const localizer = newLocalizer({ + en: { + name: "English", + default: true, + translation: enJson, + }, + fr: { + name: "Français", + translation: frJson, + }, +}); diff --git a/packages/ace-report/src/report-builders.js b/packages/ace-report/src/report-builders.js index f532dfe8..534b7cd2 100644 --- a/packages/ace-report/src/report-builders.js +++ b/packages/ace-report/src/report-builders.js @@ -10,15 +10,7 @@ const defaults = require('./defaults'); const reportConfig = config.get('report', defaults.report); const path = require('path'); -// static -const ACE_DESCRIPTION = { - '@type': 'earl:software', - 'doap:name': 'DAISY Ace', - 'doap:description': 'DAISY Accessibility Checker for EPUB', - 'doap:homepage': 'http://daisy.github.io/ace', - 'doap:created': '2017-07-01', - 'doap:release': { 'doap:revision': pkg.version }, -}; +const { localize } = require('./l10n/localize').localizer; function calculateResult(assertions) { let outcome = 'pass'; @@ -92,8 +84,8 @@ class AssertionBuilder { class ReportBuilder { constructor( - title = 'Ace Report', - description = 'Report on automated accessibility checks for EPUB', + title = localize("report-title"), + description = localize("report-desc"), ) { this._json = { '@type': 'earl:report', @@ -101,7 +93,14 @@ class ReportBuilder { 'dct:title': (title == null) ? '' : title.toString(), 'dct:description': (title == null) ? '' : description.toString(), 'dct:date': new Date().toLocaleString(), - 'earl:assertedBy': ACE_DESCRIPTION, + 'earl:assertedBy': { + '@type': 'earl:software', + 'doap:name': 'DAISY Ace', + 'doap:description': localize("ace-description"), + 'doap:homepage': 'http://daisy.github.io/ace', + 'doap:created': '2017-07-01', // TODO is this date correct? + 'doap:release': { 'doap:revision': pkg.version }, + }, outlines: {}, data: {}, properties: {}, diff --git a/packages/ace-report/src/report-template.handlebars b/packages/ace-report/src/report-template.handlebars index 19a31f8f..012a8b09 100644 --- a/packages/ace-report/src/report-template.handlebars +++ b/packages/ace-report/src/report-template.handlebars @@ -1,4 +1,4 @@ - + @@ -140,7 +140,7 @@ .engine { font-style: italic; } - .engine:before{content: '(via '} + .engine:before{content: '({{#localize "via"}}{{/localize}} '} .engine:after{content: ')'} .css:before{content: 'CSS: '} .cfi:before{content: 'CFI: '} @@ -184,31 +184,26 @@ - EPUB Accessibility Report by DAISY Ace + {{#localize "doctitle"}}{{/localize}}
-

EPUB Accessibility Report

-

Generated by - {{earl:assertedBy.doap:name}} - ({{earl:assertedBy.doap:release.doap:revision}}) - on - {{dct:date}} -

-

Title: {{earl:testSubject.metadata.dc:title}}

+

{{#localize "doctopheading"}}{{/localize}}

+

{{#generatedBy}}{{/generatedBy}}

+

{{#localize "title"}}{{/localize}} {{earl:testSubject.metadata.dc:title}}

@@ -218,20 +213,20 @@
-

Violations

-

Go to Summary of Violations | All Violations

+

{{#localize "violations"}}{{/localize}}

+

{{#localize "goto"}}{{/localize}} {{#localize "summaryviolations"}}{{/localize}} | {{#localize "allviolations"}}{{/localize}}

-

Summary of violations

+

{{#localize "summaryviolations"}}{{/localize}}

- + - - - - - + + + + + @@ -248,55 +243,55 @@ {{#violationStats "EPUB"}}{{/violationStats}} - + {{#violationStats "best-practice"}}{{/violationStats}} - + {{#violationStats "other"}}{{/violationStats}} - + {{#violationStats "total"}}{{/violationStats}}
Violation count, by ruleset and severity.{{#localize "summaryviolationscaption"}}{{/localize}}
CriticalSeriousModerateMinorTotal{{#localize "critical"}}{{/localize}}{{#localize "serious"}}{{/localize}}{{#localize "moderate"}}{{/localize}}{{#localize "minor"}}{{/localize}}{{#localize "total"}}{{/localize}}
Best Practice{{#localize "bestpractice"}}{{/localize}}
Other{{#localize "other"}}{{/localize}}
Total{{#localize "total"}}{{/localize}}
-

All Violations

+

{{#localize "allviolations"}}{{/localize}}

- impact + {{#localize "impact"}}{{/localize}} - ruleset + {{#localize "ruleset"}}{{/localize}} - rule + {{#localize "rule"}}{{/localize}} - file + {{#localize "file"}}{{/localize}} - Reset filters + {{#localize "resetfilters"}}{{/localize}}
- + - - - - - + + + + + @@ -304,22 +299,22 @@
Violations in the EPUB, with references to severity, guidelines and specific location of problem.{{#localize "allviolationscaption"}}{{/localize}}
ImpactRulesetRuleLocationDetails{{#localize "impact"}}{{/localize}}{{#localize "ruleset"}}{{/localize}}{{#localize "rule"}}{{/localize}}{{#localize "location"}}{{/localize}}{{#localize "details"}}{{/localize}}
- +
-

Metadata

+

{{#localize "metadata"}}{{/localize}}

-

Go to All Metadata | Accessibility Metadata

+

{{#localize "goto"}}{{/localize}} {{#localize "allmetadata"}}{{/localize}} | {{#localize "a11ymetadata"}}{{/localize}}

-

All Metadata

+

{{#localize "allmetadata"}}{{/localize}}

- + - - + + @@ -335,21 +330,21 @@
Publication metadata.{{#localize "pubmetadatacaption"}}{{/localize}}
NameValue{{#localize "name"}}{{/localize}}{{#localize "value"}}{{/localize}}
-

Accessibility Metadata

+

{{#localize "a11ymetadata"}}{{/localize}}

{{#if a11y-metadata.present}} -

The following accessibility metadata is present: +

{{#localize "a11ymetadatapresent"}}{{/localize}} {{#each a11y-metadata.present}} {{/each}}

{{else}} -

No accessibility metadata was found.

+

{{#localize "a11ymetadatanotfound"}}{{/localize}}

{{/if}} {{!-- a little messy because handlebars templates have no logical operators --}} {{#if a11y-metadata.missing }} -

The following accessibility metadata is missing: +

{{#localize "a11ymetadatamissing"}}{{/localize}} {{#each a11y-metadata.missing}} {{/each}} @@ -360,7 +355,7 @@

{{else}} {{#if a11y-metadata.empty}} -

The following accessibility metadata is missing: +

{{#localize "a11ymetadatamissing"}}{{/localize}} {{#each a11y-metadata.missing}} {{/each}} @@ -368,50 +363,50 @@

{{/if}} {{/if}} - +
-

Outlines

-

Go to TOC Outline | Headings Outline | HTML Outline

+

{{#localize "outlines"}}{{/localize}}

+

{{#localize "goto"}}{{/localize}} {{#localize "tocoutline"}}{{/localize}} | {{#localize "headsoutline"}}{{/localize}} | {{#localize "htmloutline"}}{{/localize}}

-

EPUB Table of Contents

+

{{#localize "tocoutline"}}{{/localize}}

{{{outlines.toc}}}
-

Headings outline

+

{{#localize "headsoutline"}}{{/localize}}

{{{outlines.headings}}}
-

HTML outline

+

{{#localize "htmloutline"}}{{/localize}}

{{{outlines.html}}}
- +
-

Images

+

{{#localize "images"}}{{/localize}}

{{#if data.images}} - + - - + - - - - + + + @@ -426,19 +421,19 @@ {{#if alt}} {{else}} - + {{/if}} {{#if describedby}} {{else}} - + {{/if}} {{#if figcaption}} {{else}} - + {{/if}} @@ -446,16 +441,16 @@ {{#if role}} {{else}} - + {{/if}} {{/each}}
Images in the EPUB, with their description{{#localize "imagescaption"}}{{/localize}}
Imagealt attribute + {{#localize "image"}}{{/localize}}alt aria-describedby content + aria-describedby Associated figcaptionLocationRolefigcaption{{#localize "location"}}{{/localize}}{{#localize "role"}}{{/localize}}
{{alt}}N/A{{#localize "na"}}{{/localize}}{{describedby}}N/A{{#localize "na"}}{{/localize}}{{figcaption}}N/A{{#localize "na"}}{{/localize}}{{location}}{{role}}N/A{{#localize "na"}}{{/localize}}
{{else}} -

No images found in this publication.

+

{{#localize "noimages"}}{{/localize}}

{{/if}} - +
@@ -469,6 +464,36 @@