Skip to content

Commit d0f1845

Browse files
os-warrenclaude
andauthored
feat(spec)!: split the translation bundle type — settings is a platform group, not a per-app one (#15178) (#19600)
Fixes #15178 Clause-②: yes ## What is ruled, and what landed Maintainer ruling batch #132 item 2 letter ② (comment 5653315643, 「同意」 2026-09-13), quoted verbatim: > 1. `packages/spec` `TranslationDataSchema` becomes two exports (names per the file's convention): the platform bundle schema (eleven groups, `settings` included) and the per-app bundle schema (`settings` absent, strict — an authored `settings` in a per-app bundle is refused with a remedy saying it is platform-only). Every reader that consumes a per-app bundle types against the per-app schema. > 2. The card's original "removal" disposition is struck: `settings` is a live platform key (`pickSettingsEntry`, `i18n-resolver.ts:2305`; console `useSettingsLabel`). > 3. `check:i18n-walk-parity`: the `settings` exemption disappears with the per-app key; `LEDGER_CEILING` 3 → 2 in the same PR (the ledger is governed — declared in the claim). > 4. Accept-set narrowing on the per-app bundle: `Clause-②: no`; ADR-0087 semantic entry — a per-app bundle carrying `settings` was inert, so the conversion drops the group and records a note; no deprecation window (「创业阶段不渐进」), launch-window convention applies. The card's own closing line (「⛔ Not a queue card: zero measured pull today」) is stale and the ruling overrides it. The removal option is struck; this is the SPLIT. **Which name took which face, and why.** `TranslationDataSchema` keeps its name and becomes the **per-app** bundle entry (ten groups); the new `PlatformTranslationDataSchema` / `PlatformTranslationBundleSchema` (types `PlatformTranslationData` / `PlatformTranslationBundle`) carry the eleven-group platform face. The ruling's "names per the file's convention" is satisfied by the file's existing habit — a qualified prefix marks the other face, as `ObjectTranslationDataSchema` already does — and the direction was chosen on a measurement, not on taste: - The card body itself names the narrowing target "the per-app `TranslationDataSchema`", and the gate's ledger reason says "removal from the per-app schema" about that same export. - Every EXISTING per-app author already types against `TranslationData`: `stack.translations`, `defineTranslationBundle`, the three examples, the CLI walker and coverage reader, and the header `os i18n extract` emits into every scaffolded bundle. Putting the narrowing on a NEW name would have left all of them accepting `settings`, and re-pointing them would have re-typed the nine platform `*.generated.ts` files the same emitter writes. - Only the genuinely-platform readers had to move, and only one of them authors `settings` at all. So "every reader that consumes a per-app bundle types against the per-app schema" holds by construction here, and the movement fell on the platform side. ## The ruling's "was inert" is falsified — the record says what was measured instead Ruling item 4 describes a per-app `settings` as inert. Measured on `origin/main` at `1ff3a8f210`, it was **live**: - `AppPlugin.loadTranslations` (`packages/runtime/src/app-plugin.ts`) hands each `stack.translations` bundle entry WHOLE to `II18nService.loadTranslations`. - `FileI18nAdapter.loadTranslations` deep-merges it into the one per-locale tree; `getTranslations(locale)` serves that tree. - Every platform plugin contributes into the SAME tree at `kernel:ready` — `SettingsServicePlugin` does exactly this with `settingsBuiltinTranslations`. - `pickSettingsEntry` reads `pickData(bundle, locale)?.settings`, and the console's `useSettingsLabel` scans every namespace carrying a `settings` branch. The tracked liveness ledger `packages/spec/liveness/translation.json` records that reader with its evidence pointer and says both doors "merge into ONE tree". So an app-authored `settings` did not sit unread — but nor did it override the platform. The app's bundles load in `AppPlugin`'s `start()` (kernel Phase 2) and the platform's at `kernel:ready` (Phase 3), and `deepMerge` gives the later source the leaf, so the platform won every key both defined. What an application actually had was a GAP FILLER on a namespace it does not own: it rendered only where the platform bundle carried no string for that key and locale. That makes the ruling's DIRECTION stronger, not weaker, and it changes only what the record must say: the drop is a visible change only on the screens where the entry was FILLING A GAP, and those fall back to the manifest's own English literal; where the platform already carried the string, nothing changes. The ADR-0087 semantic entry and the changeset both say so in those words rather than reciting the house "pure lossless delete" phrase. ## Diff **Spec (`packages/spec`)** - `src/system/translation.zod.ts` — `translationDataShape()` becomes `appTranslationDataShape()` (ten groups) and the `settings` group moves to `platformSettingsShape()`. `TranslationDataSchema` = per-app, strict, with a `guidance` prescription for both `settings` and the singular `setting`; the `setting` alias is deleted, because an alias prescribing a key the shape now rejects is a suggestion the author cannot take. `PlatformTranslationDataSchema` = the ten plus `settings`. `TranslationBundleSchema` is per-app; `PlatformTranslationBundleSchema` is new. `TranslationItemSchema` is UNCHANGED and still declares `settings`. - `src/api/protocol.zod.ts` — `GetTranslationsResponseSchema.translations` moves to the platform face. The served document is the merge of every loaded bundle, so it carries `settings`; leaving it on the per-app face would have published a declaration the server contradicts. - `src/system/i18n-resolver.ts` — `pickData` becomes generic over the entry type, and the settings resolvers take `PlatformTranslationBundle`. This WIDENS their parameter (every group is optional, so a per-app bundle is still assignable), so no caller — this repo's or the pinned sibling's — loses a call. - `src/conversions/registry.ts` — new D2 `translation-per-app-settings-removed` (`toMajor: 18`, `retiredFromLoadPath: true`), strips the group from per-app bundle ENTRIES only. An entry carrying `locale` is a `translation` ITEM and is left whole; the candidate value must additionally be a dict whose every key is a declared group, so an `objects` record holding an object literally named `settings` is not mistaken for a bundle. - `src/migrations/entries/semantic/18.translation-per-app-settings-platform-only.ts` — the ADR-0087 semantic entry, plus the regenerated `registry.ts` and the extended step-18 rationale. Regenerated with `gen:migration-registry`, never hand-merged. - `src/type-alias-convention.pin.test.ts` — the two new aliases are pinned isomorphic (both faces are all-optional, no default or transform anywhere), and the pin count moves 784 → 786 with its receipt. - `authorable-surface/system.json` — `system/TranslationData:settings` deleted DELIBERATELY, the tripwire the strict-delete route owes. The build's own deletion gate then adjudicated it and printed its proof (#4650 proof 2): "def not reachable from the 30 metadata-type roots ... an over-collected entry, never parsed against a metadata document." Eleven `system/PlatformTranslationData:*` keys arrived in the same run. - Regenerated: `api-surface/`, `export-origins/`, `declaration-map/`, `json-schema.manifest/`, `content/docs/references/**`, the strictness-ledger counts. **The gate (ruling item 3)** — `scripts/check-i18n-walk-parity.mjs`: the `settings` ledger row is gone, `LEDGER_CEILING` 3 → 2, the class-level note and the recorded self-test samples move with it. **This is the shrinking direction of a governed, shrink-only ledger**, declared in the claim and declared here. The gate's own rule at the ratchet says growth is the reviewed act; slack fails too, which is why the ceiling had to move in the same diff. **Platform readers** — `packages/services/service-settings/src/translations/{en,es-ES,ja-JP,zh-CN}.ts` and their `index.ts` move to `PlatformTranslationData` / `PlatformTranslationBundle`. They are the only bundles in the repo that author `settings`. **Published prose** — `content/docs/protocol/kernel/i18n-standard.mdx` published the eleven-group list as "rejected by name at both authoring doors", and `skills/objectstack-i18n/SKILL.md` published the same list to customer projects. A **third** published carrier, `content/docs/ui/translations.mdx`, taught `settings` as app-translatable in its own table. **All three are corrected** — an earlier draft of this sentence said "both", before the guide correction landed. `docs/qa/platform-checklist/areas/i18n.json` had a symbol anchor on the renamed shape function and a clause naming the wrong face. ## Verification ⚠️ **Provenance corrected — this table was NOT read at the final commit.** It was read at `5283099838`, which is the **6th of this branch's 10 commits**; four have landed since (`b33ea66cb3`, `d5e6b43977`, `b1e7984040`, `8dcd6a42ae`). An earlier draft of this line called it "the final commit on this branch", and the whole Verification table, the Tests section and the Reverse verification paragraph hang off it — so as written the body claimed readings that covered the derivation change and the guide correction. They did not. ⛔ Nothing below is therefore unmeasured at head; it is re-measured elsewhere, not here. What covers the later commits is the at-tier contract review of head `8dcd6a42ae` (record on card #15178), which re-derived the carrier sweep over all 9156 tracked files, rendered the migration TODO live, ran the derivation ablation in memory, and read CI by job conclusion. ✅ One row IS unaffected and re-measured at head: the **skills** table — `skills/objectstack-i18n/SKILL.md` was last touched at `2602ccec10`, earlier than `5283099838`, and every figure in it reproduces at head (494→496 lines, 4713→4752 tokens, ceiling 6338, headroom 1586). | Instrument | Reading | Which side it can fail on | | --- | --- | --- | | `check:i18n-walk-parity` | `10 declared group(s), 8 walked, 2 exempted` | The `10` is the reading that discriminates: the per-app face has ten groups, the platform face eleven. It fails if a declared group has no emitter and no ledger row, if a ledger row is stale, and — the ratchet — if the ceiling has slack. Its `--self-test` battery is 43 cases. | | `check:authorable-surface` (inside `gen:schema`) | deletion allowed with a printed proof; 11 keys added | It refuses ANY authorable key that vanishes without one of four proofs, and it refused this diff on the first attempt — that refusal is the control. | | `check:generated` | 15 of 15 artifacts current after `--fix` regenerated the 5 it proved stale, each re-checked | It can fail on a stale artifact in either direction. | | `check:adr-0087-registration` | `1 declared-breaking changeset(s), each carrying an ADR-0087 disposition` | It can fail on a breaking changeset with no marker; it reported `0 non-breaking changeset(s) seen` before the changeset was committed, which is the control leg. | | `check-changeset-no-major` | `no major bump` | ⚠️ Its clause-② axis printed `LEVEL AXIS: NOT APPLICABLE` — there is no PR payload on a local run, so it can fail HERE only on the `major` axis, not on the declaration. | | `check:spec-parsed-alias` | `1455 bare aliases, 786 pinned isomorphic, 669 paired` | It failed first with both new aliases named — that red is the control. | | `check:type-check-debt` | `4 ledger entr(ies) re-measured, 53 raw tsc errors, none above its recorded number` | Re-run after a rebuild; an earlier run exited 3 (PREREQUISITE NOT MET) on a dist older than its sources, which is NOT a reading. | | `pnpm lint` (`eslint . --no-inline-config`) | exit 0, whole repo, no narrowing | Ran over the repo's own configured universe, so no narrowing claim is needed. | **Gate families**: `scripts/pm/dispatch-gates.mjs` derived 139 for this change set; `--ran` with exit codes recorded reconciles **139 accounted, 138 run, 0 UNRUN, 1 NOT MEASURED**. The one is `check:dual-build-cjs-loads`, which exits 3 (PREREQUISITE NOT MET) without a full workspace build — recorded as NOT MEASURED and left to CI, which builds everything. The reconciliation's own caveat stands: it answers what this card DERIVES against what was RUN, and the artifact-roster families, the wide-population families and the path-scheduled CI jobs are outside that total. **Tests** (`turbo run test`, `--concurrency=2`): `@objectstack/spec` 508 files / 14,902 tests, `@objectstack/lint` 106 files, `@objectstack/service-settings` 33 files, `@objectstack/platform-objects` 51 files, the three examples 41 files, `@objectstack/cli` unit tier 222 files / 3,141 tests — all pass. `turbo run typecheck` over spec, cli, lint, platform-objects, service-settings, service-i18n, runtime and rest: 64 tasks, all pass. **Reverse verification (one-shot, not left in the tree).** With the fix committed, `settings` was put back on the per-app shape through `scripts/ablation-replace.mjs` — the mutation is proved on disk (anchor 1 → 0, blob `3a27c26f6a5c` → `a0b6be68af3e`) — and the two refusal pins went RED by name. Restored with `--restore`: blob back to `3a27c26f6a5c`, equal to HEAD, and `git diff HEAD` empty. The predicted direction was "turns red", and that is what was observed. ## Skills bundle readings (`skills/**` is a governed surface) Required because the diff touches a published skill. This is a CORRECTION, not an expansion — the added sentence exists because the old one became false. | Reading | Before | After | Delta | | --- | --- | --- | --- | | `skills/objectstack-i18n/SKILL.md`, lines | 494 | 496 | +2 | | `skills/objectstack-i18n/SKILL.md`, tokens | 4713 | 4752 | +39 (ceiling 6338, headroom 1586) | | Whole bundle, all `SKILL.md` lines | 6145 | 6147 | +2 | | Whole bundle, tokens (shipped tree) | 140374 | 140413 | +39 | Before-tokens were measured by restoring the base file, reading `check-skills-token-ratchet`, and restoring with proof (blob equal to HEAD, `git diff HEAD` empty). `check-skills-token-ratchet` passes: 34 authored files within their ceilings. ⚠️ **Landing tier.** `skills/**` is Tier H on the governed register, so this PR's landing is Tier H on one path hit. It is left as a draft awaiting that record. If the seat would rather land the rest through the queue, the remedy the directive names is to split `skills/objectstack-i18n/SKILL.md` off into its own PR — but ⛔ not to ship the corrected schema while the published skill still teaches the key the parse now refuses. ## Declared file-surface deviations The claim declared the surface as `translation.zod.ts` + siblings, the walk-parity gate + fixtures, `i18n-extract.ts` + tests, `migrations/entries/semantic/` + `registry.ts`, and `.changeset/`. Five paths outside it were edited, each forced by the ruling rather than chosen, and none widened silently: 1. `packages/spec/src/api/protocol.zod.ts` — the served response must type against the platform face or it declares a shape the server contradicts. ⚠️ **This path is held by open PR #19493's sibling declaration set — specifically it was declared disjoint against #19543, which enumerates it.** One line changes (the import) plus one line in the response schema, plus a docblock. A textual conflict is possible; this PR is not asking to land first. 2. `packages/spec/src/system/i18n-resolver.ts` — `pickSettingsEntry` reads `.settings`; without this the package does not typecheck. The parameter is widened, not narrowed. 3. `packages/services/service-settings/src/translations/*` (5 files) — the only bundles that author `settings`; type annotations only. 4. `packages/spec/src/conversions/registry.ts` — the D2 conversion ruling item 4 asks for ("the conversion drops the group and records a note"). The semantic entry alone records the judgment but rewrites nothing. 5. `content/docs/protocol/kernel/i18n-standard.mdx`, `skills/objectstack-i18n/SKILL.md`, **`content/docs/ui/translations.mdx`**, `docs/qa/platform-checklist/areas/i18n.json` — published claims this change makes false, plus one symbol anchor the rename broke (`check:platform-checklist` went red on it and is green again). ⚠️ **`content/docs/ui/translations.mdx` was added to this enumeration after the fact:** it was edited in commit `b1e7984040` and an earlier draft of this item listed only three paths, under-declaring the deviation by one. `packages/spec/src/migrations/registry.ts` is the declared overlap with open PR #19493. Its **generated regions** were regenerated with `scripts/pm/os-regen-merge.sh`'s generator (`gen:migration-registry`) and never hand-merged. ⚠️ **One line in that file IS hand-written, and a reviewer of a generated file should be told:** `registry.ts:47` carries a value import of `TranslationDataSchema`, **outside every `<os-generated …>` region** (the first region opens at `:1142`). It is necessary, not an oversight — `build-migration-registry.ts`'s `parseEntry` deliberately drops imports and carries only the initializer, so an entry that derives its text from a value needs that value in scope in the registry itself. The comment immediately above the import says so. It survives regeneration because `renderRegistry` splices only between the region markers, and `check:generated` is the instrument that would fail if that round-trip were unstable. `packages/cli/src/utils/i18n-extract.ts` was NOT edited — the walker does not move, because `settings` never had an emitter. ## Acceptance notes - **The `translation` metadata-type door is untouched and still accepts `settings`.** The ruling names the per-app BUNDLE, and `packages/spec/liveness/translation.json` is that item's ledger, so narrowing the item would have moved a ledger outside this card's surface. It leaves a question worth a decision rather than a silent choice: an app admin authoring a `translation` item through Studio can still write `settings`, and `authored-translation-sync` merges the raw stored payload into the same served tree, so the refusal this PR adds at the file door does not reach the metadata door. Raised in the report, not decided here. - Platform bundles that author only shared groups (`platform-objects`, the five plugins, the other services) keep the narrower `TranslationData` / `TranslationBundle` types. They are assignable, and re-typing ~20 files that never carry `settings` would be churn with no contract effect. noted, not filed. - `packages/lint/src/validate-translation-references.ts` still lists `settings` among the groups it deliberately does not judge. The branch is now unreachable for a per-app stack rather than wrong. noted, not filed. ## 维护者速读(草稿) - **改了什么。** 翻译包的类型一分为二:`TranslationDataSchema` 从此只表示「应用自己写的那一份」,十个分组;新的 `PlatformTranslationDataSchema` 是平台那一份,十一个,`settings` 留在它那里。应用再写 `settings` 会被按名字拒绝,并告诉作者这是平台专属。 - **为什么改。** 一个类型同时代表两种包,是这张卡上每一次误读的源头:当初的普查拿「按应用问」的问题去问一个分不出应用和平台的类型,得到零,就差点把平台每天在读的键删掉。更要紧的是实测结果:应用写的 `settings` 并不是没人读 —— 它和平台那一份合进同一棵已服务的树。但它**不是覆盖者**:应用包在 kernel 第 2 阶段加载、平台包在第 3 阶段,合并时叶子归**后到**的一方,所以**两边都定义的键,平台永远赢**。应用那份实际是个**补缺者**:只在平台包对那个键、那个语言没有字符串时才显示。 - **风险与代价(含回滚)。** 风险在于:升级后,**两边都有的键屏幕上根本不变**(平台本来就赢);只有**应用包在补空**的那些键会变 —— 那里平台压根没有字符串,所以屏幕上会回落到 **manifest 自己的英文字面量**,而不是「平台自带的字」。⚠️ 一个本地化部署里冒出一串英文,是比「换成平台的措辞」更响亮的一种结果,请按这个来衡量。这是有意的,changeset 与 ADR-0087 条目都写明了,不是静默变化。发布面动了,所以带 `minor` changeset(发射窗口惯例,`major` 会被门禁拒收)。回滚就是回滚这个 PR:没有数据迁移、没有存储改动,`os migrate meta` 的那条转换只在作者主动运行时改源码。 - **席位意见。** 这一轮的方向是 dev **第一手实测**出来的,不是复述:证伪器跑真链路,外加两个亮控 —— 一个把加载顺序反过来证明仪器对顺序敏感,一个用平台没翻译的命名空间证明 app 那份真的被加载且在服务。结论从「覆盖平台文案」改成「只填平台没有的空」,所以 ADR 条目按实测写是对的,原裁决的**方向**不动。本席**不建议拆**技能文件:拆了等于在窗口期里让已发布技能继续教一个已被拒收的键,而 `skills/**` 这道 Tier H 的门你本来就得为它开一次。⚠️ 另:本 PR 先前那份契约复审记录**已作废**(跑在已退役的档位上,台账见 #19603),新的达档复审在 the current `CONTRACT_REVIEW_TIER` 上重跑;⛔ 结果出来之前本席不做任何落地动作,也不翻 ready。 - **你要做的。** ① 这个 PR 碰了 `skills/`,按规矩属于 Tier H,落地要维护者的那句话;若不想为一条文案更正开这道门,可以把那个技能文件单独拆一个 PR——但⛔ 不能一边发布新契约、一边让已发布技能继续教一个现在会被拒收的键。② 裁决里「was inert」这句与实测不符(它是活的),ADR 条目按实测写,请确认这个改写符合原意。③ `translation` 元数据门仍接受 `settings`,那是本卡范围之外的一个口子,见上面的验收注记。 --- _Generated by [Claude Code](https://claude.ai/code)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 041c8cf commit d0f1845

28 files changed

Lines changed: 929 additions & 180 deletions

File tree

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/service-settings': minor
4+
---
5+
6+
**BREAKING for per-app translation bundles** — the translation bundle type splits in two: `settings` is a PLATFORM group and a per-app bundle may no longer declare it (#15178)
7+
8+
Clause-②: yes
9+
10+
`TranslationDataSchema` served two different bundles at once — the per-app one an
11+
application authors (`stack.translations`, `defineTranslationBundle`) and the
12+
code-authored bundles the platform packages ship. It now names the **per-app**
13+
bundle entry and declares ten groups; the new `PlatformTranslationDataSchema` /
14+
`PlatformTranslationBundleSchema` (types `PlatformTranslationData` /
15+
`PlatformTranslationBundle`) carry the eleven-group platform face, `settings`
16+
included.
17+
18+
### Migration — FROM → TO
19+
20+
| You wrote | Write instead |
21+
| --- | --- |
22+
| `defineTranslationBundle({ 'zh-CN': { settings: { mail: { title: '邮件投递' } } } })` | delete the `settings` group — there is no per-app replacement key |
23+
| `defineStack({ translations: [{ 'zh-CN': { settings: … } }] })` | delete the `settings` group from the bundle entry |
24+
| `const b: TranslationBundle = { en: { settings: … } }` — a PLATFORM package's own bundle | `const b: PlatformTranslationBundle = { en: { settings: … } }` |
25+
| `const d: TranslationData = { settings: … }` — a PLATFORM package's own locale entry | `const d: PlatformTranslationData = { settings: … }` |
26+
27+
**The one-line fix for an application: delete the `settings` group.** Settings copy
28+
is not application-authorable at all — `settings` is keyed by
29+
`SettingsManifest.namespace` and only platform code declares a manifest, so the
30+
only namespaces a per-app entry could ever address were the platform's own.
31+
`settingsCommon` is **not** affected — the Settings UI shell strings (the source
32+
badges, under `settingsCommon.sourceLabels`) stay on the per-app face; only the
33+
per-namespace manifest copy under `settings` leaves.
34+
Run `os migrate meta --from 17` to list the mechanical edits for existing
35+
sources; apply them by hand.
36+
37+
### What the deletion changes, which is not nothing
38+
39+
⚠️ This is **not** a lossless delete, and the record says so rather than claiming
40+
the house phrase. Both bundles load into ONE served tree — `AppPlugin`'s
41+
`loadTranslations` and every platform plugin's `kernel:ready` contribution both
42+
call `II18nService.loadTranslations`, which deep-merges — and the
43+
`resolveSettings*` family and the console's settings labels read that merged
44+
tree. So an app-authored `settings` branch did resolve.
45+
46+
**It was a gap filler, not an override.** The app's bundles are loaded in
47+
`AppPlugin`'s own `start()` (kernel Phase 2); the platform's settings
48+
translations arrive from `SettingsServicePlugin`'s `kernel:ready` hook (Phase
49+
3); `deepMerge` gives the **later** source the leaf. So the platform won every
50+
key both bundles defined, and a per-app entry rendered **only where the platform
51+
bundle carried no string for that key and locale** — the platform ships `en`,
52+
`zh-CN`, `ja-JP` and `es-ES`.
53+
54+
**What to expect after upgrading.** Where the platform already carried the
55+
string, nothing changes on screen — that value was the one being served all
56+
along. Where your entry was filling a gap, that Settings screen now renders the
57+
**manifest's own literal, which is English** (the `?? fallback` every
58+
`resolveSettings*` helper ends in). Those are the screens to re-read. If a
59+
platform string is wrong or missing for your locale, correct it in the platform
60+
bundle (`@objectstack/service-settings`'s `settingsBuiltinTranslations`) — do
61+
not re-add the app-side copy, which the platform overwrites on every boot
62+
wherever it has its own value.
63+
64+
No deprecation window: the per-app door refuses the key by name from this major,
65+
and the rejection carries the prescription above.
66+
67+
### Unchanged
68+
69+
The registered `translation` metadata type (`TranslationItemSchema`) still
70+
declares `settings` — this ruling covers the file-authored bundle. `GET
71+
/api/v1/i18n/translations/:locale` still declares it on its response, because the
72+
served document is the merged tree; `GetTranslationsResponseSchema` is typed
73+
against the platform face for exactly that reason.
74+
75+
Ruling batch #132 item 2 letter ② (2026-09-13) — 「同意」. The card's original
76+
"removal" disposition is struck: `settings` is a live platform key.
77+
78+
<!-- adr-0087: registered translation-per-app-settings-removed -->

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

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

399399
- **The top level is the declared group set and nothing else** — `objects`,
400400
`apps`, `messages`, `globalActions`, `dashboards`, `datasets`, `pages`,
401-
`flows`, `settings`, `metadataForms`, `settingsCommon`. A group invented
402-
beside them (`"account"`, `"fields"`, `"list"`, `"industries"`) is rejected
403-
**by name** at both authoring doors, with the group to use instead named in
404-
the rejection.
401+
`flows`, `metadataForms`, `settingsCommon`. A group invented beside them
402+
(`"account"`, `"fields"`, `"list"`, `"industries"`) is rejected **by name**
403+
at both authoring doors, with the group to use instead named in the
404+
rejection.
405+
- **`settings` is a PLATFORM group and an application bundle may not carry
406+
it.** It is keyed by `SettingsManifest.namespace`, and only platform code
407+
declares a manifest — so the only namespaces an application could ever
408+
address are the platform's own. Writing it in `stack.translations` (or in
409+
`defineTranslationBundle`) is refused by name with that prescription; the
410+
platform's own bundles author it against `PlatformTranslationData`, and the
411+
served document (`GET /i18n/translations/:locale`) carries it because it is
412+
the merge of every loaded bundle.
405413
- **Field options are a map keyed by the option's stored `value`** — never an
406414
array of `{ value, label }` pairs, and never the display label. See
407415
[Orphan Keys and Option Keys](#orphan-keys-and-option-keys).
@@ -1046,10 +1054,12 @@ options: { 'Direct Mail': '直邮' } // ❌ keyed by the label — never resolv
10461054
options: { 'direct-mail': '直邮' } // ❌ a variant spelling of the value
10471055
```
10481056

1049-
`messages`, `settings`, `settingsCommon` and
1050-
`metadataForms` are **not** checked: their keys are owned by application code,
1051-
plugins, and the platform's own metadata-type registry rather than by this
1052-
stack's metadata, so there is no set of legal names to resolve against.
1057+
`messages`, `settingsCommon` and `metadataForms` are **not** checked: their
1058+
keys are owned by application code, plugins, and the platform's own
1059+
metadata-type registry rather than by this stack's metadata, so there is no set
1060+
of legal names to resolve against. (`settings` is not checked either, and since
1061+
it left the per-app bundle it cannot appear in one at all — the parse refuses it
1062+
before any lint runs.)
10531063

10541064
### Translation Service Integration
10551065

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1615,9 +1615,9 @@ The published metadata item body, opaque by ruling (1C). Shape is the item's own
16151615
| **datasets** | `Record<string, { label?: string; description?: string; dimensions?: Record<string, object>; measures?: Record<string, object> }>` | optional | Analytics dataset translations keyed by dataset name |
16161616
| **pages** | `Record<string, { label?: string; description?: string; title?: string; subtitle?: string; … }>` | optional | Page translations keyed by page name |
16171617
| **flows** | `Record<string, { label?: string; screens?: Record<string, object> }>` | optional | Screen-flow translations keyed by flow name |
1618-
| **settings** | `Record<string, { title?: string; description?: string; groups?: Record<string, object>; keys?: Record<string, object>; … }>` | optional | Settings manifest translations keyed by namespace |
16191618
| **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 |
16201619
| **settingsCommon** | `{ sourceLabels?: object }` | optional | Cross-namespace Settings UI strings |
1620+
| **settings** | `Record<string, { title?: string; description?: string; groups?: Record<string, object>; keys?: Record<string, object>; … }>` | optional | Settings manifest translations keyed by namespace |
16211621

16221622

16231623
---

‎content/docs/references/index.mdx‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
title: Protocol Reference
3-
description: Every schema published by @objectstack/spec — 1535 schemas across 14 protocol modules
3+
description: Every schema published by @objectstack/spec — 1537 schemas across 14 protocol modules
44
---
55

66
{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
@@ -31,9 +31,9 @@ counts are sums of the rows they head. Regenerate with
3131
| [Security Protocol](/docs/references/security) | 5 | 30 | Permission sets, row-level security, sharing rules, tenancy posture. |
3232
| [Shared Protocol](/docs/references/shared) | 10 | 31 | Primitives used across every protocol — identifiers, HTTP, expressions, error maps, enums. |
3333
| [Studio Protocol](/docs/references/studio) | 3 | 35 | Studio designer metadata — the authoring surfaces for the protocols above. |
34-
| [System Protocol](/docs/references/system) | 34 | 273 | The runtime environment — logging, jobs, cache, metrics, notifications, i18n and compliance. |
34+
| [System Protocol](/docs/references/system) | 34 | 275 | The runtime environment — logging, jobs, cache, metrics, notifications, i18n and compliance. |
3535
| [UI Protocol](/docs/references/ui) | 16 | 158 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. |
36-
| **Total** | **195** | **1535** | 14 protocol modules |
36+
| **Total** | **195** | **1537** | 14 protocol modules |
3737

3838
---
3939

@@ -316,7 +316,7 @@ Studio designer metadata — the authoring surfaces for the protocols above.
316316

317317
## System Protocol
318318

319-
**Source:** `packages/spec/src/system/` · **Import:** `@objectstack/spec/system` · **34 pages, 273 schemas**
319+
**Source:** `packages/spec/src/system/` · **Import:** `@objectstack/spec/system` · **34 pages, 275 schemas**
320320

321321
The runtime environment — logging, jobs, cache, metrics, notifications, i18n and compliance.
322322

@@ -354,7 +354,7 @@ The runtime environment — logging, jobs, cache, metrics, notifications, i18n a
354354
| [`supplier-security.zod.ts`](/docs/references/system/supplier-security) | `SupplierAssessmentStatus`, `SupplierRiskLevel`, `SupplierSecurityAssessment`, `SupplierSecurityPolicy`, `SupplierSecurityRequirement` |
355355
| [`tenant.zod.ts`](/docs/references/system/tenant) | `DatabaseLevelIsolationStrategy`, `DatabaseProvider`, `QuotaEnforcementResult`, `RowLevelIsolationStrategy`, `SchemaLevelIsolationStrategy`, `Tenant`, `TenantConnectionConfig`, `TenantIsolationConfig`, `TenantIsolationLevel`, `TenantQuota`, `TenantSecurityPolicy`, `TenantUsage` |
356356
| [`tracing.zod.ts`](/docs/references/system/tracing) | `OpenTelemetryCompatibility`, `OtelExporterType`, `SamplingDecision`, `SamplingStrategyType`, `Span`, `SpanAttributeValue`, `SpanAttributes`, `SpanEvent`, `SpanKind`, `SpanLink`, `SpanStatus`, `TraceContext`, `TraceContextPropagation`, `TraceFlags`, `TracePropagationFormat`, `TraceSamplingConfig`, `TraceState`, `TracingConfig` |
357-
| [`translation.zod.ts`](/docs/references/system/translation) | `ActionResultDialogTranslation`, `CoverageBreakdownEntry`, `FieldTranslation`, `Locale`, `ObjectTranslationData`, `TranslationBundle`, `TranslationConfig`, `TranslationCoverageResult`, `TranslationData`, `TranslationDiffItem`, `TranslationDiffStatus`, `TranslationItem` |
357+
| [`translation.zod.ts`](/docs/references/system/translation) | `ActionResultDialogTranslation`, `CoverageBreakdownEntry`, `FieldTranslation`, `Locale`, `ObjectTranslationData`, `PlatformTranslationBundle`, `PlatformTranslationData`, `TranslationBundle`, `TranslationConfig`, `TranslationCoverageResult`, `TranslationData`, `TranslationDiffItem`, `TranslationDiffStatus`, `TranslationItem` |
358358
| [`worker.zod.ts`](/docs/references/system/worker) | `BatchProgress`, `QueueConfig`, `Task`, `TaskExecutionResult`, `TaskPriority`, `TaskRetryPolicy`, `TaskStatus`, `WorkerStats` |
359359

360360
---

0 commit comments

Comments
 (0)