Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add runtime localization support #1036

Merged
merged 6 commits into from
Aug 8, 2018
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,32 @@ To create a new translation for aXe, start by running `grunt translate --lang=<l

To update existing translation file, re-run `grunt translate --lang=<langcode>`. This will add new messages used in English and remove messages which were not used in English.

Additionally, locale can be applied at runtime by passing a `locale` object to `axe.configure()`. The locale object must be of the same shape as existing locales in the `./locales` directory. For example:

```js
axe.configure({
locale: {
lang: 'de',
rules: {
accesskeys: {
help: 'Der Wert des accesskey-Attributes muss einzigartig sein.'
},
// ...
},
checks: {
abstractrole: {
fail: 'Abstrakte ARIA-Rollen dürfen nicht direkt verwendet werden.'
},
'aria-errormessage': {
// Note: doT (https://github.com/olado/dot) templates are supported here.
fail: 'Der Wert der aria-errormessage {{~it.data:value}} `{{=value}}{{~}}` muss eine Technik verwenden, um die Message anzukündigen (z. B., aria-live, aria-describedby, role=alert, etc.).'
}
// ...
}
}
})
```

## Supported ARIA Roles and Attributes.

Refer [aXe ARIA support](./doc/aria-supported.md) for a complete list of ARIA supported roles and attributes by axe.
Expand Down
19 changes: 19 additions & 0 deletions axe.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,24 @@ declare namespace axe {
target: string[];
html: string;
}
interface RuleLocale {
[key: string]: {
description: string;
help: string;
};
}
interface CheckLocale {
[key: string]: {
pass: string;
fail: string;
incomplete: string | { [key: string]: string };
};
}
interface Locale {
lang?: string;
rules?: RuleLocale;
checks?: CheckLocale;
}
interface Spec {
branding?: {
brand: string;
Expand All @@ -79,6 +97,7 @@ declare namespace axe {
reporter?: ReporterVersion;
checks?: Check[];
rules?: Rule[];
locale?: Locale;
}
interface Check {
id: string;
Expand Down
5 changes: 4 additions & 1 deletion doc/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,9 @@ axe.configure({
},
reporter: "option",
checks: [Object],
rules: [Object]});
rules: [Object],
locale: Object
});
```

