Repository navigation
i18n: metadata label lookup falls through to the en bundle on a zh-CN workspace — localeChain defaults fallbackChain to ['en'] and ignores i18n.fallbackLocale, so an authored Chinese label loses to a courtesy English bundle #14882
Description
Activity
- addedbugSomething isn't workingSomething isn't workingpriority:p2Medium: important, M3Medium: important, M3
on Sep 3, 2026 Additional data point — the platform's own coverage gate and the runtime disagree about what "default locale" means:
$ os i18n check 1252 warning(s) across 2 locale(s) ⚠ Non-default locales have gaps but the default locale is fully covered.os i18n checktreats the authored (Chinese) labels as full coverage of the default localezh-CN— no zh-CN bundle is required, zero errors. The runtime label lookup does not: with no zh-CN bundle entry it walkslocaleChaintoenand serves the English bundle to a zh-CN request. So an app that passes the coverage gate 100 % for its default locale still renders that locale in English wherever a secondary-locale bundle exists.Whichever way this is ruled, the two should agree:
- if authored labels are the default-locale source (what
i18n checkassumes), the lookup must stop at the authored label for the default locale instead of trying other bundles first; - if every supported locale must ship a bundle (the
os i18n extractper-locale workflow), theni18n checkshould report the missing default-locale bundle as the error it already claims to treat as one, and the docs should say the authored label is not consulted when any other bundle exists.
App-side we can work around it by generating the zh-CN bundle from schema labels (
os i18n extract --locales=zh-CN), which is the documented per-locale layout — noting it here so the ruling can say whether that is the intended contract or a workaround.- if authored labels are the default-locale source (what
分诊路由(本评论来自分诊座位)· R+150 —
domain:spec·pm:queue· 保留bug+i18n+priority:p2。落点实测(
origin/main,fetch 后git grep):packages/spec/src/system/i18n-resolver.ts:283 function localeChain(opts?: ResolveOptions): string[] { packages/spec/src/system/i18n-resolver.ts:285 const fallbacks = opts?.fallbackChain ?? ['en'];⇒ 报告里逐字引的那两行就在
packages/spec,['en']硬编码今天仍在树上 ⇒ 前提成立,落点domain:spec(单一属主车道)。bug而非 feature,判据要紧:栈声明了i18n.fallbackLocale,而解析链不读它 ⇒ 这是「声明 ≠ 执行」,按机械边界判据属 Bug/整理侧,⛔ 不上人工地板。⚠️ 但派发时必须把范围钉在这一半:让链尊重已声明的fallbackLocale/defaultLocale。⭐ 卡上 2026-09-03T07:43Z 那条补充评论(
5522341222)里藏着一个不同性质的问题,⛔ 别顺手一起裁掉:os i18n check报「默认语言覆盖完整」(把作者写的标签当作默认语言的来源),而运行期解析不这么认为 —— 门禁与运行期对「默认语言」的定义不一致。「每个 supported locale 是否都必须出 bundle」是契约问题,不是本卡的 bug 半边。⇒ 若实现时发现不回答它就改不动,停下来拆卡送决策箱,⛔ 不要在一个 bug 修复里顺手定契约。p2 维持(⛔ 本席未提级,理由写下):这条确实是用户可见、可复现、影响一个 zh-CN 部署的默认语言(17.2.0 实测),但 ① 无数据损坏、② 只在部署额外发了第二个 locale bundle 时才发作、③ 报告人自己已给出可用规避(
os i18n extract --locales=zh-CN生成 zh-CN bundle,且那是文档化的 per-locale 布局)。⇒ 不到 p1。⚠️ 若该规避被裁为不是预期契约,严重度应重估。
Generated by Claude Code
os-project-manager commented
on Sep 5, 2026 CollaboratorMore actionsClaim: PM loop round R1
Session:session_01M59rPZZFzqhfMUPFqqZTkf
Branch:claude/issue-14882-i18n-fallback-chain-honours-config
Worktree:objectstack-issue-14882
Domain:domain:spec
File surface:packages/spec/src/system/i18n-resolver.ts(localeChainat:283–:285on1be26b0,ResolveOptions.fallbackChainat:199, the lookup-order docblock at:34) + its tests · the server-side producer of theResolveOptionspassed to the label resolvers (to be located by the dev onorigin/main— nothing outsidepackages/specpassesfallbackChaintoday, measured 01:48Z; the caller is wherei18n.fallbackLocale/defaultLocalemust be threaded from, and if it lives inpackages/rest/plugin-hono-server/packages/metadatathe dev reports the location and fixes on the producer side, not with a consumer-side patch) · generated followers · changeset. Stop on breach; explain in the report.
Container & model:M,mode:subagent,model: fable(claude-fable-5-1) —node scripts/pm/dispatch-gates.mjs --tier packages/spec/src/system/i18n-resolver.tsrun 2026-09-05T01:48Z against the tree at1be26b0: "no path-derived mandate", "Clause ② SUSPECT surface" hint; tier set from card CONTENT — the default resolution behaviour of a published@objectstack/spec/systemresolver changes (which bundle wins for a default-locale request), and the claim-time rule is "not sure ⇒ yes" ⇒CONTRACT_REVIEW_TIER(claude-fable-5-1, re-derived fromorigin/main:scripts/pm/dispatch-gates.mjsat 01:44Z).
Clause-②: yes
Serial constraints cleared:packages/spec/src/system/i18n-resolver.tshas no in-flight claim and is not on this seat's hot-file queue at 01:49Z;system/tenant.zod.tsand the generated i18n bundles are HELD by draft PR #15626 (#14478) — this card does not touch them, and if the dev's diff regenerates an i18n bundle it stops and reports before enqueuing; queued sibling #14972 (injected system-column labels,objectql/spec) is not in flight and is ⛔ not folded here (different defect, different fix). Same-round dispatches #14079 and #14954 are source-disjoint; generated corpus lands serially viascripts/pm/os-regen-merge.sh. Predecessor's linger PRs #15438 / #15639 / #15645 / #15626 touch none of this surface.
Generated by Claude Code
Claim (os-dev): dispatched dev for this card, PM loop round R1
Session:session_01M59rPZZFzqhfMUPFqqZTkf
Branch:claude/issue-14882-i18n-fallback-chain-honours-config(pushed empty at claim time)
Worktree:objectstack-issue-14882onorigin/main7087f99c
Scope: the config-honouring half only — the resolution chain reads the stack's declaredi18n.fallbackLocale/defaultLocaleinstead of a literalenlist; the gate-vs-runtime "what is the default locale" contract question from comment 5522341222 is not decided here.
File surface:packages/spec/src/system/i18n-resolver.ts+ tests, plus the server-side producer of the resolver options (location to be reported).
Generated by Claude Code
os-dev-report
{ "issue": 14882, "status": "done", "branch": "claude/issue-14882-i18n-fallback-chain-honours-config", "pr": "https://github.com/objectstack-ai/objectstack/pull/15707", "premise_still_valid": true, "summary": "Premise verified on origin/main 7087f99c: localeChain still defaults fallbackChain to a literal en list and nothing outside packages/spec passed a chain. Producer located: packages/rest/src/rest-server.ts — translateMetaItem, translateMetaItems, translateMetaTypesResponse and the public-form schema path all built { locale, packagedBase } with no chain, and they could not thread the declaration because II18nService exposed no fallback accessor (only getDefaultLocale). Fix is producer-side in three pieces: optional II18nService.getFallbackLocale?() on the spec contract; FileI18nAdapter.getFallbackLocale() in service-i18n (I18nServicePlugin already receives fallbackLocale || defaultLocale || 'en' from the stack config on both boot paths); RestServer.translateOptionsFor(i18n, locale) threading fallbackChain: [fallback] at all four seams, feature-detected so a provider without the accessor (older host, core in-memory fallback) keeps today's behaviour. The card's reproduction (defaultLocale zh-CN, fallbackLocale zh-CN, en-only bundle, zh-CN request) now resolves zh-CN -> zh-CN -> authored label; en requests still get the en bundle; a shipped zh-CN bundle still wins over the authored label. H2: the resolver's own en default is unchanged and singled out in the PR body for the contract reviewer (only reachable after this PR by a service with no declared fallback). H3 did not fire as a stop: the config-honouring half was deliverable without deciding the default-locale contract question; that case (defaultLocale zh-CN, fallbackLocale en, no zh-CN bundle) is pinned unchanged (en bundle text) at the resolver and the REST seam and is in open_questions. H5: i18n-resolver.ts changed only in docblocks; check:generated reports all 15 artifacts up to date (no api-surface or docs delta from the optional contract member). One mechanical follower: check-system-context-census reported pure line rot on five manage_metadata anchors in content/docs/permissions/system-context.mdx after the helper was inserted above them — repaired with its own --fix (one line), re-check green. Deviations: the assignee field was already the PM's at claim time (not touched); dedup search went REST 403 -> MCP (2 calls, the first 422'd on punctuation); the finding's title lost a backticked token to shell substitution on POST and was repaired by PATCH with read-back. needs:contract-review label: applied via additive REST POST at PR creation, read back present alongside the size-labeler's documentation/size/l/tests/tooling/protocol:system; PR body read-back is content-identical to what was sent, the platform only moved the rule-line + session footer block to the very end (known behaviour, not repaired).", "tests": "Union re-run on ebf647748 (git rev-parse --short HEAD, clean tree): spec src/system/i18n-resolver.test.ts 'Test Files 1 passed (1) / Tests 239 passed (239)'; service-i18n full 'Test Files 5 passed (5) / Tests 74 passed (74)'; rest meta-i18n-declared-fallback-chain + meta-object-i18n-explicit-override + meta-plural-i18n 'Test Files 3 passed (3) / Tests 34 passed (34)'; os-verify-lock 'VERDICT command-exit 0 · held the lock 21s'; check-system-context-census 'OK — 106 elevation read sites in 20 packages across 45 files, all anchored; 140 anchors resolve'; check-nul-bytes 'OK (scanned 7580 text file(s))'. Full consumer suites on the source tree of 0fe0153fb (ebf647748 differs only by the one census docs line outside every package), each under the lock with VERDICT command-exit 0: spec 'Test Files 472 passed (472) / Tests 12692 passed (12692)' (6m22s); core '49 passed / 1190 passed'; lint '96 passed / 3011 passed'; client '33 passed / 437 passed'; rest '181 passed / 3088 passed'; runtime '226 passed / 3243 passed'; cli --project unit '174 passed / 2324 passed | 6 expected fail (2330)' (integration tier declared to CI per the 2026-09-01 ruling). Typecheck exit 0 for spec (tsc + scripts + test-typecheck OK), rest, service-i18n, core, lint, client, runtime, cli. Builds: turbo closure 71 workspace packages, all exit 0. Ablation at the REST seam, fix committed first (0fe0153fb): mutation = translateOptionsFor returns { locale } only, confirmed on disk by grep -c marker=1 and removed-text=0 (subject is a same-package relative import, so no dist rebuild leg applies): 'Tests 6 failed | 7 passed (13)' — the five card-shape cases plus the zh-CN-bundle field case red; en-request, no-bundle control, feature-detection and H3 pins green; restore via git checkout HEAD -- path, git hash-object equal to the HEAD blob 01ba6324, git diff HEAD empty, git status --porcelain empty. Gates: pnpm --filter @objectstack/spec check:generated 'All 15 generated artifacts are up to date'; dispatch-gates derived (no paths, from ebf647748's merge base 7087f99c): every runnable spec/lint/repo gate exit 0 with its own verdict line — api-surface 'public API surface + factory signatures unchanged', authorable-surface, browser-reachable-entries, docs '230 generated files in sync', dual-source-exports, empty-state, entry-nameability, error-code-provenance, export-origins, exported-any, liveness, llms-txt, meta-url-spelling, react-blocks, skill-refs, spec-changes, strictness-ledger, variant-docs, yaml-examples, doc-formula-expressions; repo: nul-bytes, changeset-gate-self-tests, cross-package-test-inputs, doc-authoring, logger-receiver-detach, authz-resolver, dispatcher-error-vocabulary, auth-mount-ledger, console-injection, docs-image-tag, driver-memory-census, engine-double-contract, error-code-casing, error-status-conformance, filter-alias-parity, i18n-stale-fill, merge-driver, objectql-double-limit, objectui-changeset, org-identifier, overlay-whitelist-table, page-declaration-shape, partof-closing-keyword (also on the PR body: 'no Part-of/closing-keyword contradiction'), pm-governed-prose, pm-half-states, pm-label-desc-cap, pm-skill-ratchet, query-options-erasure, refd-timer-probe, route-envelope, single-claim-paths self-test, slot-lookup, spec-parsed-alias, stack-collection-maps, tenant-chokepoint, type-source-resolution, watch-hint-literal, where-matcher, widget-option-census, test-source-alias, type-check-coverage, published-files, published-readme-exports (after the full build: '60 published document(s) ... 198/198'), dual-build-cjs-loads ('103 published require entry point(s) ... load'), type-check-debt ('--re-measure: OK — 12 ledger entr(ies) re-measured'), plus 25 node-script gates from the derived list (adr-0087-registration 'adds no declared-breaking changeset (3 non-breaking changeset(s) seen)', changeset-no-major, empty-changeset, ci-filter-parity, comment-mask, keyed-text-bounds, platform-object-tenancy-census, plugin-teardown-shape, registry-log-declared, tenant-audit-census, undeclared-dep-imports, system-context-census after --fix, docs-audit, release-rehearsal-clone, pr-labels). Added beyond the lead and run: the whole derived list above. Not measured by design: check:react-declaration-parity (needs objectui's manifest) and CI-context-only invocations (RUNNER_TEMP, matrix shards, PR_NUMBER/claim paths). No repo-wide eslint run: not owed, CI's.", "mcp_calls": "2 — both mcp__github__search_issues for the out-of-scope dedup after REST /search returned 403 (the first 422'd on punctuation; the second returned 5 results including #14882 as the in-set control, no duplicate). Everything else (issue read, comments, claim, finding, PR, labels, report comment) went through repo-scoped REST, probed 200 first.", "open_questions": [ { "question": "Gate-vs-runtime definition of 'default locale' (card comment 5522341222): is the authored label the default-locale source, so that a request for the DEFAULT locale stops at the authored label before any other locale's bundle — or must every supported locale ship a bundle? The config-honouring half did not need this answered, but one declared shape still exposes it: defaultLocale 'zh-CN' with fallbackLocale 'en' and no zh-CN bundle. Before and after this PR a zh-CN request there gets the en bundle text (chain zh-CN -> en -> authored), pinned unchanged at the resolver (i18n-resolver.test.ts 'a chain that DECLARES en still consults en before the authored label') and at the REST seam (meta-i18n-declared-fallback-chain.test.ts §5). Callers answering it today: os i18n check (treats authored labels as full default-locale coverage, zero errors) vs the REST metadata reads (walk the declared chain before the authored label).", "options": [ "A — authored label IS the default-locale source: when the requested locale resolves to the workspace defaultLocale, the resolvers stop at the authored label before consulting any other bundle (spec-side rule; i18n check's current claim becomes true at runtime; the reporter's zh-CN extract workaround becomes optional).", "B — every supported locale must ship a bundle: keep the runtime as it is after this PR and make os i18n check report a missing default-locale bundle as the error it already claims to treat as one; document that the authored label is not consulted while another declared bundle exists.", "C — leave both as they are (declared fallbackLocale is honoured literally; the gate keeps its claim) and document the divergence." ], "recommendation": "A, because it removes a declaration the runtime does not honour (defaultLocale declares that the authored text is that locale's text; the gate already reads it that way) and makes an AI-authored app structurally harder to get wrong (no second bundle needed to keep the default language intact), at the cost of a contract change to the resolvers' order for the default locale only; B is the alternative if the maintainer wants bundles to be the single source. Not decided here; the PR keeps the case unchanged." }, { "question": "Should localeChain's literal ['en'] default (a caller that passes no fallbackChain at all) become [] — 'requested locale, then the authored label' — now that the only production caller declares a chain? Kept unchanged in this PR and singled out for the contract reviewer.", "options": [ "A — keep ['en'] (this PR): a provider that declares no fallback (older host, core in-memory fallback) keeps today's answer.", "B — change to []: an undeclared chain means no fallback, so 'declared but not executed' cannot recur; blast radius is any caller passing only { locale } — after this PR none in production in this repo, objectui does not call these resolvers; pin 'a caller that declares NO chain keeps the resolver's literal en default' flips." ], "recommendation": "B in a follow-up once the first question is ruled, because it closes the class; not in this PR because it changes a published default's behaviour and the dispatch reserved that judgment for contract review." } ], "out_of_scope_findings": [ "filed as #15694: the core in-memory i18n fallback (createMemoryI18n) never receives the declared i18n.fallbackLocale — AppPlugin injects defaultLocale and supportedLocales but not fallbackLocale, and its t() falls back to defaultLocale only, so one declaration answers differently under the memory fallback and under service-i18n (labels: finding, i18n; dedup: MCP search returned no existing card; #14382 is the unrelated boot-diagnostic duplicate-line card)." ] }
Generated by Claude Code
Clause-② determination (domain:spec seat,
session_01M59rPZZFzqhfMUPFqqZTkf, 2026-09-05T03:52Z) — yes, both limbs: content limb — PR #15707 adds a new optional contract memberII18nService.getFallbackLocale?(): string | undefinedon@objectstack/spec/contracts(a new key on a published face is the mechanical floor) and rewrites theResolveOptions.fallbackChaincontract text; path limb —packages/spec/src/contracts/i18n-service.ts,packages/spec/src/system/i18n-resolver.ts.needs:contract-reviewis now hung on BOTH carriers: the dev hung it on PR #15707 at creation (headebf647748), this seat hung it on the card in this stroke (labels read back: bug · domain:spec · i18n · needs:contract-review · pm:dispatched · priority:p2). The in-seat contract-tier review follows in this round; both carriers are cleared in one stroke on PASS.
Generated by Claude Code
5 remaining items
os-dev-report-addendum
Patch lap for PR #15707 (merge only; review already done on the card).
- New head:
1bb7857de(pushed, plain push). Merge commit:9b230fadb(origin/main1c1421401merged into the branch viabash scripts/pm/os-regen-merge.sh; no rebase, no force-push, no stash). Regeneration commit:1bb7857de. - What the regeneration changed:
content/docs/permissions/system-context.mdxonly, one table row, line numbers only — themanage_metadatarow'srest-server.tsanchors, main's:5016, :6442, :6690, :7121, :7314re-anchored to:5052, :6478, :6726, :7157, :7350bypnpm gen:system-context-censuson the merged tree (main had moved those sites by +12 lines; this branch's helper adds +36 above them). Staged diff inspected before the commit; the pre-commit hook printedcontent/docs/permissions/system-context.mdx — currentandos-regen: all deferred artifacts are current — marker cleared. - Union re-run on
1bb7857de(clean tree), each underscripts/pm/os-verify-lock.sh: specsrc/system/i18n-resolver.test.tsTest Files 1 passed (1) / Tests 239 passed (239); service-i18n fullTest Files 5 passed (5) / Tests 74 passed (74); restmeta-i18n-declared-fallback-chain+meta-object-i18n-explicit-override+meta-plural-i18nTest Files 3 passed (3) / Tests 34 passed (34); lock lineVERDICT command-exit 0 · held the lock 20s · waited 311s (5m11s);node scripts/check-system-context-census.mjsOK — 106 elevation read sites in 20 packages across 45 files, all anchored; 140 anchors resolve, 27 declared non-read;pnpm --filter @objectstack/spec check:generatedAll 15 generated artifacts are up to date. Builds on the merged tree under the lock: spec (VERDICT command-exit 0 · held the lock 130s), rest + service-i18n closures (25 successful, 25 total,VERDICT command-exit 0 · held the lock 137s). - PR body untouched:
check:partof-closing-keywordon the live body readsno Part-of/closing-keyword contradiction.needs:contract-reviewnot re-hung (the reviewing seat re-hangs and re-clears). PR readsmergeable_state: blocked(draft, checks pending) — no longerdirty. - Worktree removed again after confirming remote == local head.
Generated by Claude Code
- New head:
Review addendum — regen lap 2 on PR #15707 (domain:spec seat,
session_01M59rPZZFzqhfMUPFqqZTkf, 2026-09-05T06:48Z). Head moved1bb7857de→b881599cc: a merge oforigin/main8e500f23eplus one regeneration commit touching onlycontent/docs/permissions/system-context.mdx(line-number re-anchoring of themanage_metadatarow, produced bypnpm gen:system-context-censuson the merged tree). Re-read:origin/mainis an ancestor of the new head; the PR's file set againstorigin/mainis still the same 13 files; no source hunk changed. ACCEPT stands; Clause-② PASS stands (5549189393). Because the head moved after the carriers were cleared, both carriers were re-hung and re-cleared in this stroke so the pair check reads the fixed spelling. Landing: flip ready + auto-merge (squash) as soon as every check onb881599ccconcludes green andmergeable_stateis notdirty— main moves every few minutes, so the flip is done immediately on green rather than at a fixed time.
Generated by Claude Code
os-dev-report-addendum
Second patch lap for PR #15707 (merge only; review already done on the card).
- New head:
b881599cc(pushed, plain push, remote == local). Merge commit:352a9aba1(origin/main8e500f23emerged into the branch viabash scripts/pm/os-regen-merge.sh; no rebase, no force-push, no stash). Regeneration commit:b881599cc. - What the regeneration changed:
content/docs/permissions/system-context.mdxonly, one table row, line numbers only — themanage_metadatarow'srest-server.tsanchors, main's:5048, :6474, :6722, :7153, :7346re-anchored to:5084, :6510, :6758, :7189, :7382bypnpm gen:system-context-censuson the merged tree (main moved those sites again by +32 since the last lap; this branch's helper adds +36 above them). Staged diff inspected before the commit; the pre-commit hook printedcontent/docs/permissions/system-context.mdx — currentandos-regen: all deferred artifacts are current — marker cleared. - Union re-run on
b881599cc(clean tree) underscripts/pm/os-verify-lock.sh(first attempt returned 99 — queue wait exhausted; the parked slot resumed and the retry ran): specsrc/system/i18n-resolver.test.tsTest Files 1 passed (1) / Tests 239 passed (239); service-i18n fullTest Files 5 passed (5) / Tests 74 passed (74); restmeta-i18n-declared-fallback-chain+meta-object-i18n-explicit-override+meta-plural-i18nTest Files 3 passed (3) / Tests 34 passed (34); lock lineVERDICT command-exit 0 · held the lock 28s · waited 0s;node scripts/check-system-context-census.mjsOK — 106 elevation read sites in 20 packages across 45 files, all anchored; 140 anchors resolve, 27 declared non-read;pnpm --filter @objectstack/spec check:generatedAll 15 generated artifacts are up to date. Build on the merged tree under the lock: spec + rest + service-i18n closures25 successful, 25 total,VERDICT command-exit 0 · held the lock 339s (5m39s) · waited 132s. - PR body untouched;
needs:contract-reviewnot re-hung (the reviewing seat re-hangs and re-clears). Worktree removed again after confirming remote == local head.
Generated by Claude Code
- New head:
Landing provenance (domain:spec seat,
session_01M59rPZZFzqhfMUPFqqZTkf, 2026-09-05T07:18Z) — PR #15707 at headb881599ccflipped ready and auto-merge (squash) armed at 2026-09-05T07:16Z; enqueue read from the timeline in the same minute (added_to_merge_queueevents seen: 1).Pre-landing checks per the 2026-08-31 ruling (in-seat contract PASS ⇒ clear both carriers ⇒ land): ① in-seat contract-tier PASS on this card (
5549189393), re-affirmed on both regen laps (5549338305lap 1 via the dev, seat addendum5550076938lap 2); ②needs:contract-reviewre-hung and re-cleared on both carriers after each head move, pair checknode scripts/pm/check-clause2-carriers.mjs --pair 15707exit 0 at 2026-09-05T07:15Z; ③ every check run onb881599ccconcluded — 40 runs, 0 not green at 07:15Z; governed-surface predicate on the 13-path file list: NOT governed (2026-09-05T03:51Z, file list unchanged since). Closing-keyword two-read: first lineFixes #14882; #14972 and #15694 are verb-less references.On MERGED this seat probes
getFallbackLocaleinpackages/spec/src/contracts/i18n-service.tsandtranslateOptionsForinpackages/rest/src/rest-server.tsonorigin/mainagainst the pre-merge tip, stripspm:dispatched, clears the assignee, and posts the landing note. The reserved contract question lives in #15711 (decision inbox); the dev-filed #15694 stays bare for triage.
Generated by Claude Code
Landing note (domain:spec seat,
session_01M59rPZZFzqhfMUPFqqZTkf, 2026-09-05T08:06Z) — PR #15707 merged via the merge queue asa84e1ced6at 2026-09-05T08:05:43Z, after two regen laps (generatedsystem-context.mdxonly) and a usage-wall gap.Landing probe on
origin/maina84e1ced6at 2026-09-05T08:06Z:getFallbackLocalepresent inpackages/spec/src/contracts/i18n-service.tsandtranslateOptionsForpresent inpackages/rest/src/rest-server.ts; control — the pre-merge tipc99449ab5has 0 hits for both. Card closed byFixes #14882;pm:dispatchedstripped and the assignee cleared in one write from a fresh read, read back. Lane inventory re-pulled: only this card closed on this merge. The reserved contract question is #15711 (decision inbox); the dev-filed #15694 stays bare for triage. PR #15786 (#14972, same file, disjoint region) is checked fordirtyin the same stroke.
Generated by Claude Code
- added a commit that references this issue
on Oct 7, 2026
Summary
For an app whose metadata labels are authored in the workspace default locale (
zh-CN, inlinelabel: '填报单') and which ships only anentranslation bundle as a courtesy for English users, the metadata API serves the English bundle labels to azh-CNrequest. The authored Chinese label never wins because the label lookup's fallback chain is hard-wired to['en'], independent of the stack'si18n.fallbackLocale.The effect on screen: object title
Entry Sheet, columnsSheet / Plan / Subject / Status / Final Score, app nameKPI Assessment— sitting next to fields that have noenentry and therefore correctly show主体类型 / 填报说明, and next to platform chrome that is fully Chinese. A zh-CN deployment cannot ship anenbundle without breaking its own default language.Minimal reproduction
Platform 17.2.0,
objectstack dev, SQLite, single tenancy, fresh DB.objectstack.config.ts:kpi_entry_sheetobject:label: '填报单', fieldsname: Field.text({ label: '填报单名称' }),status: Field.select({ label: '状态' }),total_score: Field.number({ label: '最终得分' }),subject_type: Field.select({ label: '主体类型' })(noenentry forsubject_type).Same request with no
Accept-Languageheader: same English result (the workspace default is zh-CN).Control: remove the
enbundle → every label is Chinese. So the presence of a bundle for a non-requested locale changes what the requested locale sees.Where
@objectstack/specsystem— the label resolvers (lookupObjectField,lookupObjectFieldAttr,lookupAppAttr,resolveViewLabel,lookupTabLabel, …) all iteratelocaleChain(opts):and return the first bundle hit before the caller falls back to the authored label. The server-side caller does not pass a
fallbackChain, soi18n.fallbackLocale: 'zh-CN'has no effect on the chain:zh-CN → en → authored. For any object the zh-CN bundle does not mention (i.e. every object whose labels are authored in Chinese), theenbundle is consulted before the authored label.Expected
One of (in order of preference):
i18n.fallbackLocale/defaultLocaleinstead of a literal['en'], so a zh-CN workspace resolveszh-CN → (fallbackLocale) → authored label, andenis only consulted whenenis requested.defaultLocaledeclares).locale: zh-CNwhile servingenstrings; the i18n coverage tooling should flag "label served from a locale other than the requested/default one".Side effects seen in the app (for triage, same root cause)
指标/Indicator:indicator(lookup, noenentry → authored指标) next toindicator_name(hasenentry →Indicator). With the fix both read Chinese (指标/指标名称).客户满意度 Indicator), breadcrumb (Indicator,Assessment Plan), related-list section headings (Entry Line 0,Indicator 0) all flip to English on a zh-CN page.Environment
@objectstack/*17.2.0 (runtime, spec, cli, console 17.2.0) · Node 22 · better-sqlite3 · single tenancy ·objectstack dev. App: objectstack-ai/kpi (private) — the config above is a faithful excerpt.