Repository navigation
Commit b49b752
fix(i18n)!: the console translation bundle leaves out Studio's metadataForms (75 KB of 221 KB on app-crm) (#26159)
<!-- claude-projects-attribution: {"github_login":"zhuangjianguo"} -->
_Requested by **Steedos** · [project
thread](https://claude.ai/code/project/chan_018ETKaNwMZCNRYD7D8Q9Ubb?thread=cmsg_018ETKaNwMZCNRYD7D8Q9UbbRmVkRqDy26wG2qAUXw3LEJ)_
Fixes #25785
Clause-②: yes (narrowing)
Metadata-protocol: narrowed
## Summary
`GET /i18n/translations/:locale` served the i18n service's whole locale
tree, so every console open downloaded `metadataForms`, the translations
of Studio's metadata-type authoring forms. On `examples/app-crm` that is
75 KB of the 221 KB raw zh-CN bundle, the second-largest group after
`objects`. The card's other half (the `ETag` and `Cache-Control:
private, no-cache` validator) already landed with the earlier i18n
caching PR; this PR is the composition half.
**How Studio gets its forms now: it already does, from the server.** No
browser code reads `metadataForms` from this bundle. The `/meta` and
`/meta/types` handler (`translateMetaTypesResponse` in
`@objectstack/rest`) reads the same i18n service in process, resolves
`metadataForms.TYPE` for each type, and serves every form already in the
requested locale. The liveness ledger
(`packages/spec/liveness/translation.json`) names that handler as the
group's only reader. So the group only has to stop being served to the
browser. Studio loads nothing new, and there is no new route, key or
endpoint.
- **spec, `system/translation.zod.ts`.** New
`SERVER_RESOLVED_TRANSLATION_GROUPS = ['metadataForms']` and
`ServedTranslationDataSchema`: the platform face
(`PlatformTranslationDataSchema`) without those groups. It is closed
(`strictObject`), so a served document that still carries one fails it
by name.
- **spec, `api/protocol.zod.ts`.**
`GetTranslationsResponseSchema.translations` is now
`ServedTranslationDataSchema`, so the declared response is what the door
serves.
- **runtime, `domains/i18n.ts`.** Both exits (the requested locale and
the `zh` to `zh-CN` fallback) serve `servedTranslations(tree)`, a copy
without the server-resolved groups. The service's own tree is never
edited, because `/meta` translates from that same object (both the core
memory i18n and `service-i18n` hand back the stored tree). The `ETag`
still hashes the served body, so it now changes only when served bytes
change.
- **docs.** `i18n-standard.mdx` (Metadata authoring forms) says the
console bundle leaves the group out and `/meta` reads it from the
service. The reference pages are regenerated.
- **changeset.** `25785-served-translations-without-metadata-forms.md`,
patch for `@objectstack/spec` and `@objectstack/runtime`.
`metadataForms` stays authorable on every bundle face
(`TranslationDataSchema`, `PlatformTranslationDataSchema`, app bundles,
`i18n:extract`). Only the served document leaves it out.
## Measured live (`examples/app-crm`, signed-in member, same box)
| locale | build | raw | gzip | brotli | `metadataForms` | warm reopen |
|:--|:--|--:|--:|--:|:--|:--|
| zh-CN | main `0a694821` | 221,007 B | 68,580 | 55,391 | 75,127 B |
304, 0 B |
| zh-CN | this branch | 145,863 B | 45,107 | 36,555 | absent | 304, 0 B
|
| en | main `0a694821` | 213,880 B | 58,666 | 49,185 | 75,361 B | 304, 0
B |
| en | this branch | 138,502 B | 37,576 | 31,466 | absent | 304, 0 B |
Every other group kept its exact byte size (for example zh-CN `objects`
111,504, `settings` 25,868, `apps` 4,469). `GET /api/v1/meta/types` and
`GET /api/v1/meta` in zh-CN answered 31 entries on both builds, with
identical translated labels: object 对象, field 字段, dashboard 仪表板, view
视图, flow 流程, and the first section and field of each form (基础信息, 名称).
## Tests
- New
`packages/runtime/src/domains/i18n-translations-served-groups.test.ts`
(10 tests):
- A: the path and query spellings and the fallback exit serve every
other group unchanged and omit `metadataForms`. The body parses the
closed `GetTranslationsResponseSchema`, with a control showing the
schema refuses the same body when it carries the group.
- B: a boot read does not edit the service's tree. A `RestServer`
`/api/v1/meta` built over the same service, read after the boot read,
still serves the dashboard form translated (仪表板, 布局, 列数).
- C: the `ETag` is the hash of the served body, `If-None-Match` answers
304, a change confined to `metadataForms` keeps the validator, and a
change to `messages` moves it.
- New `packages/spec/src/system/served-translation-data.test.ts`: the
served face is the platform face minus the constant (with a positive
control); the response schema serves that face; each server-resolved
group stays declared on both bundle faces; a document carrying the group
is refused with `unrecognized_keys` naming it.
- `type-alias-convention.pin.test.ts`: one `Iso` pin for the new schema
(789 to 790). `dropped-refinements.baseline.json`: one entry for
`system/ServedTranslationData`, the same 3 sites as
`PlatformTranslationData`, with header totals 245 / 841.
Runs (under `scripts/pm/os-verify-lock.sh`, `--maxWorkers=2`):
- `@objectstack/runtime` full suite at `8b2ad27c`: Test Files 455 passed
(455), Tests 7415 passed, 19 skipped. VERDICT command-exit 0.
- `@objectstack/spec` full suite: Tests 21929 passed, 1 failed, the
dropped-refinements header totals. Those were recounted from the body in
`8b2ad27c`, and the file then ran 27/27. That commit touches only those
two totals, which only that test reads.
- Focused: runtime 4 files, 41 tests; spec 5 files, 434 tests; all
passed.
- Type-check of `@objectstack/spec`, `@objectstack/runtime` and
`@objectstack/client`: VERDICT command-exit 0.
- `ui/apps/console/src/loadLanguage.test.tsx` (the console's bundle
loader): 7 passed.
## Ablation (one-off, mutated on disk, then restored)
- **Filter removed.** `servedTranslations` copies every group (blob
`3154a44d` to `a28c8f4a`). 6 of 10 tests failed: path and query
spellings, fallback exit, schema parse, the `/meta` after-boot read, and
the unserved-change validator. Restored blob `3154a44d` equals `HEAD`.
- **In-place delete.** `servedTranslations` still serves the right
document but also deletes the group from the service's own tree (`return
served;` replaced with a loop deleting each server-resolved group from
`translations`, blob `3154a44d` to `5e38e852`). Exactly 2 of 10 tests
failed, both Studio pins: "a boot read does not edit the service's tree"
and the `/meta` after-boot read, which served `'Dashboard'` where
`'仪表板'` was expected. The other 8 passed, so the served bytes and the
validator do not catch this mistake and only the B pins do. Restored
blob `3154a44d` equals `HEAD`, `git diff HEAD` empty.
Both runs used `scripts/ablation-replace.mjs` with
`src/domains/i18n-translations-served-groups.test.ts` under the verify
lock. No permanent test file was added for them.
## Gates
Derived with `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` (114 commands; the list derived at the head
`8b2ad27c` is identical to the one run). Each ran with its exit code
captured before any pipe, and `--ran` reconciled them at `8b2ad27c`:
"114 derived famil(ies) accounted for — 114 run, 0 NOT-MEASURED". All
114 exited 0, among them `check:generated`, `check:api-surface`,
`check:authorable-surface`, `check:docs`, `check:liveness`,
`check:strictness-ledger`, `check:spec-parsed-alias`,
`check:exported-any`, `check:dual-build-cjs-loads` (after a full
workspace build), `check:nul-bytes` and `check:type-check-debt` under
the verify lock ("26 raw tsc error(s) total, none above its recorded
number").
The sweep started before the last commit, which only recounts the two
dropped-refinements header totals. The 10 commands that finished before
that commit, and `check:query-options-erasure`, which overlapped the
20-second window of the first ablation, were rerun at `8b2ad27c`; all
exited 0. The local-only `check:dev-prereqs` and
`check:skill-frame-freshness` also exited 0.
`dispatch-gates` warned STALE TREE: the branch is behind `main`. No file
in this diff is touched by `main`'s newer commits, and `git merge-tree`
with `origin/main` reports no conflict, so `main` was not merged in.
## Notes for review
- **Contract review owed.** Non-test files under `packages/spec/src`
moved (`system/translation.zod.ts`, `api/protocol.zod.ts`), so the
`CONTRACT_REVIEW_TIER` review applies. Neither file was on the claim's
named file surface; the fix belongs in the spec declaration, so it was
made there.
- **The response schema narrows on purpose.** `translations` was the
platform face, which is closed and declares `metadataForms`. The served
face is closed the same way without it, so a body that still carries the
group now fails the schema. A client validating this route's responses
keeps passing. A client that read `metadataForms` from this route gets
nothing; the liveness ledger and a repository search show no such reader
(the console loader, the ui dashboard inspector and `TranslationPreview`
do not read it).
- **The ui side, unchanged and still green.**
`ui/apps/console/src/loadLanguage.test.tsx` walks
`GetTranslationsResponseSchema.shape.translations` (through the
`workspace:^` link to this `packages/spec`, so it reads this change) and
requires the loader to namespace every group it finds. It passes with
the narrower list: 7 passed. `SPEC_TRANSLATION_GROUPS` in
`ui/packages/i18n` deliberately keeps its own copy of the list and is a
superset detector, so still listing `metadataForms` changes no
behaviour. Two `ui` docblocks now read a little stale — that list's
header calls itself "the keys of the `translations` shape", and
`metadata-form-i18n.ts` expects "the environment's own translation
bundle" to arrive one day as a higher-precedence source for Studio's
form labels (it would now be `/meta/types`, which already serves them
translated). Neither is on this claim's surface and neither is a
behaviour, so both are left alone and reported to the seat.
- **Undeclared field.** The fallback exit's `requestedLocale` is served
but not declared on `GetTranslationsResponseSchema`. It was like this
before and is unchanged here.
- **Not taken: scoping `objects` to the current app.** The card marks it
optional and a bigger contract change. It is reported to the seat as a
separate finding.
## 维护者速读(草稿)
**改了什么.** 控制台每次打开都会请求当前语言的翻译包。这个包不再带 `metadataForms`(Studio
元数据编辑表单的翻译)。其余翻译分组一个字节不变。
**为什么改.** 这部分在 app-crm 的中文包里占 75 KB(总共 221 KB),浏览器从来不读它。Studio
的表单翻译一直是服务端在 `/meta/types` 里直接翻好再下发的。去掉之后,中文包压缩后从 68.6 KB 降到 45.1 KB,英文从
58.7 KB 降到 37.6 KB。第二次打开仍然是 304。
**风险与代价(含回滚).** 风险是有外部客户端从这个接口读 `metadataForms`。仓内没有这样的读者。Studio
的表单翻译有测试钉住:先请求翻译包,再读 `/meta`,表单仍是中文。回滚只需要还原本 PR,不涉及数据迁移。没有新增接口,也没有新增配置键。
**席位意见.**
**你要做的.** 评审并合并。可选:如果需要,按应用裁剪 `objects` 分组另开卡(本 PR 没做)。
## Round 2 — the seat's contract review (claim amendment 6418168409)
The declarations are corrected at the top of this body: **`Clause-②: yes
(narrowing)`**, `Metadata-protocol: none`. The published
`GetTranslationsResponseSchema` accepted a body carrying `metadataForms`
and now refuses it, and the exported `GetTranslationsResponse` type
loses that member; nothing an author writes moves, so the kind stays
`none` and the level stays `patch`.
- **The changeset carries the BREAKING banner**, the remedy (read the
group from `GET /meta/types`, which serves each form already translated)
and exactly one ADR-0087 disposition: `not-required
(no-migration-prescription)`, accepted by `check-adr-0087-registration`
— "1 declared-breaking changeset(s), each carrying an ADR-0087
disposition". The reason closes the other five categories on facts: both
packages publish; no ADR-0087 id is named or touched; the diff touches
`packages/spec/**` and a `*.zod.ts` face, which closes
`type-surface-only`; the narrowed declaration is a Zod projection, which
closes `runtime-interface-only`; and no export is retired. The remedy is
written as prose rather than a literal `FROM → TO` label block, because
that label is branch 1 of the gate's migration-prescription detector and
would make the only claimable category unclaimable; the mapping it would
carry ("read `metadataForms` from `GET /meta/types`") is in the
changeset's "What to do" and above.
- **`requestedLocale` is now declared** on
`GetTranslationsResponseSchema`, optional
(`packages/spec/src/api/protocol.zod.ts`). The door has served it on the
locale-fallback exit (`zh` served as `zh-CN`) since that fallback
existed, undeclared. Same schema, same class, mechanical — the in-place
adjacent fix the amended claim names. Pinned on both sides: the spec
test fixes the shape to `locale` / `requestedLocale` / `translations`,
parses a fallback body, keeps the field optional and refuses a
non-string; the runtime test parses the real fallback body against the
schema.
- **Two `ui` docblocks this PR had made false are repaired.**
`SPEC_TRANSLATION_GROUPS`
(`ui/packages/i18n/src/utils/spec-translations.ts`) now says it is
deliberately a superset of the groups the server still serves, names
`metadataForms` as the first group to have left, and states the one
direction that is red (a group the spec serves and the list lacks).
`metadata-form-i18n.ts` (`ui/packages/app-shell`) now names
`/api/v1/meta/types` as the channel that carries an environment's own
`metadataForms`, and says this overlay covers the forms the console
renders from its own copy of the spec.
- **`origin/main` was merged in** (merge `81dc21e7`), because editing a
`ui` registry source moves that package's digest in
`scripts/sdui-manifest.sources/` and `main` had just re-shaped those
records one-per-package (#26120). No file in this diff overlaps `main`'s
range, so the merge is textually clean. The digest was then re-recorded
with the merged generator: 1 of 27 package records moved (`i18n`),
`sdui.manifest.json` byte-identical at 106 components,
`check-sdui-manifest` green.
Measured at the head this comment is written against, `4f5d2f0a`: the
dogfood battery (closure built first, then `pnpm --filter
@objectstack/dogfood test` under the verify lock, no shard and no path)
is `Test Files 296 passed | 3 skipped (299)`, `Tests 2523 passed | 18
skipped (2541)`, VERDICT 0. The `@objectstack/spec` suite is `803` files
/ `23204` tests green, the six runtime i18n files `103` tests, the
type-checks clean, and the console loader plus the `@object-ui/i18n`
transform suite `22` tests. The derived gate list is now 125 commands
and all 125 exited 0, reconciled as "125 run, 0 NOT-MEASURED".
The declaration lines and the title of this pull request are the seat's
write, not the author's (a dev writes a body once, at creation): the
title takes `!` and the body's `Clause-②:` line takes `yes (narrowing)`.
Until then the declaration of record for the contract review is the
changeset's.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Vy85beHdawtEtqNscYBy7V)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent a76f811 commit b49b752
21 files changed
Lines changed: 684 additions & 30 deletions
File tree
- .changeset
- content/docs
- protocol/kernel
- references
- api
- system
- docs/audits/2026-07-unknown-key-strictness-ledger.counts
- packages
- runtime/src/domains
- spec
- api-surface
- authorable-surface
- declaration-map
- export-origins
- json-schema.manifest
- src
- api
- system
- ui/packages
- app-shell/src/views/metadata-admin
- i18n/src/utils
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
470 | 470 | | |
471 | 471 | | |
472 | 472 | | |
473 | | - | |
474 | | - | |
475 | | - | |
| 473 | + | |
| 474 | + | |
| 475 | + | |
| 476 | + | |
| 477 | + | |
| 478 | + | |
| 479 | + | |
| 480 | + | |
| 481 | + | |
476 | 482 | | |
477 | 483 | | |
478 | 484 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1764 | 1764 | | |
1765 | 1765 | | |
1766 | 1766 | | |
| 1767 | + | |
1767 | 1768 | | |
1768 | 1769 | | |
1769 | 1770 | | |
| |||
1782 | 1783 | | |
1783 | 1784 | | |
1784 | 1785 | | |
1785 | | - | |
1786 | 1786 | | |
1787 | 1787 | | |
1788 | 1788 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
4 | | - | |
| 4 | + | |
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
| |||
32 | 32 | | |
33 | 33 | | |
34 | 34 | | |
35 | | - | |
| 35 | + | |
36 | 36 | | |
37 | | - | |
| 37 | + | |
38 | 38 | | |
39 | 39 | | |
40 | 40 | | |
| |||
319 | 319 | | |
320 | 320 | | |
321 | 321 | | |
322 | | - | |
| 322 | + | |
323 | 323 | | |
324 | 324 | | |
325 | 325 | | |
| |||
360 | 360 | | |
361 | 361 | | |
362 | 362 | | |
363 | | - | |
| 363 | + | |
364 | 364 | | |
365 | 365 | | |
366 | 366 | | |
| |||
0 commit comments