Repository navigation
Commit bb86ff9
fix(spec): one label order on an overridden item, for all eight document translators (#30236)
domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K
<!-- claude-projects-attribution: {"github_login":"os-support-ai"} -->
Fixes #30193
_Requested by **Yibo Wang** · [project
thread](https://claude.ai/code/project/chan_01BVUyyPie8gHwoPM18iABwB?thread=cmsg_01BVUyyPie8gHwoPM18iABwBCTY6Vx1RjbGoWGwHcjPnfP)_
Clause-②: yes (widening)
Metadata-protocol: none
Skills-docs: follow-up owed — skills/objectstack-platform/SKILL.md (the
ADR-0146 "Customizing an installed package" paragraph, :128, says
nothing of an override's label order) and
skills/objectstack-i18n/SKILL.md (no line on an overridden item); owed
once the serving half passes `pairBundle`, so a reader can see the
order; the skills lane is paused
Prior rulings read: packagedbase,translatepage,override label → 0 hits;
none; thread: none; repo: objectstack-ai/objectstack
## 维护者速读
- 改了什么:`.custom`
配对包里覆盖基础包的条目(视图、仪表盘、报表、对象、页面、数据集、应用、文档手册八类),每个标签按同一顺序取文字:配对包自带的译文 →
覆盖项自己写的文字 → 基础包的译文;覆盖项没改动的标签照旧用基础包译文。
-
为什么改:页面、数据集、应用、手册四类此前不比对基础包,覆盖项改过的标签会被基础包译文盖回;另外四类虽保住了覆盖文字,却读不到配对包自带的译文,因为解析器拿到的是一份合并后的词典,分不清译文出自哪个包。
- 风险与回滚:只新增一个可选项 `pairBundle`;不传时输出与现在完全相同(已用既有 1024
个测试强制空配对验证),服务端也还没有传它,所以合并后用户可见行为不变;回滚即还原本 PR。
- 先看:① `packages/spec/src/system/i18n-resolver.ts` 的 `labelCatalogs` /
`catalogFor`(规则的唯一出处);② 同文件 `translatePage` 的页头标题判定;③
`i18n-resolver.test.ts` 末尾的八类型表驱动测试。
- 两分钟验证:`cd packages/spec && pnpm exec vitest run --project local
src/system/i18n-resolver.test.ts -t "label precedence on an overridden
item"`,预期 75 个测试全部通过。
- 需要你做的:无,席位落地。
## What changes
- **One new option, `TranslateDocumentOptions.pairBundle`**: the
translations the served item's paired `.custom` package ships itself,
locale-keyed as `bundle` is.
- **One rule, one helper.** `labelCatalogs(bundle, opts)` builds two
catalogs per document: `changed` (the pair's own translations, nothing
else) and `unchanged` (locale by locale, the pair's translation laid
over the deployment catalog's). `catalogFor(catalogs, changed)` picks
one from the comparison every translator already makes,
`valueOverridesPackagedBase` against `packagedBase`. Every former "skip
the catalog when the value diverged" gate is now "read the catalog this
string earns", through these two functions, in all eight translators and
every shared nested helper (bulk-action defs, form sections and title,
the thank-you panel, global filters, report blocks, object list views,
dataset members, page tab items, interface tabs).
- **`translatePage`, `translateDataset`, `translateApp` and
`translateBook` read `packagedBase`** (their options type widens from
`ResolveOptions` to `TranslateDocumentOptions`), each string judged
against the base's counterpart: page components by `id` and THE page's
header, dataset members by `name`, navigation nodes by `id`, book groups
by `key`. A page header title the override left as the base wrote it
translates as the base's label.
- No `pairBundle`, or one that answers none of the locale chain's codes:
the catalogs are exactly the ones without it (same references). No
`packagedBase`: nothing counts as changed. So no caller's output moves
today: the server passes `packagedBase` only for object, dashboard, view
and report (`PACKAGED_BASE_ACCESSORS`, `form-routes.ts`), and passes no
`pairBundle` at all.
## Precedence, per type (before and after)
"Changed" = the served string differs from the base's
(`valueOverridesPackagedBase`). The catalog the server passes today is
one merged dictionary per locale (see the design below), so "the merged
catalog" means every package's code bundle and every authored
translation row, deep-merged, with no way to tell the pair's from the
base's.
| Type | Strings judged | Before: a string the override changed |
Before: one it left alone | After: changed | After: left alone |
|:--|:--|:--|:--|:--|:--|
| view | label, description, bulk-action defs (label, confirm text and
button, params), form sections, form title, thank-you panel | its own
text (the pair's translation is never read) | the merged catalog | the
pair's translation along the locale chain, else its own text | per
locale: the pair's translation, then the base's |
| dashboard | label, description, widget title and description, global
filter label and options | its own text (pair never read) | the merged
catalog | pair, else its own text | pair, then base, per locale |
| report | label, description, joined block label and description | its
own text (pair never read) | the merged catalog | pair, else its own
text | pair, then base, per locale |
| object | label, pluralLabel, description, field label, help, option
labels, list views | its own text (pair never read) | the merged catalog
| pair, else its own text | pair, then base, per locale |
| page | label, description, header title and subtitle, component copy,
`page:tabs` items, interface tabs, form thank-you panel | the base's
translation (`packagedBase` not read) | the merged catalog | pair, else
its own text | pair, then base, per locale |
| dataset | label, description, dimension and measure labels | the
base's translation (`packagedBase` not read) | the merged catalog |
pair, else its own text | pair, then base, per locale |
| app | label, description, navigation node labels (any depth) | the
base's translation (`packagedBase` not read) | the merged catalog |
pair, else its own text | pair, then base, per locale |
| book | label, description, group labels | the base's translation
(`packagedBase` not read) | the merged catalog | pair, else its own text
| pair, then base, per locale |
## Pair-provenance design
Measured at dfafae8: the resolver cannot tell a pair translation from
the base's.
- `packages/rest/src/meta-item-read-gate.ts:2887` `translationBundleOf`
builds `{ locale: i18n.getTranslations(locale) }`.
- `packages/services/service-i18n/src/file-i18n-adapter.ts:189`
`getTranslations` returns `deepMerge(static, authored)`:
- static = every package's code bundle, deep-merged in load order
(`loadTranslations`, :199);
- authored = every active `translation` sys_metadata row across ALL
packages, deep-merged by item name, `_packageId` stripped
(`packages/core/src/fallbacks/authored-translation-sync.ts`
`readAuthoredTranslationLayer`).
- One merged catalog per locale; provenance is lost twice. So the
resolver needs a new input.
The option: `TranslateDocumentOptions.pairBundle?: TranslationBundle`
(the same locale-keyed shape as `bundle`): the translations the served
item's paired `.custom` package itself ships, and nothing else.
| The string | Read | No hit |
|:--|:--|:--|
| changed by the override (`valueOverridesPackagedBase` true) |
`pairBundle` only, along `localeChain` | the override's own value (the
base catalog is never read) |
| not changed, or no `packagedBase` | per chain locale: `pairBundle`,
then the deployment catalog | the authored value |
- Interleaved per locale (pair zh-CN, catalog zh-CN, pair en, catalog
en) for an unchanged string, so a pair that ships only `en` never
displaces the base's `zh-CN` on an untouched label.
- A pair that answers none of the chain's locales short-circuits to the
catalogs without it, so an empty pair keeps even the no-bundle reference
identity.
## Accepted deviations
- **An object's `validations[].message` stays on the plain merged
bundle, and inline object `actions` (`translateAction`) stay as today**
(seat ruling on the design, item 2). Reason: the objectql rule evaluator
(`packages/objectql/src/validation/rule-validator.ts:3785`) answers the
same message from `i18n.t()` with no pair input, and the pinned
cross-path invariant is refusal text = served text. `action` is not one
of the eight types and has no `packagedBase`.
- **What the validations deviation means for a user** (contract review
6648581045, should-fix 2): on an overridden object, a
`validations[].message` the override reworded never takes the pair's
translation while the object's labels do, and a message it left alone
reads the merged catalog, where the pair's row can lose to the base's by
merge order. Lifting it needs a pair input on the objectql evaluator and
the resolver together: #30245.
## Server-half contract for #30062
For a served item that is a declared `.custom` override, on item, list
and container reads alike, for all eight types:
- `packagedBase` = the BASE package's item (pre-override: the document
the override replaces), never the override itself;
- `pairBundle` = `{ locale: data }` built from that `.custom` package's
own translations only: its code-shipped translation bundle and its
authored `translation` rows, both of which are the pair's own;
- keep passing `packagedBase` for object, dashboard, view and report
exactly as today, on every served item (`PACKAGED_BASE_ACCESSORS`,
`form-routes.ts` :385 :421): the object reads it for extension-folded
field labels too (ADR-0029 D9.2a, #27559), so it is not override-only.
Add the same accessor for page, dataset, app and book;
- omit `pairBundle` for an item that is not a declared override.
_Seat correction, body r1 (contract review 6648581045, should-fix 1):
the first version said "omit both for an item that is not an override",
which would have regressed the four types the server serves with
`packagedBase` today._
## Carried
- **Studio finding, filed as #30244 (ui, p2, blocked by #30062).**
Studio's own rails pair served `/meta` text
(`ui/packages/app-shell/src/views/studio-design/servedNavText.ts`), so
they share the server's answer. The Console header does not:
`ui/packages/i18n/src/useObjectLabel.ts:489` `reportLabel` (and `:482`
`pageLabel`; the dashboard and app labels have the same shape) resolves
the catalog key FIRST and the served label only as the fallback, so it
re-applies the base's translation over the server's answer. Callers:
`ui/packages/app-shell/src/layout/AppHeader.tsx:750` (the report crumb),
`ui/packages/app-shell/src/views/appReportIndex.ts:324`,
`ui/packages/app-shell/src/hooks/useRecentItemLabel.ts:122`. The
non-spec fix: for these served documents, take the served label as
already translated. Nothing in `ui/**` changes here.
## Tests
- New: one top-level describe in
`packages/spec/src/system/i18n-resolver.test.ts`, "label precedence on
an overridden item", table-driven over all eight types through the
dispatcher (`translateMetadataDocument`). Per type, a top-level label
and one or two nested ones (bulk-action label, widget title, block
label, field and option label, component copy and tab item, measure and
dimension, nested nav node, group), each row of the table above: the
pair wins; with no pair translation (no pair, and a pair shipping
nothing for the item) the override's own label beats the base's
translation in `zh-CN` and in the source locale; an unchanged label
keeps the base's translation beside a changed one; an untouched label
takes the pair's translation; the per-locale case (pair ships only `en`,
an untouched label keeps the base's `zh-CN`); no `packagedBase` =
nothing changed; an empty pair is the same answer as none, byte for
byte; no input mutated. Plus the page header restatement case. 75 tests.
- `pnpm exec vitest run --project local --maxWorkers=2` over the 25 spec
test files importing the resolver, under `os-verify-lock`: `Test Files
25 passed (25)`, `Tests 1099 passed (1099)`, `VERDICT command-exit 0`.
- The byte-identical claim, run on every existing fixture: with an
in-chain empty pair forced onto every call (`pairBundle ?? { each chain
code: {} }`), the 25 files gave 1023/1024; the one red was a
reference-identity assert with no bundle (equal value, new object),
which the short-circuit above closes.
- Ablations (each restored, verified by `git diff --stat`): the pair
never read → 41 of the 75 red (5 rows × 8 types, plus the page header
pair case); the interleave dropped → 8 red, exactly the per-locale case
once per type; changed strings reading the merged catalog → 16 red.
- `pnpm --filter @objectstack/spec typecheck` (build, scripts and test
layers) exit 0.
## Verification
- `pnpm --filter @objectstack/spec build` under the lock: exit 0. `pnpm
check:generated`: all 14 generated artifacts up to date (api-surface
included), exit 0.
- Dispatch gates: `node scripts/pm/dispatch-gates.mjs --commands`
derived 78 commands at 8476878; all 78 ran and exited 0. Four first
exited 3 (prerequisite not met, nothing measured) and were re-run green
after `git fetch --depth=1` of the teardown self-test's pinned fixture
commit and a `pnpm exec turbo run build`: `check-plugin-teardown-shape
--self-test`, `check:doc-formula-expressions`,
`check:dual-build-cjs-loads`, `check:lean-entry-closure`. `--ran`
reconciliation: 78 derived, 78 run, 0 NOT-MEASURED, 0 UNRUN.
- `GITHUB_EVENT_NAME=pull_request node
scripts/check-changeset-no-major.mjs --base origin/main --event` with
this body: exit 0; the level axis reads `Clause-②: yes (widening)` and
`Metadata-protocol: none`, a non-protocol change at `patch`.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01WGufzhytXa3Yi3uzE5pvuv)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 159930a commit bb86ff9
3 files changed
Lines changed: 893 additions & 169 deletions
File tree
- .changeset
- packages/spec/src/system
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
6081 | 6081 | | |
6082 | 6082 | | |
6083 | 6083 | | |
| 6084 | + | |
| 6085 | + | |
| 6086 | + | |
| 6087 | + | |
| 6088 | + | |
| 6089 | + | |
| 6090 | + | |
| 6091 | + | |
| 6092 | + | |
| 6093 | + | |
| 6094 | + | |
| 6095 | + | |
| 6096 | + | |
| 6097 | + | |
| 6098 | + | |
| 6099 | + | |
| 6100 | + | |
| 6101 | + | |
| 6102 | + | |
| 6103 | + | |
| 6104 | + | |
| 6105 | + | |
| 6106 | + | |
| 6107 | + | |
| 6108 | + | |
| 6109 | + | |
| 6110 | + | |
| 6111 | + | |
| 6112 | + | |
| 6113 | + | |
| 6114 | + | |
| 6115 | + | |
| 6116 | + | |
| 6117 | + | |
| 6118 | + | |
| 6119 | + | |
| 6120 | + | |
| 6121 | + | |
| 6122 | + | |
| 6123 | + | |
| 6124 | + | |
| 6125 | + | |
| 6126 | + | |
| 6127 | + | |
| 6128 | + | |
| 6129 | + | |
| 6130 | + | |
| 6131 | + | |
| 6132 | + | |
| 6133 | + | |
| 6134 | + | |
| 6135 | + | |
| 6136 | + | |
| 6137 | + | |
| 6138 | + | |
| 6139 | + | |
| 6140 | + | |
| 6141 | + | |
| 6142 | + | |
| 6143 | + | |
| 6144 | + | |
| 6145 | + | |
| 6146 | + | |
| 6147 | + | |
| 6148 | + | |
| 6149 | + | |
| 6150 | + | |
| 6151 | + | |
| 6152 | + | |
| 6153 | + | |
| 6154 | + | |
| 6155 | + | |
| 6156 | + | |
| 6157 | + | |
| 6158 | + | |
| 6159 | + | |
| 6160 | + | |
| 6161 | + | |
| 6162 | + | |
| 6163 | + | |
| 6164 | + | |
| 6165 | + | |
| 6166 | + | |
| 6167 | + | |
| 6168 | + | |
| 6169 | + | |
| 6170 | + | |
| 6171 | + | |
| 6172 | + | |
| 6173 | + | |
| 6174 | + | |
| 6175 | + | |
| 6176 | + | |
| 6177 | + | |
| 6178 | + | |
| 6179 | + | |
| 6180 | + | |
| 6181 | + | |
| 6182 | + | |
| 6183 | + | |
| 6184 | + | |
| 6185 | + | |
| 6186 | + | |
| 6187 | + | |
| 6188 | + | |
| 6189 | + | |
| 6190 | + | |
| 6191 | + | |
| 6192 | + | |
| 6193 | + | |
| 6194 | + | |
| 6195 | + | |
| 6196 | + | |
| 6197 | + | |
| 6198 | + | |
| 6199 | + | |
| 6200 | + | |
| 6201 | + | |
| 6202 | + | |
| 6203 | + | |
| 6204 | + | |
| 6205 | + | |
| 6206 | + | |
| 6207 | + | |
| 6208 | + | |
| 6209 | + | |
| 6210 | + | |
| 6211 | + | |
| 6212 | + | |
| 6213 | + | |
| 6214 | + | |
| 6215 | + | |
| 6216 | + | |
| 6217 | + | |
| 6218 | + | |
| 6219 | + | |
| 6220 | + | |
| 6221 | + | |
| 6222 | + | |
| 6223 | + | |
| 6224 | + | |
| 6225 | + | |
| 6226 | + | |
| 6227 | + | |
| 6228 | + | |
| 6229 | + | |
| 6230 | + | |
| 6231 | + | |
| 6232 | + | |
| 6233 | + | |
| 6234 | + | |
| 6235 | + | |
| 6236 | + | |
| 6237 | + | |
| 6238 | + | |
| 6239 | + | |
| 6240 | + | |
| 6241 | + | |
| 6242 | + | |
| 6243 | + | |
| 6244 | + | |
| 6245 | + | |
| 6246 | + | |
| 6247 | + | |
| 6248 | + | |
| 6249 | + | |
| 6250 | + | |
| 6251 | + | |
| 6252 | + | |
| 6253 | + | |
| 6254 | + | |
| 6255 | + | |
| 6256 | + | |
| 6257 | + | |
| 6258 | + | |
| 6259 | + | |
| 6260 | + | |
| 6261 | + | |
| 6262 | + | |
| 6263 | + | |
| 6264 | + | |
| 6265 | + | |
| 6266 | + | |
| 6267 | + | |
| 6268 | + | |
| 6269 | + | |
| 6270 | + | |
| 6271 | + | |
| 6272 | + | |
| 6273 | + | |
| 6274 | + | |
| 6275 | + | |
| 6276 | + | |
| 6277 | + | |
| 6278 | + | |
| 6279 | + | |
| 6280 | + | |
| 6281 | + | |
| 6282 | + | |
| 6283 | + | |
| 6284 | + | |
| 6285 | + | |
| 6286 | + | |
| 6287 | + | |
| 6288 | + | |
| 6289 | + | |
| 6290 | + | |
| 6291 | + | |
| 6292 | + | |
| 6293 | + | |
| 6294 | + | |
| 6295 | + | |
| 6296 | + | |
| 6297 | + | |
| 6298 | + | |
| 6299 | + | |
| 6300 | + | |
| 6301 | + | |
| 6302 | + | |
| 6303 | + | |
| 6304 | + | |
| 6305 | + | |
| 6306 | + | |
| 6307 | + | |
| 6308 | + | |
| 6309 | + | |
| 6310 | + | |
| 6311 | + | |
| 6312 | + | |
| 6313 | + | |
| 6314 | + | |
| 6315 | + | |
| 6316 | + | |
| 6317 | + | |
| 6318 | + | |
| 6319 | + | |
| 6320 | + | |
| 6321 | + | |
| 6322 | + | |
| 6323 | + | |
| 6324 | + | |
| 6325 | + | |
| 6326 | + | |
| 6327 | + | |
| 6328 | + | |
| 6329 | + | |
| 6330 | + | |
| 6331 | + | |
| 6332 | + | |
| 6333 | + | |
| 6334 | + | |
| 6335 | + | |
| 6336 | + | |
| 6337 | + | |
| 6338 | + | |
| 6339 | + | |
| 6340 | + | |
| 6341 | + | |
| 6342 | + | |
| 6343 | + | |
| 6344 | + | |
| 6345 | + | |
| 6346 | + | |
| 6347 | + | |
| 6348 | + | |
| 6349 | + | |
| 6350 | + | |
| 6351 | + | |
| 6352 | + | |
| 6353 | + | |
| 6354 | + | |
| 6355 | + | |
| 6356 | + | |
| 6357 | + | |
| 6358 | + | |
| 6359 | + | |
| 6360 | + | |
| 6361 | + | |
| 6362 | + | |
| 6363 | + | |
| 6364 | + | |
| 6365 | + | |
| 6366 | + | |
| 6367 | + | |
| 6368 | + | |
| 6369 | + | |
| 6370 | + | |
| 6371 | + | |
| 6372 | + | |
| 6373 | + | |
| 6374 | + | |
| 6375 | + | |
| 6376 | + | |
| 6377 | + | |
| 6378 | + | |
| 6379 | + | |
| 6380 | + | |
| 6381 | + | |
| 6382 | + | |
| 6383 | + | |
| 6384 | + | |
| 6385 | + | |
| 6386 | + | |
| 6387 | + | |
| 6388 | + | |
| 6389 | + | |
| 6390 | + | |
| 6391 | + | |
| 6392 | + | |
| 6393 | + | |
| 6394 | + | |
| 6395 | + | |
| 6396 | + | |
| 6397 | + | |
| 6398 | + | |
| 6399 | + | |
| 6400 | + | |
| 6401 | + | |
| 6402 | + | |
| 6403 | + | |
| 6404 | + | |
| 6405 | + | |
| 6406 | + | |
| 6407 | + | |
| 6408 | + | |
| 6409 | + | |
0 commit comments