Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions .changeset/26690-translation-books.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
'@objectstack/spec': minor
'@objectstack/rest': minor
'@objectstack/runtime': minor
'@objectstack/cli': minor
'@objectstack/lint': minor
'@object-ui/app-shell': minor
'@object-ui/i18n': minor
---

feat(spec,rest,cli): a doc book's title and section titles are translatable — `books.<name>` in the translation bundle

Clause-②: yes (widening)
Metadata-protocol: added

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.

- **`@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.
- **`@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.
- **`@objectstack/runtime`:** the dispatcher's book-tree route supplies the same i18n service, so it answers the localized tree `RestServer` answers.
- **`@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`.
- **`@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.
- **`@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.

Example (`zh-CN` bundle of an English-authored package):

```ts
books: {
crm_manual: {
label: '使用手册',
groups: { start: { label: '快速上手' }, pipeline: { label: '销售管道' } },
},
},
```
6 changes: 3 additions & 3 deletions content/docs/protocol/kernel/i18n-standard.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -406,8 +406,8 @@ Three rules the shape enforces, all of them closed since #4001:

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

```typescript
const i18n = kernel.getService('i18n');
Expand Down
1 change: 1 addition & 0 deletions content/docs/references/api/protocol.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1783,6 +1783,7 @@ The published metadata item body, opaque by ruling (1C). Shape is the item's own
| **flows** | `Record<string, { label?: string; successMessage?: string; errorMessage?: string; screens?: Record<string, object>; … }>` | optional | Screen-flow translations keyed by flow name |
| **positions** | `Record<string, { label?: string; description?: string }>` | optional | Position translations keyed by position name |
| **permissions** | `Record<string, { label?: string; description?: string }>` | optional | Permission-set translations keyed by permission-set name |
| **books** | `Record<string, { label?: string; description?: string; groups?: Record<string, object> }>` | optional | Doc-book translations keyed by book name |
| **settingsCommon** | `{ sourceLabels?: object }` | optional | Cross-namespace Settings UI strings |
| **settings** | `Record<string, { title?: string; description?: string; groups?: Record<string, object>; keys?: Record<string, object>; … }>` | optional | Settings manifest translations keyed by namespace |

Expand Down
10 changes: 5 additions & 5 deletions content/docs/references/system/book.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ const result = BookSchema.parse(data);
| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **name** | `string` | ✅ | Book name (namespace prefix recommended, like every metadata name) |
| **label** | `string` | optional | Display title |
| **label** | `string` | optional | Display title — translated by `books.<book>.label` in the translation bundle |
| **description** | `string` | optional | |
| **slug** | `string` | optional | Portal URL segment; defaults to name sans prefix |
| **icon** | `string` | optional | |
Expand All @@ -67,8 +67,8 @@ const result = BookSchema.parse(data);
| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **key** | `string` | ✅ | Stable group key (used by overrides, deep links, explicit `doc.group`) |
| **label** | `string` | ✅ | Section title — first-class, i18n-homed |
| **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. |
| **label** | `string` | ✅ | Section title — first-class, i18n-homed: translated by `books.<book>.groups.<key>.label` in the translation bundle |
| **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. |
| **order** | `number` | optional | Order of THIS group within the book |
| **include** | `string \| { tag: string }` | optional | Rule that derives membership (glob or tag) |
| **package** | `string` | optional | Scope the rule to a package id (default: the book package; cross-package via ADR-0048) |
Expand Down Expand Up @@ -115,8 +115,8 @@ Type: `'public'`
| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **key** | `string` | ✅ | Stable group key (used by overrides, deep links, explicit `doc.group`) |
| **label** | `string` | ✅ | Section title — first-class, i18n-homed |
| **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. |
| **label** | `string` | ✅ | Section title — first-class, i18n-homed: translated by `books.<book>.groups.<key>.label` in the translation bundle |
| **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. |
| **order** | `number` | optional | Order of THIS group within the book |
| **include** | `string \| { tag: string }` | optional | Rule that derives membership (glob or tag) |
| **package** | `string` | optional | Scope the rule to a package id (default: the book package; cross-package via ADR-0048) |
Expand Down
36 changes: 36 additions & 0 deletions content/docs/references/system/translation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ Platform translation data — the per-app groups plus the platform-only `setting
| **flows** | `Record<string, { label?: string; successMessage?: string; errorMessage?: string; screens?: Record<string, object>; … }>` | optional | Screen-flow translations keyed by flow name |
| **positions** | `Record<string, { label?: string; description?: string }>` | optional | Position translations keyed by position name |
| **permissions** | `Record<string, { label?: string; description?: string }>` | optional | Permission-set translations keyed by permission-set name |
| **books** | `Record<string, { label?: string; description?: string; groups?: Record<string, object> }>` | optional | Doc-book translations keyed by book name |
| **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 |
| **settingsCommon** | `{ sourceLabels?: object }` | optional | Cross-namespace Settings UI strings |
| **settings** | `Record<string, { title?: string; description?: string; groups?: Record<string, object>; keys?: Record<string, object>; … }>` | optional | Settings manifest translations keyed by namespace |
Expand Down Expand Up @@ -295,6 +296,14 @@ Translation data for a single object
| **label** | `string` | optional | Translated permission-set label — overlays the set's own `label` |
| **description** | `string` | optional | Translated permission-set description — overlays the set's own `description` |

