Repository navigation
Commit af8aab1
fix(spec,core): resolve locales through the platform table's read aliases, so a zh-TW / zh-HK reader gets zh-Hant (#30601)
domain:spec#3 · os-sales · session_01L6bhgTqqeHSwZYJmApbDCa
<!-- claude-projects-attribution: {"github_login":"os-sales"} -->
_Requested by **Sales** · [project
thread](https://claude.ai/code/project/chan_01D5bWFY3rFzgJcytD5gD4Uu?thread=cmsg_01D5bWFY3rFzgJcytD5gD4Uu5BQt6r6PAGQpDn8cZNzAPa)_
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 #30511's surface and not touched here.
- **Merged with `main` (#30589).** The merge in `51ace3fa` met #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. #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 #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.
<details><summary>Repro before the change (origin/main
2ffdb64)</summary>
```text
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
```
</details>
<details><summary>Package suites and typecheck</summary>
- `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.
</details>
<details><summary>Consumers reached (targeted locale suites, after a
full build 119/119)</summary>
- `@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.
</details>
<details><summary>Ablation (remove the table-language step)</summary>
- Mutation through `scripts/ablation-replace.mjs --delete` on the step-3
block of `resolveLocaleAmong` (anchor 1 → 0, blob 9707821 → 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 9707821 equals HEAD, `git diff HEAD`
empty, tree clean; rebuilt, preflight marker present in 2 built files;
spec 492 passed, core 102 passed.
</details>
<details><summary>Derived gates (dispatch-gates --commands at
9f692b7)</summary>
- 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.
</details>
---
_Generated by [Claude
Code](https://claude.ai/code/session_01L6bhgXFwZkpwurAYNxGpAb)_
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01L6bhgXFwZkpwurAYNxGpAb
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 14c67d4 commit af8aab1
16 files changed
Lines changed: 267 additions & 91 deletions
File tree
- .changeset
- content/docs
- automation
- references/system
- packages
- core/src/fallbacks
- plugins/plugin-email/src
- spec
- api-surface
- export-origins
- src
- contracts
- system
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
257 | 257 | | |
258 | 258 | | |
259 | 259 | | |
260 | | - | |
261 | | - | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
262 | 264 | | |
263 | 265 | | |
264 | 266 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
48 | 48 | | |
49 | 49 | | |
50 | 50 | | |
51 | | - | |
| 51 | + | |
52 | 52 | | |
53 | 53 | | |
54 | 54 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
| 8 | + | |
8 | 9 | | |
9 | 10 | | |
10 | 11 | | |
| |||
295 | 296 | | |
296 | 297 | | |
297 | 298 | | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
298 | 349 | | |
299 | 350 | | |
300 | 351 | | |
| |||
577 | 628 | | |
578 | 629 | | |
579 | 630 | | |
| 631 | + | |
| 632 | + | |
| 633 | + | |
| 634 | + | |
| 635 | + | |
| 636 | + | |
| 637 | + | |
| 638 | + | |
| 639 | + | |
| 640 | + | |
| 641 | + | |
| 642 | + | |
580 | 643 | | |
581 | 644 | | |
582 | 645 | | |
583 | 646 | | |
584 | 647 | | |
585 | 648 | | |
586 | | - | |
587 | | - | |
| 649 | + | |
| 650 | + | |
588 | 651 | | |
589 | 652 | | |
590 | 653 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | | - | |
| 3 | + | |
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
| |||
36 | 36 | | |
37 | 37 | | |
38 | 38 | | |
39 | | - | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
40 | 42 | | |
41 | 43 | | |
42 | | - | |
43 | | - | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
44 | 48 | | |
45 | 49 | | |
46 | 50 | | |
47 | 51 | | |
48 | | - | |
49 | | - | |
50 | | - | |
51 | | - | |
52 | | - | |
53 | | - | |
54 | | - | |
55 | | - | |
56 | | - | |
57 | | - | |
58 | | - | |
59 | | - | |
60 | | - | |
61 | | - | |
62 | | - | |
63 | | - | |
64 | | - | |
65 | | - | |
66 | | - | |
67 | | - | |
| 52 | + | |
68 | 53 | | |
69 | 54 | | |
70 | 55 | | |
| |||
75 | 60 | | |
76 | 61 | | |
77 | 62 | | |
78 | | - | |
79 | | - | |
80 | | - | |
81 | | - | |
82 | | - | |
83 | | - | |
84 | | - | |
85 | | - | |
86 | | - | |
87 | | - | |
| 63 | + | |
88 | 64 | | |
89 | | - | |
90 | | - | |
91 | | - | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
92 | 72 | | |
93 | 73 | | |
94 | | - | |
95 | | - | |
96 | | - | |
97 | | - | |
98 | | - | |
99 | | - | |
100 | | - | |
101 | | - | |
102 | | - | |
103 | | - | |
| 74 | + | |
104 | 75 | | |
105 | 76 | | |
106 | 77 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1494 | 1494 | | |
1495 | 1495 | | |
1496 | 1496 | | |
1497 | | - | |
1498 | | - | |
| 1497 | + | |
| 1498 | + | |
| 1499 | + | |
1499 | 1500 | | |
1500 | 1501 | | |
1501 | 1502 | | |
| |||
0 commit comments