Skip to content

fix(spec/system): read the script-subtag Chinese spellings (zh-Hant-TW, zh-Hans-CN, …) as platform locale read aliases - #30681

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-30618-script-subtag-read-aliases
Nov 5, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-30618-script-subtag-read-aliases

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Requested by Sales · project thread

domain:spec#2 · os-sales · session_01L6bhgVugtCCwBs98FeR27P

Fixes #30618

Clause-②: yes (widening)
Metadata-protocol: semantic
Skills-docs: none — skills/** carries no script-subtag or read-alias claim, measured at d8713f3f: git grep -i -E 'zh-Han[st]-[A-Za-z0-9]+' -- skills/ 0 hits, git grep -E 'PLATFORM_LOCALE_READ_ALIASES|platformLocaleTagOf|read alias|zh-Hant' -- skills/ 0 hits. The one docs page that states the read aliases, content/docs/protocol/kernel/i18n-standard.mdx (its "Read aliases" section), is fixed in this PR.

维护者速读

  • 改了什么:平台语言表的读入别名加了五行:zh-Hans-CN、zh-Hans-SG 读作 zh-Hans,zh-Hant-TW、zh-Hant-HK、zh-Hant-MO 读作 zh-Hant。解析阶梯的“同表语言”一步对已注册的 key 也查这张表,所以拼成 zh-Hant-TW / zh-Hans-CN 的包 key 或可选代码,按它写明的文字归入繁体或简体。别名表仍是逐行列举的封闭清单,不按模式推断,也不按区域回退。文档页 i18n-standard.mdx 的 Read aliases 一节同步改正。
  • 为什么改:浏览器和操作系统常发带文字子标签的语言代码(如 zh-Hant-TW)。原来这五个代码不在表里,同时有简体包和繁体包时,繁体读者拿到的是先注册的那个 zh-* 包;主干上 service-storage 的对象翻译包(zh-CN 排在 zh-Hant 前)就是这样把繁体读者答成简体。
  • 风险与回滚:只改这五个代码(作请求或作已注册的 key 时)由哪个包来答,原来能匹配的请求仍能匹配,表外代码(pt-PT、en-GB、zh-Hant-US)照旧。Setup「默认语言」对这五个代码改用表里的名称(繁體中文 / 简体中文),已存的繁体值改显示在繁體中文选项上;可接受的取值不变。回滚即还原本 PR。
  • 先看:packages/spec/src/system/platform-locales.ts 的五行别名及其注释;packages/core/src/fallbacks/fallbacks.test.ts 的共享用例表(两个解析器、两种注册顺序,含 key 侧用例);packages/spec/src/system/platform-locales.test.ts 的封闭清单用例;content/docs/protocol/kernel/i18n-standard.mdx 的 Read aliases 一节。
  • 两分钟验证:pnpm --filter @objectstack/spec exec vitest run src/system/platform-locales.test.ts src/system/i18n-resolver.test.ts,预期 2 个文件、576 例全绿。
  • 需要你做的:无,席位落地。

What changes

PLATFORM_LOCALE_READ_ALIASES (packages/spec/src/system/platform-locales.ts) gains five rows, one for the script-subtag spelling of each regional Chinese alias. platformLocaleTagOf, FROM → TO:

Code FROM TO
zh-Hans-CN undefined zh-Hans
zh-Hans-SG undefined zh-Hans
zh-Hant-TW undefined zh-Hant
zh-Hant-HK undefined zh-Hant
zh-Hant-MO undefined zh-Hant

Case is folded as for every row (ZH-HANT-tw reads as zh-Hant). PlatformLocaleReadAlias gains the five literal keys. The list stays closed: enumerated rows only, no pattern, no region fallback. zh-Hant-US, zh-Hans-US, pt-PT, de-AT, en-GB, es-419, fr-CA, it and he still answer undefined.

What the ladder answers for a script-subtag request (resolveLocaleAmong, so resolveBundleLocale and core's resolveLocale / resolveOfferedLocale), FROM → TO, measured by the ablation below:

Request over en, zh-CN, zh-Hant over en, zh-Hant, zh-CN over en, zh, zh-Hant
zh-Hant-TW, zh-Hant-HK, zh-Hant-MO zh-CN → zh-Hant zh-Hant (unchanged) zh → zh-Hant
zh-Hans-CN, zh-Hans-SG zh-CN (unchanged) zh-Hant → zh-CN zh (unchanged)

The key side. The table step maps each registered key through the table too (i18n-resolver.ts L573, unchanged), so a bundle key or offered code spelled zh-Hant-TW / zh-Hans-CN now counts as the script it names for every request the table reads as that script. FROM measured on main a2437b56 (whose i18n-resolver.ts and platform-locales.ts are unchanged since the merge base 47c457b2), TO at this head; resolveLocaleAmong, resolveBundleLocale and core's resolveLocale give the same answer in every row:

Request Keys, in registration order FROM (main) TO
zh-TW en, zh-CN, zh-Hant-TW zh-CN zh-Hant-TW
zh-TW en, zh-Hant-TW, zh-CN zh-Hant-TW zh-Hant-TW (unchanged)
zh-Hant en, zh-CN, zh-Hant-TW zh-CN zh-Hant-TW
zh-Hant en, zh-Hant-TW, zh-CN zh-Hant-TW zh-Hant-TW (unchanged)
zh-CN en, zh-Hant, zh-Hans-CN zh-Hant zh-Hans-CN
zh-CN en, zh-Hans-CN, zh-Hant zh-Hans-CN zh-Hans-CN (unchanged)
zh-Hant-TW en, zh, zh-Hant-HK zh zh-Hant-HK
zh-Hant-TW en, zh-Hant-HK, zh zh zh-Hant-HK
zh-Hant-TW en, zh-CN (one script registered) zh-CN zh-CN (unchanged)

No request that matched before stops matching. With no key of the requested script, a script-subtag request still reaches the other script's key, as before (last row).

Docblocks in platform-locales.ts: the read-alias block no longer says "zh-Hans-CN is not listed"; it says the five rows exist, that the list is closed, and that a script-subtag spelling without a row (zh-Hant-US) answers nothing because rows are read one by one, never as a pattern. The platformLocaleTagOf paragraph names zh-Hans-CN / zh-Hant-TW beside zh-CN / zh-TW.

Docs page content/docs/protocol/kernel/i18n-standard.mdx, the "Read aliases" section only (L166-207 at this head; nothing outside it moves):

  • The alias table lists the five rows. Its header is "Code", not "Stored code", and the intro names the script-subtag spellings browsers and operating systems send beside the codes stored values carry.
  • "Any other code answers undefined" stays and is true again: the section now says the list is closed and read row by row, never as a pattern, so zh-Hant-US answers undefined too.
  • The paragraph that said the ladder cannot tell the two Chinese scripts apart and answers zh / zh-CN by key order (false since PR fix(spec,core): resolve locales through the platform table's read aliases, so a zh-TW / zh-HK reader gets zh-Hant #30601 added the table step) is rewritten against the code. It names the ladder's one function (resolveLocaleAmong, which resolveBundleLocale and core's resolveLocale run) and its five steps, says the table step takes the key spelled as the table tag or else the first key the table maps to it, and says a two-script bundle answers zh, zh-CN, zh-Hans-CN Simplified and zh-TW, zh-Hant-TW Traditional in any registration order.
  • A new paragraph names the two kinds of request that registration order still decides: a code with no row (zh-Hant-US over en, zh-CN, zh-Hant gets zh-CN, and zh-Hant when that key is registered first) and a request whose table language has no key (zh-TW over en, zh-CN gets zh-CN).
  • Every example the section states is checked against the spec source at this head by a scratch probe: 41 checks, all hold.

Pins (no new file; each extends the existing harness):

  • packages/spec/src/system/platform-locales.test.ts: the five rows in the alias table (toEqual over the whole map, now fifteen), a typed index on zh-Hant-TW, a case-folded spelling of each row (ZH-HANS-cn, zh-hans-sg, ZH-HANT-tw, zh-hant-hk, Zh-Hant-Mo). The closed-list case drops zh-Hans-CN and zh-Hant-TW, keeps pt-PT, de-AT, en-GB, es-419, fr-CA, it, he, and adds zh-Hant-US and zh-Hans-US: a script-subtag code without a row still answers undefined.
  • packages/spec/src/system/i18n-resolver.test.ts: the two-registration-order loop (Simplified first / Traditional first) gains zh-Hant-TW, zh-Hant-HK, zh-Hant-MO, ZH-HANT-tw → zh-Hant and zh-Hans-CN, zh-Hans-SG, zh-hans-cn → zh-CN. The "runs before the base-language step" case gains { zh, zh-Hant } for zh-Hant-TW → zh-Hant and zh-Hans-CN → zh, and zh-Hant-TW over en, zh, zh-Hant-HK → zh-Hant-HK in both orders. A new case, "reads a key spelled with a script subtag as the script it names, in either registration order", pins the key-side rows above and the one-script control.
  • packages/core/src/fallbacks/fallbacks.test.ts: PR fix(spec,core): resolve locales through the platform table's read aliases, so a zh-TW / zh-HK reader gets zh-Hant #30601's shared case list (run on both resolveLocale and resolveBundleLocale) gains twelve request-side rows (each of the five codes in both registration orders, ZH-HANT-tw, and zh-Hant-TW over en, zh, zh-Hant) and the nine key-side rows of the table above (four requests in both registration orders, plus the one-script control). A new case drives the in-memory i18n service in both load orders for zh-Hant-TW, zh-Hant-HK, zh-Hans-CN.

Changeset .changeset/30618-script-subtag-read-aliases.md: @objectstack/spec minor. It states the rows, the request-side and key-side moves (each moved key-side row with its before), the Setup effect, and what stays. @objectstack/core is not moved (its diff is a test file, outside its files[]).

Verification

Head d8713f3f32b323798a6416046bf4704ed6a9d82b. Against the last fully built head e731b654 it adds two commits: 6a1d4627 (the docs section) and d8713f3f (the key-side pins and the changeset sentence). That delta is four files: the changeset, the docs page and two test files. platform-locales.ts (blob baefd871) and platform-locales.test.ts (blob 410cf43e) are byte-identical to e731b654, so every non-test source file the branch touches is unchanged since then. Against main the branch is six files, +149 / −29 (merge base 47c457b2). No full build this round: spec was rebuilt by the ablation, then the dependency closures of @objectstack/core and of @objectstack/lint / @objectstack/formula were built (all exit 0).

  • Tests, all exit 0 at this head: @objectstack/spec 817 files / 25799 tests + 1 todo (platform-locales.test.ts + i18n-resolver.test.ts: 2 / 576); @objectstack/core 136 / 3130 (fallbacks.test.ts 151). Carried from e731b654, code files byte-identical: @objectstack/service-settings 69 / 1178, @objectstack/service-storage 73 / 1353.
  • Typecheck exit 0 for spec and core at this head; both compile their tests through check:test-typecheck under tsconfig.test.json.
  • eslint --no-inline-config on platform-locales.ts, platform-locales.test.ts, i18n-resolver.test.ts and fallbacks.test.ts: exit 0.
  • dispatch-gates --commands at d8713f3f: 102 derived (79 at e731b654; the 23 added came with the docs page, and 6a1d4627 already derived these 102). 99 exit 0 on the first run. Three first answered PREREQUISITE NOT MET (exit 3): check:skill-examples and check:lean-entry-closure were re-run after building the client SDK and objectql closures, exit 0; check:dual-build-cjs-loads needs every package built, so its reading is carried from e731b654 (exit 0), code files byte-identical. --ran with the codes measured here: 102 derived, 101 run, 1 NOT-MEASURED (check:dual-build-cjs-loads), 0 UNRUN; with the carried reading: 102 derived, 102 run, 0 NOT-MEASURED, 0 UNRUN.
  • The derivation at d8713f3f warned STALE TREE (at least 7 commits behind origin/main, and 8 files the derivation reads changed); after a fetch, origin/main badf24df is 12 commits ahead. Re-derived from origin/main badf24df with this branch merged in (a scratch merge in a separate worktree, clean, never committed): the same 102 commands.
  • pnpm --filter @objectstack/spec check:generated: exit 0, all 14 artifacts up to date.
  • check:dev-prereqs (local-only family; it needs every package built): carried from e731b654 (exit 0), code files byte-identical. At this head it answers exit 1, naming the unbuilt dist/ folders; its --self-test, which is derived, is exit 0 here.
  • CI: read from this PR's check-runs. At this head, when read: 65 check-runs, 57 success, 8 skipped, 0 failed.
Derived gates at d8713f3f (102, each exit code captured before any pipe)
Command Exit
node scripts/check-adr-0087-registration.mjs --base origin/main 0
node scripts/check-adr-0087-registration.mjs --self-test 0
node scripts/check-adr-links.mjs 0
node scripts/check-adr-links.mjs --self-test 0
node scripts/check-changeset-no-major.mjs --base origin/main 0
node scripts/check-closing-keyword-parity.mjs 0
node scripts/check-closing-keyword-parity.mjs --self-test 0
node scripts/check-comment-mask-adoption.mjs 0
node scripts/check-comment-mask-adoption.mjs --self-test 0
node scripts/check-comment-mask-corpus.mjs 0
node scripts/check-dev-prereqs.mjs --self-test 0
node scripts/check-doc-route-spelling.mjs --advisory 0
node scripts/check-doc-route-spelling.mjs --self-test 0
node scripts/check-docs-section-name.mjs 0
node scripts/check-docs-section-name.mjs --self-test 0
node scripts/check-dts-emitted.mjs --self-test 0
node scripts/check-empty-changeset.mjs --base origin/main 0
node scripts/check-empty-changeset.mjs --self-test 0
node scripts/check-issue-citations.mjs 0
node scripts/check-plugin-teardown-shape.mjs 0
node scripts/check-plugin-teardown-shape.mjs --self-test 0
node scripts/check-section-landing-index.mjs 0
node scripts/check-section-landing-index.mjs --self-test 0
node scripts/ci/assert-turbo-remote-cache.mjs --self-test 0
node scripts/ci/plan-test-core-shards.mjs --self-test 0
node scripts/ci/prune-turbo-cache.mjs --self-test 0
node scripts/docs-audit/check-affected-docs.mjs 0
node scripts/docs-audit/check-drift-comment.mjs 0
node scripts/pm/release-rehearsal-clone.mjs --self-test 0
node scripts/release-pending-publish.mjs --self-test 0
pnpm --filter @objectstack/lint run check:doc-formula-expressions 0
pnpm --filter @objectstack/lint run check:doc-security-posture 0
pnpm --filter @objectstack/spec run check:api-surface 0
pnpm --filter @objectstack/spec run check:authorable-surface 0
pnpm --filter @objectstack/spec run check:browser-reachable-entries 0
pnpm --filter @objectstack/spec run check:docs 0
pnpm --filter @objectstack/spec run check:dual-source-exports 0
pnpm --filter @objectstack/spec run check:duration-unit-keys 0
pnpm --filter @objectstack/spec run check:empty-state 0
pnpm --filter @objectstack/spec run check:entry-nameability 0
pnpm --filter @objectstack/spec run check:export-origins 0
pnpm --filter @objectstack/spec run check:exported-any 0
pnpm --filter @objectstack/spec run check:liveness 0
pnpm --filter @objectstack/spec run check:llms-txt 0
pnpm --filter @objectstack/spec run check:skill-examples 0 (re-run after building the client SDK closure; first answered 3, PREREQUISITE NOT MET)
pnpm --filter @objectstack/spec run check:skill-refs 0
pnpm --filter @objectstack/spec run check:strictness-ledger 0
pnpm --filter @objectstack/spec run check:variant-docs 0
pnpm --filter @objectstack/spec run check:yaml-examples 0
pnpm check:changeset-gate-self-tests 0
pnpm check:ci-filter-parity 0
pnpm check:closed-ceremony-text 0
pnpm check:corpus-claim-drift 0
pnpm check:cross-package-test-inputs 0
pnpm check:dispatcher-error-vocabulary 0
pnpm check:doc-anchors 0
pnpm check:doc-authoring 0
pnpm check:doc-frontmatter 0
pnpm check:docs-audit-scope 0
pnpm check:docs-redirects 0
pnpm check:docs-single-h1 0
pnpm check:docs-spec-enumerations 0
pnpm check:docs-transcript-drift 0
pnpm check:driver-memory-census 0
pnpm check:dts-closure 0
pnpm check:dual-build-cjs-loads 3 here (PREREQUISITE NOT MET: unbuilt packages, no full build this round); 0 carried from e731b65, code files byte-identical
pnpm check:engine-double-contract 0
pnpm check:gitlink-declared 0
pnpm check:issue-citations 0
pnpm check:kernel-hook-pairs 0
pnpm check:keyed-text-bounds 0
pnpm check:lean-entry-closure 0 (re-run after building objectql's closure; first answered 3, PREREQUISITE NOT MET)
pnpm check:logger-receiver-detach 0
pnpm check:merge-driver 0
pnpm check:nul-bytes 0
pnpm check:objectql-double-limit 0
pnpm check:org-identifier 0
pnpm check:page-declaration-shape 0
pnpm check:platform-object-tenancy-census 0
pnpm check:pm-prior-rulings 0
pnpm check:published-files 0
pnpm check:published-readme-links 0
pnpm check:query-options-erasure 0
pnpm check:react-page-adapter-contract 0
pnpm check:refd-timer-probe 0
pnpm check:registry-log-declared 0
pnpm check:rest-log-spy-declared 0
pnpm check:role-word 0
pnpm check:skill-identifier-liveness 0
pnpm check:slot-lookup 0
pnpm check:sourcemap-no-sources-content 0
pnpm check:spec-docblock-symbol-anchors 0
pnpm check:spec-parsed-alias 0
pnpm check:system-context-census 0
pnpm check:test-source-alias 0
pnpm check:tier-file-adoption 0
pnpm check:type-check-coverage 0
pnpm check:type-check-debt 0
pnpm check:undeclared-dep-imports 0
pnpm check:vendor-version-stamps 0
pnpm check:watch-hint-literal 0
pnpm check:where-matcher 0
Ablation: drop the five rows (run at d8713f3f)
  • Mutation through scripts/ablation-replace.mjs --delete on the five rows and their comment: anchor 1 → 0, blob baefd871 → be1e0adf; on disk the rows count 0. Spec rebuilt (VERDICT command-exit 0); ablation-dist-preflight --absent for "zh-Hant-TW": "zh-Hant" and "zh-Hans-CN": "zh-Hans": exit 0 for both.
  • spec platform-locales.test.ts + i18n-resolver.test.ts: 21 failed / 555 passed (576). Red: the five rows, their five case-folded spellings, the whole-map and typed-key cases, the two-order loop entries (zh-Hant-TW / zh-Hant-HK / zh-Hant-MO / ZH-HANT-tw under "Simplified first", zh-Hans-CN / zh-Hans-SG / zh-hans-cn under "Traditional first"), the { zh, zh-Hant } case, and the new key-side case (first failing assertion: zh-TW over en, zh-CN, zh-Hant-TW received zh-CN). The two zh-Hant-HK assertions added to the { zh, zh-Hant } case come after an assertion that is already red there, so vitest stops before them; core's list below fails those rows one by one.
  • core fallbacks.test.ts: 26 failed / 125 passed (151). Red: on each of resolveLocale and resolveBundleLocale, the seven request-side rows (each code in the order where the other script is registered first, ZH-HANT-tw, and zh-Hant-TW over en, zh, zh-Hant) and five key-side rows (zh-TW and zh-Hant over en, zh-CN, zh-Hant-TW, zh-CN over en, zh-Hant, zh-Hans-CN, zh-Hant-TW over en, zh, zh-Hant-HK in both orders); plus the in-memory service case in both load orders. Green, as the FROM column says: the key-side rows in the order where the script-subtag key is registered first, and the one-script control.
  • A probe of the nine key-side rows through spec's resolveLocaleAmong and resolveBundleLocale (source) and core's resolveLocale (core source over spec's built dist/) answers exactly the FROM column on the mutated tree, and exactly the TO column after the restore.
  • Restore: git checkout HEAD -- packages/spec/src/system/platform-locales.ts; blob after restore baefd8712cec92d2a22aac2a8f3fd909a62635a5 equals the HEAD blob, git diff HEAD empty, git status --porcelain empty. Rebuilt (exit 0); both markers present (preflight exit 0); spec 576 passed, core 151 passed.
  • The consumer suites were not read in this run: with only spec's dist/ on disk, service-settings and service-storage failed to resolve workspace package entries (@objectstack/metadata-core) in both legs alike, a setup failure and not a reading. Their reading is carried from the round-one ablation at eadbe6e8 (same platform-locales.ts blob baefd871): service-settings 1178 and service-storage 1353 green with and without the rows, so no consumer test pins these answers.
Consumer probe (each direct caller, 14 codes, with and without the rows; carried from e731b654, code files byte-identical)
  • platformLocaleTagOf: the five codes and ZH-HANT-tw move undefined → table tag; zh-TW, zh-CN, zh-Hant, zh-Hans unchanged; zh-Hant-US, zh-Hans-US, pt-PT, fr-CA stay undefined.
  • service-settings localeSelfName: the five codes move from the runtime display name to the table label (see Acceptance notes); the other nine unchanged.
  • service-settings option source, four scenarios: admits identical for all 14 codes; options move only where a stored or offered code is one of the five.
  • service-settings notOfferedCopy: unchanged for all 14.
  • service-storage storageTestCopyLocaleOf: unchanged for all 14.

Acceptance notes

  • The docs page is fixed in this PR (this round). The seat widened the file surface to the "Read aliases" section of content/docs/protocol/kernel/i18n-standard.mdx (platform locale table: script-subtag codes (zh-Hant-TW, zh-Hans-CN, zh-Hant-HK) are not read aliases, so a two-script bundle answers them with the first registered zh-* key #30618 comment 6671045459), and commit 6a1d4627 edits that section only: the five rows, the closed-list sentence made true, and the ladder paragraphs rewritten against the code (see What changes). So the Skills-docs: line is none, and the follow-up the earlier body owed is closed.
  • Review S1 adopted (this round). The contract review (comment 6671200273, PASS at e731b654) asked that the key-side move be pinned and stated. Commit d8713f3f pins it in core's shared list and in spec's table-step cases, both registration orders, with the one-script control, and adds the changeset sentence. FROM was measured on main first; it agrees with the review's three rows (zh-TW over en, zh-CN, zh-Hant-TW: zh-CN → zh-Hant-TW; zh-Hant-TW over en, zh, zh-Hant-HK: zh → zh-Hant-HK; control zh-Hant-TW over en, zh-CN: zh-CN, unchanged).
  • The closed list, measured, not re-decided. A script-subtag code with any other region (zh-Hant-US, zh-Hans-US) has no row, so it keeps the old path: over service-storage's shipped bundle it reaches zh-CN, the first zh-* key, for either script. That is the decision of record on this card (enumerated rows only).
  • Consumers measured, none edited. Every direct caller of platformLocaleTagOf / PLATFORM_LOCALE_READ_ALIASES (git grep, three non-test call sites; the map itself is read only inside platform-locales.ts):
    • packages/spec/src/system/i18n-resolver.ts L569/L573 (resolveLocaleAmong table step): moves, as the tables above, on the request side (L569) and on the key side (L573).
    • packages/services/service-settings/src/localization-locale-options.ts L67 (localeSelfName): moves. The five codes are named from the table (zh-Hant-TW 中文(繁體,台灣) → 繁體中文, zh-Hant-HK 中文(繁體字,中國香港特別行政區) → 繁體中文, zh-Hans-CN 中文(简体,中国) → 简体中文, likewise zh-Hans-SG, zh-Hant-MO). Through resolveOfferedLocale, a stored zh-Hant-TW over offered en, zh-CN, zh-Hant used to take the place of the 简体中文 option (zh-Hant-TW=简体中文) and now sits on 繁體中文 (zh-CN=简体中文, zh-Hant-TW=繁體中文); a stored zh-Hans-CN over en, zh-Hant, zh-CN moved the same way. The accepted set (admits) is unchanged for all 14 probed codes in 4 scenarios, and notOfferedCopy is unchanged.
    • packages/services/service-storage/src/storage-test-copy.ts L152 (storageTestCopyLocaleOf): does not move. Its subtag truncation already reached the same tag (zh-Hant-TW → zh-Hant), for all 14 probed codes.
    • Ladder consumer reached after the main merge: service-storage's object translations (zh-CN registered before zh-Hant, from i18n(service-storage): ship the file and upload-session objects in all eleven platform languages #30649) now serve a zh-Hant-TW / zh-Hant-HK / zh-Hant-MO reader the zh-Hant objects; the row-less control zh-Hant-US still reaches zh-CN.
    • Consumer suites green with and without the rows (service-settings 69 files / 1178 tests, service-storage 73 / 1353): no consumer test pins these answers.
  • Declarations, measured. Clause-②: yes (widening): the published map and PlatformLocaleReadAlias gain five keys, nothing removed or narrowed (check:api-surface green: the snapshot records names and kinds); the key-side move is a widening too. Metadata-protocol: semantic: no key or value is added to what an author writes, but a bundle key or translation item an author keys zh-Hant (or zh-CN / zh-Hans) now serves zh-Hant-* (or zh-Hans-*) readers it did not serve over a two-script bundle, and a key an author spells zh-Hant-TW is now read as zh-Hant. Level minor on @objectstack/spec, the one package the diff moves.
  • The first round-one ablation run was a no-op on its dist/ half, and is not a reading reported here. Its mutation landed, but the spec build in the mutated leg never got the shared lock (VERDICT queue-timeout (exit 99)), so dist/ still carried the rows. Every later leg aborts unless the build verdict is command-exit 0 and the dist preflight passes; both held in this round's run.
  • Docblocks left as they are, still true: resolveLocaleAmong's step 3 (i18n-resolver.ts) and core's resolveLocale / resolveOfferedLocale docblocks give zh-TW / zh-HK examples, not an exhaustive list. The unreleased .changeset/30001-platform-locale-table.md says the map is "typed on its ten literal keys", which is true of that change; this changeset states the five added.
  • No labels sent: the dispatch named none, and skip-changeset does not apply (the diff publishes @objectstack/spec).

Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_01L6bhgVugtCCwBs98FeR27P

…m locale aliases

PLATFORM_LOCALE_READ_ALIASES gains one row per regional Chinese alias
for its script-subtag spelling: zh-Hans-CN and zh-Hans-SG read as
zh-Hans; zh-Hant-TW, zh-Hant-HK and zh-Hant-MO read as zh-Hant. The
list stays closed: enumerated rows only, no pattern and no region
fallback.

So resolveLocaleAmong's table step reaches a registered zh-Hant for a
zh-Hant-TW reader over a two-script bundle in either registration
order, where the first registered zh-* key used to answer.

Pins: platform-locales.test.ts (each row, case-folded; the closed-list
case keeps pt-PT, de-AT, en-GB, es-419, fr-CA, it and he and gains two
unlisted script-subtag codes), i18n-resolver.test.ts (both orders) and
core's shared case list in fallbacks.test.ts (both resolvers, both
orders, and the in-memory service).

Co-authored-by: Claude <[email protected]>
Claude-Session: https://claude.ai/code/session_01L6bhgVugtCCwBs98FeR27P
@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 1 package(s): @objectstack/spec, touching 1 documentable anchor(s).

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

  • content/docs/protocol/kernel/i18n-standard.mdx (via PLATFORM_LOCALE_READ_ALIASES (symbol, a top-level const object))
What this run could not see

Coarse fallback — 170 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 a2437b560bd8872feece6ec8b57c6fdf21cff1f4 → packageMentionDocs.

Which tree this was computed on

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

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

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

…s, and the ladder paragraph names the table step

The Read aliases section of content/docs/protocol/kernel/i18n-standard.mdx
lists the five script-subtag rows (zh-Hans-CN and zh-Hans-SG read as
zh-Hans; zh-Hant-TW, zh-Hant-HK and zh-Hant-MO as zh-Hant), and says a
script-subtag spelling without a row (zh-Hant-US) still answers
undefined: the list is closed and read row by row.

The ladder paragraph said the ladder alone cannot tell the two Chinese
scripts apart and answers zh / zh-CN by key order. That has been false
since resolveLocaleAmong gained its table-language step. It now names
the five steps, says which requests the table step answers in any key
order, and which still fall to base language and the first registered
variant: a code the table does not name, and a request whose table
language has no key.

Co-authored-by: Claude <[email protected]>
Claude-Session: https://claude.ai/code/session_01L6bhgVugtCCwBs98FeR27P
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#2 · os-sales · session_01L6bhgVugtCCwBs98FeR27P

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: e731b654da21108d974a0e0b3fc968aa508998bd
Local-runs: none
Scope: packages/spec/src/system/platform-locales.ts + packages/spec/src/system/i18n-resolver.ts (resolveLocaleAmong step 3, L569-575), packages/core/src/fallbacks/memory-i18n.ts (resolveLocale, resolveOfferedLocale, createMemoryI18n), packages/services/service-settings/src/localization-locale-options.ts (localeSelfName L67, the option source L120-133), packages/services/service-storage/src/storage-test-copy.ts (storageTestCopyLocaleOf L152), packages/plugins/plugin-email/src/email-service.ts L1512, and the three pins in the diff (platform-locales.test.ts, i18n-resolver.test.ts, core fallbacks.test.ts), read as the gate of the rows. The changeset is judged under ②; the docs page is outside hits and not reviewed here.

Written 2026-11-05T22:28Z. Inputs: card #30618 body and all four comments (6666534830, 6669501005, 6670989840, 6671045459), PR #30681 body and file list, the net diff origin/main...e731b654 (5 files, +93/−15), and the check-runs on the head, read at 2026-11-05T22:25Z. After the inputs were read the branch gained one more commit (content/docs/protocol/kernel/i18n-standard.mdx only, +29/−14, the page the amendment 6671045459 carries); the hits file's blob baefd871 is byte-identical on both heads. This record names the head above.

Gate verdicts on this head (check-run conclusions, as they stood): success 22, among them Check Changeset, PR-body guards, Governed Surface Queue Guard, Spec property liveness, Lint · ESLint, Lint · docs & code guards, Lint · CI/release/engine gates, Lint · PM dispatch-gates, Lint · repo & tooling gates, Type Check · source gates, Type Check · debt ledger, Check PR Size, Flag docs affected, Dogfood Regression Gate, Plan Test Core shards, Build UI Deps. Skipped 9 (opt-in and path-filtered). Still pending: Test Core 1/8-8/8. Never concluded on this head: Build Core, Test UI 1/16-16/16, Build & E2E UI, Live E2E UI, Type Check UI, Type Check · workspace, Type Check · consumer gates were cancelled at 22:24Z by the superseding push, and the two failure rows (Test UI, TypeScript Type Check) are the 4-second aggregate jobs of those cancelled needs, with no output. So the core and UI test suites and the workspace typecheck have no CI verdict on this head; the dev's report states them green locally (spec 25798, core 3112, service-settings 1178, service-storage 1353), which this record reads but does not re-run.

① Derived judgments

  1. PLATFORM_LOCALE_READ_ALIASES gains five rows; platformLocaleTagOf answers five more codes, case-folded (platform-locales.ts L83-88, L100-105). Right. Each row's target is the one ruling F ([Decision] One locale list, one completeness rule — the long-term i18n regime across Console, platform metadata, cloud and mobile #29349, 6580034558) fixes: a spelling that names its script names the table tag (zh-Hans-* → zh-Hans, zh-Hant-* → zh-Hant), and the regional spellings stay read aliases. No rule, no pattern: the five are enumerated, TAG_BY_FOLDED_CODE is a Map over 26 lowercased entries (11 tags + 15 aliases) with no collision, and zh-Hant-US / zh-Hans-US / pt-PT / en-GB still answer undefined (pinned).
  2. PlatformLocaleReadAlias gains five literal keys. Right as a widening: keyof typeof grows, nothing leaves. The only in-tree reader of the type is the test (git grep), so no exhaustive Record over it moves; the changeset names the five keys for any out-of-tree one.
  3. Ladder, request side (i18n-resolver.ts L569-575): over a two-script bundle, in the order where the other script was registered first, zh-Hant-TW / -HK / -MO move zh-CN → zh-Hant, over { zh, zh-Hant } they move zh → zh-Hant, and zh-Hans-CN / -SG over [zh-Hant, zh-CN] move zh-Hant → zh-CN. Each move is the one ruling F wants. No request loses a match: step 3 only precedes steps 4-5, and any key it picks shares the request's base language, so 4-5 would have answered some key anyway. Pinned in three suites, both orders.
  4. Ladder, key side — not named in the PR body or the changeset, unpinned. The second find at L573 maps each AVAILABLE key through the table, so a bundle key or an offered locale spelled with a script subtag (LocaleSchema admits zh-Hans-CN; ui/i18n.zod.ts L78 lists it as an authorable key) now counts as its script instead of being an unknown zh-* variant: zh-TW over [en, zh-CN, zh-Hant-TW] moves zh-CN → zh-Hant-TW; zh-Hant over the same list moves the same way; zh-Hant-TW over [en, zh, zh-Hant-HK] moves zh → zh-Hant-HK. Right by ruling F (the script the key names wins over registration order) and a widening, but it is an accept-set change of the same family as item 3 and the record of this PR should carry it (S1 under ④).
  5. Consumers. localeSelfName (service-settings L67-69) names the five codes from the table (zh-Hant-TW → 繁體中文): right, the table names the language and not the region, exactly as zh-TW / zh-HK are named today. Through resolveOfferedLocale a stored zh-Hant-TW over offered [en, zh-CN, zh-Hant] now replaces the 繁體中文 slot (L124-131) instead of the 简体中文 one; admits unchanged. storageTestCopyLocaleOf (service-storage L150-154) does not move: its subtag truncation reached zh-Hant at n=2 before and the row answers at n=3 now. email-service.ts L1512 folds through the same ladder, so a template bundle holding both scripts now serves a zh-Hant-TW recipient the Traditional row: right. The other resolveBundleLocale callers (operation/validation messages, builtinSystemFieldLabel, objectql, approvals, audit, runtime, automation, messaging) run over catalogs with no Traditional key, where step 3 finds nothing and the answer is unchanged.
  6. Nothing else derived. No authorable key or value is added (the spellings already parse); check:generated reports its 14 artifacts unchanged and api-surface records names and kinds, so the type widening is not a snapshot move. The unreleased .changeset/30001-platform-locale-table.md ("ten literal keys", "any other code answers undefined") describes its own change and is superseded in the same release by this changeset: not a finding.
  7. Consequence of the decision of record, noted, not re-decided. zh-Hant-US / zh-Hans-US have no row, so over a two-script bundle the ladder still hands them the first zh-* key while storageTestCopyLocaleOf truncates them to zh-Hant / zh-Hans: the one place a spelling that names its script is still answered by key order. Pre-existing for those spellings, unchanged here, and the card's closed-list decision (6669501005).

② Semver level

@objectstack/spec: minor, one package: right. @objectstack/core's diff is a test file outside its files[], so no core changeset (Check Changeset green). Clause-②: yes (widening) holds: the map, the type and the function's accept set grow; nothing is refused or removed, and the key-side move in ①.4 is also a widening. Metadata-protocol: semantic is the right kind, not none: a value an author already writes changes meaning to the ladder — a bundle keyed zh-Hant now serves zh-Hant-* readers it did not, and a bundle keyed zh-Hant-TW is now read as zh-Hant. added would be wrong (LocaleSchema admitted the spellings before), narrowed wrong (nothing refused). Same kind as #30538's table step, which declared the same. Kind ⇒ at least minor on the moved package (AGENTS.md changeset rule): matches. Widening a keyof literal union is minor here; an exhaustive Record consumer would need the five keys, and none exists in the tree.

③ Boundary flags

  • Docs page (Skills-docs: follow-up owed). Answered by the seat's amendment 6671045459: the Read-aliases section of content/docs/protocol/kernel/i18n-standard.mdx goes in this PR. At this head it is not yet in the diff; the branch has since gained a docs-only commit for it. Outside hits, not judged here; the PR body's Skills-docs: line is the dev's to restate.
  • Closed-list consequence (zh-Hant-US). ①.7: measured, decision of record, not escalated.
  • "No new maintainer word needed." The card asked which of rows or rule, and said widening the list may need the maintainer. The claim chose enumerated rows and recorded why. Ruling F's 「zh / zh-CN / zh-TW … 等写法作为读入别名保留」 is non-exhaustive by its wording, and the rows add spellings of the same languages to the same closed list, so this stays within ruling F; recorded on the card, not escalated.
  • Ablation attempt 1 (dist half void). Reported as not a reading; attempt 2 is the one cited. Not a gate input here.
  • Docblocks left as examples (i18n-resolver.ts L539-546, memory-i18n.ts L44-45, L66-69): read; they give zh-TW examples and remain true.
  • open_questions: none. Security findings: none claimed, none found.

④ Design judgment

packages/spec/src/system/platform-locales.ts — holds. Judged against ruling F and its first consumer, resolveLocaleAmong step 3, over the scenario the card accepts: a script-subtag reader over a bundle holding both scripts, in either registration order. Every docblock sentence is true against the code: L62-64 (what the aliases read), L66-68 (the five rows and their targets), L70-73 (closed list, no region or script fallback, zh-Hant-US answers nothing), L83 (five regional aliases above: zh-CN, zh-SG, zh-TW, zh-HK, zh-MO), L110-111, L125-127 (a two-script bundle answers zh-Hans-CN Simplified and zh-Hant-TW Traditional in any key order). The satisfies clause (every value a PlatformLocaleTag) holds for the five targets; the map stays frozen. Break attempts: case folding (ZH-HANS-cn, Zh-Hant-Mo) folds through the same lowercased index as every row, pinned; a one-script bundle (zh-Hant-TW over [en, zh-CN]) finds nothing at step 3 and reaches zh-CN at step 5 exactly as before, which the changeset claims and no pin states for a script-subtag spelling; a bare zh key is Simplified for zh-Hant-TW and reached by zh-Hans-CN, pinned both ways; registration order, pinned in both orders on the spec ladder, core's two resolvers and the in-memory service; zh-Hant-US answers undefined by design and is pinned so; resolveOfferedLocale over Setup's options moves the selected slot and keeps admits (①.5); a deployment offering both zh-CN and zh-Hans-CN now shows two options both labelled 简体中文, the same property zh + zh-CN or zh-TW + zh-HK have on main (the table names the language), not introduced here; a non-string code still answers undefined. The one gap is the key-side move of ①.4: a true consequence of the rows through the ladder's second find, in the direction ruling F wants, that neither the FROM → TO tables, the changeset nor the shared case list name, so a later change to L573 could drop it unnoticed.

Must-fix: none.

Should-fix:

  • S1 (this PR): pin and state the key-side move. packages/core/src/fallbacks/fallbacks.test.ts shared case list (L318-330 at this head) and packages/spec/src/system/i18n-resolver.test.ts (the "runs before the base-language step" case, L1086-1090): add zh-TW over [en, zh-CN, zh-Hant-TW] → zh-Hant-TW and zh-Hant-TW over [en, zh, zh-Hant-HK] → zh-Hant-HK, plus the one-script control zh-Hant-TW over [en, zh-CN] → zh-CN that the changeset claims; and one sentence in .changeset/30618-script-subtag-read-aliases.md saying a bundle key or offered code spelled zh-Hant-TW / zh-Hans-CN now counts as its script for every request of that script.

Implemented-by: claude/issue-30618-script-subtag-read-aliases
Reviewed-by: session_01L6bhgVugtCCwBs98FeR27P

VERDICT: PASS


Generated by Claude Code

…ag keys (#30618)

A bundle key or offered code spelled zh-Hant-TW / zh-Hans-CN is mapped
through the table too, so it counts as its script: zh-TW and zh-Hant over
[en, zh-CN, zh-Hant-TW] reach zh-Hant-TW, zh-CN over [en, zh-Hant,
zh-Hans-CN] reaches zh-Hans-CN, and zh-Hant-TW over [en, zh, zh-Hant-HK]
reaches zh-Hant-HK, in both registration orders. With one script
registered (zh-Hant-TW over [en, zh-CN]) the related language still
answers. Pinned in core's shared case list and in spec's table-step
cases; the changeset says the key side goes through the table.

Co-authored-by: Claude <[email protected]>
Claude-Session: https://claude.ai/code/session_01L6bhgVugtCCwBs98FeR27P
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#2 · os-sales · session_01L6bhgVugtCCwBs98FeR27P

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: d8713f3f32b323798a6416046bf4704ed6a9d82b
Local-runs: none
Scope: packages/spec/src/system/platform-locales.ts + packages/spec/src/system/i18n-resolver.ts (resolveLocaleAmong step 3, L569-575, unchanged against main), packages/core/src/fallbacks/memory-i18n.ts (unchanged against main), and the two pins the delta touches (packages/core/src/fallbacks/fallbacks.test.ts, packages/spec/src/system/i18n-resolver.test.ts), read as the gate of the key-side move; the changeset is judged under ②. The docs page content/docs/protocol/kernel/i18n-standard.mdx is a prose face outside hits: read for consistency with the code, not reviewed.

Delta re-review, written 2026-11-05T23:20Z. Inputs: card #30618 (body, comments 6666534830, 6669501005, 6670989840, 6671045459 and the patch-round report 6672039879), the PR's r2 body and file list, the delta e731b654..d8713f3f (two commits: the docs section, then the S1 pins and one changeset sentence; four files, +57/−15), the net diff against main for the two test files, and the check-runs on this head, read at 2026-11-05T23:19Z.

Carried from the record on e731b654 (comment 6671200273). packages/spec/src/system/platform-locales.ts (blob baefd871) and packages/spec/src/system/platform-locales.test.ts (blob 410cf43e) are byte-identical on both heads (git diff e731b654 d8713f3f --stat lists neither), and i18n-resolver.ts and memory-i18n.ts are unchanged against main. Every judgment of that record on the hits file — ①.1-3 and ①.5-7, ②, ③, ④ "holds" — stands here unchanged and is not restated. This record judges the delta: whether S1 is closed.

Gate verdicts on this head (check-run conclusions as they stood): success 57 — Test Core 1/8-8/8 and its aggregate, Test UI 1/16-16/16 and its aggregate, TypeScript Type Check, Type Check · source gates / workspace / consumer gates / debt ledger / UI, Build Core, Build & E2E UI, Live E2E UI, Build Docs, every Lint row, Check Changeset, PR-body guards, Governed Surface Queue Guard, Spec property liveness, Check Documentation Links, Flag docs affected, Check PR Size, Dogfood Regression Gate. Skipped 10 (opt-in and path-filtered). Still pending: a second Check Changeset and a second PR-body guards instance (re-triggered after the PR body was patched to r2; the first instances of both concluded success). No failure, no cancellation.

① Derived judgments

  1. The key-side rows pin what i18n-resolver.ts L573 does. Read against the step: platformLocaleTagOf(requested) must be defined (L569), then the key spelled as the tag (L572), else the first key the table maps to that tag (L573). Each new row follows:
    • zh-TW and zh-Hant over [en, zh-CN, zh-Hant-TW] → zh-Hant-TW: tag zh-Hant; no key spelled so; zh-CN maps to zh-Hans, zh-Hant-TW to zh-Hant. FROM on main: zh-Hant-TW answered undefined at L573, so step 5 handed the first zh-* key, zh-CN. Right.
    • zh-CN over [en, zh-Hant, zh-Hans-CN] → zh-Hans-CN: tag zh-Hans; zh-Hant is skipped at L573, zh-Hans-CN taken. FROM: step 5, zh-Hant. Right.
    • zh-Hant-TW over [en, zh, zh-Hant-HK] → zh-Hant-HK: tag zh-Hant; zh maps to zh-Hans, skipped. FROM: tag undefined, step 4 base zh. Right.
    • Reverse orders ([en, zh-Hant-TW, zh-CN], [en, zh-Hans-CN, zh-Hant], [en, zh-Hant-HK, zh]): the same key, now at L573 where step 5 found it first before. Unchanged, pinned so. Right.
    • One-script control zh-Hant-TW over [en, zh-CN] → zh-CN: L573 finds nothing, step 4 nothing, step 5 zh-CN, as before. Right.
      Core's shared list carries all nine rows on both resolveLocale and resolveBundleLocale (fallbacks.test.ts L331-341); spec carries the four moving requests in both orders plus the control in one new case (i18n-resolver.test.ts L1095-1107) and the zh-Hant-HK pair in the bare-zh case (L1091-1092). The three rows S1 asked for are among them.
  2. The changeset's new sentence — "a bundle key or offered code spelled zh-Hant-TW / zh-Hans-CN now counts as its script for every request the table reads as that script" — is true against the tree, and more exact than the wording S1 proposed: L573 runs only inside the tag !== undefined branch of L570, so a request the table does not read (zh-Hant-US) never reaches the key-side match and is still answered by steps 4-5 over a zh-Hant-TW-keyed bundle. Its three FROM → TO examples match ①.1.
  3. Nothing else derived from the delta. The two test files are outside @objectstack/core's and @objectstack/spec's published files[]; the changeset's level, package and both declarations are unchanged; platformLocaleTagOf's accept set is the first round's.

② Semver level

Unchanged from the first record: @objectstack/spec: minor, Clause-②: yes (widening), Metadata-protocol: semantic, no core changeset (test file only). The delta edits one sentence of the changeset body; the level and the declarations are untouched and still match what the diff publishes. Check Changeset: success.

③ Boundary flags

  • S1 (first record): closed by d8713f3f — pinned in both suites in both orders with the control, and stated in the changeset (①.1-2).
  • Docs page (Skills-docs): the seat's amendment 6671045459 is carried out by 6a1d4627, which edits the Read-aliases section only (L166-207). The r2 body's Skills-docs: none is right as to skills/**. Read for consistency only: the table now lists the five rows under "Code", the closed-list sentence names zh-Hant-US, the ladder paragraph names resolveLocaleAmong's five steps and the key-spelled-as-tag-else-first-mapped rule, and the registration-order paragraph's two examples (zh-Hant-US over [en, zh-CN, zh-Hant] → zh-CN, zh-Hant when registered first; zh-TW over [en, zh-CN] → zh-CN) read correctly against L569-584. Prose face, not reviewed here.
  • check:dual-build-cjs-loads and check:dev-prereqs carried from e731b654 in the dev's report: every non-test source file is byte-identical between the heads, and the CI rows that cover built output (Build Core, Type Check · workspace, Test Core) concluded success on this head. Accepted.
  • open_questions: none. Security findings: none.

④ Design judgment

packages/spec/src/system/platform-locales.ts — holds, carried (byte-identical to the head of comment 6671200273; see that record's ④). The delta adds the gate the first record found missing: a change to i18n-resolver.ts L573 that dropped the key-side mapping would now red nine rows in core and one case in spec, and the one-script control keeps the "related language beats none" step pinned for a script-subtag request. No new claim is made about the hits file.

Must-fix: none.

Should-fix: none.

Implemented-by: claude/issue-30618-script-subtag-read-aliases
Reviewed-by: session_01L6bhgVugtCCwBs98FeR27P

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#2 · os-sales · session_01L6bhgVugtCCwBs98FeR27P

维护者速读

  • 改了什么:平台语言表的读入别名加了五行,zh-Hans-CN、zh-Hans-SG 读作 zh-Hans,zh-Hant-TW、zh-Hant-HK、zh-Hant-MO 读作 zh-Hant。浏览器和操作系统发来这种带文字子标签的写法时,语言回退会按文字找到对应的包,不再按注册顺序碰运气。i18n 标准文档页的别名表和回退说明一起改了。
  • 为什么改:台湾、香港的繁体中文读者,浏览器发 zh-Hant-TW,以前表里查不到,回退落到先注册的 zh-* 键,常常拿到简体中文。
  • 风险与回滚:只放宽。以前能匹配的请求都还匹配,只有这几种写法改到它们该去的那种文字;列表仍是封闭的,没有行的写法(如 zh-Hant-US)照旧。@objectstack/spec 出 minor。回滚即 revert 本 PR。
  • 先看:packages/spec/src/system/platform-locales.ts 的五行别名和文档注释 → packages/core/src/fallbacks/fallbacks.test.ts 的共享用例 → content/docs/protocol/kernel/i18n-standard.mdx 的 Read aliases 一节。
  • 两分钟验证:pnpm --filter @objectstack/core exec vitest run src/fallbacks/fallbacks.test.ts,预期全过;删掉五行别名后 26 条转红。
  • 需要你做的:无,席位落地。
  • 席位意见:接受。首轮契约复核 PASS(6671200273,Head-sha e731b654),一条 should-fix(key 侧过表的移动没写也没钉),席位采纳;补丁轮后 delta 复核 PASS(6672120777,Head-sha d8713f3f),没有新发现。

席位核对

ACCEPT · head d8713f3f32b323798a6416046bf4704ed6a9d82b · 2026-11-05T23:22Z

  1. 形态:
    • draft,base main,正文 Fixes #30618,全文只有这一个闭合关键词。
    • 6 个文件都在认领 6669501005 的文件面内,加上修订 6671045459 放进来的文档页那一节:platform-locales.ts、三个测试文件、i18n-standard.mdx 的 Read aliases 一节、一个 changeset。
    • i18n-resolver.ts 逻辑、ui/**、service-settings、service-storage 代码、skills/**、docs/adr/**、.claude/**、AGENTS.md 都没碰。
  2. 前提实测:认领时在 origin/main b8929f48 上读到别名表只有十行,文档注释写着 “zh-Hans-CN is not listed”,platform-locales.test.ts 把 zh-Hans-CN、zh-Hant-TW 钉为 undefined。席位又在 origin/main 上读了 resolveLocaleAmong:第 3 步先把请求过表,再找拼成表标签的键,找不到就找表映射到同一标签的第一个键(L568-575);core 的 resolveLocale 直接调它(memory-i18n.ts L52)。所以多出的五行既改请求侧,也改键侧,与复核和 dev 的实测一致。
  3. 门禁 check-runs:
    • head d8713f3f 共 69 条 check-run,59 success、10 skipped、0 failure,全部跑完,Test Core、Test UI 各分片、Build Core、Check Changeset、PR-body guards(正文改后重跑的那一次也在内)、Governed Surface Queue Guard 都在其中。
    • 首轮复核 6671200273 和 delta 复核 6672120777 都在 Served-tier: CONTRACT_REVIEW_TIER 下 PASS,覆盖 hits 的 platform-locales.ts,以及 i18n-resolver.ts、core 回退、service-settings 的 localeSelfName、service-storage 的 storageTestCopyLocaleOf 这些消费方。platform-locales.ts 与 e731b654 逐字节相同,首轮判断沿用。
    • dev 本地:spec 817 个文件 / 25799 条、core 136 / 3130,typecheck 都是 0;check:generated exit 0;补丁 head 上 102 条派生门禁,--ran 对账 0 NOT-MEASURED,其中 check:dual-build-cjs-loads 和 check:dev-prereqs 沿用 e731b654 的读数(代码文件逐字节相同,CI 上对应行在本 head 是 success)。
    • 消融:删掉五行别名后 spec 21 条、core 26 条转红,还原后 blob 等于 HEAD。
  4. 席位自读(TIER_DEFAULT,hits 之外的部分):
    • 测试:platform-locales.test.ts 把五行加进别名列表和大小写用例,“列表封闭”那条改成 zh-Hant-US、zh-Hans-US 仍答 undefined,并加了类型断言;core 和 spec 的共享用例覆盖请求侧和键侧,两种注册顺序都有,单一文字的对照 zh-Hant-TW over [en, zh-CN] → zh-CN 也钉住了。
    • 文档页:别名表补了五个写法;封闭列表那句说明逐行读、不按模式猜,zh-Hant-US 仍答 undefined;回退一段按 resolveLocaleAmong 的五步重写,并写明两种仍按注册顺序决定的情况。逐句对照 L555-584 核过。
    • changeset 逐句核过:@objectstack/spec minor,Clause-②: yes (widening) / Metadata-protocol: semantic,与认领一致;“for every request the table reads as that script” 比复核原话更准,因为没有行的写法进不了第 3 步;Setup 默认语言选项那几句与 dev 测到的 localeSelfName 移动一致;core 只动测试,不出 changeset。
  5. 接受的偏差:
    • dev 记下的范围外观察:zh-Hant-US、zh-Hans-US 这类没有行的写法照旧按注册顺序走,在 service-storage 的翻译包上两种文字都会落到 zh-CN。dropped:这是认领记下的决定(列表封闭,只加枚举的行)带来的已知结果,文档页已写明;要加更多行得有人测到真实读者发这种写法。
    • PR 没设 assignee 和 reviewer,Tier S 不交人。
  6. 范围外去向和 Skills-docs::Skills-docs: none — … 行在,属实:skills/** 里两条 grep 都是 0 命中,文档页已在本 PR 修正。第 5 项的观察 dropped。
  7. 落地条款:check-governed-merges.mjs --pr 30681 已在本 head 读过:6 个路径 0 个命中 governed 登记,178 行(+149 / -29)≤ 3000,属于 Tier S;落地前再读一次。CI 已全绿,经 relay pr_ready + automerge_enable,跟到 MERGED,同一动作里摘 pm:dispatched。

Generated by Claude Code

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

1 participant