Skip to content

fix(spec,core): resolve locales through the platform table's read aliases, so a zh-TW / zh-HK reader gets zh-Hant - #30601

Merged
objectstack-fleet[bot] merged 10 commits into
mainfrom
claude/project-thread-mros1u
Nov 5, 2026
Merged

objectstack-fleet[bot] merged 10 commits into
mainfrom
claude/project-thread-mros1u

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

domain:spec#3 · os-sales · session_01L6bhgTqqeHSwZYJmApbDCa

Requested by Sales · project thread

Fixes #30538

Clause-②: yes (widening)
Metadata-protocol: semantic
Skills-docs: follow-up #30603 — skills/objectstack-i18n/SKILL.md (its in-memory fallback paragraph restates the four-step ladder without the table step; skills/** is a Tier H surface, so it is not edited here)

Branch: claude/project-thread-mros1u (the card thread's platform-assigned branch, restated in the claim 6663692830; not the claude/issue-30538-… convention).

维护者速读

  • 改了什么:服务端两个语言解析器(spec 的 resolveBundleLocale、core 的 resolveLocale)改为同一份实现,先按平台语言表的读别名找同一语言,再退到基础语言和第一个变体。
  • 为什么改:繁体读者(zh-TW / zh-HK / zh-MO)原来拿到第一个注册的 zh-* 包,也就是简体;各 translations 包正在补 zh-Hant,补上后繁体读者仍看简体。
  • 风险与回滚:只改「哪一个已注册的包来答」,原来能匹配的请求仍能匹配;表里没有的代码(es-MX、en-GB)照旧。回滚即还原这一个 PR。
  • 先看:packages/spec/src/system/i18n-resolver.ts 的 resolveLocaleAmong 第 3 步;packages/core/src/fallbacks/memory-i18n.ts;packages/core/src/fallbacks/fallbacks.test.ts 的共享用例表。
  • 两分钟验证:pnpm --filter @objectstack/core exec vitest run src/fallbacks/fallbacks.test.ts,预期 107 例全绿,其中共享用例表对两个解析器各跑一遍,另有一例断言 resolveOfferedLocale 与 resolveLocale 答案相同。
  • 需要你做的:无,席位落地。

What changes

  • @objectstack/spec/system gains resolveLocaleAmong(requested, available), the platform's one locale-matching ladder over a list: exact, case-insensitive, the same platform locale table language, base language, first registered variant.
  • The new step maps the requested code through platformLocaleTagOf (the table's read aliases, the one alias list; no second list is added) and picks the key spelled as that table tag, else the first key the table maps to it. So zh-TW, zh-HK, zh-MO reach a registered zh-Hant, and zh, zh-CN, zh-SG, zh-Hans reach zh-CN, in either registration order.
  • resolveBundleLocale keeps its exact fast path and then runs resolveLocaleAmong over the bundle's keys. Core's resolveLocale is resolveLocaleAmong (core already imports @objectstack/spec/system, so there is no new import edge and no cycle). The two answers cannot drift.
  • Docblocks that promised the old order are corrected: resolveBundleLocale, core's resolveLocale, platformLocaleTagOf (its paragraph stated that resolveBundleLocale({ 'zh-Hant': …, 'zh-Hans': … }, 'zh') answers zh-Hant, which this PR makes false), the email ladder of record (SendTemplateInput.locale in packages/spec/src/contracts/email-service.ts), its copy in content/docs/automation/email-templates.mdx, the stack.zod.ts email floor docblock, and plugin-email's ladder comment in email-service.ts.
  • api-surface/system.json and export-origins/system.json regenerated for the new export.
  • Changeset: @objectstack/spec minor, @objectstack/core minor.

Tests

  • packages/spec/src/system/i18n-resolver.test.ts, new top-level describe resolveBundleLocale — the platform table language step: zh-TW / zh-HK / zh-MO / zh-tw / zh-Hant → zh-Hant and zh-CN / zh / zh-SG / zh-Hans / zh-cn → zh-CN in both registration orders; table-tag spelling preferred; a bare zh key counts as Simplified; ja → ja-JP, es → es-ES, en → en-US against alias-spelled bundles; unknown codes (es-MX, en-GB, pt-BR over pt-PT, fr, empty bundle) fall back as before.
  • packages/plugins/plugin-email/src/template-locale-fold.test.ts, a moved pin: it asserted that a zh-MO recipient of a zh-TW + zh-HK + zh-CN bundle got the Simplified zh-CN row, which is this card's defect. It now asserts the Traditional zh-HK row in both list orders (plus zh-TW → zh-Hant and zh → zh-CN over en-US + zh-CN + zh-Hant), and the variant-expansion pin moves to a code the table does not know (es-CO → es-AR, both list orders).
  • packages/core/src/fallbacks/fallbacks.test.ts, new top-level describe resolveLocale and resolveBundleLocale — one shared case list: one 20-row table run against both resolvers, plus the in-memory i18n service serving a zh-HK reader the zh-Hant bundle and a zh reader the zh-CN one. The existing resolveLocale pins (including zh-TW over ['en', 'zh'] → zh) are unchanged and green.

Acceptance notes

  • Declarations differ from the claim's expectation, measured. Clause-②: yes (widening): the shared implementation needs a list-shaped entry point, so @objectstack/spec/system publishes one new function (no export removed or narrowed). Metadata-protocol: semantic: a translation bundle key or email template row an author writes as zh-Hant now serves zh-TW / zh-HK / zh-MO readers it did not, a bundle registering zh-Hant before zh-CN now serves zh readers the Simplified key, and the published resolveBundleLocale docblock promised the old four-step order. check-changeset-no-major reading in Verification.
  • PM assumption disproved: "before step 4" is not enough. At 2ffdb649, resolveBundleLocale({ zh: {}, 'zh-Hant': {} }, 'zh-TW') answered 'zh': the base-language step hands a Traditional reader a bare zh key, which the table reads as Simplified. The table step therefore runs before base language (step 3 of 5). It only adds earlier answers, so no request that matched before stops matching.
  • File surface beyond the claim's list. Docblocks only: platform-locales.ts (the platformLocaleTagOf paragraph; the table and aliases are untouched), contracts/email-service.ts, stack.zod.ts, content/docs/automation/email-templates.mdx, plugin-email's email-service.ts comment. Each restated the old order in a way this change makes false for a bundle that holds both Chinese scripts. One test: plugin-email's template-locale-fold.test.ts, whose pin asserted the defect (see Tests).
  • Restatements left as they are (each still true for the tables they describe today): docs/qa/platform-checklist/areas/integration-system.json acceptance[3] copies the email ladder (a revisioned QA file; the showcase bundle carries zh-CN only, so its verify line is unchanged); packages/plugins/plugin-auth/src/idp-locale-claim.ts's resolveLocale summary (plugin-auth's copy tables are keyed en / zh-CN / ja-JP / es-ES, whose answers do not move). Carrier: the spec 席 3 seat.
  • The Console's pickRenderableLocale (ui/packages/app-shell/src/hooks/useUserLocale.ts) still runs the old four steps; it is i18n phase 2 (Console UI): ui/packages/i18n reads PLATFORM_LOCALES, adds zh-Hant, and moves pack keys to table tags #30511's surface and not touched here.
  • Merged with main (fix(service-settings): judge OS_LOCALIZATION_LOCALE once the boot's locale list is whole, and one locale matcher for Setup and the IdP claim #30589). The merge in 51ace3fa met fix(service-settings): judge OS_LOCALIZATION_LOCALE once the boot's locale list is whole, and one locale matcher for Setup and the IdP claim #30589's new resolveOfferedLocale in packages/core/src/fallbacks/memory-i18n.ts, a second matching ladder. The resolution makes it return resolveLocaleAmong(...), so the repo keeps one ladder. fix(service-settings): judge OS_LOCALIZATION_LOCALE once the boot's locale list is whole, and one locale matcher for Setup and the IdP claim #30589's pin in fallbacks.test.ts that resolveLocale answers zh-Hans for zh-TW asserted this card's defect; it now expects zh-Hant, and a new case asserts that resolveOfferedLocale and resolveLocale give the same answer. The contract review's delta (6667101706) measured the one answer that moves for fix(service-settings): judge OS_LOCALIZATION_LOCALE once the boot's locale list is whole, and one locale matcher for Setup and the IdP claim #30589's callers, which are Setup's default-language options (service-settings/src/localization-locale-options.ts :125) and the IdP locale claim (plugin-auth/src/idp-locale-claim.ts :148). It is a tie-break inside the table step: when a list offers two codes of one table language, the code spelled as the table tag wins over the first one offered (zh over ['zh-CN', 'zh-Hans'] answers zh-Hans, where main answered zh-CN). Admission never changes, no shipped bundle registers such a pair, and the @objectstack/core changeset carries the sentence. Review findings S1 (three spots in email-template.zod.ts, its reference page regenerated) and S2 (the builtinSystemFieldLabel and validation-message.ts docblocks) landed in 16eafb26.
  • Observation, not filed: resolveBundleLocale's exact fast path reads bundle[requested], so over a plain-object bundle a request spelled like an Object.prototype member (constructor) answers that name. Unchanged by this PR and not measured at a public door; resolveLocaleAmong uses includes and does not do this.

Verification

Round 2, at head 51ace3fa (S1 and S2, plus origin/main merged in), after a full build (119/119):

  • CI: 57 success, 8 skipped, 0 failed.
  • Tests, all exit 0: @objectstack/spec 816 files / 25719 tests + 1 todo; @objectstack/core 135 / 3075 (fallbacks.test.ts 107); @objectstack/service-settings 69 / 1178; @objectstack/plugin-email 48 / 801; plugin-auth's locale tests 6 / 202.
  • Typecheck exit 0 for spec, core and service-settings; spec check:generated exit 0.
  • dispatch-gates --commands: 109 derived, all exit 0; --ran: 109 run, 0 NOT-MEASURED, 0 UNRUN.

Round 1 readings below are at head 9f692b76 unless named.

Repro before the change (origin/main 2ffdb64)
resolveBundleLocale({ en, zh-CN, zh-Hant }, 'zh-TW')  -> zh-CN
resolveLocale('zh-HK', ['en', 'zh-CN', 'zh-Hant'])     -> zh-CN
resolveBundleLocale({ zh-Hant, zh-CN }, 'zh')          -> zh-Hant
resolveBundleLocale({ zh, zh-Hant }, 'zh-TW')          -> zh
Package suites and typecheck
  • pnpm --filter @objectstack/spec test: Test Files 815 passed (815), Tests 25668 passed, 1 todo. pnpm --filter @objectstack/spec typecheck: exit 0.
  • pnpm --filter @objectstack/core test: Test Files 135 passed (135), Tests 3070 passed. pnpm --filter @objectstack/core typecheck (tsc, examples, test-typecheck): exit 0.
  • pnpm --filter @objectstack/plugin-email test: Test Files 48 passed (48), Tests 796 passed. typecheck: exit 0.
  • spec check:generated named api-surface/ and export-origins/ stale; both regenerated and committed.
  • eslint --no-inline-config on the 9 changed source and test files: exit 0. check-nul-bytes: exit 0.
Consumers reached (targeted locale suites, after a full build 119/119)
  • @objectstack/runtime (the /i18n REST door's resolveLocale, hook refusal catalogue, flow run locale): 8 files, 126 passed.
  • @objectstack/plugin-auth (IdP locale claim, MFA and IP-range refusal copy): 6 files, 201 passed.
  • @objectstack/service-settings (currency lock, locale options copy): 2 files, 38 passed.
  • @objectstack/objectql (validation message negotiation): 4 files, 44 passed.
  • @objectstack/plugin-approvals: 2 files, 25 passed. @objectstack/plugin-audit: 2 files, 19 passed.
  • @objectstack/plugin-email (template locale fold): 4 files, 98 passed after the moved pin above (1 failed before it: the defect pin).
  • @objectstack/service-automation: 3 files, 43 passed. @objectstack/service-messaging: 2 files, 30 passed. @objectstack/service-i18n: 2 files, 52 passed.
Ablation (remove the table-language step)
  • Mutation through scripts/ablation-replace.mjs --delete on the step-3 block of resolveLocaleAmong (anchor 1 → 0, blob 97078210 → 47872494), spec rebuilt, ablation-dist-preflight --absent: marker absent from all 592 built files.
  • spec i18n-resolver.test.ts: 10 failed / 482 passed (every zh-TW / zh-HK / zh-MO / zh / zh-SG / zh-Hans case of the new describe). core fallbacks.test.ts: 15 failed / 87 passed (the zh rows of the shared case list for both resolvers, and the in-memory service case).
  • Restore: blob after restore 97078210 equals HEAD, git diff HEAD empty, tree clean; rebuilt, preflight marker present in 2 built files; spec 492 passed, core 102 passed.
Derived gates (dispatch-gates --commands at 9f692b7)
  • 109 derived families run, every one exit 0. --ran reconciliation: 109 derived, 109 run, 0 NOT-MEASURED, 0 UNRUN (a derived zero).
  • check-changeset-no-major --base origin/main: no major bump; the level axis is PR-scoped and reads this body in CI.
  • check-adr-0087-registration --base origin/main: no declared-breaking changeset.
  • Families CI runs beyond the derived set: read from this PR's check-runs.

Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_01L6bhgXFwZkpwurAYNxGpAb

…plate row

The fold pin asserted a zh-MO recipient of a zh-TW + zh-HK + zh-CN bundle got
the Simplified row; the table-language step now answers zh-HK. The variant
expansion pin moves to a code the table does not know (es-CO).

Claude-Session: https://claude.ai/code/session_01L6bhgXFwZkpwurAYNxGpAb
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation protocol:system tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 5, 2026
@github-actions

github-actions Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/core, @objectstack/plugin-email, @objectstack/spec, touching 7 documentable anchor(s). ⚠️ 5 changed file(s) yielded no anchor (packages/spec/api-surface/system.json, packages/spec/export-origins/system.json, packages/spec/src/stack.zod.ts, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/automation/email-templates.mdx (via EmailTemplateDefinitionSchema (symbol, a top-level const), SendTemplateInput (symbol, a top-level interface), resolveBundleLocale (symbol, a top-level function))
  • content/docs/kernel/runtime-services/email-service.mdx (via SendTemplateInput (symbol, a top-level interface))

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

  • content/docs/releases/v17/17-3.mdx (via SendTemplateInput (symbol, a top-level interface))

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
  • 5 changed file(s) yielded no anchor (packages/spec/api-surface/system.json, packages/spec/export-origins/system.json, packages/spec/src/stack.zod.ts, …) — pages documenting those are invisible to this run
  • 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); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 103 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.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 180 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 ffeda137b8412b11c909a6d00357f46e0cd555d2 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from d705b033206cd021842ba64830ad066dad9e2eaf — the merge of head 4f96f9a7aebec5d4d4b3f7992e18674717e6b7b5 into base ffeda137b8412b11c909a6d00357f46e0cd555d2, 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 d705b033206cd021842ba64830ad066dad9e2eaf && git checkout d705b033206cd021842ba64830ad066dad9e2eaf
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ffeda137b8412b11c909a6d00357f46e0cd555d2 4f96f9a7aebec5d4d4b3f7992e18674717e6b7b5 && git checkout -B drift-repro ffeda137b8412b11c909a6d00357f46e0cd555d2 && git merge --no-ff 4f96f9a7aebec5d4d4b3f7992e18674717e6b7b5

node scripts/docs-audit/affected-docs.mjs --json ffeda137b8412b11c909a6d00357f46e0cd555d2

⚠️ 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 ffeda137b8412b11c909a6d00357f46e0cd555d2 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#3 · os-sales · session_01L6bhgTqqeHSwZYJmApbDCa

Contract-review-claim: session_01L6bhgTqqeHSwZYJmApbDCa · head 9f692b7 · 2026-11-05T17:42Z

Full review at CONTRACT_REVIEW_TIER (published schema face: system/i18n-resolver.ts, system/platform-locales.ts, contracts/email-service.ts, stack.zod.ts), run by an isolated at-tier subagent of this seat.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#3 · os-sales · session_01L6bhgTqqeHSwZYJmApbDCa

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 9f692b766ff8c2fcaaa11986d827a28a62e92e3e
Local-runs: none
Scope: packages/spec/src/contracts/email-service.ts, packages/spec/src/stack.zod.ts, packages/spec/src/system/i18n-resolver.ts, packages/spec/src/system/platform-locales.ts + packages/core/src/fallbacks/memory-i18n.ts, packages/plugins/plugin-email/src/email-service.ts, packages/plugins/plugin-email/src/template-loader.ts, packages/spec/src/system/email-template.zod.ts, packages/spec/src/system/validation-message.ts, packages/spec/src/system/operation-message.ts, packages/runtime/src/domains/i18n.ts, packages/objectql/src/engine-validation-messages.ts, packages/plugins/plugin-auth/src/idp-locale-claim.ts, ui/packages/app-shell/src/hooks/useUserLocale.ts, ruling F of #29349 (comment 6580034558) and its tracking card #29428, follow-up card #30603, the three test files of the diff

Inputs read: card #30538 (body, triage 6661339702, re-route 6661724543, claim 6663592658, amend 6663692830, dev report 6664917386); PR #30601 body, file list (13 files, +223/−53) and the net diff of 2ffdb649…main → 9f692b76; the check-runs on the head. Read-only: nothing built, run or re-run. Dispatch order and the dispatching seat's conclusions were not inputs.

Gate verdicts, read at 2026-11-05T18:06Z (the last run completed 18:06Z): 69 check-runs on 9f692b76, 59 success, 10 skipped, 0 failed, 0 pending. Every required context is success — Lint & Repo Gates, TypeScript Type Check, Test Core (8/8 shards), Build Core, Governed Surface Queue Guard, Type Check UI, Test UI (16/16 shards), Build & E2E UI, Live E2E UI — with Lint UI skipped (no ui/ path in the diff) and Check Changeset success. These conclusions are the derived-gate verdicts of this record; nothing was re-run.

① Derived judgments

  • Accept set of both resolvers unchanged — right. resolveLocaleAmong inserts the table step between case-insensitive and base-language and returns only on a hit; steps 4 and 5 are the old steps 3 and 4 (core's requestedLocale.split('-')[0].toLowerCase() and l.split('-')[0].toLowerCase() are the same predicates as lower.split('-')[0] and code.toLowerCase().split('-')[0]), and the empty-list early return is kept. No request that matched at 2ffdb649 stops matching; only which registered key answers moves.
  • Which key answers moves, in the table's direction — right. Traced in the live tree: zh-TW / zh-HK / zh-MO → zh-Hant; zh / zh-CN / zh-SG / zh-Hans → zh-CN (or a zh-Hans-spelled key) in either registration order; zh-TW over {zh, zh-Hant} → zh-Hant (was zh); also en → en-US over an en-GB-first list, es → es-ES over es-MX-first, pt → pt-BR over pt-PT-first (each was first-registered-variant before). Every one of those is the alias the table declares, and the changeset names the Chinese rows.
  • Public surface: one export added, none removed or narrowed — right. resolveLocaleAmong(requested: string, available: readonly string[]) leaves @objectstack/spec/system through export * from './i18n-resolver'; api-surface/system.json and export-origins/system.json carry it. resolveBundleLocale's signature is unchanged; core's resolveLocale(string, string[]) is unchanged and still exported from @objectstack/core (fallbacks/index.ts:11).
  • No new dependency edge — right. memory-i18n.ts already imported normalizeSupportedLocales from @objectstack/spec/system; platform-locales.ts imports nothing from i18n-resolver.ts, so the new ./platform-locales.js import is acyclic.
  • Every caller moves with it — right, and enumerated. 17 non-test call sites at the head: spec pickData (:596), builtinSystemFieldLabel (:4488), validation-message.ts:394, operation-message.ts:332; runtime /i18n door (domains/i18n.ts:264,292) and hook-refusal-catalogue.ts:113; objectql negotiatedMessageLocale; plugin-approvals, plugin-audit, service-automation engine.ts:1462, service-messaging notification-text.ts:353; plugin-email email-service.ts:1512; plugin-auth ×5 (idp-locale-claim.ts:146 and four COPY_LOCALES pickers); service-settings ×2; core createMemoryI18n.t(). All are pass-through (?? requested or exact-load of the answer); none re-implements a ladder. A zh-TW IdP claim with zh-Hant supported now stores zh-Hant — the card's intent at the one write site.

② Semver level

  • @objectstack/spec minor, @objectstack/core minor — right. Both packages move (i18n-resolver.ts, memory-i18n.ts); a semantic kind takes at least minor on a moved package (Post-Task Checklist 3). The changeset carries both declaration lines; the PR body carries the same two.
  • Clause-②: yes (widening) — right on the tree. A new export is a widening of the published surface; nothing is removed, so (narrowing) does not apply and the claim's expected no was wrong once the shared ladder needed a list-shaped entry point.
  • Metadata-protocol: semantic — right. Judged on which registered bundle answers a reader, not on the body's account: a translation bundle or email_template row an author keys zh-Hant now serves zh-TW / zh-HK / zh-MO readers it did not, and a bundle registering zh-Hant before zh-CN now serves zh readers the Simplified key where it served Traditional. The meaning of an authored key changed; none (patch) would under-declare it. The claim's expected none was a reading before the measurement.

③ Boundary flags

  • Table step placed BEFORE base-language (dev flag) — right. At 2ffdb649, resolveBundleLocale({ zh: {}, 'zh-Hant': {} }, 'zh-TW') answers zh through the base-language step, and the table reads a bare zh key as Simplified; "before step 4" alone would have kept the defect for that bundle shape. With the step at position 3 the new pins hold, and the pre-existing pin zh-TW over ['en', 'zh'] → zh still holds because step 3 finds no zh-Hant-mapped key and falls to base-language. Correct placement, correctly reasoned.
  • The moved plugin-email pin (dev flag) — right. The old assertion zh-MO over [zh-TW, zh-HK, zh-CN] → zh-CN pinned the card's defect; it now asserts zh-HK in both list orders. The variant-expansion pin keeps its property on codes the table does not know (es-CO → es-AR, both orders): the registry loader sorts the bundle's tags (template-loader.ts:175-178), so "ascending tag order" still holds and the pin is not weakened, only moved off the defect.
  • Skills-docs follow-up skills/objectstack-i18n: the in-memory fallback's locale ladder omits the platform locale table step that PR #30601 adds #30603 — right. skills/** is Tier H and absent from the PR's file list; skills/objectstack-i18n: the in-memory fallback's locale ladder omits the platform locale table step that PR #30601 adds #30603 is open, scoped to skills/objectstack-i18n/SKILL.md:376-377, and triggered on this PR's merge. The PR body's Skills-docs: line names it.
  • open_questions: none in report 6664917386 — nothing to answer.
  • Deviations judged: declarations moved (② above, right); file surface beyond the claim is docblocks and one defect pin (right, but incomplete — ④ S1/S2); gate runner restarted on the final head (fine); consumer suites run targeted, CI Test Core the authority (the check-runs answer it, line above); branch claude/project-thread-mros1u is the platform exception restated in claim 6663692830 (accepted).
  • out_of_scope_findings judged: the skill paragraph became skills/objectstack-i18n: the in-memory fallback's locale ladder omits the platform locale table step that PR #30601 adds #30603 (filed, right); the QA checklist integration-system.json acceptance[3] and plugin-auth idp-locale-claim.ts:24 restatements are true for today's four-locale tables and belong to a prose sweep (④ S3); the Object.prototype fast-path read is pre-existing and not reached from a list-based door (④ S6, a card).
  • Console divergence (escalated, not a dev flag): pickRenderableLocale is i18n phase 2 (Console UI): ui/packages/i18n reads PLATFORM_LOCALES, adds zh-Hant, and moves pack keys to table tags #30511's surface (④ S4).

④ Design judgment

Judged on the live tree against the scenario the card accepts: a zh-TW / zh-HK / zh-MO reader with both zh-CN and zh-Hant registered in either order; zh / zh-CN / zh-SG reaching an alias-spelled zh-CN; ja → ja-JP, es → es-ES; an unknown tag falling back as before.

packages/spec/src/system/i18n-resolver.ts — holds. resolveLocaleAmong step 3 maps the request through platformLocaleTagOf (case-folded, Map-backed, closed list) and picks a key spelled as the table tag, else the first key the table maps to that tag. Break attempts: case variants (ZH-TW, a zh-hant-keyed bundle, ja-jp) all fold correctly because both the request and each key go through the same case-insensitive index; a bundle keyed by a table tag vs an alias spelling ({zh-TW, zh-Hant} for zh-HK → zh-Hant; {zh-CN, zh-TW} for zh-Hant → zh-TW) answers the table tag first, then the first alias — pinned; an odd bundle holding both zh-CN and zh-Hans now answers zh with zh-Hans instead of first-registered, which the docblock states; pt-BR over {pt-PT, pt} → pt and pt-PT over {pt-BR} → pt-BR as before; an empty list → undefined; unknown tags (es-MX, en-GB, fr) never enter step 3. resolveBundleLocale keeps its exact fast path and delegates; the duplicated exact test is harmless. The email ladder (email-service.ts:1494-1520) folds the recipient tag then the declared fallback through the same function with a tried set, so a Traditional recipient now loads the Traditional row and the en-US floor is unchanged. Two findings: S5 script-subtag spellings (zh-Hant-TW, zh-Hans-CN) are not on the closed alias list, so step 3 is skipped and the first registered zh-* key still answers them over a two-script bundle — the card's defect for a spelling the table deliberately does not name (platform-locales.ts:66-67); a table change is ruling F territory (#29428), not this PR's. S2 the builtinSystemFieldLabel docblock at :4473-4476 still says "exact → case-insensitive → base language → variant" inside this very file.

packages/spec/src/system/platform-locales.ts — holds. Docblock-only; the table and the aliases are byte-unchanged. The corrected platformLocaleTagOf paragraph is accurate against the new ladder (it is asked of the request and of each key; pt means pt-BR and pt-BR is still served by a pt key; a two-script bundle answers by script in any key order), and the sentence it replaced (zh and zh-CN answering zh-Hant by key order) was exactly the defect.

packages/spec/src/contracts/email-service.ts — holds. SendTemplateInput.locale rung 1 now names the five steps in the order resolveLocaleAmong runs them, with the qualified example (zh-TW → zh-CN only when the bundle has no Traditional row). Matches email-service.ts:1494-1520 and the fold test.

packages/spec/src/stack.zod.ts — holds, with one sibling missed. The warnEmailTemplateLocaleFloor docblock names the five steps and defers the full ladders to SendTemplateInput.locale. S1: the same package's email-template.zod.ts restates the four-step ladder three times — the EMAIL_TEMPLATE_FLOOR_LOCALE docblock (:38-39), the EmailTemplateDefinitionSchema.locale docblock (:181-182, "zh-TW reaches zh-CN") and that key's served .describe() (:224, the same sentence), which gen:docs projects into content/docs/references/system/email-template.mdx:51. That is the sentence this PR corrects in contracts/email-service.ts, stack.zod.ts and the mdx, left standing on the schema an author reads it from.

Must-fix: none.

Should-fix:

Implemented-by: claude/project-thread-mros1u
Reviewed-by: session_01L6bhgTqqeHSwZYJmApbDCa

VERDICT: PASS


Generated by Claude Code

resolveOfferedLocale (from main) now delegates to resolveLocaleAmong, so the
offered-locale matcher and the resolvers run one ladder.

Claude-Session: https://claude.ai/code/session_01L6bhgXFwZkpwurAYNxGpAb
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#3 · os-sales · session_01L6bhgTqqeHSwZYJmApbDCa

Contract review — delta

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 51ace3fae5c97dd050177739eb5bd2c390dd2a2a
Local-runs: none
Scope: packages/spec/src/contracts/email-service.ts, packages/spec/src/stack.zod.ts, packages/spec/src/system/i18n-resolver.ts, packages/spec/src/system/platform-locales.ts, packages/spec/src/system/email-template.zod.ts, packages/spec/src/system/validation-message.ts + packages/core/src/fallbacks/memory-i18n.ts (the merge resolution), packages/core/src/fallbacks/fallbacks.test.ts, packages/services/service-settings/src/localization-locale-options.ts, packages/plugins/plugin-auth/src/idp-locale-claim.ts, the .changeset/30456-default-language-boot-window-one-matcher.md #30589 shipped, content/docs/references/system/email-template.mdx

Delta reviewed: the three commits since 9f692b76 (record 6665489168) — 5e21830b1 (S1/S2 docblocks), 16eafb266 (regenerated reference page) and the merge 51ace3fae of origin/main (merge-base now 6701c08a). Net diff against main: 16 files, +265/−91; the three new paths are email-template.zod.ts, validation-message.ts and the reference mdx. The earlier record's ①–④ stand for everything the delta does not touch. Read-only; nothing built or run.

Gate verdicts on 51ace3fa, read at 2026-11-05T19:21Z: 65 check-runs, 57 success, 8 skipped, 0 failed, 0 pending — every required context success (Lint & Repo Gates, TypeScript Type Check, Test Core, Build Core, Governed Surface Queue Guard, Type Check UI, Test UI, Build & E2E UI, Live E2E UI, Check Changeset); Lint UI skipped.

S1 and S2 — true at this head

  • S1 email-template.zod.ts: the EMAIL_TEMPLATE_FLOOR_LOCALE docblock (:38-40), the EmailTemplateDefinitionSchema.locale docblock (:182-184, :188-189) and the served .describe() (:227-228) now name the five steps and say "zh-TW reaches zh-Hant, else zh-CN". True against resolveLocaleAmong as run by the email ladder (email-service.ts:1512): step 3 reaches a zh-Hant row when the bundle has one, step 5 reaches zh-CN otherwise. The regenerated content/docs/references/system/email-template.mdx:51 carries the describe byte-for-byte; TypeScript Type Check (which runs check:generated) is success on this head.
  • S2: builtinSystemFieldLabel (i18n-resolver.ts:4474-4476) states the five-step order it runs through resolveBundleLocale at :4488; renderValidationMessage (validation-message.ts:359-361) names resolveBundleLocale and the same order, which it calls at :394. Both true.

The merge resolution in memory-i18n.ts — a contract change, judged

main (#30589, 1c5cee673) added resolveOfferedLocale(requested, offered): exact ?? case-insensitive, then the FIRST offered code whose platformLocaleTagOf equals the request's, then resolveLocale's base-language and variant steps. The resolution makes it return resolveLocaleAmong(requested, offered), the same function resolveLocale now is.

  • Behaviour-identical for every input fix(service-settings): judge OS_LOCALIZATION_LOCALE once the boot's locale list is whole, and one locale matcher for Setup and the IdP claim #30589's callers pass? Identical on every step but one tie-break. Steps 1, 2, 4 and 5 are the same predicates (main's step 2 came before base-language too, so the step order is unchanged). The one difference is inside the table step: main took the first offered code of the request's table language; resolveLocaleAmong first takes a code spelled as the table tag itself (case-folded), then the first code the table maps to it. The two differ only when the offered list holds two or more codes of ONE table language with the tag-spelled one not listed first: zh or zh-CN over ['zh-CN', 'zh-Hans'] answers zh-Hans (main: zh-CN); zh-HK over ['zh-TW', 'zh-Hant'] answers zh-Hant (main: zh-TW). Whether a request is admitted at all never changes; which of two same-language codes answers it does.
  • Callers, all read at this head: packages/services/service-settings/src/localization-locale-options.ts:120 (admits: existence only — unaffected) and :125 (matched, the index of the offered option a stored second spelling is shown under — moved only by the tie-break above); packages/plugins/plugin-auth/src/idp-locale-claim.ts:148 (resolveIdpLocale: the stored sys_user.locale — moved only by the tie-break above). No other caller in packages/** or ui/**; resolveOfferedLocale stays exported from @objectstack/core (fallbacks/index.ts:11). localization-locale-options.ts:93 also calls resolveLocale directly, to pick the "not offered" copy's language over its own four-locale COPY_LOCALES table — an alias-spelled table whose answers the table step does not move (the earlier record's ja → ja-JP / es → es-ES rows).
  • Different answers than main, beyond the intended zh-TW → zh-Hant fix: one class, the tie-break on a deployment that offers both an alias spelling and the table tag of one language (zh-CN beside zh-Hans, zh-TW beside zh-Hant). No shipped bundle registers such a pair (i18n phase 3 (platform metadata and built-in emails): extraction and bundle checks read PLATFORM_LOCALES; backfill the seven missing locales #30512 registers zh-CN + zh-Hant), and where one does, answering the table's own tag is the table's rule (platform-locales.ts header: new writes use the table tag). Accepted as the single-ladder consequence the resolution intends, not a defect. Also note resolveLocale (main's four steps) and resolveOfferedLocale (main's five) are now one function, so the /i18n door, the in-memory t() and the two fix(service-settings): judge OS_LOCALIZATION_LOCALE once the boot's locale list is whole, and one locale matcher for Setup and the IdP claim #30589 doors can no longer disagree — the claim the new docblock makes is true.
  • The changed pin (fallbacks.test.ts:649-651): resolveLocale('zh-TW', ['zh-Hans', 'zh-Hant']) was pinned at zh-Hans by fix(service-settings): judge OS_LOCALIZATION_LOCALE once the boot's locale list is whole, and one locale matcher for Setup and the IdP claim #30589 as a demonstration of resolveLocale's shortfall; that is this card's defect, and the pin now reads zh-Hant. Right. The added case (:631-641) asserts resolveOfferedLocale and resolveLocale agree on five rows, which is trivially true of one function but pins the delegation against a future re-fork. The shared 20-row case list is unchanged.

Changeset coverage

  • No file in service-settings or plugin-auth changes; the tie-break reaches them through their @objectstack/core dependency, and @objectstack/core carries a minor changeset in this PR. Under Post-Task Checklist 3 a changeset keys on a package the diff moves, so none is owed for those two. fix(service-settings): judge OS_LOCALIZATION_LOCALE once the boot's locale list is whole, and one locale matcher for Setup and the IdP claim #30589's own unreleased changeset (30456-default-language-boot-window-one-matcher.md) describes resolveOfferedLocale's order as exact/case → table language → base/variant, which stays true.
  • Should-fix (this PR, prose): .changeset/30538-locale-table-read-aliases.md says "resolveBundleLocale and resolveLocale now run one ladder" and "every caller of either function moves with it", but does not name resolveOfferedLocale, the third function the merged tree routes through resolveLocaleAmong, nor the tie-break above (table-tag spelling preferred over first-listed). One sentence naming both keeps the published CHANGELOG.md sentence true for the Setup default-language and IdP-claim doors.

Declarations

  • Clause-②: yes (widening) — still right: one export added (resolveLocaleAmong), none removed; resolveOfferedLocale's signature and export are unchanged.
  • Metadata-protocol: semantic — still right, for the reasons of record 6665489168; the tie-break adds no new kind.
  • @objectstack/spec minor, @objectstack/core minor — still right; the merge moves no further package of this PR's.

Findings

Implemented-by: claude/project-thread-mros1u
Reviewed-by: session_01L6bhgTqqeHSwZYJmApbDCa

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#3 · os-sales · session_01L6bhgTqqeHSwZYJmApbDCa

Contract review — delta

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 4f96f9a7aebec5d4d4b3f7992e18674717e6b7b5
Local-runs: none
Scope: packages/spec/src/contracts/email-service.ts, packages/spec/src/stack.zod.ts, packages/spec/src/system/i18n-resolver.ts, packages/spec/src/system/platform-locales.ts, packages/spec/src/system/email-template.zod.ts, packages/spec/src/system/validation-message.ts + .changeset/30538-locale-table-read-aliases.md (the only file this delta touches), packages/core/src/fallbacks/memory-i18n.ts and packages/spec/src/system/i18n-resolver.ts read to judge it

Delta reviewed: the three commits since 51ace3fa (record 6667101706) — c3b51e208, 6f32f3136, 4f96f9a7a. git diff 51ace3fa 4f96f9a7 touches exactly one file, .changeset/30538-locale-table-read-aliases.md (+3/−1): one new paragraph on resolveOfferedLocale, and "either function" → "these functions" in the Unchanged paragraph. No source, test or generated file moves, so records 6665489168 and 6667101706 stand in full for the tree. Read-only; nothing built or run.

Gate verdicts on 4f96f9a7, read at 2026-11-05T19:53Z (the last run completed 19:52Z): 69 check-runs, 58 success, 10 skipped, 1 cancelled, 0 failed, 0 pending. Every required context is success — Lint & Repo Gates, TypeScript Type Check, Test Core, Build Core, Governed Surface Queue Guard, Type Check UI, Test UI, Build & E2E UI, Live E2E UI, Check Changeset — with Lint UI skipped. The one cancelled run is an advisory PR-body guards run at 19:24:19Z, superseded by the body r2 edit: its re-run of the same name completed success at 19:24:41Z. Nothing was re-run by this review.

The new changeset paragraph — true, and it matches the finding

  • "resolveOfferedLocale (@objectstack/core, the matcher Setup's default-language options and the IdP locale claim use) now answers through the same ladder": true at this head — memory-i18n.ts:74 is return resolveLocaleAmong(requested, offered), and its two callers are service-settings/src/localization-locale-options.ts:120,125 and plugin-auth/src/idp-locale-claim.ts:148.
  • "a Traditional request reaches an offered zh-Hant": true — step 3 of resolveLocaleAmong (i18n-resolver.ts) maps zh-TW / zh-HK / zh-MO to zh-Hant and finds the key spelled as that tag.
  • "the code spelled as the table tag wins over the first one offered: zh over ['zh-CN', 'zh-Hans'] answers zh-Hans, and zh-HK over ['zh-TW', 'zh-Hant'] answers zh-Hant": true — the first clause of step 3 (available.find((code) => code.toLowerCase() === folded)) runs before the alias clause, and both examples are the tie-break measured in record 6667101706 (main's fix(service-settings): judge OS_LOCALIZATION_LOCALE once the boot's locale list is whole, and one locale matcher for Setup and the IdP claim #30589 answered zh-CN and zh-TW there). This is the sentence that record's should-fix asked for; the published CHANGELOG.md sentence for the Setup and IdP doors is now true.
  • Declarations unchanged and still right: Clause-②: yes (widening), Metadata-protocol: semantic, @objectstack/spec minor + @objectstack/core minor; the PR body carries the same two lines (r2, 19:23Z).

Findings

Implemented-by: claude/project-thread-mros1u
Reviewed-by: session_01L6bhgTqqeHSwZYJmApbDCa

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#3 · os-sales · session_01L6bhgTqqeHSwZYJmApbDCa

维护者速读

  • 改了什么:服务端两个语言解析器(spec 的 resolveBundleLocale、core 的 resolveLocale)改为同一份实现,先按平台语言表的读别名找同一语言,再退到基础语言和第一个变体。
  • 为什么改:繁体读者(zh-TW / zh-HK / zh-MO)原来拿到第一个注册的 zh-* 包,也就是简体;各 translations 包正在补 zh-Hant,补上后繁体读者仍看简体。
  • 风险与回滚:只改「哪一个已注册的包来答」,原来能匹配的请求仍能匹配;表里没有的代码(es-MX、en-GB)照旧。合入 main 后,fix(service-settings): judge OS_LOCALIZATION_LOCALE once the boot's locale list is whole, and one locale matcher for Setup and the IdP claim #30589 新加的 resolveOfferedLocale(设置里的默认语言选项、IdP 语言声明)也走同一套规则;同一语言列出两个代码时,按表里的拼写优先(如 zh 在 zh-CN、zh-Hans 之间选 zh-Hans),目前没有内置语言包这样注册。回滚即还原这一个 PR。
  • 先看:packages/spec/src/system/i18n-resolver.ts 的 resolveLocaleAmong 第 3 步;packages/core/src/fallbacks/memory-i18n.ts;packages/core/src/fallbacks/fallbacks.test.ts 的共享用例表。
  • 两分钟验证:pnpm --filter @objectstack/core exec vitest run src/fallbacks/fallbacks.test.ts,预期 107 例全绿,其中共享用例表对两个解析器各跑一遍,另有一例断言 resolveOfferedLocale 与 resolveLocale 答案相同。
  • 需要你做的:无,席位落地。

席位意见:契约复审三轮都 PASS。第一轮的 S1、S2 在本 PR 里修了,S3–S6 另立卡或挂到对应卡上。合入 main 时和 #30589 冲突,处理成全仓只剩一套语言匹配规则;第二轮复审实测了这对 #30589 调用方的唯一影响(一个并列取舍),并要求在 changeset 里写明,第三轮确认已写。

席位核对 — ACCEPT · domain:spec#3 · head 4f96f9a7aebec5d4d4b3f7992e18674717e6b7b5

  • Shape: 16 files (+267 −91), draft, base main, first card reference Fixes #30538.
    • Hand-written:
      • resolveLocaleAmong and the resolveBundleLocale rewrite in packages/spec/src/system/i18n-resolver.ts, with their tests.
      • Core's resolveLocale and resolveOfferedLocale routed through it (packages/core/src/fallbacks/memory-i18n.ts), with the shared case table in fallbacks.test.ts.
      • Docblocks in platform-locales.ts, email-template.zod.ts, validation-message.ts, contracts/email-service.ts, stack.zod.ts and plugin-email's email-service.ts.
      • content/docs/automation/email-templates.mdx.
      • plugin-email's moved pin.
      • The changeset.
    • Regenerated: api-surface/system.json, export-origins/system.json and content/docs/references/system/email-template.mdx.
    • No content/docs/releases/ edit.
  • Premise measured: on origin/main 14c67d44, i18n-resolver.ts has no resolveLocaleAmong and never reads platformLocaleTagOf (0 hits; 7 at this head). Main's resolveLocale (memory-i18n.ts :47) and resolveOfferedLocale (:93) are separate ladders. The body's repro on main (resolveBundleLocale({ en, zh-CN, zh-Hant }, 'zh-TW') → zh-CN) is the card's defect: a Traditional reader gets the Simplified bundle.
  • Gates:
    • CI on this head: 69 check-runs over 65 names; newest per name 57 success, 8 skipped, 0 red. One advisory PR-body guards run was cancelled when the body edit superseded it, and its re-run passed. check-expected-skips --pr 30601: every skip is in main's roster (exit 0).
    • Card thread at 51ace3fa, after a full 119/119 build, all exit 0:
      • Tests: spec 816 files / 25719 tests; core 135 / 3075 (fallbacks.test.ts 107); service-settings 69 / 1178; plugin-email 48 / 801; plugin-auth locale 6 / 202.
      • Typecheck clean for spec, core and service-settings, and check:generated passes.
      • Derived gates: 109 run, 0 NOT-MEASURED.
    • At 4f96f9a7 the only change is the changeset, so the four changeset gates were re-run, all exit 0: check-changeset-no-major, check-empty-changeset and its self-test, check:changeset-gate-self-tests, and check-adr-0087-registration.
    • Ablation and repro are in the body (round 1). There is no dogfood battery, which is right for this PR: no ! and no (narrowing) arm.
    • git merge-tree against main 14c67d44 merges clean.
  • Contract review:
    • 6665489168 on 9f692b76: PASS, Served-tier: CONTRACT_REVIEW_TIER, no must-fix, S1–S6.
    • Delta 6667101706 on 51ace3fa: PASS. It answered S1 and S2, and checked the merge resolution as a contract change. resolveOfferedLocale now returns resolveLocaleAmong(...), and the callers are service-settings localization-locale-options.ts :120 and :125 and plugin-auth idp-locale-claim.ts :148. It found one moved answer, a tie-break inside the table step, with admission unchanged, and raised one should-fix: say it in the changeset.
    • Delta 6667783088 on 4f96f9a7: PASS, with the changeset paragraph true and nothing new.
  • Declarations:
    • Clause-②: yes (widening): one export added, none removed. resolveOfferedLocale's signature is unchanged.
    • Metadata-protocol: semantic: a zh-Hant bundle or template row now serves Traditional readers.
    • @objectstack/spec minor and @objectstack/core minor, the same in the body and the changeset.
    • No changeset is owed for service-settings or plugin-auth: no file there moves, and the tie-break reaches them through the core minor, which now says so.
    • Skills-docs: follow-up #30603 holds: skills/objectstack-i18n/SKILL.md still restates the four-step ladder, and skills/** is a Tier H surface.
  • Accepted deviations:
  • Carried:
  • Landing: not governed (check-governed-merges --pr 30601: 0 of 16 paths, 358 lines ≤ 3000). The hand-off is pr_ready plus automerge_enable, through the merge queue.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 5, 2026
Merged via the queue into main with commit af8aab1 Nov 5, 2026
69 of 70 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-mros1u branch November 5, 2026 20:20
os-elon-musk pushed a commit that referenced this pull request Nov 5, 2026
… restate four steps

PR #30601 put both server locale resolvers on resolveLocaleAmong
(exact, case-insensitive, platform locale table language, base
language, first registered variant). Comments in objectql,
service-automation, plugin-auth and plugin-email, the translations
docs page and the email-template-render checklist item still listed
the old four steps. They now name the resolver and its five steps,
and say the table step only adds answers for the table's aliases.
Comment, docs and checklist text only; no behaviour change.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cytQjG9X1CrCgytUM5
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:system size/m tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Server locale resolvers ignore the platform read aliases, so a zh-TW / zh-HK reader gets Simplified Chinese even where a zh-Hant bundle exists

1 participant