feat(spec,rest): the authored label is the default locale's text — ResolveOptions.defaultLocale skips the fallback chain, and a chain-less caller no longer falls to a literal en (#15711) - #15888
Conversation
…s default [] , seam pass-through, pins, docs, changesets Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M59rPZZFzqhfMUPFqqZTkf
…; build-time regenerated artifacts Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M59rPZZFzqhfMUPFqqZTkf
…the retired implicit default Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M59rPZZFzqhfMUPFqqZTkf
…el (regen lap: census page re-derived from the merged tree) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M59rPZZFzqhfMUPFqqZTkf
📓 Docs Drift CheckThis PR changes 2 package(s): 11 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 4 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 131 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 dd4a7beabc5ca3921831db7bcae24a7bf0fee298 && git checkout dd4a7beabc5ca3921831db7bcae24a7bf0fee298
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ea03c7c16fc27dcd489378faf8ba99abf9ae7124 2c453f0b2680953c6af620c2f5f79b2a770710dc && git checkout -B drift-repro ea03c7c16fc27dcd489378faf8ba99abf9ae7124 && git merge --no-ff 2c453f0b2680953c6af620c2f5f79b2a770710dc
node scripts/docs-audit/affected-docs.mjs --json ea03c7c16fc27dcd489378faf8ba99abf9ae7124
|
…el (regen lap 2: census page re-derived from the merged tree) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M59rPZZFzqhfMUPFqqZTkf
Fixes #15711
Ruled A on #15711 (maintainer ruling recorded by the director seat, comment 5549577889): the authored label IS the default-locale text, ruled in spec; and the resolver's literal
['en']default becomes[]in the same card. The operative paragraphs, verbatim:Not taken (per the ruling, not re-argued here): A′ (rule in packages/rest only), B (every locale ships a bundle), C (document the divergence).
What changed
packages/spec/src/system/i18n-resolver.ts—ResolveOptions.defaultLocale?: string(docblock states the ruled rule and cites [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 checksays the former,localeChaindoes the latter; the resolver's literal['en']default rides on the answer #15711 / 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);localeChainreturns[locale]when the requested locale namesdefaultLocale(BCP-47 tags compare case-insensitively, the same ruleresolveBundleLocalestep 2 applies; a different tag such aszhagainstzh-CNis a different locale — see the open question in the report) and otherwise[locale, ...(fallbackChain ?? [])]. ThefallbackChaindocblock, the file header and the two KEY-BY-KEY docblocks no longer describe a literalendefault.packages/rest/src/rest-server.ts—translateOptionsForthreadsdefaultLocalefromi18n.getDefaultLocale()when the method exists and answers a non-empty string, exactly as it feature-detectsgetFallbackLocale; the docblock's former prohibition ("Not derived fromgetDefaultLocale()… a contract question this seam must not answer on its own") is replaced by the ruled rule. Nothing else in the file.packages/spec/src/contracts/i18n-service.ts—getDefaultLocale/getFallbackLocaledocblocks note the ruled rule (docs only; the optional signatures are unchanged).packages/spec/src/system/i18n-resolver.test.ts: the two pins the ruling names are replaced by five [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 checksays the former,localeChaindoes the latter; the resolver's literal['en']default rides on the answer #15711 cases — a default-locale request with a declaredenchain answers the authored填报单/KPI 考核管理(control: the same options withoutdefaultLocalestill answerEntry Sheet); afrrequest still walks to theenbundle and anenrequest still gets it; a shippedzh-CNbundle still wins and a key it omits is authored, neveren;zh-cnnameszh-CN; a chain-less caller answers authored on both a default and a non-default request, with anenrequest and a declared-ennon-default request as controls. Two older KEY-BY-KEY pins (translatePage翻译契约 pages 无组件文案面:.strict() 四键且 title/subtitle 归 page:header,卡片/KPI 文案无键可写;dashboards.widgets 同按 id 定位却有 title/description 翻译位 #6080,translateFlowi18n: nothing readsTranslationData.flows— a screen flow still renders its authored English in every locale (the runner half #7646 deferred) #11287) that used the retired implicitennow declarefallbackChain: ['en']— their point is chain traversal, and the received values under the new default were the authored texts, i.e. the second facet doing what it says.packages/rest/src/meta-i18n-declared-fallback-chain.test.ts§4 and §5 flipped at the REST seam: azh-CNrequest ondefaultLocale: 'zh-CN'/fallbackLocale: 'en'/enbundle only serves填报单on the single read, the app read, the list read, the types listing and the header-less request;frservesEntry Sheet;enservesEntry Sheet; a provider with nogetFallbackLocalegets no chain and no inventeden; a provider with nogetDefaultLocalegets no default and the declaredenchain is walked exactly as 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 pinned it.content/docs/ui/translations.mdx("How a locale is chosen": the declared chain, the default-locale rule, the[]default) andcontent/docs/protocol/kernel/i18n-standard.mdx("Locale Fallback": system default = the declared fallback, plus the metadata-label rule).content/docs/permissions/system-context.mdx: five line anchors re-anchored bycheck-system-context-census --fixbecause the seam docblock moved the anchoredrest-server.tssites by 18 lines (pure line rot, the gate's own repair).@objectstack/specminor with the launch-window**BREAKING**banner for the['en']to[]default and the ADR-0087 disposition the gate accepts — measured:not-required (no-migration-prescription),check-adr-0087-registrationexit 0;registeredis not available because no ADR-0087 registry entry exists or would be true for a resolver default.@objectstack/restpatch. The spec changeset carries the ruling's caveat verbatim: "out-of-repo hosts unmeasured".Mechanism hypotheses (dispatch §2), verified on
8e7653b35(origin/main at claim time)grep -n "translateMetadataDocument\|resolveMetadataTypeLabel\|resolveMetadataTypeDescription\|resolveMetadataFormLabels" packages/rest/src/rest-server.tsfinds four resolver call sites (:3297,:3541,:3569-:3576,:9666at the base), every one spreading or passingRestServer.translateOptionsFor(i18n, locale); no site builds{ locale }by hand. Holds — no second seam.packages/spec/src/contracts/i18n-service.ts:52getDefaultLocale?(): stringand:87getFallbackLocale?(): string | undefined, both optional. The seam tolerates an absent method for each (passes nothing, never invents'en'); pinned in §4 with the accessor deleted from the double.os i18n checksays the former,localeChaindoes the latter; the resolver's literal['en']default rides on the answer #15711; the literal-['en']sentence count inrest-server.tsis 0 after the edit.packages/cli/src/commands/serve.ts:3029andpackages/plugins/plugin-dev/src/dev-i18n.ts:247(fallbackLocale || defaultLocale || 'en').i18n-resolver.test.ts:3592and:3603(the chain-less third pin) andmeta-i18n-declared-fallback-chain.test.ts§5, plus §4, which the second facet and the pass-through also flip (its own comment said agetDefaultLocale()derivation would). Consumers of the specResolveOptionstype outside spec:grep -rln 'ResolveOptions\|fallbackChain' packages --include='*.ts'hitspackages/rest/src/rest-server.tsand its test only (packages/runtime/src/security/resolve-execution-context.tsdeclares an unrelated local interface of the same name). objectui at the pina472b07167a3(.objectui-sha):git grep -l 'fallbackChain\|translateMetadataDocument\|ResolveOptions' a472b07167a3hits onlypackages/i18n/src/utils/spec-formatters.tsand its test — objectui's own formatter-config key, not the spec type; controlpickLocalizedhits.pnpm --filter @objectstack/spec check:generatedate22c25b9eand again at the final commitb7312986c: "✓ All 15 generated artifacts are up to date". No shard moved:api-surface/records exports by name (the interface already existed),content/docs/references/**carries Zod.describe()text only,authorable-surface/is untouched (no Zod schema changed). Nothing hand-edited.Pin sweep (per-card clause):
grep -rn "literal en default\|consults en before the authored\|fallbackChain ?? \['en'\]" packages scripts content docs skills examples— every hit was in the edited files; thei18n-service.tsandfile-i18n-adapter.tsdocblocks that narrate what happened BEFORE #14882 keep their past tense.Tests (final commit
b7312986c)pnpm exec turbo build --filter='@objectstack/rest^...' --concurrency=2— 24 tasks successful, 5m29s,VERDICT command-exit 0.pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 src/system/i18n-resolver.test.ts— final run:Tests 248 passed (248). (First run atacbfe264b: 2 failed / 246 passed — the two KEY-BY-KEY pins above, receivedOpen the assistant panel from the right edge…andCreate Opportunity?, the authored texts.)pnpm --filter @objectstack/rest test(whole package):Test Files 183 passed (183) · Tests 3122 passed (3122); the flipped seam file alone:Tests 19 passed (19).grep -rln 'translateMetadataDocument\|resolveViewLabel\|resolveActionLabel\|resolveFieldLabel' packages examples --include='*.test.ts'):service-i18n/src/authored-translations.test.ts16 passed;plugin-approvals/src/translations/approval-status-vocabulary.test.ts6 passed.pnpm --filter @objectstack/rest typecheckexit 0 (tsc +check:test-typecheckOK, 0 files in debt);pnpm --filter @objectstack/spec typecheckexit 0 (tsc, scripts program,check:test-typecheckOK, 54 files / 261 errors / 145 signatures held — ledger unchanged); speccheck:test-typecheckre-run after the last test-file commit: same verdict.@objectstack/spec/systemthroughexports, no vitest alias for spec — the ledger pair incheck-test-source-alias.mjs):localeChainmutated back toopts?.fallbackChain ?? ['en']with no short-circuit and a used string marker; on-disk proofinjected=1 removed_left=0;pnpm --filter @objectstack/spec buildemitted the ESM/CJS bundles ("Build success") and then its DTS step failed withTS6133: 'isDefaultLocale' is declared but its value is never read(the mutation orphans the helper; the JS artifact the suite consumes was already written);node scripts/ablation-dist-preflight.mjs @objectstack/spec 'ablation-15711-marker': marker present in 4 built files. Mutated: spec resolver file4 failed | 244 passed(the four [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 checksays the former,localeChaindoes the latter; the resolver's literal['en']default rides on the answer #15711 pins that depend on the rule; thefrcontrol stayed green by design), rest seam file6 failed | 13 passed(§4 legacy/undefined provider; §5 single read, app/list/types, header-less, shipped-bundle omitted key; the no-getDefaultLocaleandfr/encontrols stayed green by design). Restore:git checkout HEAD -- ABSOLUTE_PATHfrom an EXIT/INT/TERM trap,git diff HEADempty,git hash-objectequal to the HEAD blob4c95369c…on both legs,git status --porcelain0 lines; rebuild exit 0; preflight--absent: marker absent from all 217 built files, tree clean; spec248 passed (248), rest19 passed (19). Direction: red as predicted, no reversal.TURBO_SCM_BASE=$BASE pnpm exec turbo ls --affectedlists 75 packages (apackages/specsource edit reaches nearly every workspace package). Run locally: spec's resolver file, the whole rest suite, the two resolver-calling test files elsewhere, spec + rest typecheck. Spec's full suite and the remaining affected packages are declared to CI; the resolver is consumed only through the four rest seams and those two test files.pnpm exec eslint --no-inline-config --format jsonover the five TS files this branch touches (the fullgit diff --name-only 8e7653b35..HEADTS set) — 5 files linted, 0 errors, 0 warnings. Population read fromeslint.config.mjs(files: ['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}']plus thepackages/**blocks, all withparserOptions: { ecmaVersion, sourceType }only); invariance: the config states it runs noparserOptions.projectand no typed rules, so this diff cannot move the verdict of any untouched file.Gates (derived with
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 97 families)--base 8e7653b35, the recorded base, rather than the movingorigin/mainthe derivation prints), check:changeset-gate-self-tests, closing-keyword-parity, doc-frontmatter, doc-route-spelling, docs-section-name, section-landing-index, check-affected-docs, check-drift-comment, doc-anchors, doc-authoring, docs-audit-scope, docs-redirects, docs-single-h1, docs-transcript-drift, keyed-text-bounds, comment-mask-adoption/corpus, registry-log-declared, undeclared-dep-imports, system-context-census (after its--fix), cross-package-test-inputs, test-source-alias, type-check-coverage, route-envelope, role-word, org-identifier, watch-hint-literal, corpus-claim-drift, spec-parsed-alias, nul-bytes, dts-closure, published-files, published-readme-links, ci-filter-parity, platform-object-tenancy-census, plugin-teardown-shape, reference-carrier-shape, lint doc-formula-expressions / doc-security-posture, merge-driver, objectui-changeset, engine-double-contract, page-declaration-shape, type-source-resolution, vendor-version-stamps, skill-identifier-liveness, query-options-erasure, slot-lookup, where-matcher, widget-option-census, react-page-adapter-contract, refd-timer-probe, logger-receiver-detach, objectql-double-limit, driver-memory-census, dispatcher-error-vocabulary, authz-resolver; spec: api-surface, authorable-surface, docs, export-origins, strictness-ledger, liveness (viacheck:generated), exported-any, dual-source-exports, empty-state, entry-nameability, browser-reachable-entries, llms-txt, objectui-pin-citations, skill-refs, variant-docs, yaml-examples.check:type-check-debt(33 closure dependencies of the ledgered packages unbuilt; wants the whole./packages/*closure),check:dual-build-cjs-loads(44 packages without dist), speccheck:skill-examples(@objectstack/client-reactnot built). CI's.--ranreconciliation (✗ 18 of 97): all 18 are--self-testvariants of gate scripts this diff does not edit (pluscheck-dev-prereqs --self-testandrelease-rehearsal-clone --self-test); owed only when a gate script is edited, so declared to CI.Related cards
#15694 remains open; its fence ("do not implement getFallbackLocale() by hand") is discharged by this ruling — the engine lane consumes the declared fallback in its own card. #14882 is not addressed here beyond flipping the pins its PR left as they answered then; #14882 remains as the PM reads it, untouched by this PR.
Generated by Claude Code