Skip to content

Commit 5bca14f

Browse files
feat(i18n)!: the Console's language packs follow the platform locale table, keyed by its tags, and Traditional Chinese (zh-Hant) ships (#30902)
<!-- claude-projects-attribution: {"github_login":"os-tesla"} --> _Requested by **Tesla** · [project thread](https://claude.ai/code/project/chan_01AXsQkLFKRdQCqQ3RTr6Jao?thread=cmsg_01AXsQkLFKRdQCqQ3RTr6JaoYVQTBC6cJe24DirKzoUe7C)_ domain:ui#2 · os-tesla · session_01L9qibyWSiXh6B2sMVHfXYC Part of #30511. Every item the card asks for is in this PR; the seat closes the card when it lands, and phase 4 (the locale menu) follows. Supersedes #30595 (the same reviewed diff, resumed from `claude/project-thread-xplvvb@9b15e9a` on this thread's own branch, then merged with `main` through #30640's lazy `marketplace` namespace and #30601's shared locale ladder). Clause-②: yes (narrowing) — widens: a `zh-Hant` pack (the `zhHant` export of `@object-ui/i18n/locales`, the `@object-ui/i18n/locales/zh-Hant` module), the `resolveBuiltInLanguage` export of `@object-ui/i18n`, and every stored or browser code the platform locale table reads as one of its tags (`zh-TW`, `zh-HK`, `zh-MO` → `zh-Hant`; `zh`, `zh-CN`, `zh-SG` → `zh-Hans`; `pt` → `pt-BR`). Narrows: the Simplified Chinese and Portuguese packs are keyed `zh-Hans` and `pt-BR`, no longer `zh` and `pt`, in `builtInLocales`, `BUILT_IN_LANGUAGE_CODES`, `getLoadedBuiltInLocales()` and `getAvailableLanguages()`, and their modules `@object-ui/i18n/locales/zh` and `@object-ui/i18n/locales/pt` are `@object-ui/i18n/locales/zh-Hans` and `@object-ui/i18n/locales/pt-BR`. Metadata-protocol: none Skills-docs: follow-up owed — skills/objectui/guides/i18n.md ## 维护者速读 - 改了什么:控制台的语言包改为跟随平台语言表,表里每个语言一个包,以表的标签为键(`zh` → `zh-Hans`,`pt` → `pt-BR`),新增繁体中文 `zh-Hant`,由简体包自动转换生成。刚合入的应用市场按需文案包同样按标签拆成 11 份,也有繁体。存值语言(`sys_user.locale`)挑选改用平台统一的匹配顺序,与服务端一致。 - 为什么改:裁决 F(#29349)要求控制台的语言集合取自平台语言表;此前控制台自带十个包,表里的繁体中文缺席,测试照样全绿。台湾、香港用户此前看到的是简体。 - 风险与回滚:只动前端与 i18n 门禁脚本,不动 spec 与服务端。旧存值 `zh-CN`、`zh`、`pt` 照常渲染;`zh-TW`、`zh-HK` 用户从简体变为繁体。仓外代码若按 `builtInLocales.zh` 或 `@object-ui/i18n/locales/zh` 取包会断,changeset 写了改法。首屏英文包字节不变。回滚即 revert 本 PR。 - 先看:`ui/packages/i18n/src/locales/registry.ts`(按标签加载与 `resolveBuiltInLanguage`)、`ui/packages/i18n/src/namespaces/marketplace/index.ts`(应用市场文案按标签加载)、`ui/packages/i18n/scripts/zh-hant.mjs`(繁体转换规则)。 - 两分钟验证:`pnpm --dir ui exec vitest run packages/i18n/`,预期全部通过;再把 `ui/packages/i18n/src/locales/index.ts` 里 `'zh-Hant': zhHant,` 一行删掉重跑 `all-locales-key-parity`,预期转红并点名 `zh-Hant`。 - 需要你做的:无。os-zhuang 已于 2026-11-06 14:26Z 批准本 PR(head `c1f166cc`),批准后的推送只有三次 merge main、两次 `zh-Hant` 重新生成、一行测试改写(changeset 的 FROM → TO)和三个测试文件的模块顶层预热导入(main #30969 的新规则),契约复审 `CONTRACT_REVIEW_TIER` delta PASS 在案(评论 6693289236,Head-sha `d9b3ddde571af4950702457bce4edb4de51ccce2`)。席位落地。 - **席位意见**:ui 席 2 对照 head `c1f166cc` 的 diff 复核:152 个文件,语言包键随平台语言表改为标签(`zh` → `zh-Hans`、`pt` → `pt-BR`),新增由简体自动生成的 `zh-Hant`,应用市场按需文案包同样按标签拆分;旧存值 `zh`、`zh-CN`、`pt` 经表的读别名照常解析,首屏英文包字节不变、上限未动。属收窄(BREAKING,patch,changeset 带 FROM → TO 与 ADR-0087 标记),独立复审员按 `CONTRACT_REVIEW_TIER` 先判 FAIL(lint 入口守卫),修复后 delta 复审 PASS;CI 57 项全绿。复审提出的后续已立卡:#30910(繁体会话读写内联标签取到简体键)、#30911(skills i18n 指南仍教 `zh`/`pt`);服务端与 studio 命名空间缺繁体归 #29428。同意落地,待人批。 ## What changes **The pack set comes from the platform locale table.** `ui/packages/i18n` no longer keeps its own list of ten languages. It reads `PLATFORM_LOCALES` from `@objectstack/spec/system` (phase 1, PR #30007). - **Pack files.** `src/locales/zh.ts` is now `zh-Hans.ts`, `pt.ts` is now `pt-BR.ts`, and `zh-Hant.ts` is new. `builtInLocales` is keyed by table tag, in table order, and `satisfies Record<PlatformLocaleTag, unknown>`. A tag the table gains without a pack, or a pack the table lacks, is a compile error. - **`registry.ts`.** `LAZY_LOCALE_LOADERS` is typed by the table's tags, and `BUILT_IN_LANGUAGE_CODES` is `PLATFORM_LOCALE_TAGS`. - The new export `resolveBuiltInLanguage(code)` names the pack that a stored or browser code reads as. It tries the table's tag or read alias first (`platformLocaleTagOf`, case-insensitive), then each shorter prefix (`zh-Hant-TW` → `zh-Hant`, `en-GB` → `en`). - `loadBuiltInLocale` and `isBuiltInLanguage` go through it, so every legacy code still loads its pack. - **`i18n.ts`.** `fallbackLng` is a map built from the table. Every tag falls back to the fallback language. Every read alias falls back to its tag, then to the fallback language. So an instance whose language is a legacy `zh` or `zh-CN` renders the `zh-Hans` resource. - **`provider.tsx`.** The boot effect and `changeLanguage` now write a built-in catalogue under its table tag, the bundle `createI18n` keyed it by. - Before, an instance in an alias (`zh`, `zh-TW`, `pt`) got the resident catalogue written a second time under the alias. That fired an `added` event, which re-rendered every reader and refetched every chart keyed on the resolver. - `ObjectChart.refreshIndicatorLocale-10580` found it: its zh chart never left the skeleton. **zh-Hant is generated.** `scripts/zh-hant.mjs` is the converter. It uses: - OpenCC `s2twp`, with `opencc-js` pinned at 1.3.1 as a devDependency; - the phrase overrides of the www.objectos.ai converter; - the locative 里 repairs. `scripts/gen-zh-hant.mjs` writes `src/locales/zh-Hant.ts` from `zh-Hans.ts` (`pnpm --filter @object-ui/i18n gen:zh-hant`). It refuses to write when a pinned conversion case fails. There are two additions over the site's rules, both pinned as conversion cases: - **`STRADDLE_OVERRIDES`**: 并发布 → 並發布, 头发送 → 頭發送, 是只读 → 是只讀, 只影响 → 只影響. `s2twp` segments by its Simplified phrase dictionary before it converts. A phrase that straddles a word boundary (审阅「并发」布) therefore cannot be repaired at the Traditional phrase stage. The fix puts the Simplified phrase into the segmentation dictionary and at the head of the first group. - **`TWP_PHRASE_OVERRIDES`** gains 操作運行時 → 操作執行時. Other details of the generated pack: - **Gate-only keys.** The six gate-only `console.ai.*` outbound keys are left out of zh-Hant, as they are from every non-gate pack. Comments replace them so line numbers stay aligned with zh-Hans. - **Freshness test.** `src/__tests__/zh-hant-generated.test.ts` reds when the committed pack is not byte-for-byte what the generator writes, and names the command that repairs it. It is a package test, not a `check:*` gate (ruling F adds none). - **Merge-driver ledger.** It records `zh-Hant.ts` as `NOT_DRIVER_MANAGED` (gen `gen:zh-hant`). The pack is generated whole, but a `REGEN_ARTIFACTS` row needs a `check:` script, and this phase adds none. - **Type-check coverage.** The converter's `zh-hant.d.mts` twin joins the package's `tsconfig.test.json`, so `check:type-check-coverage` reads it. - **Lint-rule coverage.** `ui/scripts/check-lint-rule-coverage.mjs` declares the same twin in `UNREACHED_GROUPS`, beside the two script twins it already declares. Its tests now accept an `objectstack#` card on a ledger row as well as an `objectui#` one. **The lazy `marketplace` namespace follows (#30640).** `src/namespaces/marketplace/zh.ts` is now `zh-Hans.ts`, `pt.ts` is now `pt-BR.ts`, and `zh-Hant.ts` is new, generated by the same command. - `MARKETPLACE_LOADERS` is typed `Record<PlatformLocaleTag, …>`, eleven literal imports, so a table tag without a catalogue is a compile error. - `resolveMarketplaceLanguage` reads a stored code through `resolveBuiltInLanguage`, so `zh-TW` loads the Traditional catalogue and `pt` the `pt-BR` one. The loader stores the catalogue under the table tag. - The all-packs door (`locales/index.ts`) deep-merges each tag's catalogue into its pack, as main's `complete()` already did, and still throws on a leaf defined twice. - The converter gains `generateZhHantNamespaceCatalogue` and a frozen `ZH_HANT_TARGETS` list (the locales pack and the marketplace catalogue); `gen:zh-hant` writes both, and the freshness test walks the list. - `apps/console/vite.config.ts` names one `i18n-ns-marketplace-<tag>` chunk per table tag. - #30840's eleven `install.preview` keys came in through the renames and are in `zh-Hans`, `pt-BR` and the regenerated `zh-Hant`. **Readers keyed by the old codes.** - **`app-shell` readers.** `LoadingScreen` (the boot line), `outboundAgentText` and `flowResponse` read the loaded-pack snapshot by table tag, through `resolveBuiltInLanguage`. - **`app-shell` `pickRenderableLocale`** (the `sys_user.locale` → UI language pick) is now `resolveLocaleAmong` from `@objectstack/spec/system` (#30601), the ladder the server's `resolveLocale` uses, instead of a copy of it. A stored `zh-TW` meets a list offering both scripts and renders `zh-Hant`; before, it took the first `zh` variant, `zh-Hans`. A variant the table reads as the language itself (`es-ES` for `es`) is taken ahead of the first offered variant, as the server takes it. - **`components` page tabs.** The Traditional rows of the exact-locale label dictionary now serve a `zh-Hant` session, and `zh-HK` / `zh-MO` sessions too. Before, every non-exact `zh` fell to the Simplified rows. - **`apps/console/vite.config.ts`.** One locale chunk per table tag. **Gates read the table.** - **`check-i18n-en-drift.mjs`** takes its locales from `PLATFORM_LOCALE_TAGS` and merges each namespace's partial pack in (main's `PACK_NAMESPACE_DIRS`), throwing on a key defined twice. A follower pack that the base commit does not have (this PR's renames and `zh-Hant`) reads as empty at the base and is named in the summary. `en` missing at the base, or any pack missing at HEAD, still throws. - **`check-i18n-dead-keys.mjs`** strips map-chain tags by the table. - **`check-eager-locale-catalogues.mjs`** keeps its deliberate explicit list, now the eleven tags. Its test pins that list to the table and to the registry's loader table. **Paths.** One doc link (`ui/packages/plugin-gantt/docs/verification/README.md`, which `check-doc-links` reads) and one test comment named `zh.ts`. Both now name `zh-Hans.ts`. ## Tests - **Suites that read the table.** The parity, region-fallback, entry-shape and en-drift suites read the table. - `all-locales-key-parity` walks `PLATFORM_LOCALE_TAGS`, so a table language with no pack reds it by name. - `region-locale-fallback-11326` covers both scripts: `zh-CN` and `zh-SG` read Simplified; `zh-TW`, `zh-HK` and `zh-MO` read Traditional. - **`zh-hant-generated.test.ts` (new)** covers: - byte-for-byte freshness and the generated header; - every pinned conversion case; - 保存 → 儲存 as the converter's answer; - the gate-only keys dropped, and nothing else; - the converter's declaration twin. - **`provider-load-rerender-10382.test.tsx`.** An instance booted in `zh`, `zh-CN`, `zh-TW` or `pt` reads its tag's resident catalogue, renders once, and keeps no bundle under the alias. A catalogue that lands after mount reaches a `zh-TW` reader. - **`i18n.test.ts`.** Main's CJK-Latin title spacing (#30126) is pinned for both `zh-Hans` and `zh-Hant` booted by tag. - **`check-i18n-en-drift.test.ts`.** Fixtures write a const named `pack`, because a tag is not an identifier. A new describe pins the base-absent follower rule, its CLI line, and the two cases that still throw. - **`git-paths-nested-layout.test.ts`.** It pinned that any pack missing at the base throws. Under the follower rule only `en` does, so the case now pins both halves: `en` missing at the base throws, and a follower the base lacks reads as empty and is named. - **`userLocale-10059.test.tsx`.** A new describe pins the table step: - `zh-TW`, `zh-HK`, `zh-MO` and `zh-Hant-TW` → `zh-Hant`; - `zh`, `zh-CN` and `zh-SG` → `zh-Hans`; - `pt` → `pt-BR`; - a deployment's own spelling; - the control, where no Traditional spelling is offered. The pin "first offered variant" moved from `zh` to `es` (`es` over `en`, `es-MX`, `es-AR` → `es-MX`), because the table reads a bare `zh` as Simplified. A new case pins the shared ladder's table step: `es` over `en`, `es-MX`, `es-ES` → `es-ES`. - **Marketplace namespace suites** (`marketplace-namespace`, `marketplace-namespace-lazy`, `MarketplacePage.namespaceGate-30588`) are keyed by tag, and pin `zh-TW` / `zh-HK` → `zh-Hant` and `pt` → `pt-BR` resolution plus a `zh-TW` reader's lazy load. `zh-hant-generated` runs each case over `ZH_HANT_TARGETS`, with a counter-probe on the list itself. - **`page-tabs-builtin-label-i18n-4645.test.tsx`.** `zh-Hant`, `zh-HK` and `zh-MO` sessions read 詳情 / 備註 / 概覽, and two of those are dict-only tokens. The `zh-Hans` / `zh-SG` control reads Simplified. - **LocaleSwitcher suites** (`appLocales`, `persistence`, `unsavedEditsAsk-13254`). They pick and expect the menu's names for the table's tags through `localeLabel`, and the fallback-menu cases expect one entry per tag. The component itself is not edited. - **About 100 other suites** hard-coded ten packs, a `zh` / `pt` key or a `'zh'` lookup. They now read the table or the tag. For example, `toHaveLength(10)` became `PLATFORM_LOCALE_TAGS.length`, and where a table names every pack, a converted `zh-Hant` twin sits beside each retagged `zh-Hans` row. ## Readings (head `f7e9df2d`, base `98f640d5`) <details> <summary>Ablations, suites, type-check and gates</summary> The heavy runs were taken on `e94c13c8`. `f7e9df2d` adds one merge of main (11 commits) and the regenerated `zh-Hant`; the i18n package, the en-drift test, the 32 test files those 11 commits added or changed, the i18n checks and the ADR-0087 and no-major checks were rerun on it. - **Green on main, red by name here.** On main before this branch, the table already named `zh-Hans`, `zh-Hant` and `pt-BR` and the Console had no pack keyed by any of them, yet `all-locales-key-parity` and `region-locale-fallback-11326` were green (2 files, 58 tests; taken for #30595). - **Ablations.** Each was restored afterwards, with `git diff HEAD` empty. - The `zh-Hant` entry removed from `builtInLocales` → `all-locales-key-parity` 3 failed / 42 passed, and the diff names `zh-Hant` (taken for #30595). - One `zh-Hans` value edited without regenerating → `zh-hant-generated` red, naming the command that repairs it (taken for #30595; the test now walks both targets). - The `zh-Hant` loader removed from the marketplace namespace → `tsc` TS2741 (`"zh-Hant"` missing from the `Record<PlatformLocaleTag, …>`), and `marketplace-namespace` + `marketplace-namespace-lazy` 6 failed / 64 passed, including "a Traditional Chinese reader (a stored zh-TW) gets the zh-Hant catalogue". Restored: green. - **Suites.** - Full ui suite (`pnpm --dir ui run test`) on `d0632482`: 6631 files passed, 7 skipped; 21 files red. 19 of them read built `dist` / `.d.ts` output that the skip-dts build had not written; the other 2 named the old `builtInLocales.zh` key and were fixed in `d0632482`. After the full workspace build, the 21 files rerun: 21 / 21, 513 tests. - `packages/i18n/`, `scripts/`, `packages/app-shell/src/hooks/` and the 36 test files main had added since the first merge, on `e94c13c8`: 401 files passed, 2 skipped; 7830 tests. - On `f7e9df2d`: `packages/i18n/`, the en-drift test and the 32 test files of the last merge: 131 files, 2014 tests, green. - Dogfood on `e94c13c8`, after the full workspace build: `pnpm --filter @objectstack/dogfood test`: 424 files passed, 3 skipped; 3538 tests. - **Build and type-check.** - The full workspace build with type declarations (`turbo run build --filter='!@objectstack/docs'`): 118 / 118. It caught one gap that the skip-dts runs had not: `loadMarketplaceCatalogue` indexed the tag-typed loader table with a `string`. `e94c13c8` makes `hasMarketplaceCatalogue` a type guard. - `turbo run type-check --filter='...[origin/main]'`: 101 / 101. ui `type-check:coverage`, `:e2e`, `:scripts`, `:vitest-setup` and `:vitest-config`: exit 0. - **Gates.** - The 76 derived dispatch gates exit 0 on `e94c13c8`. `dispatch-gates --ran` reads 76 derived, 76 run, 0 NOT-MEASURED, 0 UNRUN. - ui `check:eager-closure`, `check:eager-locale-catalogues`, `check:i18n-keys`, `check:i18n-drift`, `check:i18n-dead-keys`, `check:spec-symbols`, `check:phantom-deps`, `check:unreferenced-sources`, `check:sdui-registration-pins` and `node scripts/check-sdui-manifest.mjs`: exit 0. - After the Console's build, the `i18n-locale-en` chunk holds the single module `en.ts`, whose source is identical to main's; 57,625 gz bytes. No size ceiling is raised. `zh-Hant` is 62,336 gz bytes, and the marketplace `zh-Hant` catalogue 4,560, both loaded on demand. - `check-adr-0087-registration --base origin/main` and `check-changeset-no-major --base origin/main`: exit 0 on `f7e9df2d`, the latter also with this body as `--event` (level axis `yes (narrowing)` + `none`, every entry at `patch`). - eslint on the 144 changed ui sources: 0 errors. </details> ## Acceptance notes - **Native read of zh-Hant.** The conversion follows the site's rules, but a Traditional reader should still skim the pack. Renderings worth a look: - 儀表盤 (Taiwan usage is often 儀表板); - 只讀 (often 唯讀); - 釋出 and 發布 both appearing for "publish"; - 後臺 / 啟動臺 (臺 vs 台); - 會話 for "session" (Taiwan usage is often 工作階段), and 退出登入 (often 登出); - the curly quotes “…” kept from Simplified (Taiwan usage is often 「…」). A rule change goes in `scripts/zh-hant.mjs` and in the site's converter, never in a hand edit of the pack. - **Studio.** The Studio namespace still ships `en` and `zh`. A `zh-Hant` or `zh-TW` reader sees Studio's Simplified catalogue (ruling #25922's namespace, untouched here). - **The language menu (phase 4).** `LocaleSwitcher` is not edited. Its hand-kept names are keyed `zh` and `pt`, so `Intl.DisplayNames` names the three tags they no longer match: 简体中文, 繁體中文 and Português (Brasil). The menu gains 繁體中文 as an eleventh entry. - **Inline locale maps.** `pickLocalized` is unchanged. Take a metadata label written `{ 'zh-CN': …, 'zh-TW': … }`: a `zh-Hant` reader takes the first `zh-` key. A `zh-TW`-browser reader did the same before this PR, because that browser's language then resolved to `zh`. Teaching `pickLocalized` the table is a follow-up candidate, not done here. - **One ladder.** The Console's `sys_user.locale` pick and the server's `resolveLocale` now share `resolveLocaleAmong`, so the stored-locale ladder has one copy. The server's catalogues are its own and are not touched here. - **ADR-0087.** The changeset holds `not-required (no-migration-prescription)`, and its marker names why. - The FROM → TO lines are source rewrites for consumers of the `./locales` subpath, which is the class `runtime-export-retired` serves. - But that category reads only a package's root entry and refuses a subpath operand, and the root entry retires nothing here. - The seat may prefer to raise the gap. - **Studio keys.** #30721's studio keys came in with the merge unchanged; Studio stays `en` + `zh`, so nothing in it is retagged. - **Unchanged on purpose.** The root `ci.yml` step is still named "followed by the nine translations", because the workflow is not edited. The eager-locale gate keeps its explicit list, now pinned to the table by its test. ## Verification Two minutes, from `ui/`: 1. `pnpm exec vitest run packages/i18n/`: everything passes. 2. Delete the `'zh-Hant': zhHant,` line from `packages/i18n/src/locales/index.ts` and run `pnpm exec vitest run packages/i18n/src/__tests__/all-locales-key-parity.test.ts`: red, naming `zh-Hant`. 3. `pnpm --filter @object-ui/i18n gen:zh-hant`: writes the same two `zh-Hant.ts` files (the pack and the marketplace catalogue) that are committed, so `git diff` stays empty. A sample of the generated pack, with Taiwan vocabulary from `s2twp`: | `zh-Hans.ts` | `zh-Hant.ts` | | --- | --- | | `stop: '结束模拟登录'` | `stop: '結束模擬登入'` | | `manage_users: '管理用户'` | `manage_users: '管理使用者'` | | `manage_metadata: '管理元数据'` | `manage_metadata: '管理中繼資料'` | | `manage_platform_settings: '管理平台设置'` | `manage_platform_settings: '管理平台設定'` | | `manage_connector_connections: '管理连接器连接'` | `manage_connector_connections: '管理聯結器連線'` | | `save: '保存'` | `save: '儲存'` | <!-- signed: domain:ui#2 · os-tesla · session_01L9qibw7DekEe3wx5f3zphf --> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 9ed2ee1 commit 5bca14f

153 files changed

Lines changed: 7717 additions & 825 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
---
2+
'@object-ui/i18n': patch
3+
'@object-ui/app-shell': patch
4+
'@object-ui/components': patch
5+
'@object-ui/console': patch
6+
---
7+
8+
feat(i18n)!: the Console's language packs are the platform locale table's, keyed by its tags, and Traditional Chinese (`zh-Hant`) ships, generated from Simplified (#30511)
9+
10+
Clause-②: yes (narrowing) — widens: a `zh-Hant` pack (the `zhHant` export of `@object-ui/i18n/locales`, the `@object-ui/i18n/locales/zh-Hant` module), the `resolveBuiltInLanguage` export of `@object-ui/i18n`, and every stored or browser code the platform locale table reads as one of its tags (`zh-TW`, `zh-HK`, `zh-MO` → `zh-Hant`; `zh`, `zh-CN`, `zh-SG` → `zh-Hans`; `pt` → `pt-BR`). Narrows: the Simplified Chinese and Portuguese packs are keyed `zh-Hans` and `pt-BR`, no longer `zh` and `pt`, in `builtInLocales`, `BUILT_IN_LANGUAGE_CODES`, `getLoadedBuiltInLocales()` and `getAvailableLanguages()`, and their modules `@object-ui/i18n/locales/zh` and `@object-ui/i18n/locales/pt` are `@object-ui/i18n/locales/zh-Hans` and `@object-ui/i18n/locales/pt-BR`.
11+
Metadata-protocol: none
12+
Skills-docs: follow-up owed — `skills/objectui/guides/i18n.md` (its "Built-in locales" list names ten packs, with `zh` and `pt`)
13+
14+
<!-- adr-0087: not-required (no-migration-prescription) No metadata changes shape: no spec key, object definition, stored row or authorable spelling is added, removed or renamed, so `objectstack migrate meta` has nothing to rewrite and no ledger entry could project. The FROM/TO lines below are source rewrites for TypeScript and JavaScript consumers of `@object-ui/i18n`'s `./locales` subpath (two members of `builtInLocales`, two modules). That is the class `runtime-export-retired` exists for, but it reads a package's root entry only and refuses a subpath operand, and the root entry retires nothing here; so this category is held, and the gap is named here rather than left to the detector. -->
15+
16+
**BREAKING** for code that reads a built-in pack by its old key or module path.
17+
18+
- **Why.** The Console's packs follow the platform locale table (`PLATFORM_LOCALES` in `@objectstack/spec/system`, ruling F of #29349) instead of a list of their own: one pack per table tag, keyed by the tag, in the table's order. A language the table gains is a pack the parity and drift suites demand on the run that first sees it.
19+
- **Traditional Chinese.** `src/locales/zh-Hant.ts` is generated from `zh-Hans.ts` by `pnpm --filter @object-ui/i18n gen:zh-hant` (OpenCC `s2twp`, pinned, with the phrase rules of the www.objectos.ai converter) and is never edited by hand; a package test fails when it is not what the generator writes. It carries no `console.ai.*` gate-only outbound keys, exactly as the other non-gate packs.
20+
- **The marketplace namespace follows.** The lazy `marketplace` namespace keys its catalogues by the same tags (`@object-ui/i18n/namespaces/marketplace`'s `MARKETPLACE_LANGUAGE_CODES` and `resolveMarketplaceLanguage` answer `zh-Hans`, `zh-Hant`, `pt-BR`), ships a `zh-Hant` catalogue generated by the same command, and resolves a stored code through the table as the packs do, so a `zh-TW` reader's marketplace pages are Traditional too.
21+
- **One locale ladder.** The Console picks which offered language renders a user's stored `sys_user.locale` with `resolveLocaleAmong` from `@objectstack/spec/system`, the order the server's `resolveLocale` uses, instead of a copy of it: a deployment offering both scripts renders a `zh-TW` row in its Traditional spelling, and a variant the table reads as the language itself (`es-ES` for `es`) is taken ahead of the first offered variant, as the server takes it.
22+
- **Stored codes keep working.** A `sys_user.locale` value, a saved device language or an `Accept-Language` tag written before the table is read through the table's aliases: `zh-CN` still renders Simplified Chinese, `pt` Portuguese (Brazil), and `zh-TW` / `zh-HK` / `zh-MO` now render Traditional Chinese instead of Simplified. `loadBuiltInLocale`, `isBuiltInLanguage` and i18next's fallback chain accept every alias.
23+
- **Studio.** The Studio namespace still ships `en` and `zh` only; a `zh-Hant` reader sees its Simplified catalogue there.
24+
25+
The fix, for code that names a pack:
26+
27+
- FROM `builtInLocales.zh` TO `builtInLocales['zh-Hans']`
28+
- FROM `builtInLocales.pt` TO `builtInLocales['pt-BR']`
29+
- FROM `import zh from '@object-ui/i18n/locales/zh'` TO `import zh from '@object-ui/i18n/locales/zh-Hans'`
30+
- FROM `import pt from '@object-ui/i18n/locales/pt'` TO `import pt from '@object-ui/i18n/locales/pt-BR'`
31+
- FROM comparing a code from `BUILT_IN_LANGUAGE_CODES`, `getAvailableLanguages()` or the keys of `getLoadedBuiltInLocales()` with `'zh'` or `'pt'` TO comparing `resolveBuiltInLanguage(code)` with `'zh-Hans'` or `'pt-BR'`

‎pnpm-lock.yaml‎

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎scripts/regen-artifacts.mjs‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -913,6 +913,33 @@ export const NOT_DRIVER_MANAGED = Object.freeze([
913913
+ 'serves that day and drop every edit the patch list does not carry, and it needs the network, '
914914
+ 'which a merge must not. A conflict in a vendored component stays a human\'s; never routed.',
915915
},
916+
// objectstack#30511 (ruling F of #29349): the Console's Traditional Chinese pack.
917+
{
918+
path: 'ui/packages/i18n/src/locales/zh-Hant.ts',
919+
gen: 'gen:zh-hant',
920+
owner: '@object-ui/i18n',
921+
why:
922+
'generated WHOLE from `zh-Hans.ts` by `gen:zh-hant`, so "discard both sides and re-run" '
923+
+ 'loses no human decision. Still not routed: a `REGEN_ARTIFACTS` row needs a `check:` script '
924+
+ 'as its currency proof, and ruling F of #29349 adds no gate to this phase. The currency proof '
925+
+ 'is the package test `src/__tests__/zh-hant-generated.test.ts`, which reds a pack that is not '
926+
+ 'byte-for-byte the generator\'s output wherever the ui unit tests run, and the driver is local '
927+
+ 'anyway (never the queue\'s merge). The pack is line-aligned with `zh-Hans.ts`, one line per '
928+
+ 'line, so two branches editing different keys text-merge apart; a conflict is resolved by '
929+
+ 'taking either side and running `pnpm --filter @object-ui/i18n gen:zh-hant`.',
930+
},
931+
// objectstack#30511: the marketplace namespace's Traditional Chinese catalogue, same command.
932+
{
933+
path: 'ui/packages/i18n/src/namespaces/marketplace/zh-Hant.ts',
934+
gen: 'gen:zh-hant',
935+
owner: '@object-ui/i18n',
936+
why:
937+
'generated WHOLE from the namespace\'s `zh-Hans.ts` by the same `gen:zh-hant` run that writes '
938+
+ 'the pack above, and not routed for the same reason: no `check:` script proves its currency. '
939+
+ 'The proof is the same package test, which walks every generated target. Line-aligned with '
940+
+ 'its source, so a conflict is resolved by taking either side and running '
941+
+ '`pnpm --filter @object-ui/i18n gen:zh-hant`.',
942+
},
916943
]);
917944

918945
/** Marker file (inside the git dir, never the worktree) listing paths the driver deferred. */

‎ui/apps/console/src/__tests__/i18nAfterSignIn-12034.test.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ import { App } from '../App';
159159
import { loadLanguage } from '../loadLanguage';
160160
import { loadLocales } from '../loadLocales';
161161

162-
const zh = builtInLocales.zh;
162+
const zh = builtInLocales['zh-Hans'];
163163
const TOKEN = 'tok-12034';
164164
const USER = { id: 'u_12034', email: 'ada@example.com', name: 'Ada' } as unknown as AuthUser;
165165
const SESSION = { token: TOKEN, expiresAt: new Date(Date.now() + 3_600_000) };

‎ui/apps/console/src/components/StudioRoute.accessNotice-12035.test.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ describe('/studio refused: the sentence arrives in the caller language (objectui
159159
});
160160

161161
describe('CONTROL: a caller with Studio access enters Studio as today', () => {
162-
it.each(['en', 'zh'] as const)('%s', async (lang) => {
162+
it.each(['en', 'zh-Hans'] as const)('%s', async (lang) => {
163163
inLanguage(lang);
164164
systemPermissions = [STUDIO_ENTRY_CAPABILITY];
165165
renderStudio('/studio/');

‎ui/apps/console/src/pages/SharedRecordPage-26767.test.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ describe('SharedRecordPage — a shared non-chat record renders as read-only row
165165
});
166166

167167
it('under zh the value words come from the zh pack', async () => {
168-
const { zh, en } = builtInLocales;
168+
const { 'zh-Hans': zh, en } = builtInLocales;
169169
// Guard: these are real zh rows, not English copies.
170170
expect(zh.common.yes).not.toBe(en.common.yes);
171171
expect(zh.sharedRecord.redactedFields).not.toBe(en.sharedRecord.redactedFields);

‎ui/apps/console/src/pages/auth/__tests__/AcceptInvitationRoute.test.tsx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -345,12 +345,12 @@ describe('objectui#3811 — console routes DefaultAcceptInvitationPage', () => {
345345
renderRoute({ lang: 'zh' });
346346
await screen.findByTestId('accept-invitation-page');
347347

348-
const zh = builtInLocales.zh.organization.accept as Record<string, string>;
348+
const zh = builtInLocales['zh-Hans'].organization.accept as Record<string, string>;
349349
expect(zh.accept).toBe('接受邀请');
350350
expect(screen.getByRole('button', { name: zh.accept })).toBeInTheDocument();
351351
expect(screen.getByText(zh.organization)).toBeInTheDocument();
352352
expect(screen.getByText(zh.expiresAt)).toBeInTheDocument();
353353
// The pack that used to serve this screen's other half is gone entirely.
354-
expect((builtInLocales.zh as Record<string, unknown>).acceptInvitation).toBeUndefined();
354+
expect((builtInLocales['zh-Hans'] as Record<string, unknown>).acceptInvitation).toBeUndefined();
355355
});
356356
});

‎ui/apps/console/src/pages/auth/__tests__/LoginPage.ssoLabels-12762.test.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ async function expectSsoCopy(expected: Record<keyof typeof ENGLISH, string>) {
9090

9191
describe('console LoginPage — SSO copy under zh (objectui#12762)', () => {
9292
it('draws the zh bundle value of each SSO label, and no English default', async () => {
93-
const zh = builtInLocales.zh.auth.login;
93+
const zh = builtInLocales['zh-Hans'].auth.login;
9494
for (const key of SSO_KEYS) {
9595
expect(typeof zh[key], `zh auth.login.${key}`).toBe('string');
9696
expect(zh[key], `zh auth.login.${key}`).not.toBe(ENGLISH[key]);

‎ui/apps/console/src/pages/auth/__tests__/loginFormLabels-everyMember-12762.test.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ function labelMembers(): string[] {
9797
}
9898

9999
const MEMBERS = labelMembers();
100-
const ZH_LOGIN = builtInLocales.zh.auth.login as Record<string, unknown>;
100+
const ZH_LOGIN = builtInLocales['zh-Hans'].auth.login as Record<string, unknown>;
101101

102102
function createMockClient(): AuthClient {
103103
return {

‎ui/apps/console/src/pages/auth/__tests__/registerRefusalCodes-10998.test.tsx‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ function refusingServer(code: string, message: string): typeof fetch {
6565
}) as typeof fetch;
6666
}
6767

68-
type Lang = 'zh' | 'en';
68+
type Lang = 'zh-Hans' | 'en';
6969

7070
/** Labels as the page renders them in `lang`, read from that pack. */
7171
function registerLabels(lang: Lang) {
@@ -114,7 +114,7 @@ afterEach(() => {
114114
vi.restoreAllMocks();
115115
});
116116

117-
const zhErrors = builtInLocales.zh.auth.register.errors;
117+
const zhErrors = builtInLocales['zh-Hans'].auth.register.errors;
118118
const enErrors = builtInLocales.en.auth.register.errors;
119119

120120
describe('console RegisterPage — audience refusals under zh-CN (objectui#10998)', () => {
@@ -125,7 +125,7 @@ describe('console RegisterPage — audience refusals under zh-CN (objectui#10998
125125

126126
it('SELF_REGISTRATION_CLOSED shows the localized text, not the server message', async () => {
127127
const text = await submitRefused(
128-
'zh',
128+
'zh-Hans',
129129
'SELF_REGISTRATION_CLOSED',
130130
SERVER_MESSAGE.SELF_REGISTRATION_CLOSED,
131131
);
@@ -136,7 +136,7 @@ describe('console RegisterPage — audience refusals under zh-CN (objectui#10998
136136

137137
it('EMAIL_DOMAIN_NOT_ALLOWED shows the localized text, not the server message', async () => {
138138
const text = await submitRefused(
139-
'zh',
139+
'zh-Hans',
140140
'EMAIL_DOMAIN_NOT_ALLOWED',
141141
SERVER_MESSAGE.EMAIL_DOMAIN_NOT_ALLOWED,
142142
);
@@ -146,7 +146,7 @@ describe('console RegisterPage — audience refusals under zh-CN (objectui#10998
146146

147147
it('a code the page does not map still shows the raw server message', async () => {
148148
const raw = 'The server refused this sign-up for a reason the console has no text for.';
149-
const text = await submitRefused('zh', 'UNMAPPED_REFUSAL_FOR_TEST', raw);
149+
const text = await submitRefused('zh-Hans', 'UNMAPPED_REFUSAL_FOR_TEST', raw);
150150
expect(text).toBe(raw);
151151
});
152152
});

0 commit comments

Comments
 (0)