Skip to content

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

Draft
os-project-manager wants to merge 5 commits into
mainfrom
claude/issue-15711-default-locale-authored-label
Draft

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
os-project-manager wants to merge 5 commits into
mainfrom
claude/issue-15711-default-locale-authored-label

Conversation

@os-project-manager

Copy link
Copy Markdown
Collaborator

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:

Ruled: A. ResolveOptions gains an optional defaultLocale?: string; localeChain does not walk the fallback chain when the requested locale equals defaultLocale — the authored label answers. The serving layer passes i18n.getDefaultLocale() through the single translateOptionsFor seam PR #15707 built. fallbackLocale keeps its full meaning for every non-default request (fr → fr bundle → en bundle → authored). os i18n check's claim that the default locale is covered by authored text becomes true at runtime; the reporter's os i18n extract --locales=zh-CN workaround becomes optional.

Second facet, ruled with it: fallbackChain ?? ['en'] becomes fallbackChain ?? [] — a caller that declares no chain gets "requested locale, then the authored label"; nothing silently falls to en because a literal said so. Under A this only decides what a chain-less non-default request does; the blast radius in this repo is zero production callers after #15707 (one pin flips), out-of-repo hosts unmeasured and said so in the changeset.

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

Mechanism hypotheses (dispatch §2), verified on 8e7653b35 (origin/main at claim time)

  • M1 — single seam: grep -n "translateMetadataDocument\|resolveMetadataTypeLabel\|resolveMetadataTypeDescription\|resolveMetadataFormLabels" packages/rest/src/rest-server.ts finds four resolver call sites (:3297, :3541, :3569-:3576, :9666 at the base), every one spreading or passing RestServer.translateOptionsFor(i18n, locale); no site builds { locale } by hand. Holds — no second seam.
  • M2 — packages/spec/src/contracts/i18n-service.ts:52 getDefaultLocale?(): string and :87 getFallbackLocale?(): 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.
  • M3 — the seam docblock's stale prohibition rewritten to the ruled rule, citing [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; the literal-['en'] sentence count in rest-server.ts is 0 after the edit.
  • M4 — both boot paths still collapse the declaration before constructing the service, untouched: packages/cli/src/commands/serve.ts:3029 and packages/plugins/plugin-dev/src/dev-i18n.ts:247 (fallbackLocale || defaultLocale || 'en').
  • M5 — pins: i18n-resolver.test.ts:3592 and :3603 (the chain-less third pin) and meta-i18n-declared-fallback-chain.test.ts §5, plus §4, which the second facet and the pass-through also flip (its own comment said a getDefaultLocale() derivation would). Consumers of the spec ResolveOptions type outside spec: grep -rln 'ResolveOptions\|fallbackChain' packages --include='*.ts' hits packages/rest/src/rest-server.ts and its test only (packages/runtime/src/security/resolve-execution-context.ts declares an unrelated local interface of the same name). objectui at the pin a472b07167a3 (.objectui-sha): git grep -l 'fallbackChain\|translateMetadataDocument\|ResolveOptions' a472b07167a3 hits only packages/i18n/src/utils/spec-formatters.ts and its test — objectui's own formatter-config key, not the spec type; control pickLocalized hits.
  • M6 — measured, see the changeset line above.
  • M7 — pnpm --filter @objectstack/spec check:generated at e22c25b9e and again at the final commit b7312986c: "✓ 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; the i18n-service.ts and file-i18n-adapter.ts docblocks that narrate what happened BEFORE #14882 keep their past tense.

