@@ -47,9 +47,9 @@ and integration with the I18nService.
4747
48481 . ** Runtime format — ` objects.* ` (` TranslationData ` )** : each locale is authored as one
4949 ` TranslationData ` value. All translatable content for an object (label, fields,
50- options, views, sections, actions) is grouped under ` objects.{object_name} ` , with
51- global groups (` apps ` , ` messages ` , ` globalActions ` , ` dashboards ` , ` settings ` ,
52- ` metadataForms ` ) at the top level.
50+ options, views, sections, tabs, actions) is grouped under ` objects.{object_name} ` ,
51+ with global groups (` apps ` , ` messages ` , ` globalActions ` , ` dashboards ` , ` pages ` ,
52+ ` flows ` , ` settings ` , ` metadataForms ` , ` settingsCommon ` ) at the top level.
5353
54542 . ** Bundle registration** : per-locale files are assembled with
5555 ` defineTranslationBundle({ en, 'zh-CN': … }) ` into a ` TranslationBundle `
@@ -152,7 +152,7 @@ i18n/
152152
153153The canonical authoring path: one ` TranslationData ` per locale, assembled with
154154` defineTranslationBundle ` and registered on the stack. This mirrors the shipped
155- example apps (` src/translations/{en,zh-CN}.ts ` + ` index.ts ` ):
155+ ` examples/app-todo ` (` src/translations/{en,zh-CN,ja-JP }.ts ` + ` index.ts ` ):
156156
157157<!-- os:check -->
158158``` typescript
@@ -256,15 +256,16 @@ All translatable content for a single object is aggregated under
256256
257257| Sub-key | Holds |
258258| :--------| :------|
259- | ` label ` / ` pluralLabel ` / ` description ` | Object-level text (` label ` is required ) |
259+ | ` label ` / ` pluralLabel ` / ` description ` | Object-level text (every key optional ) |
260260| ` fields.{field_name} ` | ` label ` , ` help ` , ` placeholder ` , ` options ` (option value → label) per field |
261261| ` _views.{view_name} ` | ` label ` , ` description ` , ` emptyState.title ` / ` emptyState.message ` |
262- | ` _actions.{action_name} ` | ` label ` , ` confirmText ` , ` successMessage ` , ` params.{param_name} ` , ` resultDialog ` |
263- | ` _sections.{section_name} ` | Form section / tab ` label ` , ` description ` |
262+ | ` _actions.{action_name} ` | ` label ` , ` description ` , ` confirmText ` , ` successMessage ` , ` params.{param_name} ` , ` resultDialog ` |
263+ | ` _sections.{section_name} ` | Form section ` label ` , ` description ` |
264+ | ` _tabs.{tab_name} ` | Filter-preset tab ` label ` , keyed by ` ViewTabSchema.name ` |
264265
265266Top-level groups alongside ` objects ` : ` apps ` (label, description, navigation),
266- ` messages ` , ` globalActions ` (object-less actions), ` dashboards ` , ` settings ` ,
267- ` metadataForms ` , ` settingsCommon ` .
267+ ` messages ` , ` globalActions ` (object-less actions), ` dashboards ` , ` pages ` , ` flows ` ,
268+ ` settings ` , ` metadataForms ` , ` settingsCommon ` .
268269
269270> ** Validation messages are not a translation group.** ` validationMessages ` was
270271> removed in spec 17.0.0 — nothing ever read it, so a translated rule
@@ -297,10 +298,10 @@ parse, ship, and resolve to nothing.
297298
298299` os validate ` / ` os lint ` / ` os compile ` check this direction and report it as
299300warnings (` translation-target-unknown ` , ` translation-option-key-unknown ` ): a key
300- naming an object, field, view, action, param, section, app, nav item, dashboard
301- or widget that does not exist is listed alongside the names that do. A bundle
302- keyed to something since renamed still parses — the label just renders silently
303- in its source locale while every neighbouring label resolves.
301+ naming an object, field, view, action, param, section, app, nav item, dashboard,
302+ widget, flow or flow-screen field that does not exist is listed alongside the
303+ names that do. A bundle keyed to something since renamed still parses — the label
304+ just renders silently in its source locale while every neighbouring one resolves.
304305
305306---
306307
@@ -342,9 +343,9 @@ export default defineTranslation({
342343Rules that differ from a file bundle:
343344
344345- ** ` locale ` is required.** A file bundle names its locales as map keys; an item
345- carries its own. An item whose locale cannot be resolved is skipped by the
346- runtime sync — a silent skip, which is why the field is mandatory rather
347- than inferred from the item name .
346+ carries its own. The runtime sync falls back to the item * name * when that looks
347+ like a BCP-47 tag, then skips the item with an ` [i18n] … — skipped ` warning
348+ naming the row — a server log line the author never sees. Always set ` locale ` .
348349- ** One locale per item.** Author ` zh-CN ` and ` ja-JP ` as two items.
349350- Published items are loaded at boot and on every publish (no restart), and
350351 layer ** over** the file bundles — an authored value wins over a shipped one
@@ -358,10 +359,8 @@ Exact Zod shape: `node_modules/@objectstack/spec/src/system/translation.zod.ts`
358359A second object-first shape keyed on ` o.{object_name} ` (with ` app ` , ` nav ` ,
359360` dashboard ` , ` reports ` , ` notifications ` , ` errors ` , ` _globalOptions ` , ` _meta ` ,
360361` namespace ` , and ` _actions.confirmMessage ` ) was once documented for
361- Studio-authored translations. ** No resolver ever read it** , so items authored
362- that way saved successfully and rendered nothing. It was removed —
363- those keys are now rejected at save time with a message naming the group to
364- use instead. Never author them, in files or at runtime.
362+ Studio-authored translations. ** No resolver ever read it.** Both doors now reject
363+ it — files as well as items — each key carrying its own replacement guidance.
365364
366365---
367366
@@ -411,11 +410,12 @@ os i18n check --locales=zh-CN # scope to specific locales
411410os i18n check --strict --threshold=95 # CI gate: locale parity + minimum coverage
412411```
413412
414- It compares registered bundles against source metadata and reports missing
415- object/field/option/view/action keys per locale. Missing keys in the default
413+ It compares registered bundles against source metadata and reports missing keys
414+ per locale across every declared surface: objects (fields, options, views,
415+ sections, tabs, actions, params), global actions, apps and navigation, dashboards
416+ and widgets, pages, flow screens, metadata forms. Missing keys in the default
416417locale are errors; ` --strict ` promotes non-default gaps to errors and
417- ` --show-keys ` lists every missing key. ` os lint --i18n-strict ` folds the same
418- gate into linting.
418+ ` --show-keys ` lists every missing key. ` os lint --i18n-strict ` folds it into lint.
419419
420420### ` os i18n extract --check ` — freshness, not coverage
421421
@@ -434,10 +434,9 @@ missing file and printing the regenerate command.
434434** Use both gates — they answer different questions.** ` os i18n check ` asks * are
435435the strings translated?* (coverage: human work). ` extract --check ` asks * are the
436436generated bundles still what the schema produces?* (freshness: machine output).
437- Renaming a label, adding an object, or removing a spec key leaves coverage at
438- 100% while the bundles quietly go stale — which is exactly how the platform's
439- own bundles ended up carrying translations for keys the schema had already
440- deleted, plus fields with no entry in any locale.
437+ Renaming a label or removing a spec key leaves coverage at 100% while the
438+ bundles go stale — which is how the platform's own bundles ended up carrying
439+ translations for keys the schema had deleted, plus fields with no entry anywhere.
441440
442441It runs in the same ** merge mode** as a normal extract, so it never asks for
443442re-translation: an up-to-date bundle re-extracts byte-identically. Requires
@@ -448,8 +447,8 @@ re-translation: an up-to-date bundle re-extracts byte-identically. Requires
448447The spec models coverage results for tooling: ` TranslationCoverageResult `
449448(totals, ` coveragePercent ` , per-group ` breakdown ` ) and ` TranslationDiffItem ` —
450449` key ` (dot path), ` status ` (` missing | redundant | stale ` ), ` locale ` , optional
451- ` sourceHash ` for stale detection, and AI-enrichment fields ( ` aiSuggested ` ,
452- ` aiConfidence ` ). Full Zod shape:
450+ ` objectName ` , optional ` sourceHash ` for stale detection, and AI-enrichment
451+ fields ( ` aiSuggested ` , ` aiConfidence ` ). Full Zod shape:
453452` node_modules/@objectstack/spec/src/system/translation.zod.ts ` —
454453` TranslationCoverageResultSchema ` , ` TranslationDiffItemSchema ` .
455454
@@ -498,12 +497,13 @@ registers when no i18n plugin is present):
498497- ** ` getTranslations(locale) ` ** — full snapshot for a locale
499498- ** ` loadTranslations(locale, data) ` ** — programmatic load; deep-merges, so multiple
500499 plugins can each contribute their own ` objects.* ` slice
501- - ** ` getLocales() ` ** / ** ` getDefaultLocale() ` ** / ** ` setDefaultLocale() ` **
500+ - ** ` getLocales() ` ** / ** ` setSupportedLocales() ` ** (narrows ` getLocales ` to the app's
501+ declared ` i18n.supportedLocales ` ) / ** ` getDefaultLocale() ` ** / ** ` setDefaultLocale() ` **
502502
503503The in-memory fallback additionally resolves locale codes
504504(exact → case-insensitive → base language ` zh-CN ` → ` zh ` → variant ` zh ` → ` zh-CN ` ).
505505
506- The contract also declares optional methods — ` getCoverage ` ,
506+ The contract also declares optional methods — ` getFieldLabels ` , ` getCoverage ` ,
507507` suggestTranslations ` — that ** no shipped implementation provides** . Treat them
508508as extension points for a custom workbench or TMS adapter. (` getAppBundle ` /
509509` loadAppBundle ` were removed along with the ` o.* ` shape they returned.)
@@ -545,10 +545,12 @@ Scaffold ready-to-edit translation files from your stack config:
545545os i18n extract --locales=zh-CN --out=./src/translations
546546```
547547
548- This writes ` <locale>.objects.generated.ts ` TypeScript modules (not JSON) — the
549- default locale is filled from schema labels, other locales follow ` --fill `
550- (` empty | default | todo ` ). Other flags: ` --default-locale ` , ` --filter ` (regex
551- over object/app names or key paths), ` --dry-run ` , ` --json ` .
548+ This writes ` <locale>.objects.generated.ts ` TypeScript modules (not JSON), plus
549+ ` <locale>.metadata-forms.generated.ts ` unless ` --no-metadata-forms ` — the default
550+ locale is filled from schema labels, other locales follow ` --fill `
551+ (` empty | default | todo ` ). Other flags: ` --default-locale ` , ` --filter ` (regex over
552+ object/app names or key paths), ` --no-merge ` , ` --no-objects-only ` ,
553+ ` --source-hashes ` , ` --dry-run ` , ` --json ` .
552554
553555### 2. Translate
554556
@@ -572,18 +574,17 @@ Commit the translation files, import them into your bundle, and register it via
572574
573575## CRM I18n Blueprint
574576
575- Reference implementation shape:
576-
577- - Bundle entry: ` src/translations/index.ts ` (or ` crm.translation.ts ` )
578- - Locale files: ` src/translations/{en,zh-CN,ja-JP,es-ES}.ts `
577+ The shipped ` examples/app-crm ` is the bundled layout: one
578+ ` src/translations/crm.translation.ts ` holding ` en ` + ` zh-CN ` . ` examples/app-todo `
579+ is the per-locale layout — ` src/translations/{en,zh-CN,ja-JP}.ts ` + ` index.ts ` .
579580
580581Use this structure for metadata apps:
581582
582583| Layer | CRM Pattern |
583584| :--| :--|
584- | Stack config | ` i18n ` with an explicit locale list; per-locale source files by convention |
585- | Translation assembly | One ` defineTranslationBundle ` call that imports per-locale files |
586- | Locale content | Object-scoped translations (` objects.account .fields.* ` , ` _views ` , ` _actions ` ) + global app/messages |
585+ | Stack config | ` i18n ` with an explicit locale list; the source layout is a convention |
586+ | Translation assembly | One ` defineTranslationBundle ` call — inline locales, or importing per-locale files |
587+ | Locale content | Object-scoped translations (` objects.crm_account .fields.* ` , ` _views ` , ` _actions ` ) + global app/messages |
587588| Naming integrity | Translation object/field keys exactly match metadata machine names |
588589
589590For new locales, copy one locale file as a baseline, then run ` os i18n check `
@@ -595,10 +596,8 @@ before release.
595596
596597### ❌ The Retired ` o.* ` Shape
597598
598- Everything reads ` objects.* ` . The ` o.* ` dialect was removed — it is
599- not a "Studio format", not a secondary format, just gone. Files registered in
600- that shape resolve to nothing; runtime items in that shape are rejected at
601- save time.
599+ Everything reads ` objects.* ` . The ` o.* ` dialect was removed — not a "Studio
600+ format", not a secondary format, just gone. Both doors reject it, files included.
602601
603602``` typescript
604603// WRONG — in a file bundle AND in a `translation` item
@@ -647,7 +646,7 @@ options: { in_progress: '进行中' }
647646
648647### ❌ Ignoring Coverage Reports
649648
650- Stale translations can cause confusion. Always run ` os i18n check ` before releases.
649+ Run ` os i18n check ` before releases; ` extract --check ` is what sees staleness .
651650
652651---
653652
0 commit comments