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 1 commit
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
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._previousLocale = 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._setPreviousLocale = function() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Not a huge deal, but I'd prefer we use named functions where we can in axe-core. It tends to make debugging a lot easier.

Copy link
Member Author

Choose a reason for hiding this comment

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

I think you're mistaking this method for a function expression. Because this is a method, we'll see its implicit name in stack traces.

Given the code:

function F () {}

F.prototype.foo = function () {
  throw new Error('boom')
}

const f = new F
f.foo()

We'll clearly see "foo" in the stack trace:

node foo.js
/Users/stephen/dev/src/github.com/dequelabs/axe-core/foo.js:4
  throw new Error('boom')
  ^

Error: boom
    at F.foo (/Users/stephen/dev/src/github.com/dequelabs/axe-core/foo.js:4:9)
    at Object.<anonymous> (/Users/stephen/dev/src/github.com/dequelabs/axe-core/foo.js:8:3)
    at Module._compile (internal/modules/cjs/loader.js:702:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:713:10)
    at Module.load (internal/modules/cjs/loader.js:612:32)
    at tryModuleLoad (internal/modules/cjs/loader.js:551:12)
    at Function.Module._load (internal/modules/cjs/loader.js:543:3)
    at Function.Module.runMain (internal/modules/cjs/loader.js:744:10)
    at startup (internal/bootstrap/node.js:238:19)
    at bootstrapNodeJSCore (internal/bootstrap/node.js:572:3)

if (this._previousLocale) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think instead of calling this previousLocale, we should call this defaultLocale or originalLocale. Same for the function names. We're not going back to the previous one. This is going back to the original locale (which is what reset should do). You may want to put a test in place for that as well.

Copy link
Contributor

@jeeyyy jeeyyy Aug 8, 2018

Choose a reason for hiding this comment

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

+1 or fallbackLocale?

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 a fallback, but instead, it's the default. I agree with Wilco that it should be called 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._previousLocale = locale;
};

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

Audit.prototype._resetLocale = function() {
// If no previous locale exists, then we haven't applied one
// and the default is still in-tact.
const previousLocale = this._previousLocale;
if (!previousLocale) {
return;
}

// Apply the previous locale
this.applyLocale(previousLocale);
};

/**
* 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 {
impact: a.impact,
Copy link
Contributor

Choose a reason for hiding this comment

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

What if they want to override the impact?

Copy link
Member Author

Choose a reason for hiding this comment

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

This is not currently supported with the existing localization setup. Adding it now falls out of scope of the task I was given.

If it's something we know we want, we can add support after this PR lands.

Copy link
Contributor

Choose a reason for hiding this comment

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

+1 to Stephen's comment. You can change impact by doing this:

axe.configure({
  checks: [{
    id:'foo',
    metadata: { impact: 'minor' }
  }]
});

I think that instead of copying over impact, we should just spread the rest of the props in (...a). That way we can't accidentally forget to copy stuff over if we add stuff to metadata in the future.

Copy link
Member Author

Choose a reason for hiding this comment

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

👍 I'll spread a here.

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 {
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._setPreviousLocale();

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