You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
[Decision] the default locale's text — is the authored label the default-locale source (a default-locale request stops there), or must every supported locale ship a bundle? os i18n check says the former, localeChain does the latter; the resolver's literal ['en'] default rides on the answer #15711
Filed by the domain:spec execution seat (session_01M59rPZZFzqhfMUPFqqZTkf, 2026-09-05T03:56Z) on the triage seat's routing note on #14882 (5545589675), which pinned the bug card to the config-honouring half and reserved this question, quoted verbatim:
and on the dev report for PR #15707 (5549139634, open_questions), which delivered the bug half without answering it and pinned the exposed case unchanged. Labels: needs-user-decision + i18n; domain:* routing is the triage seat's once ruled (the landing point depends on the option). ⛔ Not dispatchable: the decision is the maintainer's. Sequencing: PR #15707 (#14882, in flight) is NOT blocked by this card and does not decide it.
The question
A workspace authors its metadata labels in its default locale (i18n.defaultLocale: 'zh-CN', inline label: '填报单'). Is the authored label the default locale's text — so a request for the default locale stops at the authored label before any other locale's bundle is consulted — or must every supported locale, the default included, ship a translation bundle? Today the platform answers both ways:
The runtime says "walk the chain first". The @objectstack/spec/system label resolvers walk requested locale → fallbackChain → authored label, and the chain is consulted BEFORE the authored label: re-check git grep -n "fallbackChain ?? \['en'\]" origin/main -- packages/spec/src/system/i18n-resolver.ts (control: the same file hits function localeChain). Measured at 581d8f84c: i18n-resolver.ts:285.
What PR #15707 changes, and what it leaves exposed
#15707 threads the DECLARED fallback into the chain (II18nService.getFallbackLocale() → fallbackChain: [fallback] at every REST metadata seam). Both boot paths collapse the declaration to fallbackLocale || defaultLocale || 'en' before constructing the service (packages/cli/src/commands/serve.ts:3022, packages/plugins/plugin-dev/src/dev-i18n.ts:247 at 581d8f84c), so after #15707 a workspace that declares defaultLocale: 'zh-CN' and no fallbackLocale resolves zh-CN → zh-CN → authored and the reporter's case is fixed without this ruling.
Still exposed after #15707, pinned there AS IT BEHAVES TODAY so this card can rule it:
zh-CN → en → authored ⇒ Entry Sheet — the declared en outranks the author's 填报单 (pinned: i18n-resolver.test.ts "a chain that DECLARES en still consults en before the authored label"; meta-i18n-declared-fallback-chain.test.ts §5)
any caller that passes NO fallbackChain (a host outside this repo; the core in-memory i18n fallback, which declares nothing — #15694)
zh-CN
en only
zh-CN → en → authored ⇒ Entry Sheet — the resolver's literal ['en'] default (pinned: "a caller that declares NO chain keeps the resolver's literal en default")
Production callers of these resolvers at 581d8f84c: packages/rest/src/rest-server.ts only (four seams). Nothing else in this repo calls them outside tests (git grep -ln "translateMetadataDocument" origin/main -- packages | grep -v "packages/spec/\|packages/rest/" answers two test files and two CHANGELOGs); the dev measured that objectui does not call them (its resolveActionLabel is a local helper with its own formatter config). The serving layer already knows the default locale at the seam: a request with no Accept-Language and no ?locale= resolves to i18n.getDefaultLocale() (rest-server.ts:3181 at 581d8f84c).
Options × real cost
option
what
real cost
A — the authored label IS the default-locale text (rule in spec)
ResolveOptions gains defaultLocale?: string; localeChain does not walk the chain when locale === defaultLocale (the authored label answers); the serving layer passes i18n.getDefaultLocale(). fallbackLocale keeps its full meaning for every NON-default request (fr → fr bundle → en bundle → authored).
one optional key on a published face (@objectstack/spec minor) + one rule in localeChain + the REST seam passing one more value; the gate's claim becomes true at runtime; the reporter's os i18n extract --locales=zh-CN workaround becomes optional; two pins flip
A′ — the same rule, in the serving layer only
REST passes fallbackChain: [] when the resolved request locale equals getDefaultLocale(); spec text states the rule, the resolver stays mechanical
zero new contract surface; but the rule lives in packages/rest — any other host calling the resolvers must re-implement it, and the spec cannot pin it
B — every supported locale must ship a bundle
runtime unchanged; os i18n check reports a missing default-locale bundle as an ERROR (severity flip); docs say the authored label is not consulted while another declared bundle exists
every app that authors labels inline in its default locale (the documented authoring style) turns red at the gate until it ships a generated bundle that duplicates its own source text — the stale-translation class this repo already closed cards on (#9672, #10026)
C — document the divergence
nothing changes; docs state that a declared fallbackLocale is consulted before the authored label and that the gate counts authored text as coverage
zero code; the trap stays for fallbackLocale: 'en' on a non-en default (a reflexive line in AI-authored configs) and the gate keeps claiming coverage the runtime does not honour
Second facet, rides on the first — the resolver's literal default.localeChain answers ['en'] when a caller passes no fallbackChain at all. Dev recommendation (report 5549139634): change it to [] — "requested locale, then the authored label" — in a follow-up once the first facet is ruled, so "declared but not executed" cannot recur through a chain-less caller; blast radius = callers passing only { locale }, none left in production in this repo after #15707, one pin flips. It is a behaviour change on a published @objectstack/spec/system default (which text answers moves; nothing starts failing) — the changeset level is part of the ruling. Under A the default locale is unaffected either way; the facet then only decides what a chain-less NON-default request does.
Related: #14882 · #15694
Filed by the
domain:specexecution seat (session_01M59rPZZFzqhfMUPFqqZTkf, 2026-09-05T03:56Z) on the triage seat's routing note on #14882 (5545589675), which pinned the bug card to the config-honouring half and reserved this question, quoted verbatim:and on the dev report for PR #15707 (
5549139634,open_questions), which delivered the bug half without answering it and pinned the exposed case unchanged. Labels:needs-user-decision+i18n;domain:*routing is the triage seat's once ruled (the landing point depends on the option). ⛔ Not dispatchable: the decision is the maintainer's. Sequencing: PR #15707 (#14882, in flight) is NOT blocked by this card and does not decide it.The question
A workspace authors its metadata labels in its default locale (
i18n.defaultLocale: 'zh-CN', inlinelabel: '填报单'). Is the authored label the default locale's text — so a request for the default locale stops at the authored label before any other locale's bundle is consulted — or must every supported locale, the default included, ship a translation bundle? Today the platform answers both ways:os i18n checkon the reporter's stack (comment5522341222on i18n: metadata label lookup falls through to theenbundle on a zh-CN workspace —localeChaindefaultsfallbackChainto ['en'] and ignoresi18n.fallbackLocale, so an authored Chinese label loses to a courtesy English bundle #14882):1252 warning(s) across 2 locale(s)·⚠ Non-default locales have gaps but the default locale is fully covered.— zero errors with nozh-CNbundle at all. Re-check the message site:git grep -n "default locale is fully covered" origin/main -- packages/cli/src/commands/i18n/check.ts(control: the same file must hitThreshold). Measured at581d8f84c:check.ts:177, printed wheneverreport.totals.errors === 0.@objectstack/spec/systemlabel resolvers walkrequested locale → fallbackChain → authored label, and the chain is consulted BEFORE the authored label: re-checkgit grep -n "fallbackChain ?? \['en'\]" origin/main -- packages/spec/src/system/i18n-resolver.ts(control: the same file hitsfunction localeChain). Measured at581d8f84c:i18n-resolver.ts:285.What PR #15707 changes, and what it leaves exposed
#15707 threads the DECLARED fallback into the chain (
II18nService.getFallbackLocale()→fallbackChain: [fallback]at every REST metadata seam). Both boot paths collapse the declaration tofallbackLocale || defaultLocale || 'en'before constructing the service (packages/cli/src/commands/serve.ts:3022,packages/plugins/plugin-dev/src/dev-i18n.ts:247at581d8f84c), so after #15707 a workspace that declaresdefaultLocale: 'zh-CN'and nofallbackLocaleresolveszh-CN → zh-CN → authoredand the reporter's case is fixed without this ruling.Still exposed after #15707, pinned there AS IT BEHAVES TODAY so this card can rule it:
defaultLocale: 'zh-CN',fallbackLocale: 'en'zh-CNenonlyzh-CN → en → authored⇒Entry Sheet— the declaredenoutranks the author's填报单(pinned:i18n-resolver.test.ts"a chain that DECLARES en still consults en before the authored label";meta-i18n-declared-fallback-chain.test.ts§5)fallbackChain(a host outside this repo; the core in-memory i18n fallback, which declares nothing — #15694)zh-CNenonlyzh-CN → en → authored⇒Entry Sheet— the resolver's literal['en']default (pinned: "a caller that declares NO chain keeps the resolver's literal en default")Production callers of these resolvers at
581d8f84c:packages/rest/src/rest-server.tsonly (four seams). Nothing else in this repo calls them outside tests (git grep -ln "translateMetadataDocument" origin/main -- packages | grep -v "packages/spec/\|packages/rest/"answers two test files and two CHANGELOGs); the dev measured that objectui does not call them (itsresolveActionLabelis a local helper with its own formatter config). The serving layer already knows the default locale at the seam: a request with noAccept-Languageand no?locale=resolves toi18n.getDefaultLocale()(rest-server.ts:3181at581d8f84c).Options × real cost
ResolveOptionsgainsdefaultLocale?: string;localeChaindoes not walk the chain whenlocale === defaultLocale(the authored label answers); the serving layer passesi18n.getDefaultLocale().fallbackLocalekeeps its full meaning for every NON-default request (fr → fr bundle → en bundle → authored).@objectstack/specminor) + one rule inlocaleChain+ the REST seam passing one more value; the gate's claim becomes true at runtime; the reporter'sos i18n extract --locales=zh-CNworkaround becomes optional; two pins flipfallbackChain: []when the resolved request locale equalsgetDefaultLocale(); spec text states the rule, the resolver stays mechanicalpackages/rest— any other host calling the resolvers must re-implement it, and the spec cannot pin itos i18n checkreports a missing default-locale bundle as an ERROR (severity flip); docs say the authored label is not consulted while another declared bundle existsfallbackLocaleis consulted before the authored label and that the gate counts authored text as coveragefallbackLocale: 'en'on a non-endefault (a reflexive line in AI-authored configs) and the gate keeps claiming coverage the runtime does not honourSecond facet, rides on the first — the resolver's literal default.
localeChainanswers['en']when a caller passes nofallbackChainat all. Dev recommendation (report5549139634): change it to[]— "requested locale, then the authored label" — in a follow-up once the first facet is ruled, so "declared but not executed" cannot recur through a chain-less caller; blast radius = callers passing only{ locale }, none left in production in this repo after #15707, one pin flips. It is a behaviour change on a published@objectstack/spec/systemdefault (which text answers moves; nothing starts failing) — the changeset level is part of the ruling. Under A the default locale is unaffected either way; the facet then only decides what a chain-less NON-default request does.四维分析(从业务角度)
一句话问题:一个用中文写标签、顺手附了一份英文包的应用,中文用户看到的该是作者写的中文,还是必须再另出一份中文包才能保住中文?
fallbackLocale: 'en'配非en默认语言的栈,以及不传链的调用方 —— AI 生成的配置反射式写fallbackLocale: 'en'很常见,拉动中等、真实。defaultLocale声明的就是作者写标签的语言,门禁已按此读;A 让「声明 = 执行」,且规则住在 spec(契约的位置)。A′ 同一规则住在 rest,别的宿主要各自重写、spec 无法 pin。B 把作者文本降级为「不算覆盖」,与文档化的内联作者写法相悖,还引入一份会与源文本漂移的生成包(i18n: a corrected source description never reaches an already-translated locale, and no gate can tell the stale translation from a current one #9672 / A source label/description edit leaves the TRANSLATED locale bundles stale, and neither i18n gate can see it (measured on the #9807 diff) #10026 就是这一类)。⇒ A。fallbackLocale: 'en'对默认语言无害;B / C 保留陷阱,且门禁的「默认语言 100% 覆盖」继续与运行期不一致。第二面改[]后,不传链的宿主也不再被一个字面'en'静默改写。⇒ A + 第二面改[]。localeChain一条规则(小);A′ = 零契约面但规则错位;B = 门禁严重度翻转 + 每个应用的生成包义务(扩散到全部应用);C = 零。⇒ A′ 最紧、A 次紧;B 明确扩散。推荐
A(规则住 spec:
ResolveOptions.defaultLocale,serving 层从getDefaultLocale()传入,localeChain对默认语言不走链),第二面与 A 同批裁:荐改[](未声明即无回退),以显式 changeset 记行为变化;若维护者不愿动已发布默认,保留['en']与 A 亦相容(只影响不传链调用方的非默认语言请求)。⛔ B 不荐(扩散 + 漂移类);C 只在维护者认定「显式fallbackLocale字面优先」是刻意契约时成立。若裁 A:一张
domain:spec卡(resolver 规则 + 选项 + 两个 pin 翻转 +i18n-resolver.ts文档),REST 侧传值在同一 PR(#15707 已建好translateOptionsFor这个单一缝);os i18n check无需改动。若裁 B:一张domain:cli卡(严重度翻转)+ 文档卡。若裁 C:一张文档卡。第二面若裁改[]:并入 A 的卡。置信缺口:objectstack 之外的宿主是否调用这些 resolver 未测(本仓只读到 rest 一个生产调用方);
os i18n check判「默认语言已覆盖」的具体规则未逐行读(只读了报告输出与消息位置check.ts:177);objectui 侧的 label 消费链只采信 dev 报告的读数,本席未复核。os-decision-facets
defaultLocale声明的是作者语言,门禁已按此读 ⇒ A(规则住 spec);A′ 规则错位;B 与内联作者写法相悖并引入漂移类生成包。fallbackLocale: 'en'配非en默认语言 + 不传链的调用方。[]—— 反射式fallbackLocale: 'en'与不传链都不再被字面'en'静默改写。[](显式 changeset);⛔ B 不荐;C 仅当「显式 fallbackLocale 字面优先」被认定为刻意契约。os i18n check覆盖判定规则未逐行读;objectui 侧 label 链未复核。Generated by Claude Code