diff --git a/skills/plugin-designer/references/i18n.md b/skills/plugin-designer/references/i18n.md new file mode 100644 index 000000000..436d61717 --- /dev/null +++ b/skills/plugin-designer/references/i18n.md @@ -0,0 +1,91 @@ +# i18n for a dx-maps plugin + +Eight rules, each one paid for by a defect on a production map. Every example is real code. + +## 1. One key per SENTENCE, with named placeholders + +Never assemble a sentence from fragments. Word order is not universal, and the parts a language needs +are not the parts English needs. + +```json +"results_live": "{count} of {total} {unit} shown" // library/i18n/locales/en/dashboard.json:64 +``` + +The measured case: `messages.upg100WallIntroB` is `"."` in English — a bare full stop, because the verb +sits earlier. In German it is `" beten."`, in Hindi `" प्रार्थना करते हुए।"`, in Chinese `"為他們禱告。"`. +A head+emphasis wrapper with no tail would have silently dropped that text in three locales. + +## 2. Counts take a LABEL form, not a plural form + +There is no pluralRules layer yet. Write `"Countries: {count}"`, not `"{count} countries"`, so no locale +needs a plural rule the code cannot express. Where a singular/plural pair already exists, branch at the +call site and keep both keys — and keep every call site on the same rule: + +```js +{{ rowCount === 1 ? T('dashboard.group', 'group') : T('dashboard.groups', 'groups') }} +// DashboardDataTable.vue — the header count and the footer total must not disagree at one row +``` + +## 3. Inline markup: COMPOSE THE PARTS. Do not reach for `` + +The fleet has **no** `` usage. The one place that considered it rejected it, and the reasoning +generalises: the string is often a prop rendered by a library component that has no slots. + +```js +// poster-wall.vue:214-228 — three keys (head, emphasis, tail), each escaped, one tag emitted here +const escHtml = (s) => String(s).replace(/[&<>"]/g, …) +``` + +Composing also shrinks the trusted surface: `v-html` on a whole catalogue string trusts every +translation of that key; composing trusts one line of your code. + +## 4. Deliberate English gets `dir="ltr"`, not silence + +Prose that is intentionally English (a citation, a code, a fixed figure) is marked where it sits, so an +RTL page does not flip it and so the gate can tell it apart from a leak. + +```html +{{ fmt(linkMatched) }} / {{ fmt(pinRows.length) }} +``` + +`check:hardcoded-english` reads that marker, and inherits it down the subtree. + +## 5. A `te()` guard or a proxy-`t` miss is a FALLBACK, not a leak + +Hardcoded English behind a catalogue lookup is correct: it renders only when a key is missing, which is +better than rendering blank. Do not "fix" these, and do not delete them. + +```js +function tabLabel(tab) { // research-map.vue + const key = 'tabs.' + (tab?.i18nKey || tab?.id) + return tab && te(key) ? t(key) : (tab?.label ?? '') // the literal is the last resort +} +``` + +The same shape as a Proxy whose miss path is the catalogue (`people-groups-grid.vue:315`, +`t.search || 'Search people groups…'` → `tStatic('grid.search')`). + +## 6. No resolved labels at module scope, and no `tStatic` frozen in a computed + +An array built before the locale is known freezes the first locale seen. Use getters. + +```js +{ id: 'prayer', get label() { return tStatic('dashboard.prayer') || 'Prayer' } } +// dashboard.config.js:81 — getter syntax, so the label is read per access, not per build +``` + +## 7. A namespaced translator hides the prefix from every scanner + +`useOptionalT('neighbor')` means call sites pass BARE keys, so a search for `'neighbor.foo'` finds +nothing and a gate that matches full dotted keys scores the file clean. If you wrap a translator, say so +where you define it — `check:i18n-keys-exist` reads the namespace out of the factory's one-line body. + +## 8. Run these before you land + +``` +npm run check:hardcoded-english # full scan, ON DEMAND — gates:pre runs only its self-test +npm run check:i18n-keys-exist # every literal key named in code exists in English +``` + +`check:i18n-parity` and `check:i18n-never-translated` cover the catalogue; these two cover your code. +Neither sees a string your code never asks for, which is why rule 4 exists.