Locales

Every rule's title/description, and every occurrence's summary/hint, is localized via a key-based dictionary lookup — not hard-coded per language.

Current locale coverage

LocaleFileKeysUntranslated
en (English)src/i18n/en.json1355— (the canonical/fallback set)
de (German)src/i18n/de.json13550
es (Spanish)src/i18n/es.json13550
fr (French)src/i18n/fr.json13550
ja (Japanese)src/i18n/ja.json13550

"Untranslated" counts values that match the English text character for character, apart from words that are rightly the same in a language — French Orientation, Interruptions and Occurrences, German Frame, Frames and Element, Spanish Selector. Those are listed as such, each with the English text it was checked against, and counted as translated. If the English changes, the entry no longer applies and the string counts as untranslated until someone checks it again. The count cannot see an English word left inside an otherwise translated sentence; that is found by reading the text, and none remain in the locales above. Nor does it measure whether a translation is accurate or uses a language's usual accessibility terms; that takes a reader of the language.

Reading coverage from the package

The same numbers are available at run time, from the dictionaries the installed package ships, so a tool or a docs site can show them without a checkout of the repository:

const { getLocaleCoverage } = require('@surea11y/core');
 
getLocaleCoverage();
// {
// sourceLocale: 'en',
// totalKeys: 1355,
// locales: [
// { locale: 'de', total: 1355, translated: 1355, missing: 0, orphaned: [], percent: 100 },
// { locale: 'es', total: 1355, translated: 1355, missing: 0, orphaned: [], percent: 100 },
// ...
// ]
// }

Per locale, total is the number of English keys it is measured against, translated how many of its values differ from English or are listed as rightly the same, missing how many keys it lacks (they show in English), and orphaned the keys it has that English does not. total - translated is the "Untranslated" column above, missing keys included. Keys a profile leaves out of a locale by choice are not counted. The repository's own coverage report and this function share one implementation, so they always agree; the report counts core's dictionaries and each profile's separately, this function the merged dictionary a scan uses. It is a supported export (API_STABILITY.md, extension points).

Locale files are plain JSON: a flat map of key to translated string, in the same key order as en.json. Nothing else lives in them, so contributing a language means editing text and never touching code.

Every locale carries every key en.json has. Keeping it that way is the job of npm run i18n:sync: run it after any change to en.json and it rewrites every non-English locale file to match — adding keys that are new, dropping keys en.json no longer has, and leaving existing translations alone. A key it adds is seeded with the English text, which counts as untranslated until someone replaces it.

Selecting a locale

runDomRulesInPage(url, null, { locale: 'fr' }, null);

Default is 'en' if omitted. Any string is accepted; an unrecognized locale is never an error.

How a locale is chosen

Two things happen, in this order. Getting them mixed up is the usual source of confusion, so they are described separately.

Step 1 — pick a dictionary (once per scan)

  1. Use the dictionary matching your code. de finds de.json. Case doesn't matter — pt-br, pt-BR and PT-BR all find pt-BR.json.
  2. Otherwise, drop everything after the first - and try that. de-DE finds de.json; so does de-AT.
  3. Otherwise, use English.

So you only need a de-DE.json if German in Austria and Germany should actually read differently. Ship de.json and every German variant is covered.

Step 2 — resolve each string (per string)

Within the chosen dictionary, every string is looked up on its own:

  1. Take the key's value from the chosen dictionary.
  2. If that key is missing, take the English one.
  3. If English is missing it too — which shouldn't happen for a built-in key, but can for a hand-rolled one — use the literal text the rule itself carries.

The result is never a blank, an undefined, or a thrown error. A half-finished translation renders in your language where it exists and in English everywhere else.

Knowing which locale you actually got

Graceful fallback has one drawback: ask for a language the build doesn't carry and you get fluent English back, with nothing in the strings to say so. Every result therefore reports the resolution once, at the top:

"engine": {
"tag": "a11ycore",
"schemaVersion": "1.0.0",
"locale": { "requested": "ko", "resolved": "en", "reason": "unknown-locale" }
}

requested is what you asked for (after trimming; en if you passed nothing or a non-string), resolved is the dictionary that was used, and reason is one of:

reasonMeaning
okYou got exactly what you asked for, and that dictionary carries every key.
primary-subtagYour code carried a subtag with no dictionary of its own, so its base language was used — you asked for de-DE and got de. Normal and expected; nothing to fix. A difference in case alone is not this: DE reports ok.
dictionary-not-loadedThe project ships that language, but this copy of the engine doesn't carry it and none was supplied. In practice: the standalone browser bundle without its locale side file.
unknown-localeThe project has no such translation at all, so English was used. ko and pt-BR both land here today.
partial-dictionaryThe dictionary was used but is missing some keys, so those individual strings fell back to English.

Treat the list as open — a later release can add a value, so match on the ones you care about and let the rest fall through a default.

If you need a particular language, requested !== resolved is the condition to check in CI. Note it is also true for the harmless primary-subtag case, so gate on reason === 'unknown-locale' || reason === 'dictionary-not-loaded' if a base-language match is good enough for you. See the output schema for the field's place in the result, and API_STABILITY.md for what is guaranteed about it.

Where the dictionaries live

Which languages are available depends on how you load the engine.

How you load itWhat you get
require('@surea11y/core')Every locale, built in. Nothing to configure.
A binding (Playwright, Cypress, …)Every locale, built in. Nothing to configure.
surea11y.browser.jsEnglish. Load surea11y.i18n.<locale>.js after it for anything else.

