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
| Locale | File | Keys | Values still in English |
|---|---|---|---|
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)
- Use the dictionary matching your code.
defindsde.json. Case doesn't matter —pt-br,pt-BRandPT-BRall findpt-BR.json. - Otherwise, drop everything after the first
-and try that.de-DEfindsde.json; so doesde-AT. - 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:
- Take the key's value from the chosen dictionary.
- If that key is missing, take the English one.
- 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": "ja", "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:
| reason | Meaning |
|---|---|
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 it | What you get |
|---|---|
The bundle is split because it travels over the network to every page that uses it, and no page needs all four languages. Keeping English inline and the rest optional took about 280 KB off the download and stops it growing as languages are added. Nothing else changes: the Node package and the bindings are unaffected.
<script src="surea11y.browser.js"></script><script src="surea11y.i18n.de.js"></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.
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.
Where keys are used
Two independent key namespaces, both resolved the same way:
- Rule-level:
meta.i18n.titleKey/meta.i18n.descriptionKey— resolve a rule'stitle/descriptionon everychecksResults[]entry. - Occurrence-level:
i18n.summaryKey/i18n.hintKey, withi18n.paramsfor{{placeholder}}interpolation — resolve an occurrence'ssummary/hint. See the output schema.
Both 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.