Skip to content

Commit 031d97a

Browse files
feat(spec,rest,cli): a doc book's title and section titles are translatable — books.<name> in the translation bundle (#26819)
<!-- claude-projects-attribution: {"github_login":"os-support-ai"} --> _Requested by **Yibo Wang** · [project thread](https://claude.ai/code/project/chan_01BVUyyPie8gHwoPM18iABwB?thread=cmsg_01BVUyyPie8gHwoPM18iABwB9f5JerDTtsejdoL5dyVSwJ)_ Fixes #26690 Clause-②: yes (widening) Metadata-protocol: added Skills-docs: #26820. The docs are updated here. ## 维护者速读 - **改了什么:** 翻译包新增 `books` 分组,书名、书的描述和每个分组标题(按分组 `key`)都能翻译;`GET /meta/book`(列表和单项)和 `GET /meta/book/:name/tree` 按请求语言返回,控制台 `/apps/<包>/docs` 侧栏随之显示译文。`os i18n extract` 会生成这些键,`os i18n check` 报缺失为 `i18n/missing-book`。 - **为什么改:** 文档正文能按语言出,但书名和侧栏分组名只能写一种语言,中文用户看到半中半英的手册(HotCRM 使用手册)。 - **风险与回滚:** 只加不删:英文读者拿到的是原样字符串(同一引用);书的 `translations` 内联写法仍被拒绝,只是提示改为指向新键。回滚即还原本 PR。 - **先看:** `packages/spec/src/system/i18n-resolver.ts`(`translateBook`)、`packages/rest/src/meta-item-read-gate.ts`(树接口翻译这一步)、`packages/spec/src/system/translation.zod.ts`(`books` 分组形状)。 - **两分钟验证:** `pnpm --filter @objectstack/runtime exec vitest run src/domains/meta-list-projection-parity.test.ts -t 26690`,预期 zh-CN 拿到「帮助中心 / 开始」,en-US 拿到「Help Centre / Start」,两个传输层一致。 - **需要你做的:** 无,席位落地。 ## What changes One translation path for a doc book's spine — the translation bundle, the same path an app's label and navigation labels take. No inline locale map comes back on the book (the retired 17.0.0 map stays a tombstone; sibling groups such as `apps.<name>.navigation.<id>.label` and `pages.<name>.label` use the bundle, not inline maps). - **`@objectstack/spec` — `system/translation.zod.ts`:** a `books` group on the shared per-app shape, so the bundle entry (`TranslationData`), the registered `translation` item and the platform bundle all accept it. Keyed by the book `name`: `label`, `description`, and `groups.GROUP_KEY.label` (by the group's stable `key`, as a navigation node is keyed by its `id`). Strict at every level; `name` / `title` point at `label`, `sections` / `chapters` / `toc` point at `groups`, the singular `book` points at `books`. The group counts in the `settings` refusal prose and the shape docblocks move fourteen → fifteen (and fifteen → sixteen for the platform face). - **`@objectstack/spec` — `system/i18n-resolver.ts`:** `translateBook`, registered in `METADATA_DOCUMENT_TRANSLATORS`. `TRANSLATABLE_METADATA_TYPES` is derived from that table and `@objectstack/rest` reads it, so `GET /meta/book` (list and item, both transports) is served in the request locale with no further wiring. A key the bundle does not carry keeps the authored string; a document with nothing to translate comes back as the same reference. - **`@objectstack/spec` — `system/book.zod.ts`:** the retired inline-`translations` prescription now names the bundle keys to move the strings to (it used to say the tree and portal render verbatim, which this PR makes untrue). Book `label` and group `label` describes name their bundle keys. - **`@objectstack/rest` — `createMetaBookTreeAnswer`:** step 5 translates the resolved book through `translateMetaDocument` (the same table) before rendering the spine, so the tree and the list the sidebar is built from agree. Translated after the §6.7 audience gate; a label is not an access input. `MetaBookTreeSources` gains `resolveI18nService()`; `RestServer`'s route supplies it. - **`@objectstack/runtime` — dispatcher book-tree route:** spreads `metaTranslationSources`, so it answers the localized tree `RestServer` answers. - **`@objectstack/cli`:** `walkBooks` in `os i18n extract` emits `books.NAME.label` / `.description` (`pushOptional`) and `books.NAME.groups.KEY.label` (`pushEntry`, the label is required); coverage reports a gap as `i18n/missing-book`. - **Liveness:** `translation/books` seeded `live` with its reader, drilled to `label`, `description`, `groups` and `groups.label`. - **Console sidebar:** no code change. The docs portal builds its sidebar from `GET /meta/book` (`use-book-data.ts`), the client sends `Accept-Language` on every request, and the console already remounts the metadata subtree on a language switch. A console test pins the chain. - **`examples/app-showcase`:** zh-CN strings for `showcase_manual` (title, description, three section titles), so the new walk does not grow `check:i18n-coverage`. - **Docs:** `ui/doc-pages.mdx` gains a "Translating the spine" section (the inline-map callout now points at it); `ui/translations.mdx` and `protocol/kernel/i18n-standard.mdx` list the new group. - **`@objectstack/lint` — `translation-target-unknown`:** resolves `books.NAME` against the declared books (a sibling package's included) and the implicit per-package book (the package id, its one `all` group read off `deriveImplicitPackageBook`), and `books.NAME.groups.KEY` against that book's group keys. A mistyped book name or group key is an `error` with the nearest declared name. The rule's header and its `explainRule` paragraph cover the new arm. - **`@object-ui/i18n` — `SPEC_TRANSLATION_GROUPS`:** gains `books`, so the console's language loader walks the served group (the existing `loadLanguage` pin requires it). The Studio translation-preview coverage pin moves to `2/15 (13%)` with the `books` category key. ## Why A package that ships its docs in two languages showed a half-translated manual. Each doc's body already followed the reader's locale (`doc.translations`, `NAME.LOCALE.md`), but the book title and every sidebar section title stayed in the authoring language: the book's inline `translations` map was retired in 17.0.0 because nothing read it, and the translation bundle had no key for a book. This gives the spine a translation slot on the one path navigation items and pages already use. ## Tests - `packages/spec/src/system/i18n-book.test.ts` (new, 11): the three doors accept `books`; the face is closed; the aliases; the tombstone names the new keys; a zh-CN reader gets title, description and every section title; the translated book renders a translated spine through `resolveBookTree`; an en-US reader gets the same reference; per-key fallback; BCP-47 ladder and fallback chain; no mutation. - `packages/cli/test/i18n-book-walk.test.ts` (new, 4): the walk, seeding, a bundle written at the extracted paths parses, and the `book` coverage bucket. - `packages/runtime/src/domains/meta-list-projection-parity.test.ts`: the census fixture bundle gains a `books` entry, so every list, item and tree cell compares both transports under `Accept-Language: zh-CN` / `?locale=zh-CN` as well. New reference pins: the zh-CN book list (and en-US unchanged), the zh-CN book item, and the tree on BOTH transports — zh-CN title and section title, en-US authored, the doc entry still localized by `doc.translations`. - `ui/apps/console/src/pages/docs-portal.bookLocale-26690.test.tsx` (new, 2): the real `/apps/:appName/docs` routes, layout, reader and sidebar over an adapter that answers `GET /meta/book` by running the authored book through the spec's translator with a bundle (bundle → served book → sidebar): zh-CN shows the translated title (`aria-label`) and section titles; en-US is unchanged. - `packages/lint/src/validate-translation-references.test.ts` (5 new): a declared book and its group keys are clean; an unknown book name; unknown group keys, a group label among them; a sibling package's book; the implicit per-package book and an orphan group under it. Removing the `books` loop reds three of them. ## Acceptance notes - **`MetaBookTreeSources.resolveI18nService` is a required member.** Both in-repo transports supply it. A third-party implementer of this exported port would need to add it; making it optional would let a transport silently serve untranslated labels. Flagged for the contract review: the seat may prefer it optional. - **The synthetic *Uncategorized* group stays English.** `resolveBookTree` hardcodes its label; it is not an authored group and has no bundle key. Pre-existing, unchanged by this PR. - **The implicit per-package book** (no authored book; name = package id) also resolves `books.PACKAGE_ID.label` if a bundle carries it — an effect of translating whatever book the tree resolves, not a declared feature. - **`translation-target-unknown` resolves `books.NAME` and `books.NAME.groups.KEY`** against the declared books (a sibling package's included) and the implicit per-package book (the package id, one `all` group read off `deriveImplicitPackageBook`): a mistyped book name or group key is an `error` with the nearest declared name. Added in the review's patch round (claim amendment 6449487635). - No `!` in the title and no `(narrowing)` arm, so no full dogfood run is owed. ## Verification Head `1f17d1834` for the suites below. The patch round at `4b8fb9a1b` (the `books` lint arm and the two UI fixes) re-ran: `@objectstack/lint` test 7435 passed, typecheck exit 0; the UI `loadLanguage`, `@object-ui/i18n` and app-shell preview suites, 235 files passed; `check:generated` exit 0; the 31 lint, changeset, i18n and UI gate families from `dispatch-gates`, every exit 0 (the derived set is still 123). Every build and test ran through `scripts/pm/os-verify-lock.sh` on a shared box. <details><summary>Build and package suites</summary> - `pnpm build`: 116/116 tasks, exit 0 (the first run found the Studio translation preview's exhaustive group table missing `books`; fixed in this PR). - `@objectstack/spec` test: 735 files, 22021 passed; typecheck exit 0. - `@objectstack/rest` test: 6498 passed; typecheck exit 0. - `@objectstack/runtime` test: 7448 passed; typecheck exit 0. - `@objectstack/cli` unit: 4938 passed; typecheck exit 0. - UI: `vitest run` of the console book-locale test and the Studio translation-preview test: 2 files, 23 passed. `type-check` of `@object-ui/app-shell` and `@object-ui/console`: exit 0. - `eslint` over the 16 touched TS/TSX files: 0 errors, 0 warnings. </details> <details><summary>Ablations (each red, then restored; <code>git diff HEAD</code> empty)</summary> - Drop `book: translateBook` from `METADATA_DOCUMENT_TRANSLATORS`: `i18n-book.test.ts` 2 of 11 fail; 3 runtime parity tests fail (list, item, tree). - Replace the tree's translate step with the untranslated book: the tree test fails on both transports. - Drop `walkBooks(config, out)`: `i18n-book-walk.test.ts` 4 of 4 fail. - Console test, adapter returns the authored book instead of the translated one: the zh-CN sidebar test fails. (The console test reads the built spec, so a source-only ablation of the translator does not reach it; the chain it pins is served book → sidebar.) </details> <details><summary>Derived gates</summary> - `dispatch-gates --commands`: 123 commands; `dispatch-gates --ran`: 123 derived, 123 run, 0 NOT-MEASURED, 0 UNRUN; every exit 0. The three build-output gates (`check:skill-examples`, `check:dual-build-cjs-loads`, `check:i18n-coverage`) and `check:test-source-alias`, `check:i18n`, `check:i18n-walk-parity` were re-run after the full build. - The 12 artifact-roster gates whose roster sits in a touched directory (`check-published-list-mirrors`, `check:meta-url-spelling`, `check:authz-resolver`, `check:changeset-fixed`, `check:engine-double-contract`, `check:error-code-casing`, `check:filter-alias-parity`, `check:i18n-stale-fill`, `check:published-readme-exports`, `check:route-ledger-census`, `check-dts-references`, `check:stack-collection-maps`): all exit 0. - `check:generated` exit 0; `check:liveness` exit 0 (translation state counts 38 → 41). </details> ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (`translateBook` joins the existing translators in `system/i18n-resolver.ts`, the table the readers already dispatch through.) - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` passes (eslint over the touched files, above). - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. - [x] 6. `packages/spec` touched: its generated artifacts are regenerated and committed. - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English (apart from the maintainer quick-read block, which the dispatch procedure writes in Chinese). - [x] The *Before Submitting* list in CONTRIBUTING.md is done. --- _Generated by [Claude Code](https://claude.ai/code/session_01WGufzf4dTFmjsttVz7Pm8j)_ 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01WGufzf4dTFmjsttVz7Pm8j --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent eab4bba commit 031d97a

35 files changed

Lines changed: 931 additions & 48 deletions

File tree

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/rest': minor
4+
'@objectstack/runtime': minor
5+
'@objectstack/cli': minor
6+
'@objectstack/lint': minor
7+
'@object-ui/app-shell': minor
8+
'@object-ui/i18n': minor
9+
---
10+
11+
feat(spec,rest,cli): a doc book's title and section titles are translatable — `books.<name>` in the translation bundle
12+
13+
Clause-②: yes (widening)
14+
Metadata-protocol: added
15+
16+
A package that ships its docs in two languages showed a half-translated manual: each doc's body followed the reader's locale (`doc.translations`, `<name>.<locale>.md`), but the book title and every sidebar section title stayed in the authoring language. The book's own inline `translations` map was retired in 17.0.0 because nothing read it, and the translation bundle had no key for a book.
17+
18+
- **`@objectstack/spec`:** `TranslationData` (and the registered `translation` item and the platform bundle, which share its groups) gains `books`, keyed by the book's `name`: `label`, `description`, and `groups.<key>.label` for each group by its stable `key` (strict: any other key is refused; `name` / `title` point at `label`, and `sections` / `chapters` / `toc` point at `groups`; the singular group spelling `book` points at `books`). A new translator, `translateBook`, overlays them and is registered in the metadata-document dispatch, so `GET /meta/book` (list and item) is served in the request's locale. A key the bundle does not carry keeps the authored string. The rejection for the retired inline `translations` map on a book now names the bundle keys to move the strings to.
19+
- **`@objectstack/rest`:** `GET /meta/book/:name/tree` translates the book it resolves through the same dispatch before rendering the spine, so the tree, the book list and the console's docs sidebar agree. `MetaBookTreeSources` gains `resolveI18nService()`, which both transports supply.
20+
- **`@objectstack/runtime`:** the dispatcher's book-tree route supplies the same i18n service, so it answers the localized tree `RestServer` answers.
21+
- **`@objectstack/cli`:** `os i18n extract` scaffolds `books.<name>.label` / `.description` and `books.<name>.groups.<key>.label` from the stack's `books[]`, and `os i18n check` / `os lint` coverage report an untranslated one as `i18n/missing-book`.
22+
- **`@objectstack/lint`:** `translation-target-unknown` resolves `books.<name>` against the declared books (a sibling package's included, and a package id for its implicit book) and `books.<name>.groups.<key>` against that book's group `key`s, so a mistyped book name or group key is an error instead of a translation nothing reads.
23+
- **`@object-ui/i18n`:** the console's language loader recognises `books` as a translation group, so a served document that carries only book translations is namespaced like any other.
24+
25+
Example (`zh-CN` bundle of an English-authored package):
26+
27+
```ts
28+
books: {
29+
crm_manual: {
30+
label: '使用手册',
31+
groups: { start: { label: '快速上手' }, pipeline: { label: '销售管道' } },
32+
},
33+
},
34+
```

‎content/docs/protocol/kernel/i18n-standard.mdx‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -406,8 +406,8 @@ Three rules the shape enforces, all of them closed since #4001:
406406

407407
- **The top level is the declared group set and nothing else** — `objects`,
408408
`picklists`, `apps`, `messages`, `globalActions`, `dashboards`, `datasets`,
409-
`reports`, `pages`, `flows`, `positions`, `permissions`, `metadataForms`,
410-
`settingsCommon`. A group invented beside them
409+
`reports`, `pages`, `flows`, `positions`, `permissions`, `books`,
410+
`metadataForms`, `settingsCommon`. A group invented beside them
411411
(`"account"`, `"fields"`, `"list"`, `"industries"`) is rejected **by name**
412412
at both authoring doors, with the group to use instead named in the
413413
rejection.
@@ -496,7 +496,7 @@ what the overlay writes back as the node's `title`.
496496
The contract is `t(key, locale, params?)` — the target locale is always passed
497497
explicitly, and the key itself is returned when no translation is found (there
498498
is no `defaultValue` option). The key is a **dot path into `TranslationData`**,
499-
so it starts at one of the fourteen declared groups.
499+
so it starts at one of the fifteen declared groups.
500500

501501
```typescript
502502
const i18n = kernel.getService('i18n');

‎content/docs/references/api/protocol.mdx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1783,6 +1783,7 @@ The published metadata item body, opaque by ruling (1C). Shape is the item's own
17831783
| **flows** | `Record<string, { label?: string; successMessage?: string; errorMessage?: string; screens?: Record<string, object>; … }>` | optional | Screen-flow translations keyed by flow name |
17841784
| **positions** | `Record<string, { label?: string; description?: string }>` | optional | Position translations keyed by position name |
17851785
| **permissions** | `Record<string, { label?: string; description?: string }>` | optional | Permission-set translations keyed by permission-set name |
1786+
| **books** | `Record<string, { label?: string; description?: string; groups?: Record<string, object> }>` | optional | Doc-book translations keyed by book name |
17861787
| **settingsCommon** | `{ sourceLabels?: object }` | optional | Cross-namespace Settings UI strings |
17871788
| **settings** | `Record<string, { title?: string; description?: string; groups?: Record<string, object>; keys?: Record<string, object>; … }>` | optional | Settings manifest translations keyed by namespace |
17881789

‎content/docs/references/system/book.mdx‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ const result = BookSchema.parse(data);
4747
| Property | Type | Required | Description |
4848
| :--- | :--- | :--- | :--- |
4949
| **name** | `string` | ✅ | Book name (namespace prefix recommended, like every metadata name) |
50-
| **label** | `string` | optional | Display title |
50+
| **label** | `string` | optional | Display title — translated by `books.<book>.label` in the translation bundle |
5151
| **description** | `string` | optional | |
5252
| **slug** | `string` | optional | Portal URL segment; defaults to name sans prefix |
5353
| **icon** | `string` | optional | |
@@ -67,8 +67,8 @@ const result = BookSchema.parse(data);
6767
| Property | Type | Required | Description |
6868
| :--- | :--- | :--- | :--- |
6969
| **key** | `string` | ✅ | Stable group key (used by overrides, deep links, explicit `doc.group`) |
70-
| **label** | `string` | ✅ | Section title — first-class, i18n-homed |
71-
| **translations** | `never` | optional | [REMOVED] Inline `translations` on a book (and on a book group) was removed in @objectstack/spec 17.0.0 (ADR-0049) — no resolver ever read it. The book tree endpoint and the docs portal render `label` / `description` verbatim in every locale, so a localized book shipped its authoring-locale strings to every reader. Delete the key. NOTE the near neighbour that DOES work: `doc.translations` is live and read on every doc render path — localize the docs themselves, and the portal picks the reader's locale up from there. This edit left `os migrate meta` with the protocol-16.x conversions: apply it by hand, or run the tool on a release that still carries the conversion — https://objectstack.ai/docs/protocol-upgrade. |
70+
| **label** | `string` | ✅ | Section title — first-class, i18n-homed: translated by `books.<book>.groups.<key>.label` in the translation bundle |
71+
| **translations** | `never` | optional | [REMOVED] Inline `translations` on a book (and on a book group) was removed in @objectstack/spec 17.0.0 (ADR-0049) — no resolver ever read it, so a localized book shipped its authoring-locale strings to every reader. Delete the key and move each locale's strings into the translation bundle: `books.<book>.label` / `.description` and `books.<book>.groups.<group_key>.label` (`os i18n extract` seeds them). The book list, the book tree and the docs sidebar resolve those per reader locale. A doc's own title and body still localize through `doc.translations`. This edit left `os migrate meta` with the protocol-16.x conversions: apply it by hand, or run the tool on a release that still carries the conversion — https://objectstack.ai/docs/protocol-upgrade. |
7272
| **order** | `number` | optional | Order of THIS group within the book |
7373
| **include** | `string \| { tag: string }` | optional | Rule that derives membership (glob or tag) |
7474
| **package** | `string` | optional | Scope the rule to a package id (default: the book package; cross-package via ADR-0048) |
@@ -115,8 +115,8 @@ Type: `'public'`
115115
| Property | Type | Required | Description |
116116
| :--- | :--- | :--- | :--- |
117117
| **key** | `string` | ✅ | Stable group key (used by overrides, deep links, explicit `doc.group`) |
118-
| **label** | `string` | ✅ | Section title — first-class, i18n-homed |
119-
| **translations** | `never` | optional | [REMOVED] Inline `translations` on a book (and on a book group) was removed in @objectstack/spec 17.0.0 (ADR-0049) — no resolver ever read it. The book tree endpoint and the docs portal render `label` / `description` verbatim in every locale, so a localized book shipped its authoring-locale strings to every reader. Delete the key. NOTE the near neighbour that DOES work: `doc.translations` is live and read on every doc render path — localize the docs themselves, and the portal picks the reader's locale up from there. This edit left `os migrate meta` with the protocol-16.x conversions: apply it by hand, or run the tool on a release that still carries the conversion — https://objectstack.ai/docs/protocol-upgrade. |
118+
| **label** | `string` | ✅ | Section title — first-class, i18n-homed: translated by `books.<book>.groups.<key>.label` in the translation bundle |
119+
| **translations** | `never` | optional | [REMOVED] Inline `translations` on a book (and on a book group) was removed in @objectstack/spec 17.0.0 (ADR-0049) — no resolver ever read it, so a localized book shipped its authoring-locale strings to every reader. Delete the key and move each locale's strings into the translation bundle: `books.<book>.label` / `.description` and `books.<book>.groups.<group_key>.label` (`os i18n extract` seeds them). The book list, the book tree and the docs sidebar resolve those per reader locale. A doc's own title and body still localize through `doc.translations`. This edit left `os migrate meta` with the protocol-16.x conversions: apply it by hand, or run the tool on a release that still carries the conversion — https://objectstack.ai/docs/protocol-upgrade. |
120120
| **order** | `number` | optional | Order of THIS group within the book |
121121
| **include** | `string \| { tag: string }` | optional | Rule that derives membership (glob or tag) |
122122
| **package** | `string` | optional | Scope the rule to a package id (default: the book package; cross-package via ADR-0048) |

‎content/docs/references/system/translation.mdx‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ Platform translation data — the per-app groups plus the platform-only `setting
185185
| **flows** | `Record<string, { label?: string; successMessage?: string; errorMessage?: string; screens?: Record<string, object>; … }>` | optional | Screen-flow translations keyed by flow name |
186186
| **positions** | `Record<string, { label?: string; description?: string }>` | optional | Position translations keyed by position name |
187187
| **permissions** | `Record<string, { label?: string; description?: string }>` | optional | Permission-set translations keyed by permission-set name |
188+
| **books** | `Record<string, { label?: string; description?: string; groups?: Record<string, object> }>` | optional | Doc-book translations keyed by book name |
188189
| **metadataForms** | `Record<string, { label?: string; description?: string; sections?: Record<string, object>; fields?: Record<string, object> }>` | optional | Translations for metadata-type configuration forms keyed by metadata type |
189190
| **settingsCommon** | `{ sourceLabels?: object }` | optional | Cross-namespace Settings UI strings |
190191
| **settings** | `Record<string, { title?: string; description?: string; groups?: Record<string, object>; keys?: Record<string, object>; … }>` | optional | Settings manifest translations keyed by namespace |
@@ -295,6 +296,14 @@ Translation data for a single object
295296
| **label** | `string` | optional | Translated permission-set label — overlays the set's own `label` |
296297
| **description** | `string` | optional | Translated permission-set description — overlays the set's own `description` |
297298

299+
### Nested Shape: `PlatformTranslationData.books[string]`
300+
301+
| Property | Type | Required | Description |
302+
| :--- | :--- | :--- | :--- |
303+
| **label** | `string` | optional | Translated book title — overlays the book's own `label` |
304+
| **description** | `string` | optional | Translated book description — overlays the book's own `description` |
305+
| **groups** | `Record<string, { label: string }>` | optional | Group (section) translations keyed by the group `key` |
306+
298307
### Nested Shape: `PlatformTranslationData.metadataForms[string]`
299308

300309
| Property | Type | Required | Description |
@@ -344,6 +353,7 @@ Served translation data — the platform groups the browser reads, without the s
344353
| **flows** | `Record<string, { label?: string; successMessage?: string; errorMessage?: string; screens?: Record<string, object>; … }>` | optional | Screen-flow translations keyed by flow name |
345354
| **positions** | `Record<string, { label?: string; description?: string }>` | optional | Position translations keyed by position name |
346355
| **permissions** | `Record<string, { label?: string; description?: string }>` | optional | Permission-set translations keyed by permission-set name |
356+
| **books** | `Record<string, { label?: string; description?: string; groups?: Record<string, object> }>` | optional | Doc-book translations keyed by book name |
347357
| **settingsCommon** | `{ sourceLabels?: object }` | optional | Cross-namespace Settings UI strings |
348358
| **settings** | `Record<string, { title?: string; description?: string; groups?: Record<string, object>; keys?: Record<string, object>; … }>` | optional | Settings manifest translations keyed by namespace |
349359

@@ -453,6 +463,14 @@ Translation data for a single object
453463
| **label** | `string` | optional | Translated permission-set label — overlays the set's own `label` |
454464
| **description** | `string` | optional | Translated permission-set description — overlays the set's own `description` |
455465

466+
### Nested Shape: `ServedTranslationData.books[string]`
467+
468+
| Property | Type | Required | Description |
469+
| :--- | :--- | :--- | :--- |
470+
| **label** | `string` | optional | Translated book title — overlays the book's own `label` |
471+
| **description** | `string` | optional | Translated book description — overlays the book's own `description` |
472+
| **groups** | `Record<string, { label: string }>` | optional | Group (section) translations keyed by the group `key` |
473+
456474
### Nested Shape: `ServedTranslationData.settingsCommon`
457475

458476
| Property | Type | Required | Description |
@@ -564,6 +582,7 @@ Per-app translation data for objects, apps, and UI messages
564582
| **flows** | `Record<string, { label?: string; successMessage?: string; errorMessage?: string; screens?: Record<string, object>; … }>` | optional | Screen-flow translations keyed by flow name |
565583
| **positions** | `Record<string, { label?: string; description?: string }>` | optional | Position translations keyed by position name |
566584
| **permissions** | `Record<string, { label?: string; description?: string }>` | optional | Permission-set translations keyed by permission-set name |
585+
| **books** | `Record<string, { label?: string; description?: string; groups?: Record<string, object> }>` | optional | Doc-book translations keyed by book name |
567586
| **metadataForms** | `Record<string, { label?: string; description?: string; sections?: Record<string, object>; fields?: Record<string, object> }>` | optional | Translations for metadata-type configuration forms keyed by metadata type |
568587
| **settingsCommon** | `{ sourceLabels?: object }` | optional | Cross-namespace Settings UI strings |
569588

@@ -673,6 +692,14 @@ Translation data for a single object
673692
| **label** | `string` | optional | Translated permission-set label — overlays the set's own `label` |
674693
| **description** | `string` | optional | Translated permission-set description — overlays the set's own `description` |
675694

695+
### Nested Shape: `TranslationData.books[string]`
696+
697+
| Property | Type | Required | Description |
698+
| :--- | :--- | :--- | :--- |
699+
| **label** | `string` | optional | Translated book title — overlays the book's own `label` |
700+
| **description** | `string` | optional | Translated book description — overlays the book's own `description` |
701+
| **groups** | `Record<string, { label: string }>` | optional | Group (section) translations keyed by the group `key` |
702+
676703
### Nested Shape: `TranslationData.metadataForms[string]`
677704

678705
| Property | Type | Required | Description |
@@ -743,6 +770,7 @@ One locale of translations — the `translation` metadata type
743770
| **flows** | `Record<string, { label?: string; successMessage?: string; errorMessage?: string; screens?: Record<string, object>; … }>` | optional | Screen-flow translations keyed by flow name |
744771
| **positions** | `Record<string, { label?: string; description?: string }>` | optional | Position translations keyed by position name |
745772
| **permissions** | `Record<string, { label?: string; description?: string }>` | optional | Permission-set translations keyed by permission-set name |
773+
| **books** | `Record<string, { label?: string; description?: string; groups?: Record<string, object> }>` | optional | Doc-book translations keyed by book name |
746774
| **metadataForms** | `Record<string, { label?: string; description?: string; sections?: Record<string, object>; fields?: Record<string, object> }>` | optional | Translations for metadata-type configuration forms keyed by metadata type |
747775
| **settingsCommon** | `{ sourceLabels?: object }` | optional | Cross-namespace Settings UI strings |
748776
| **locale** | `string` | ✅ | BCP-47 locale this item translates (e.g. "zh-CN") |
@@ -862,6 +890,14 @@ Translation data for a single object
862890
| **label** | `string` | optional | Translated permission-set label — overlays the set's own `label` |
863891
| **description** | `string` | optional | Translated permission-set description — overlays the set's own `description` |
864892

893+
### Nested Shape: `TranslationItem.books[string]`
894+
895+
| Property | Type | Required | Description |
896+
| :--- | :--- | :--- | :--- |
897+
| **label** | `string` | optional | Translated book title — overlays the book's own `label` |
898+
| **description** | `string` | optional | Translated book description — overlays the book's own `description` |
899+
| **groups** | `Record<string, { label: string }>` | optional | Group (section) translations keyed by the group `key` |
900+
865901
### Nested Shape: `TranslationItem.metadataForms[string]`
866902

867903
| Property | Type | Required | Description |

‎content/docs/ui/doc-pages.mdx‎

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -302,11 +302,34 @@ expands to *the rest* — every doc the group's rule would have claimed but that
302302
names, in `order`-then-label sequence. An entry can also be an object to attach a `label`
303303
override, a `badge` or an `icon`, or to point at an external `href` instead of a doc.
304304

305+
#### Translating the spine
306+
307+
A book's title, its description and each group's section title translate through the
308+
translation bundle, like an app's label and its navigation labels — keyed by the book's
309+
`name` and each group's `key`:
310+
311+
```typescript
312+
translations: [{
313+
'zh-CN': {
314+
books: {
315+
crm_docs: {
316+
label: 'CRM 文档',
317+
groups: { overview: { label: '概览' }, guides: { label: '使用指南' }, admin: { label: '管理' } },
318+
},
319+
},
320+
},
321+
}],
322+
```
323+
324+
`GET /meta/book`, the book tree and the docs sidebar serve the reader's locale; a key the
325+
bundle does not carry keeps the authored string. `os i18n extract` seeds the keys, and
326+
`os i18n check` reports a missing one as `i18n/missing-book`. The docs themselves localize
327+
through `doc.translations` and `<name>.<locale>.md` variants.
328+
305329
<Callout type="warn">
306330
Inline `translations` on a **book** or a **book group** is rejected: no resolver ever read
307-
it, so a localized spine shipped its authoring-locale strings to every reader. The near
308-
neighbour that *does* work is `doc.translations`, read on every doc render path — localize
309-
the docs themselves.
331+
it, so a localized spine shipped its authoring-locale strings to every reader. Move those
332+
strings into the bundle's `books` group above.
310333
</Callout>
311334

312335
### How `doc` and `book` compose

0 commit comments

Comments
 (0)