### Nested Shape: `PlatformTranslationData.books[string]`

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **label** | `string` | optional | Translated book title — overlays the book's own `label` |
| **description** | `string` | optional | Translated book description — overlays the book's own `description` |
| **groups** | `Record<string, { label: string }>` | optional | Group (section) translations keyed by the group `key` |

### Nested Shape: `PlatformTranslationData.metadataForms[string]`

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

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

### Nested Shape: `ServedTranslationData.books[string]`

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **label** | `string` | optional | Translated book title — overlays the book's own `label` |
| **description** | `string` | optional | Translated book description — overlays the book's own `description` |
| **groups** | `Record<string, { label: string }>` | optional | Group (section) translations keyed by the group `key` |

### Nested Shape: `ServedTranslationData.settingsCommon`

| Property | Type | Required | Description |
Expand Down Expand Up @@ -564,6 +582,7 @@ Per-app translation data for objects, apps, and UI messages
| **flows** | `Record<string, { label?: string; successMessage?: string; errorMessage?: string; screens?: Record<string, object>; … }>` | optional | Screen-flow translations keyed by flow name |
| **positions** | `Record<string, { label?: string; description?: string }>` | optional | Position translations keyed by position name |
| **permissions** | `Record<string, { label?: string; description?: string }>` | optional | Permission-set translations keyed by permission-set name |
| **books** | `Record<string, { label?: string; description?: string; groups?: Record<string, object> }>` | optional | Doc-book translations keyed by book name |
| **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 |
| **settingsCommon** | `{ sourceLabels?: object }` | optional | Cross-namespace Settings UI strings |

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

### Nested Shape: `TranslationData.books[string]`

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **label** | `string` | optional | Translated book title — overlays the book's own `label` |
| **description** | `string` | optional | Translated book description — overlays the book's own `description` |
| **groups** | `Record<string, { label: string }>` | optional | Group (section) translations keyed by the group `key` |

### Nested Shape: `TranslationData.metadataForms[string]`

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

### Nested Shape: `TranslationItem.books[string]`

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **label** | `string` | optional | Translated book title — overlays the book's own `label` |
| **description** | `string` | optional | Translated book description — overlays the book's own `description` |
| **groups** | `Record<string, { label: string }>` | optional | Group (section) translations keyed by the group `key` |

### Nested Shape: `TranslationItem.metadataForms[string]`

| Property | Type | Required | Description |
Expand Down
29 changes: 26 additions & 3 deletions content/docs/ui/doc-pages.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -302,11 +302,34 @@ expands to *the rest* — every doc the group's rule would have claimed but that
names, in `order`-then-label sequence. An entry can also be an object to attach a `label`
override, a `badge` or an `icon`, or to point at an external `href` instead of a doc.

#### Translating the spine

A book's title, its description and each group's section title translate through the
translation bundle, like an app's label and its navigation labels — keyed by the book's
`name` and each group's `key`:

```typescript
translations: [{
'zh-CN': {
books: {
crm_docs: {
label: 'CRM 文档',
groups: { overview: { label: '概览' }, guides: { label: '使用指南' }, admin: { label: '管理' } },
},
},
},
}],
```

`GET /meta/book`, the book tree and the docs sidebar serve the reader's locale; a key the
bundle does not carry keeps the authored string. `os i18n extract` seeds the keys, and
`os i18n check` reports a missing one as `i18n/missing-book`. The docs themselves localize
through `doc.translations` and `<name>.<locale>.md` variants.

<Callout type="warn">
Inline `translations` on a **book** or a **book group** is rejected: no resolver ever read
it, so a localized spine shipped its authoring-locale strings to every reader. The near
neighbour that *does* work is `doc.translations`, read on every doc render path — localize
the docs themselves.
it, so a localized spine shipped its authoring-locale strings to every reader. Move those
strings into the bundle's `books` group above.
</Callout>

### How `doc` and `book` compose
Expand Down
Loading
Loading