The bundle is split because it travels over the network to every page that uses it, and no page needs every language. Keeping English inline and the rest optional keeps the other languages off the download, and stops it growing as languages are added. Each side file, surea11y.i18n.<locale>.js, is about as large as the English text it replaces (200 to 250 KB today), and a page loads only the ones it uses. Nothing else changes: the Node package and the bindings are unaffected.

<script src="surea11y.browser.js"></script>
<script src="surea11y.i18n.ja.js"></script>
<script>
const result = a11ycore.runa11yCoreInPage(location.href, null, { locale: 'ja' }, null);
</script>

Ask for a language whose file you didn't load and you get English, with engine.locale.reason set to dictionary-not-loaded — different from unknown-locale, which means the project has no such translation at all.

In Node, the same side file is a module: require('@surea11y/core/i18n/de') returns { locale, messages } (locale is 'de', messages the flat key-to-text map), without needing the bundle.

A pack's messages are in its own dictionaries, which the pack lists per locale; a locale the pack has no dictionary for shows its messages in English.

The review dictionaries

The steps that tell a person how to answer an undecided finding (howToCheck, see the output schema) live in dictionaries of their own, in the same five locales. They are kept out of the engine's dictionaries so the steps never enter the bundle or a page: they are added after the scan, in Node, or by code that imports them. Core 1.14.0 and later.

Who reads themHow
runDomRulesInPage, addHowToCheck, howToCheckFor, renderHtmlReportFrom the installed package, looked up by the locale in lower case (fr-ca, then fr), English last.
Code that runs in a browser (a Cypress spec)applyHowToCheck(result, { en, fr }) from @surea11y/core/how-to-check, with the dictionaries it imports as @surea11y/core/review/<locale>.
a11ycore.openReport(result) in the browser bundleFetched from beside the bundle, or given as { reviewDictionaries }.

A step a locale lacks shows in English, as a message does. A pack rule's own steps are English unless its pack's dictionaries translate them (core 1.15.0 and later). getLocaleCoverage() doesn't count these dictionaries. The steps carry no keys, so to show them in another locale, add them again:

const { addHowToCheck } = require('@surea11y/core');
 
const inFrench = addHowToCheck(result, { locale: 'fr' });

The procedure dictionaries

Since core 1.15.0 the review steps are written with core's test procedures, how a person assesses each criterion by hand, and the review dictionaries are made from them. The procedure dictionaries, @surea11y/core/procedures/<locale>, hold the general procedures, the tools' names, the words of a page made from them, and the names of WCAG's principles, guidelines and criteria and of each facet, in each language's published translation of WCAG. All five locales have every text, so a page of procedures is in one language throughout; procedureLanguages() says so for each locale, and for a pack's standard, which may be read in fewer.

Supplying a dictionary yourself

engineOptions.messages accepts { [locale]: { key: value } } and takes precedence over anything built in or loaded from a side file. Useful for overriding a handful of strings, or for a language you maintain privately:

runDomRulesInPage(url, null, {
locale: 'de',
messages: { de: { img_altPresent_title: 'Eigener Text' } }
}, null);

Keys you don't supply fall back normally, so a partial override is fine.

Language-specific matching

Five rules judge text by known phrases, so they carry word lists as well as messages: link-name-quality ("click here", 「こちら」), heading-quality ("Untitled", 「見出し」), form-control-label-quality ("Label", 「入力欄」), page-title-patterns ("Home", 「トップページ」) and media-alternative-transcript-evidence ("transcript", 「文字起こし」). Each list covers en, de, es, fr and ja. The skip-link rule recognises skip-link wording in the same five languages.

English is always checked. The list for the element's own language, taken from the nearest lang attribute (the <html> element's for a page title), is added on top. link-name-quality checks every language's list where no lang is declared anywhere up the link's tree, since the link may be in any of them. Checking every list everywhere would flag words that are generic in one language and a real name in another, such as "Suite" or "Plus" on an English page. Transcript words are the exception: they are distinctive in every language and a page can link a transcript in another language, so all of them are always checked. Text is NFKC-normalized first, so full-width forms such as 「見出し2」 match.

page-title-patterns also counts each Chinese, Japanese or Korean character as two when deciding whether a title is too short, since 「お問い合わせ」 is a complete title in six characters.

Where keys are used

Several key namespaces, all resolved the same way:

  • Rule-level: meta.i18n.titleKey / meta.i18n.descriptionKey — resolve a rule's title/description on every checksResults[] entry.
  • HTML report: the report_* keys label the page @surea11y/core/report renders — headings, table columns, outcome and severity names, the headline, the pager. The report reads them in the locale the scan resolved to, so a dictionary translates the report too.
  • Composite-level: each rulesResults[] rollup's title/description, using each language's published name for the Success Criterion.
  • Occurrence-level: i18n.summaryKey / i18n.hintKey, with i18n.params for {{placeholder}} interpolation — resolve an occurrence's summary/hint. A lean's i18n.whyKey / i18n.cautionKey resolve its why and caution the same way. See the output schema.
  • Review: <prefix>_review_<CODE>_step<n>, _pass and _fail, in the review dictionaries — an undecided occurrence's howToCheck.
  • Procedures: wcag_<criterion>_<facet>_step<n>, _pass and _fail, tool_<id> and render_*, in the procedure dictionaries.

The rule, composite and occurrence keys are included in the result alongside the already-resolved text, so you can re-render in a different locale from a saved result without re-scanning.