Repository navigation
fix(spec): one label order on an overridden item, for all eight document translators - #30236
objectstack-fleet[bot] wants to merge 3 commits into
Conversation
…e eight document translators A translation the paired .custom package ships wins, then the override's own text, then the base's translation (ADR-0146 D2). page, dataset, app and book now take TranslateDocumentOptions and judge every string against packagedBase, as view, dashboard, report and object already did. The new optional pairBundle option carries the pair's own translations; without it every translator answers as before. Claude-Session: https://claude.ai/code/session_01WGufzhytXa3Yi3uzE5pvuv Co-authored-by: Claude <noreply@anthropic.com>
…slators One table-driven block in i18n-resolver.test.ts, the same rows for view, dashboard, report, object, page, dataset, app and book: the pair's translation wins, with none the override's own label beats the base's translation, an unchanged label keeps the base's translation beside a changed one (nested labels included), and a pair that ships only `en` leaves an untouched label on the base's zh-CN. An empty pairBundle is pinned to give the same answer as none. labelCatalogs now hands back the catalogs it would without a pair when the pair answers none of the chain's locales, so an empty pair keeps the no-bundle reference identity too. Claude-Session: https://claude.ai/code/session_01WGufzhytXa3Yi3uzE5pvuv Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufzhytXa3Yi3uzE5pvuv Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 13 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 6 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 170 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 9fa594ce28ed246f0461d856a497f1cd9ee922ff && git checkout 9fa594ce28ed246f0461d856a497f1cd9ee922ff
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin afc761c35fa475d1a1a48cda5982be994a7f079f 8476878165579f66320292abdddcdbaa156d6149 && git checkout -B drift-repro afc761c35fa475d1a1a48cda5982be994a7f079f && git merge --no-ff 8476878165579f66320292abdddcdbaa156d6149
node scripts/docs-audit/affected-docs.mjs --json afc761c35fa475d1a1a48cda5982be994a7f079f
|
|
domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K Contract-review-claim: session_01WGufzfgvGexKn9vLHsy89K · head 8476878 · 2026-11-05T02:55Z Full review of the first head at |
|
domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K Contract reviewServed-tier: ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgmentThe design is sound: one comparison, two catalogs built once per document, the interleave at the right grain, and the page header restatement reasoned through (a title the override left as the base wrote it translates as the base's label, test.diff :320). Three should-fix items, no must-fix:
Implemented-by: VERDICT: PASS |
|
domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K 维护者速读
席位意见:fable 契约复审一轮 PASS,没有必须修的。三条建议都已处理:
席位核对 — ACCEPT · domain:spec#3 · head
|
domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K
Fixes #30193
Requested by Yibo Wang · project thread
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 pausedPrior 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
TranslateDocumentOptions.pairBundle: the translations the served item's paired.custompackage ships itself, locale-keyed asbundleis.labelCatalogs(bundle, opts)builds two catalogs per document:changed(the pair's own translations, nothing else) andunchanged(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,valueOverridesPackagedBaseagainstpackagedBase. 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,translateAppandtranslateBookreadpackagedBase(their options type widens fromResolveOptionstoTranslateDocumentOptions), each string judged against the base's counterpart: page components byidand THE page's header, dataset members byname, navigation nodes byid, book groups bykey. A page header title the override left as the base wrote it translates as the base's label.pairBundle, or one that answers none of the locale chain's codes: the catalogs are exactly the ones without it (same references). NopackagedBase: nothing counts as changed. So no caller's output moves today: the server passespackagedBaseonly for object, dashboard, view and report (PACKAGED_BASE_ACCESSORS,form-routes.ts), and passes nopairBundleat 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.page:tabsitems, interface tabs, form thank-you panelpackagedBasenot read)packagedBasenot read)packagedBasenot read)packagedBasenot read)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:2887translationBundleOfbuilds{ locale: i18n.getTranslations(locale) }.packages/services/service-i18n/src/file-i18n-adapter.ts:189getTranslationsreturnsdeepMerge(static, authored):loadTranslations, :199);translationsys_metadata row across ALL packages, deep-merged by item name,_packageIdstripped (packages/core/src/fallbacks/authored-translation-sync.tsreadAuthoredTranslationLayer).The option:
TranslateDocumentOptions.pairBundle?: TranslationBundle(the same locale-keyed shape asbundle): the translations the served item's paired.custompackage itself ships, and nothing else.valueOverridesPackagedBasetrue)pairBundleonly, alonglocaleChainpackagedBasepairBundle, then the deployment catalogennever displaces the base'szh-CNon an untouched label.Accepted deviations
validations[].messagestays on the plain merged bundle, and inline objectactions(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 fromi18n.t()with no pair input, and the pinned cross-path invariant is refusal text = served text.actionis not one of the eight types and has nopackagedBase.validations[].messagethe 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: An overridden object's validation message never takes its .custom pair's translation, while the object's labels do #30245.Server-half contract for #30062
For a served item that is a declared
.customoverride, 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.custompackage's own translations only: its code-shipped translation bundle and its authoredtranslationrows, both of which are the pair's own;packagedBasefor 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, i18n: a tenant's rewrite of an objectExtensions-contributed validation rule message still loses to that package's translation, on the refusal and on /meta #27559), so it is not override-only. Add the same accessor for page, dataset, app and book;pairBundlefor 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
packagedBasetoday.Carried
/metatext (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:489reportLabel(and:482pageLabel; 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 inui/**changes here.Tests
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 inzh-CNand 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 onlyen, an untouched label keeps the base'szh-CN); nopackagedBase= 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=2over the 25 spec test files importing the resolver, underos-verify-lock:Test Files 25 passed (25),Tests 1099 passed (1099),VERDICT command-exit 0.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.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 buildunder the lock: exit 0.pnpm check:generated: all 14 generated artifacts up to date (api-surface included), exit 0.node scripts/pm/dispatch-gates.mjs --commandsderived 78 commands at 8476878; all 78 ran and exited 0. Four first exited 3 (prerequisite not met, nothing measured) and were re-run green aftergit fetch --depth=1of the teardown self-test's pinned fixture commit and apnpm exec turbo run build:check-plugin-teardown-shape --self-test,check:doc-formula-expressions,check:dual-build-cjs-loads,check:lean-entry-closure.--ranreconciliation: 78 derived, 78 run, 0 NOT-MEASURED, 0 UNRUN.GITHUB_EVENT_NAME=pull_request node scripts/check-changeset-no-major.mjs --base origin/main --eventwith this body: exit 0; the level axis readsClause-②: yes (widening)andMetadata-protocol: none, a non-protocol change atpatch.Generated by Claude Code