i18n reference for plugin authors: eight rules, each one paid for by a production defect
skills/plugin-designer/references/i18n.md, beside backend/glossary/targets/workflow. Every example is real code with a file and line, and every claim was checked against main rather than written from memory — a reference that is wrong costs more than no reference. ★ ONE CORRECTION TO THE BRIEF, and it matters for whoever reads this next. The ask named poster-wall.vue as the fleet's `<i18n-t>` example. It is the opposite: that file's comment explains why `<i18n-t>` could NOT be used there — the string is a prop rendered by a library component with no slots — and there is no `<i18n-t>` usage anywhere in the tree. So the rule is written as "compose the parts, do not reach for i18n-t", which is what the code actually does and what a plugin author can actually follow. Telling them to use a component the codebase has never used would have sent them down a path with no example to copy. The rules: one key per SENTENCE with named placeholders (the Hindi tail case, where "." in English is " प्रार्थना करते हुए।" in Hindi and a wrapper would have dropped it); label form for counts while there is no pluralRules layer; compose markup from escaped parts; dir="ltr" for deliberate English so the gate can tell it from a leak; te()/proxy-t fallbacks are NOT leaks and must not be "fixed"; no resolved labels at module scope (getters, or the first locale freezes); a namespaced translator hides the prefix from every scanner; and the two gates that cover your code rather than the catalogue. Rule 8 carries the precision dx-shepherd gave me today: check:hardcoded-english is ON DEMAND, and gates:pre runs only its self-test — so an author who assumes the suite covers it will land leaks. skills/ is shared, so this is a source sha for studio rather than something I land.
This commit is contained in:
parent
dbbcd5bc45
commit
f99681a9d9
1 changed files with 91 additions and 0 deletions
91
skills/plugin-designer/references/i18n.md
Normal file
91
skills/plugin-designer/references/i18n.md
Normal file
|
|
@ -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 `<i18n-t>`
|
||||
|
||||
The fleet has **no** `<i18n-t>` 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
|
||||
<b dir="ltr">{{ fmt(linkMatched) }} / {{ fmt(pinRows.length) }}</b> <!-- resources-map.vue:2182 -->
|
||||
```
|
||||
|
||||
`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.
|
||||
Loading…
Reference in a new issue