diff --git a/.changeset/26820-i18n-skill-books-group.md b/.changeset/26820-i18n-skill-books-group.md new file mode 100644 index 00000000000..70dc1d2323f --- /dev/null +++ b/.changeset/26820-i18n-skill-books-group.md @@ -0,0 +1,12 @@ +--- +'@objectstack/skills': patch +--- + +docs(skills): objectstack-i18n teaches the `books` translation group, with an eval for a two-language doc book + +Clause-②: no +Metadata-protocol: none + +- **Before.** The top-level group list in `skills/objectstack-i18n/SKILL.md` named fourteen of the bundle's fifteen groups and left out `books`, the group the spec added so a doc book's title and section titles follow the reader's locale. An agent writing a zh-CN bundle from the skill left the docs sidebar in one language or invented a key under `pages`. +- **Now.** The list names `books` with its three keys — `books.{book}.label` / `.description` by `Book.name` and `books.{book}.groups.{group_key}.label` by each group's stable `key` — says the pages themselves localize through `doc.translations` and that a book's inline `translations` map stays retired; `os i18n extract --no-objects-only` seeds the group like every other. Eval 9 in `evals/bundles-locales-coverage.json` checks that an answer for a two-language manual spells the book label and a group label by key and never a `pages.*` key. Five eval sentences that restated a rule the skill already teaches were cut to pay for it. +- No schema, key or behaviour changes. diff --git a/scripts/check-skills-token-ratchet.mjs b/scripts/check-skills-token-ratchet.mjs index 053eb781e4b..7b3c0408635 100644 --- a/scripts/check-skills-token-ratchet.mjs +++ b/scripts/check-skills-token-ratchet.mjs @@ -968,7 +968,25 @@ export const CEILINGS = new Map([ // pin duplicated case 5's (77 bytes cut, 20 tokens); the remainder is the new // case; pinned AT the measurement, zero headroom. The raising PR's body carries // the same arithmetic. - ['skills/objectstack-i18n/evals/bundles-locales-coverage.json', 1642], + // 1642 -> 1790 (2026-10-28, the owed Skills-docs follow-up of the spec PR that + // added the `books` translation group): one eval prompt for a doc book's + // spine copy — `books..label` / `.description` and + // `books..groups..label`. Same ruling as the rows above, tap label + // verbatim and untranslated: 「A:先削重复,余额按实测提高」. PAID DOWN FIRST: five + // sentences restated a rule whose home the reader already reads — case 1's + // "no ICU plural/select" (SKILL.md "No ICU MessageFormat" + its own + // `{count, plural` pin), case 3's "retired o.* dialect" (SKILL.md "The + // Retired o.* Shape" + its own `o: {` pin), case 5's "extract --check is the + // freshness gate" (SKILL.md "os i18n extract --check — freshness, not + // coverage"), case 6's "inherited by every field referencing the picklist" + // (SKILL.md's `picklists` entry in the top-level group list), case 8's + // "neither is a pages.*.components key" (its own `components.open_block` + // pin): 227 bytes = 57 units (6567 -> 6340 bytes, 1642 -> 1585); the new case + // adds 817 bytes = 205 units (6340 -> 7157 bytes, 1585 -> 1790): + // ceil($(wc -c < skills/objectstack-i18n/evals/bundles-locales-coverage.json) / 4) == ceil(7157 / 4) + // pinned AT the measurement, zero headroom. The raising PR's body carries the + // same arithmetic. + ['skills/objectstack-i18n/evals/bundles-locales-coverage.json', 1790], // 1211 -> 1458 (card #26302): one eval for a demo whose seed names people — // expected: both in `demoStaff` with a declared `position` / // `permissionSets`, created by `os dev`; NO `role:` alias and NO platform diff --git a/skills/objectstack-i18n/SKILL.md b/skills/objectstack-i18n/SKILL.md index 25c1db25b29..b0212beed57 100644 --- a/skills/objectstack-i18n/SKILL.md +++ b/skills/objectstack-i18n/SKILL.md @@ -171,10 +171,13 @@ Top-level groups alongside `objects`: `apps` (label, description, navigation), an **error** whose message names the `objects.{owner}._actions.{action_name}` key to write instead), `picklists` (`label`, `options` by stored value — every field referencing the picklist inherits them), `dashboards`, `datasets`, `reports`, `pages`, `flows`, `positions`, -`permissions` (permission sets, spelled as the stack's `permissions[]`), `metadataForms`, -`settingsCommon`; `reports`, `positions` and `permissions` hold `label` + `description` by name, -and a joined report's block headings sit at `reports.{report}.blocks.{block_name}.{label,description}`, -keyed by the block's required `name`. +`permissions` (permission sets, spelled as the stack's `permissions[]`), `books` (a doc book's +spine, by `Book.name`: `books.{book}.label` / `.description`, and `groups.{group_key}.label` by +each group's stable `key`, never its position — `books.crm_manual.groups.getting_started.label`; +the pages themselves localize through `doc.translations`, and the inline `translations` map a +book once carried stays retired), `metadataForms`, `settingsCommon`; `reports`, `positions` and +`permissions` hold `label` + `description` by name, and a joined report's block headings sit at +`reports.{report}.blocks.{block_name}.{label,description}`, keyed by the block's required `name`. Two keys sit deeper than their group's `label`: a `page:tabs` item at `pages.{page}.components.{tabs_id}.items.{value}.label` (by the item's `value`; an item without one has no key) and an approval step at `flows.{flow}.approvals.{node_id}.label`. diff --git a/skills/objectstack-i18n/evals/bundles-locales-coverage.json b/skills/objectstack-i18n/evals/bundles-locales-coverage.json index a1b344a3ccd..01e05b88d13 100644 --- a/skills/objectstack-i18n/evals/bundles-locales-coverage.json +++ b/skills/objectstack-i18n/evals/bundles-locales-coverage.json @@ -4,7 +4,7 @@ { "id": 1, "prompt": "Add Simplified Chinese for the `support_case` object: its label and plural, the `status` field with options New / Open / Closed, the `close` action, and a welcome message that greets the user by name.", - "expected_output": "A `TranslationData` per locale under `objects.support_case` — `label`, `pluralLabel`, `fields.status.label`, `fields.status.options` keyed by the STORED option value (`new`, `open`, `closed`, never the display label), `_actions.close.label` — assembled with `defineTranslationBundle({ en, 'zh-CN': zhCN })` and registered under `defineStack({ translations })`. The message uses double-brace interpolation, `'welcome.user': '欢迎,{{userName}}!'`; there is no ICU plural/select.", + "expected_output": "A `TranslationData` per locale under `objects.support_case` — `label`, `pluralLabel`, `fields.status.label`, `fields.status.options` keyed by the STORED option value (`new`, `open`, `closed`, never the display label), `_actions.close.label` — assembled with `defineTranslationBundle({ en, 'zh-CN': zhCN })` and registered under `defineStack({ translations })`. The message uses double-brace interpolation, `'welcome.user': '欢迎,{{userName}}!'`.", "files": [], "assertions": { "must_contain": ["defineTranslationBundle", "objects", "pluralLabel", "options", "_actions", "{{"], @@ -24,7 +24,7 @@ { "id": 3, "prompt": "A customer admin wants to relabel `account` as 客户 in their Studio without waiting for a redeploy.", - "expected_output": "A `translation` metadata item (author with `defineTranslation`, shape `TranslationItemSchema`) carrying `locale: 'zh-CN'` and the same `objects.account.label` group as a file bundle; published items layer over the shipped bundles at boot and on every publish, and deleting the item restores the shipped value. One locale per item; the retired `o.*` dialect is rejected on this door too.", + "expected_output": "A `translation` metadata item (author with `defineTranslation`, shape `TranslationItemSchema`) carrying `locale: 'zh-CN'` and the same `objects.account.label` group as a file bundle; published items layer over the shipped bundles at boot and on every publish, and deleting the item restores the shipped value. One locale per item.", "files": [], "assertions": { "must_contain": ["translation", "locale", "objects", "defineTranslation"], @@ -44,7 +44,7 @@ { "id": 5, "prompt": "`os i18n check` reports zh-CN is missing app, navigation and dashboard keys even though we just ran `os i18n extract`. What is wrong?", - "expected_output": "Diagnoses that `--objects-only` is on by default, so extract wrote only the `objects` / `globalActions` subtree while check also demands `apps`, `navigation`, `dashboard`, `widget`, `page` and `flow` keys. Re-runs `os i18n extract --no-objects-only` (or hand-authors those groups), re-checks with `os i18n check --locales=zh-CN`, and adds `--strict` / `--threshold` in CI; `extract --check` is the separate freshness gate.", + "expected_output": "Diagnoses that `--objects-only` is on by default, so extract wrote only the `objects` / `globalActions` subtree while check also demands `apps`, `navigation`, `dashboard`, `widget`, `page` and `flow` keys. Re-runs `os i18n extract --no-objects-only` (or hand-authors those groups), re-checks with `os i18n check --locales=zh-CN`, and adds `--strict` / `--threshold` in CI.", "files": [], "assertions": { "must_contain": ["--no-objects-only", "os i18n check", "apps", "--strict"], @@ -54,7 +54,7 @@ { "id": 6, "prompt": "Add zh-CN for the `pipeline_by_stage` report, the `sales_manager` permission set, the `regional_lead` position and the `deal_stage` picklist's option labels.", - "expected_output": "Top-level groups keyed by name: `reports.pipeline_by_stage` and `positions.regional_lead` (`label`, `description`), `permissions.sales_manager` (the same, spelled as the stack's `permissions[]`, not the camelCase name), `picklists.deal_stage.options` by stored value, inherited by every field referencing the picklist.", + "expected_output": "Top-level groups keyed by name: `reports.pipeline_by_stage` and `positions.regional_lead` (`label`, `description`), `permissions.sales_manager` (the same, spelled as the stack's `permissions[]`, not the camelCase name), `picklists.deal_stage.options` by stored value.", "files": [], "assertions": { "must_contain": ["reports", "permissions", "positions", "picklists"], @@ -74,12 +74,22 @@ { "id": 8, "prompt": "In zh-CN the `Products` grid heading on the `crm_opportunity` form and the `Open Tasks` block heading of the joined `showcase_task_overview` report are still English.", - "expected_output": "`objects.crm_opportunity_line_item.fields.opportunity.inlineTitle` — the field lives on the child object, so the key is the child's, and an untranslated `inlineTitle` keeps its authored text (no fallback to the child's plural label); `reports.showcase_task_overview.blocks.open_block.label`, keyed by the block's required `name`. Neither is a `pages.*.components` key.", + "expected_output": "`objects.crm_opportunity_line_item.fields.opportunity.inlineTitle` — the field lives on the child object, so the key is the child's, and an untranslated `inlineTitle` keeps its authored text (no fallback to the child's plural label); `reports.showcase_task_overview.blocks.open_block.label`, keyed by the block's required `name`.", "files": [], "assertions": { "must_contain": ["inlineTitle", "crm_opportunity_line_item", "blocks", "open_block"], "must_not_contain": ["fields.opportunity_line_item", "components.open_block"] } + }, + { + "id": 9, + "prompt": "Our `crm_manual` doc book ships in English and zh-CN, but the docs sidebar still shows the book title and its `getting_started` section title in English.", + "expected_output": "The `books` group, keyed by `Book.name`: `books.crm_manual.label` / `.description` and `books.crm_manual.groups.getting_started.label`, by the group's stable `key` (never its position); `os i18n extract --no-objects-only` seeds all three. The pages' own titles and bodies localize through `doc.translations`, and the inline `translations` map a book once carried stays retired.", + "files": [], + "assertions": { + "must_contain": ["books", "groups", "getting_started", "doc.translations"], + "must_not_contain": ["pages.crm_manual", "sections.getting_started"] + } } ] }