You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 031d97a
Browse filesBrowse the repository at this point in the historyBrowse 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>
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):
Copy file name to clipboardExpand all lines: content/docs/references/system/book.mdx
+5-5Lines changed: 5 additions & 5 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -47,7 +47,7 @@ const result = BookSchema.parse(data);
47
47
| Property | Type | Required | Description |
48
48
| :--- | :--- | :--- | :--- |
49
49
|**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 |
51
51
|**description**|`string`| optional ||
52
52
|**slug**|`string`| optional | Portal URL segment; defaults to name sans prefix |
53
53
|**icon**|`string`| optional ||
@@ -67,8 +67,8 @@ const result = BookSchema.parse(data);
67
67
| Property | Type | Required | Description |
68
68
| :--- | :--- | :--- | :--- |
69
69
|**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.|
72
72
|**order**|`number`| optional | Order of THIS group within the book |
73
73
|**include**|`string \| { tag: string }`| optional | Rule that derives membership (glob or tag) |
74
74
|**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'`
115
115
| Property | Type | Required | Description |
116
116
| :--- | :--- | :--- | :--- |
117
117
|**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.|
120
120
|**order**|`number`| optional | Order of THIS group within the book |
121
121
|**include**|`string \| { tag: string }`| optional | Rule that derives membership (glob or tag) |
122
122
|**package**|`string`| optional | Scope the rule to a package id (default: the book package; cross-package via ADR-0048) |
0 commit comments