#### Parameters
Expand Down Expand Up @@ -183,6 +185,7 @@ axe.configure({
* `tags` - array(optional, default `[]`). A list if the tags that "classify" the rule. In practice, you must supply some valid tags or the default evaluation will not invoke the rule. The convention is to include the standard (WCAG 2 and/or section 508), the WCAG 2 level, Section 508 paragraph, and the WCAG 2 success criteria. Tags are constructed by converting all letters to lower case, removing spaces and periods and concatinating the result. E.g. WCAG 2 A success criteria 1.1.1 would become ["wcag2a", "wcag111"]
* `matches` - string(optional, default `*`). A filtering CSS selector that will exclude elements that do not match the CSS selector.
* `disableOtherRules` - Disables all rules not included in the `rules` property.
* `locale` - A locale object to apply (at runtime) to all rules and checks, in the same shape as `/locales/*.json`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this enough for users to understand how to utilize the templating feature?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably not. I'm hoping to get some feedback here that helps me create the missing portion(s) of documentation.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could add a note to the README: https://github.com/dequelabs/axe-core#localization

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@WilcoFiers and I have plans to work on documentation for this today.


**Returns:** Nothing

Expand Down
184 changes: 184 additions & 0 deletions lib/core/base/audit.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,191 @@ function Audit(audit) {

this.defaultConfig = audit;
this._init();

// A copy of the "default" locale. This will be set if the user
// provides a new locale to `axe.configure()` and used to undo
// changes in `axe.reset()`.
this._defaultLocale = null;
}

/**
* Build and set the previous locale. Will noop if a previous
* locale was already set, as we want the ability to "reset"
* to the default ("first") configuration.
*/

Audit.prototype._setDefaultLocale = function() {
if (this._defaultLocale) {
return;
}

const locale = {
checks: {},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we keep track of a lang key here for any reason? The existing locale files include that alongside rules and checks.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not used by anything, so saving it didn't seem super important. What do you think we need it for?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a good point. I think we should include a lang key on the results object. I think we should do that separately from this PR though.

rules: {}
};

// XXX: unable to use `for-of` here, as doing so would
// require us to polyfill `Symbol`.
const checkIDs = Object.keys(this.data.checks);
for (let i = 0; i < checkIDs.length; i++) {
const id = checkIDs[i];
const check = this.data.checks[id];
const { pass, fail, incomplete } = check.messages;
locale.checks[id] = {
pass,
fail,
incomplete
};
}

const ruleIDs = Object.keys(this.data.rules);
for (let i = 0; i < ruleIDs.length; i++) {
const id = ruleIDs[i];
const rule = this.data.rules[id];
const { description, help } = rule;
locale.rules[id] = { description, help };
}

this._defaultLocale = locale;
};

/**
* Reset the locale to the "default".
*/

Audit.prototype._resetLocale = function() {
// If the default locale has not already been set, we can exit early.
const defaultLocale = this._defaultLocale;
if (!defaultLocale) {
return;
}

// Apply the default locale
this.applyLocale(defaultLocale);
};

/**
* Merge two check locales (a, b), favoring `b`.
*
* Both locale `a` and the returned shape resemble:
*
* {
* impact: string,
* messages: {
* pass: string | function,
* fail: string | function,
* incomplete: string | {
* [key: string]: string | function
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think the value can be a function.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can, and will be. The result of calling dot.compile() on the string is a function.

* }
* }
* }
*
* Locale `b` follows the `axe.CheckLocale` shape and resembles:
*
* {
* pass: string,
* fail: string,
* incomplete: string | { [key: string]: string }
* }
*/

const mergeCheckLocale = (a, b) => {
let { pass, fail } = b;
// If the message(s) are Strings, they have not yet been run
// thru doT (which will return a Function).
if (typeof pass === 'string') {
pass = axe.imports.doT.compile(pass);
}
if (typeof fail === 'string') {
fail = axe.imports.doT.compile(fail);
}
return {
...a,
messages: {
pass: pass || a.messages.pass,
fail: fail || a.messages.fail,
incomplete:
typeof a.messages.incomplete === 'object'
? // TODO: for compleness-sake, we should be running
// incomplete messages thru doT as well. This was
// out-of-scope for runtime localization, but should
// eventually be addressed.
{ ...a.messages.incomplete, ...b.incomplete }
: b.incomplete
}
};
};

/**
* Merge two rule locales (a, b), favoring `b`.
*/

const mergeRuleLocale = (a, b) => {
let { help, description } = b;
// If the message(s) are Strings, they have not yet been run
// thru doT (which will return a Function).
if (typeof help === 'string') {
help = axe.imports.doT.compile(help);
}
if (typeof description === 'string') {
description = axe.imports.doT.compile(description);
}
return {
...a,
help: help || a.help,
description: description || a.description
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is missing the helpUrl property. I think we should spread a (...a) so we just copy over the remainder.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good, will update!

};
};

/**
* Apply locale for the given `checks`.
*/

Audit.prototype._applyCheckLocale = function(checks) {
const keys = Object.keys(checks);
for (let i = 0; i < keys.length; i++) {
const id = keys[i];
if (!this.data.checks[id]) {
throw new Error(`Locale provided for unknown check: "${id}"`);
}

this.data.checks[id] = mergeCheckLocale(this.data.checks[id], checks[id]);
}
};

/**
* Apply locale for the given `rules`.
*/

Audit.prototype._applyRuleLocale = function(rules) {
const keys = Object.keys(rules);
for (let i = 0; i < keys.length; i++) {
const id = keys[i];
if (!this.data.rules[id]) {
throw new Error(`Locale provided for unknown rule: "${id}"`);
}
this.data.rules[id] = mergeRuleLocale(this.data.rules[id], rules[id]);
}
};

/**
* Apply the given `locale`.
*
* @param {axe.Locale}
*/

Audit.prototype.applyLocale = function(locale) {
this._setDefaultLocale();

if (locale.checks) {
this._applyCheckLocale(locale.checks);
}

if (locale.rules) {
this._applyRuleLocale(locale.rules);
}
};

/**
* Initializes the rules and checks
*/
Expand Down Expand Up @@ -367,4 +550,5 @@ Audit.prototype._constructHelpUrls = function(previous = null) {
Audit.prototype.resetRulesAndChecks = function() {
'use strict';
this._init();
this._resetLocale();
};
7 changes: 6 additions & 1 deletion lib/core/public/configure.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* global reporters */
function configureChecksRulesAndBranding(spec) {
/*eslint max-statements: ["error",20]*/
/*eslint max-statements: ["error",21]*/
'use strict';
var audit;

Expand Down Expand Up @@ -47,6 +47,11 @@ function configureChecksRulesAndBranding(spec) {
if (spec.tagExclude) {
audit.tagExclude = spec.tagExclude;
}

// Support runtime localization
if (spec.locale) {
audit.applyLocale(spec.locale);
}
}

axe.configure = configureChecksRulesAndBranding;
Loading