Tests (final commit b7312986c)

  • Closure build under the shared verify lock: 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 at acbfe264b: 2 failed / 246 passed — the two KEY-BY-KEY pins above, received Open the assistant panel from the right edge… and Create 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).
  • The only two test files outside spec/rest that call the resolvers (grep -rln 'translateMetadataDocument\|resolveViewLabel\|resolveActionLabel\|resolveFieldLabel' packages examples --include='*.test.ts'): service-i18n/src/authored-translations.test.ts 16 passed; plugin-approvals/src/translations/approval-status-vocabulary.test.ts 6 passed.
  • pnpm --filter @objectstack/rest typecheck exit 0 (tsc + check:test-typecheck OK, 0 files in debt); pnpm --filter @objectstack/spec typecheck exit 0 (tsc, scripts program, check:test-typecheck OK, 54 files / 261 errors / 145 signatures held — ledger unchanged); spec check:test-typecheck re-run after the last test-file commit: same verdict.
  • Ablation (dist-resolved: packages/rest imports @objectstack/spec/system through exports, no vitest alias for spec — the ledger pair in check-test-source-alias.mjs): localeChain mutated back to opts?.fallbackChain ?? ['en'] with no short-circuit and a used string marker; on-disk proof injected=1 removed_left=0; pnpm --filter @objectstack/spec build emitted the ESM/CJS bundles ("Build success") and then its DTS step failed with TS6133: '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 file 4 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 check says the former, localeChain does the latter; the resolver's literal ['en'] default rides on the answer #15711 pins that depend on the rule; the fr control stayed green by design), rest seam file 6 failed | 13 passed (§4 legacy/undefined provider; §5 single read, app/list/types, header-less, shipped-bundle omitted key; the no-getDefaultLocale and fr/en controls stayed green by design). Restore: git checkout HEAD -- ABSOLUTE_PATH from an EXIT/INT/TERM trap, git diff HEAD empty, git hash-object equal to the HEAD blob 4c95369c… on both legs, git status --porcelain 0 lines; rebuild exit 0; preflight --absent: marker absent from all 217 built files, tree clean; spec 248 passed (248), rest 19 passed (19). Direction: red as predicted, no reversal.
  • Narrowing declared: TURBO_SCM_BASE=$BASE pnpm exec turbo ls --affected lists 75 packages (a packages/spec source 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.
  • Repo-level lint, delivered as a proven narrowing: pnpm exec eslint --no-inline-config --format json over the five TS files this branch touches (the full git diff --name-only 8e7653b35..HEAD TS set) — 5 files linted, 0 errors, 0 warnings. Population read from eslint.config.mjs (files: ['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}'] plus the packages/** blocks, all with parserOptions: { ecmaVersion, sourceType } only); invariance: the config states it runs no parserOptions.project and 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)

  • Green: check-adr-0087-registration, check-changeset-no-major, check-empty-changeset (all three run with --base 8e7653b35, the recorded base, rather than the moving origin/main the 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 (via check:generated), exported-any, dual-source-exports, empty-state, entry-nameability, browser-reachable-entries, llms-txt, objectui-pin-citations, skill-refs, variant-docs, yaml-examples.
  • NOT MEASURED (PREREQUISITE NOT MET, each says so itself): 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), spec check:skill-examples (@objectstack/client-react not built). CI's.
  • UNRUN by the --ran reconciliation (✗ 18 of 97): all 18 are --self-test variants of gate scripts this diff does not edit (plus check-dev-prereqs --self-test and release-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

…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
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation protocol:system tests tooling labels Sep 5, 2026
@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/rest, @objectstack/spec, touching 8 documentable anchor(s).

11 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/automation/email-templates.mdx (via II18nService (symbol, a top-level interface))
  • content/docs/kernel/services-checklist.mdx (via II18nService (symbol, a top-level interface), defaultLocale (symbol, a field of interface ResolveOptions))
  • content/docs/protocol/kernel/i18n-standard.mdx (via II18nService (symbol, a top-level interface), defaultLocale (symbol, a field of interface ResolveOptions))
  • content/docs/protocol/kernel/index.mdx (via II18nService (symbol, a top-level interface))
  • content/docs/protocol/objectui/actions.mdx (via /forms/:slug (route, bridged from symbol translateOptionsFor — its route source's handler names it))
  • content/docs/protocol/objectui/layout-dsl.mdx (via /forms/:slug (route, bridged from symbol translateOptionsFor — its route source's handler names it))
  • content/docs/ui/actions.mdx (via /forms/:slug (route, bridged from symbol translateOptionsFor — its route source's handler names it))
  • content/docs/ui/forms.mdx (via /forms/:slug (route, bridged from symbol translateOptionsFor — its route source's handler names it))
  • content/docs/ui/public-data-collection.mdx (via /forms/:slug (route, bridged from symbol translateOptionsFor — its route source's handler names it))
  • content/docs/ui/translations.mdx (via defaultLocale (symbol, a field of interface ResolveOptions))
  • content/docs/ui/views.mdx (via /forms/:slug (route, bridged from symbol translateOptionsFor — its route source's handler names it))

4 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/implementation-status.mdx (via RestServer (symbol, a top-level class))
  • content/docs/releases/v12.mdx (via RestServer (symbol, a top-level class))
  • content/docs/releases/v16.mdx (via RestServer (symbol, a top-level class))
  • content/docs/releases/v17.mdx (via II18nService (symbol, a top-level interface), RestServer (symbol, a top-level class))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 131 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json ea03c7c16fc27dcd489378faf8ba99abf9ae7124packageMentionDocs.

Which tree this was computed on

This run read content/docs from dd4a7beabc5ca3921831db7bcae24a7bf0fee298 — the merge of head 2c453f0b2680953c6af620c2f5f79b2a770710dc into base ea03c7c16fc27dcd489378faf8ba99abf9ae7124, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs ea03c7c16fc27dcd489378faf8ba99abf9ae7124 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment