From a99929c44604eb77bf2b0e905ac1aac31c78ca79 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 6 Sep 2026 07:06:40 +0000 Subject: [PATCH] docs(i18n): document inline I18nLabel locale maps in the translations guide MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The user-facing guide described exactly one localisation route — a plain-string label translated in a bundle — and never mentioned the second authorized form of an `I18nLabel`, the inline locale map. Since the coverage gate began reading a map's own locales, an author writing one gets a real `missing translation for locale "..."` finding about a prop this page gave them no vocabulary for. Adds a row to "What you can translate" and a "Current boundaries" bullet stating, in order: both forms are authorized; the map is rendered but never extracted; coverage counts the locales the map carries; and no bundle key exists for it, nor is one synthesised from a node's position in the component tree (a settled maintainer ruling, transcribed as a refusal). Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01Vbw3RPgdtqesx4azk9SbW8 --- content/docs/ui/translations.mdx | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/content/docs/ui/translations.mdx b/content/docs/ui/translations.mdx index bb11e44876..1064f4fdc7 100644 --- a/content/docs/ui/translations.mdx +++ b/content/docs/ui/translations.mdx @@ -79,6 +79,7 @@ export default defineStack({ | Page labels and `page:header` copy | `pages..label` / `description` / `title` / `subtitle` | | Screen-flow wizards (flow label, screen headings, screen field copy) | `flows..label` / `flows..screens..title` / `.fields..label` / `.placeholder` — see the boundary note below | | Global actions, settings, messages | `globalActions`, `settings`, `messages` | +| A label written as an inline locale map (`label: { en: 'Members', 'zh-CN': '成员' }`) | Nowhere — it is written on the metadata and resolved at render time; see **Current boundaries** below | The metadata types resolved per request are **object, view, action, app, dashboard, dataset, and page** — a field's labels are translated as part of @@ -282,6 +283,31 @@ up-to-date bundle re-extracts byte-identically. Honest limits worth knowing before you plan around them: +- **Both forms of a label are authorized — and an inline locale map is + rendered but never extracted.** A label may be a plain string, translated in + a bundle under the key from the table above, or an **inline locale map** — + `label: { en: 'Members', 'zh-CN': '成员' }` — written out on the metadata and + resolved at render time (`pickLocalized` in the UI, `resolveI18nLabel` on the + server). The map is the localisation route for the props that have no bundle + key at all, and a page localised that way is fully localised. But it never + reaches the bundle: `os i18n extract` scaffolds no row for it, so a + translator working from your bundles will not find those strings — every + locale a map is to carry is one you write in the map itself. + + **Coverage still counts it (#14749).** `os lint` reads the map's own + locales: the ones it carries count as covered, the ones it omits are + reported against `supportedLocales`. A map written `{ en, 'zh-CN' }` under + `supportedLocales: ['en', 'zh-CN', 'ja-JP']` therefore produces a real + `missing translation for locale "ja-JP"` finding — close it by adding the + locale to the map. Two tools treat the same prop differently and both are + right: the gate reports what the author did and did not write, while the + extractor refuses to invent a key that a later reordering of two sibling + components would silently reassign. + + **No bundle key exists for a map, and none is synthesised from a node's + position in the component tree.** That is a settled refusal (maintainer + ruling 2026-09-03, #14749) — not a gap awaiting a fix, and not something to + plan around. - **Validation messages are translatable, but substituted whole — there is no interpolation.** Author the message on the rule (`object.validations[].message`), which the engine returns on every rejected