From aa6236f5acf204314d7c4aa95804126c44aca2d3 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 31 Aug 2026 14:03:13 +0000 Subject: [PATCH 1/2] docs(skills): correct objectstack-i18n behavioral claims against the implementation Flight 8 of the published-skills factual sweep (#13658). Every correction is settled against the implementing code plus an executed probe; net -1 line. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01EnE7G31tqbxN1rqpQmzurT --- skills/objectstack-i18n/SKILL.md | 95 ++++++++++++------------- skills/objectstack-i18n/evals/README.md | 4 +- 2 files changed, 49 insertions(+), 50 deletions(-) diff --git a/skills/objectstack-i18n/SKILL.md b/skills/objectstack-i18n/SKILL.md index 760267a080..66bdb84424 100644 --- a/skills/objectstack-i18n/SKILL.md +++ b/skills/objectstack-i18n/SKILL.md @@ -47,9 +47,9 @@ and integration with the I18nService. 1. **Runtime format — `objects.*` (`TranslationData`)**: each locale is authored as one `TranslationData` value. All translatable content for an object (label, fields, - options, views, sections, actions) is grouped under `objects.{object_name}`, with - global groups (`apps`, `messages`, `globalActions`, `dashboards`, `settings`, - `metadataForms`) at the top level. + options, views, sections, tabs, actions) is grouped under `objects.{object_name}`, + with global groups (`apps`, `messages`, `globalActions`, `dashboards`, `pages`, + `flows`, `settings`, `metadataForms`, `settingsCommon`) at the top level. 2. **Bundle registration**: per-locale files are assembled with `defineTranslationBundle({ en, 'zh-CN': … })` into a `TranslationBundle` @@ -152,7 +152,7 @@ i18n/ The canonical authoring path: one `TranslationData` per locale, assembled with `defineTranslationBundle` and registered on the stack. This mirrors the shipped -example apps (`src/translations/{en,zh-CN}.ts` + `index.ts`): +`examples/app-todo` (`src/translations/{en,zh-CN,ja-JP}.ts` + `index.ts`): ```typescript @@ -256,15 +256,16 @@ All translatable content for a single object is aggregated under | Sub-key | Holds | |:--------|:------| -| `label` / `pluralLabel` / `description` | Object-level text (`label` is required) | +| `label` / `pluralLabel` / `description` | Object-level text (every key optional) | | `fields.{field_name}` | `label`, `help`, `placeholder`, `options` (option value → label) per field | | `_views.{view_name}` | `label`, `description`, `emptyState.title` / `emptyState.message` | -| `_actions.{action_name}` | `label`, `confirmText`, `successMessage`, `params.{param_name}`, `resultDialog` | -| `_sections.{section_name}` | Form section / tab `label`, `description` | +| `_actions.{action_name}` | `label`, `description`, `confirmText`, `successMessage`, `params.{param_name}`, `resultDialog` | +| `_sections.{section_name}` | Form section `label`, `description` | +| `_tabs.{tab_name}` | Filter-preset tab `label`, keyed by `ViewTabSchema.name` | Top-level groups alongside `objects`: `apps` (label, description, navigation), -`messages`, `globalActions` (object-less actions), `dashboards`, `settings`, -`metadataForms`, `settingsCommon`. +`messages`, `globalActions` (object-less actions), `dashboards`, `pages`, `flows`, +`settings`, `metadataForms`, `settingsCommon`. > **Validation messages are not a translation group.** `validationMessages` was > removed in spec 17.0.0 — nothing ever read it, so a translated rule @@ -297,10 +298,10 @@ parse, ship, and resolve to nothing. `os validate` / `os lint` / `os compile` check this direction and report it as warnings (`translation-target-unknown`, `translation-option-key-unknown`): a key -naming an object, field, view, action, param, section, app, nav item, dashboard -or widget that does not exist is listed alongside the names that do. A bundle -keyed to something since renamed still parses — the label just renders silently -in its source locale while every neighbouring label resolves. +naming an object, field, view, action, param, section, app, nav item, dashboard, +widget, flow or flow-screen field that does not exist is listed alongside the +names that do. A bundle keyed to something since renamed still parses — the label +just renders silently in its source locale while every neighbouring one resolves. --- @@ -342,9 +343,9 @@ export default defineTranslation({ Rules that differ from a file bundle: - **`locale` is required.** A file bundle names its locales as map keys; an item - carries its own. An item whose locale cannot be resolved is skipped by the - runtime sync — a silent skip, which is why the field is mandatory rather - than inferred from the item name. + carries its own. The runtime sync falls back to the item *name* when that looks + like a BCP-47 tag, then skips the item with an `[i18n] … — skipped` warning + naming the row — a server log line the author never sees. Always set `locale`. - **One locale per item.** Author `zh-CN` and `ja-JP` as two items. - Published items are loaded at boot and on every publish (no restart), and 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` A second object-first shape keyed on `o.{object_name}` (with `app`, `nav`, `dashboard`, `reports`, `notifications`, `errors`, `_globalOptions`, `_meta`, `namespace`, and `_actions.confirmMessage`) was once documented for -Studio-authored translations. **No resolver ever read it**, so items authored -that way saved successfully and rendered nothing. It was removed — -those keys are now rejected at save time with a message naming the group to -use instead. Never author them, in files or at runtime. +Studio-authored translations. **No resolver ever read it.** Both doors now reject +it — files as well as items — each key carrying its own replacement guidance. --- @@ -411,11 +410,12 @@ os i18n check --locales=zh-CN # scope to specific locales os i18n check --strict --threshold=95 # CI gate: locale parity + minimum coverage ``` -It compares registered bundles against source metadata and reports missing -object/field/option/view/action keys per locale. Missing keys in the default +It compares registered bundles against source metadata and reports missing keys +per locale across every declared surface: objects (fields, options, views, +sections, tabs, actions, params), global actions, apps and navigation, dashboards +and widgets, pages, flow screens, metadata forms. Missing keys in the default locale are errors; `--strict` promotes non-default gaps to errors and -`--show-keys` lists every missing key. `os lint --i18n-strict` folds the same -gate into linting. +`--show-keys` lists every missing key. `os lint --i18n-strict` folds it into lint. ### `os i18n extract --check` — freshness, not coverage @@ -434,10 +434,9 @@ missing file and printing the regenerate command. **Use both gates — they answer different questions.** `os i18n check` asks *are the strings translated?* (coverage: human work). `extract --check` asks *are the generated bundles still what the schema produces?* (freshness: machine output). -Renaming a label, adding an object, or removing a spec key leaves coverage at -100% while the bundles quietly go stale — which is exactly how the platform's -own bundles ended up carrying translations for keys the schema had already -deleted, plus fields with no entry in any locale. +Renaming a label or removing a spec key leaves coverage at 100% while the +bundles go stale — which is how the platform's own bundles ended up carrying +translations for keys the schema had deleted, plus fields with no entry anywhere. It runs in the same **merge mode** as a normal extract, so it never asks for re-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 The spec models coverage results for tooling: `TranslationCoverageResult` (totals, `coveragePercent`, per-group `breakdown`) and `TranslationDiffItem` — `key` (dot path), `status` (`missing | redundant | stale`), `locale`, optional -`sourceHash` for stale detection, and AI-enrichment fields (`aiSuggested`, -`aiConfidence`). Full Zod shape: +`objectName`, optional `sourceHash` for stale detection, and AI-enrichment +fields (`aiSuggested`, `aiConfidence`). Full Zod shape: `node_modules/@objectstack/spec/src/system/translation.zod.ts` — `TranslationCoverageResultSchema`, `TranslationDiffItemSchema`. @@ -498,12 +497,13 @@ registers when no i18n plugin is present): - **`getTranslations(locale)`** — full snapshot for a locale - **`loadTranslations(locale, data)`** — programmatic load; deep-merges, so multiple plugins can each contribute their own `objects.*` slice -- **`getLocales()`** / **`getDefaultLocale()`** / **`setDefaultLocale()`** +- **`getLocales()`** / **`setSupportedLocales()`** (narrows `getLocales` to the app's + declared `i18n.supportedLocales`) / **`getDefaultLocale()`** / **`setDefaultLocale()`** The in-memory fallback additionally resolves locale codes (exact → case-insensitive → base language `zh-CN` → `zh` → variant `zh` → `zh-CN`). -The contract also declares optional methods — `getCoverage`, +The contract also declares optional methods — `getFieldLabels`, `getCoverage`, `suggestTranslations` — that **no shipped implementation provides**. Treat them as extension points for a custom workbench or TMS adapter. (`getAppBundle` / `loadAppBundle` were removed along with the `o.*` shape they returned.) @@ -545,10 +545,12 @@ Scaffold ready-to-edit translation files from your stack config: os i18n extract --locales=zh-CN --out=./src/translations ``` -This writes `.objects.generated.ts` TypeScript modules (not JSON) — the -default locale is filled from schema labels, other locales follow `--fill` -(`empty | default | todo`). Other flags: `--default-locale`, `--filter` (regex -over object/app names or key paths), `--dry-run`, `--json`. +This writes `.objects.generated.ts` TypeScript modules (not JSON), plus +`.metadata-forms.generated.ts` unless `--no-metadata-forms` — the default +locale is filled from schema labels, other locales follow `--fill` +(`empty | default | todo`). Other flags: `--default-locale`, `--filter` (regex over +object/app names or key paths), `--no-merge`, `--no-objects-only`, +`--source-hashes`, `--dry-run`, `--json`. ### 2. Translate @@ -572,18 +574,17 @@ Commit the translation files, import them into your bundle, and register it via ## CRM I18n Blueprint -Reference implementation shape: - -- Bundle entry: `src/translations/index.ts` (or `crm.translation.ts`) -- Locale files: `src/translations/{en,zh-CN,ja-JP,es-ES}.ts` +The shipped `examples/app-crm` is the bundled layout: one +`src/translations/crm.translation.ts` holding `en` + `zh-CN`. `examples/app-todo` +is the per-locale layout — `src/translations/{en,zh-CN,ja-JP}.ts` + `index.ts`. Use this structure for metadata apps: | Layer | CRM Pattern | |:--|:--| -| Stack config | `i18n` with an explicit locale list; per-locale source files by convention | -| Translation assembly | One `defineTranslationBundle` call that imports per-locale files | -| Locale content | Object-scoped translations (`objects.account.fields.*`, `_views`, `_actions`) + global app/messages | +| Stack config | `i18n` with an explicit locale list; the source layout is a convention | +| Translation assembly | One `defineTranslationBundle` call — inline locales, or importing per-locale files | +| Locale content | Object-scoped translations (`objects.crm_account.fields.*`, `_views`, `_actions`) + global app/messages | | Naming integrity | Translation object/field keys exactly match metadata machine names | For new locales, copy one locale file as a baseline, then run `os i18n check` @@ -595,10 +596,8 @@ before release. ### ❌ The Retired `o.*` Shape -Everything reads `objects.*`. The `o.*` dialect was removed — it is -not a "Studio format", not a secondary format, just gone. Files registered in -that shape resolve to nothing; runtime items in that shape are rejected at -save time. +Everything reads `objects.*`. The `o.*` dialect was removed — not a "Studio +format", not a secondary format, just gone. Both doors reject it, files included. ```typescript // WRONG — in a file bundle AND in a `translation` item @@ -647,7 +646,7 @@ options: { in_progress: '进行中' } ### ❌ Ignoring Coverage Reports -Stale translations can cause confusion. Always run `os i18n check` before releases. +Run `os i18n check` before releases; `extract --check` is what sees staleness. --- diff --git a/skills/objectstack-i18n/evals/README.md b/skills/objectstack-i18n/evals/README.md index e059e2d2f5..f1148327e4 100644 --- a/skills/objectstack-i18n/evals/README.md +++ b/skills/objectstack-i18n/evals/README.md @@ -13,11 +13,11 @@ When implemented, evals will follow this structure: ``` evals/ ├── bundle-shape/ -│ ├── test-objects-vs-o-keys.md # runtime `objects.*` vs secondary `o.*` format +│ ├── test-objects-vs-o-keys.md # runtime `objects.*`; retired `o.*` is rejected │ ├── test-snake-case-keys.md # object/field keys match metadata machine names │ └── test-option-machine-values.md # lowercase option values, not display labels ├── interpolation/ -│ └── test-double-brace-params.md # {{userName}}, not {userName}; ICU is experimental +│ └── test-double-brace-params.md # {{userName}}, not {userName}; no ICU engine ├── coverage-workflow/ │ ├── test-extract-command.md # os i18n extract --locales/--out flags & TS output │ └── test-check-command.md # os i18n check --strict/--threshold CI gate From 5d16fd477ed0b39eb49c5f53ac263bffa94d52c3 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 31 Aug 2026 14:23:56 +0000 Subject: [PATCH 2/2] docs(skills): tighten the i18n corrections to fit the shrink-only token ratchet The corrections in the previous commit were factually right but grew SKILL.md by 124 tokens against a ceiling that only ever shrinks. Same facts, fewer bytes: enumerations point at the authority instead of transcribing it, and duplicated prose around the retired o.* dialect is stated once. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01EnE7G31tqbxN1rqpQmzurT --- skills/objectstack-i18n/SKILL.md | 53 ++++++++++++++------------------ 1 file changed, 23 insertions(+), 30 deletions(-) diff --git a/skills/objectstack-i18n/SKILL.md b/skills/objectstack-i18n/SKILL.md index 66bdb84424..9d89523685 100644 --- a/skills/objectstack-i18n/SKILL.md +++ b/skills/objectstack-i18n/SKILL.md @@ -261,7 +261,7 @@ All translatable content for a single object is aggregated under | `_views.{view_name}` | `label`, `description`, `emptyState.title` / `emptyState.message` | | `_actions.{action_name}` | `label`, `description`, `confirmText`, `successMessage`, `params.{param_name}`, `resultDialog` | | `_sections.{section_name}` | Form section `label`, `description` | -| `_tabs.{tab_name}` | Filter-preset tab `label`, keyed by `ViewTabSchema.name` | +| `_tabs.{tab_name}` | Filter-preset tab `label` (`ViewTabSchema.name`) | Top-level groups alongside `objects`: `apps` (label, description, navigation), `messages`, `globalActions` (object-less actions), `dashboards`, `pages`, `flows`, @@ -299,9 +299,9 @@ parse, ship, and resolve to nothing. `os validate` / `os lint` / `os compile` check this direction and report it as warnings (`translation-target-unknown`, `translation-option-key-unknown`): a key naming an object, field, view, action, param, section, app, nav item, dashboard, -widget, flow or flow-screen field that does not exist is listed alongside the -names that do. A bundle keyed to something since renamed still parses — the label -just renders silently in its source locale while every neighbouring one resolves. +widget or flow screen that does not exist is listed alongside the names that do. +A bundle keyed to something since renamed still parses — the label just renders +silently in its source locale while every neighbouring one resolves. --- @@ -343,9 +343,8 @@ export default defineTranslation({ Rules that differ from a file bundle: - **`locale` is required.** A file bundle names its locales as map keys; an item - carries its own. The runtime sync falls back to the item *name* when that looks - like a BCP-47 tag, then skips the item with an `[i18n] … — skipped` warning - naming the row — a server log line the author never sees. Always set `locale`. + carries its own. The sync falls back to a BCP-47-looking item *name*, then skips + the item with an `[i18n] … — skipped` warning nobody watches. - **One locale per item.** Author `zh-CN` and `ja-JP` as two items. - Published items are loaded at boot and on every publish (no restart), and layer **over** the file bundles — an authored value wins over a shipped one @@ -358,9 +357,8 @@ Exact Zod shape: `node_modules/@objectstack/spec/src/system/translation.zod.ts` A second object-first shape keyed on `o.{object_name}` (with `app`, `nav`, `dashboard`, `reports`, `notifications`, `errors`, `_globalOptions`, `_meta`, -`namespace`, and `_actions.confirmMessage`) was once documented for -Studio-authored translations. **No resolver ever read it.** Both doors now reject -it — files as well as items — each key carrying its own replacement guidance. +`namespace`, `_actions.confirmMessage`) was once documented for Studio authoring. +**No resolver ever read it**, and both doors now reject it — files included. --- @@ -411,11 +409,10 @@ os i18n check --strict --threshold=95 # CI gate: locale parity + minimum covera ``` It compares registered bundles against source metadata and reports missing keys -per locale across every declared surface: objects (fields, options, views, -sections, tabs, actions, params), global actions, apps and navigation, dashboards -and widgets, pages, flow screens, metadata forms. Missing keys in the default -locale are errors; `--strict` promotes non-default gaps to errors and -`--show-keys` lists every missing key. `os lint --i18n-strict` folds it into lint. +per locale for every surface the extractor walks — objects and their sub-keys, +global actions, apps, dashboards, pages, flow screens, metadata forms. Gaps in +the default locale are errors, `--strict` promotes the rest, `--show-keys` lists +them all; `os lint --i18n-strict` folds the same gate into lint. ### `os i18n extract --check` — freshness, not coverage @@ -434,9 +431,8 @@ missing file and printing the regenerate command. **Use both gates — they answer different questions.** `os i18n check` asks *are the strings translated?* (coverage: human work). `extract --check` asks *are the generated bundles still what the schema produces?* (freshness: machine output). -Renaming a label or removing a spec key leaves coverage at 100% while the -bundles go stale — which is how the platform's own bundles ended up carrying -translations for keys the schema had deleted, plus fields with no entry anywhere. +Renaming a label or removing a spec key leaves coverage at 100% while bundles go +stale — how the platform's own ended up translating keys the schema had deleted. It runs in the same **merge mode** as a normal extract, so it never asks for re-translation: an up-to-date bundle re-extracts byte-identically. Requires @@ -497,8 +493,7 @@ registers when no i18n plugin is present): - **`getTranslations(locale)`** — full snapshot for a locale - **`loadTranslations(locale, data)`** — programmatic load; deep-merges, so multiple plugins can each contribute their own `objects.*` slice -- **`getLocales()`** / **`setSupportedLocales()`** (narrows `getLocales` to the app's - declared `i18n.supportedLocales`) / **`getDefaultLocale()`** / **`setDefaultLocale()`** +- **`getLocales()`** / **`setSupportedLocales()`** / **`getDefaultLocale()`** / **`setDefaultLocale()`** The in-memory fallback additionally resolves locale codes (exact → case-insensitive → base language `zh-CN` → `zh` → variant `zh` → `zh-CN`). @@ -506,7 +501,7 @@ The in-memory fallback additionally resolves locale codes The contract also declares optional methods — `getFieldLabels`, `getCoverage`, `suggestTranslations` — that **no shipped implementation provides**. Treat them as extension points for a custom workbench or TMS adapter. (`getAppBundle` / -`loadAppBundle` were removed along with the `o.*` shape they returned.) +`loadAppBundle` went with the `o.*` shape they returned.) ### Plugin Setup @@ -545,12 +540,11 @@ Scaffold ready-to-edit translation files from your stack config: os i18n extract --locales=zh-CN --out=./src/translations ``` -This writes `.objects.generated.ts` TypeScript modules (not JSON), plus -`.metadata-forms.generated.ts` unless `--no-metadata-forms` — the default -locale is filled from schema labels, other locales follow `--fill` -(`empty | default | todo`). Other flags: `--default-locale`, `--filter` (regex over -object/app names or key paths), `--no-merge`, `--no-objects-only`, -`--source-hashes`, `--dry-run`, `--json`. +This writes `.objects.generated.ts` TypeScript modules (not JSON), plus a +`.metadata-forms.generated.ts` companion unless `--no-metadata-forms` — +the default locale is filled from schema labels, other locales follow `--fill` +(`empty | default | todo`). `os i18n extract --help` lists the rest: `--filter`, +`--default-locale`, `--no-merge`, `--source-hashes`, `--dry-run`, `--json`, … ### 2. Translate @@ -574,9 +568,8 @@ Commit the translation files, import them into your bundle, and register it via ## CRM I18n Blueprint -The shipped `examples/app-crm` is the bundled layout: one -`src/translations/crm.translation.ts` holding `en` + `zh-CN`. `examples/app-todo` -is the per-locale layout — `src/translations/{en,zh-CN,ja-JP}.ts` + `index.ts`. +`examples/app-crm` ships the bundled layout (one `crm.translation.ts`, `en` + +`zh-CN`); `examples/app-todo` the per-locale one (`{en,zh-CN,ja-JP}.ts` + `index.ts`). Use this structure for metadata apps: