Skip to content

feat(i18n)!: the Console's language packs follow the platform locale table, keyed by its tags, and Traditional Chinese (zh-Hant) ships - #30595

Closed
objectstack-fleet[bot] wants to merge 7 commits into
mainfrom
claude/project-thread-xplvvb
Closed

objectstack-fleet[bot] wants to merge 7 commits into
mainfrom
claude/project-thread-xplvvb

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Sales · project thread

domain:ui#5 · os-sales · session_01L6bhgThgWPADcpU1xXCBsw

Part of #30511. Every item the card asks for is in this PR. The card stays open for the seat to close when this lands, and phase 4 (the locale menu) follows it.

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 (its "Built-in locales" list names ten packs, with zh and pt)

维护者速读

  • 改了什么:控制台的语言包改为跟随平台语言表,表里每个语言一个包,以表的标签为键(zh → zh-Hans,pt → pt-BR)。新增繁体中文 zh-Hant,由简体包经 OpenCC 分词转换器生成,规则照搬官网转换器,不手写。
  • 为什么改:裁决 F([Decision] One locale list, one completeness rule — the long-term i18n regime across Console, platform metadata, cloud and mobile #29349)要求控制台的语言集合取自平台语言表,表里有而控制台缺的语言要让现有测试变红。此前控制台自带十个包,表里的 zh-Hant 缺席,测试照样全绿。
  • 风险与回滚:
    • 改动范围:只动 ui/,外加根目录 scripts/regen-artifacts.mjs 的一条台账,不动 spec 和服务端。ui/ 里涉及 i18n 包及其 provider、app-shell 三处按标签读包、页签标签字典、存值语言挑选、控制台分包配置、三个 i18n 门禁脚本,以及相应测试。
    • 旧存值:zh-CN、zh、pt 经表的别名照常渲染;zh-TW、zh-HK 存值从简体变为繁体。
    • 仓外代码:若按 builtInLocales.zh 或 @object-ui/i18n/locales/zh 取包会断,changeset 写了 FROM → TO。
    • 回滚:revert 本 PR 即可。
  • 先看:
    1. ui/packages/i18n/src/locales/registry.ts:resolveBuiltInLanguage 与按标签的加载表。
    2. ui/packages/i18n/src/provider.tsx:语言包按标签写入,别名语言不再重复写一份。
    3. ui/packages/i18n/scripts/zh-hant.mjs:转换规则与跨词边界修正。
    4. ui/packages/app-shell/src/hooks/useUserLocale.ts:pickRenderableLocale 新增一步,按表读同一语言。
  • 两分钟验证:
    1. 运行 pnpm --dir ui exec vitest run packages/i18n/,预期全部通过。
    2. 删掉 builtInLocales 里 zh-Hant 那一行再跑一次,all-locales-key-parity 会转红并点名 zh-Hant。
  • 需要你做的:

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.

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). A new step before the base-language match takes the first offered code that the table reads as the same language. Without it, a stored zh-TW met the built-in list and took its first zh variant, zh-Hans.
  • 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. 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 (Console and CLI polish from the real-user walkthrough #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, because the table reads a bare zh as Simplified.

  • 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 9b15e9ad, base 75bbebb2)

The runs below were taken on 9571f5e2. 9b15e9ad is a no-change merge (the ours strategy) that records the earlier push of the first commit, so the tree is the same.

  • 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).
  • 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. Restored: 49 / 49.
    • One zh-Hans value edited without regenerating (保存 → 保存草稿) → zh-hant-generated 1 failed / 5 passed: "zh-Hant.ts is stale: run pnpm --filter @object-ui/i18n gen:zh-hant and commit src/locales/zh-Hant.ts".
    • The provider fix reversed → 6 failed / 8 passed: the five new cases in provider-load-rerender-10382 and the zh case of ObjectChart.refreshIndicatorLocale-10580. Restored: 14 / 14.
  • Suites.
    • packages/i18n/ plus the 33 test files main added or changed since the branch point, run after the merge: 128 files, 1834 tests, green.
    • The 1030 language-sensitive ui suites before the provider fix. The only reds were three LocaleSwitcher suites (menu names) and the chart case that found the provider bug. All are fixed and green on rerun.
    • Every ui test file that names an alias code (zh, zh-CN, zh-TW, zh-HK, zh-SG, pt, pt-PT), on the final head: 775 files (774 passed, 1 skipped), 8827 tests.
    • pnpm --dir ui exec vitest run scripts/ on the final head: green.
    • Dogfood: the closure build, then pnpm --filter @objectstack/dogfood test: 420 files passed, 3 skipped; 3496 tests.
  • Type-check.
    • turbo run type-check --filter=...[75bbebb2] over the ui packages: 102 / 102. It caught booleanValueLabel.locale-11689, which 9070a29f fixes.
    • type-check:coverage, type-check:scripts, type-check:vitest-config and type-check:e2e: exit 0.
  • Gates.
    • The 75 derived dispatch gates exit 0. dispatch-gates --ran reads 75 run, 0 NOT-MEASURED, 0 UNRUN.
    • ui check:i18n-keys, check:i18n-drift, check:spec-symbols, check:unreferenced-sources and check:phantom-deps: exit 0.
    • After the Console's vite build, check:eager-locale-catalogues and check:eager-closure exit 0. i18n-locale-en is 59,579 gz bytes: the single module en.ts, byte-identical to the base. zh-Hant is 62.9 KB gz, loaded on demand.
    • check:dts-closure: exit 0 after the Console's build:plugin.
    • check-changeset-no-major.mjs with this body as the event: exit 0. The level axis reads yes (narrowing) and none, with every entry at patch.
    • check:nul-bytes: exit 0. eslint on the 136 changed ui sources: 0 errors.

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 app-shell: a published library that is 66% Console product surface — retire the 67 zero-reference exports now, decide the split, and fold the second i18n table into @object-ui/i18n #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.

  • Server parity. pickRenderableLocale documented the server's resolveLocale order (packages/core memory-i18n). Its new step is console-only, and the server's fallback does not read the table's aliases yet.

  • 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.
  • Landing order. Console first load: the English language pack has 63 bytes left under its size ceiling, so any card that adds English console text fails CI #30588 moves the marketplace.* subtree into ui/packages/i18n/src/namespaces/marketplace/ and lands first. After it merges, this branch merges main, gives the new namespace its zh-Hans, zh-Hant and pt-BR files, and reruns gen:zh-hant.

  • 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 zh-Hant.ts that is 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: '儲存'

🤖 Generated with Claude Code

https://claude.ai/code/session_01L6bhgThgWPADcpU1xXCBsw

…pack, key packs by table tag

The Console's language packs are now the platform locale table
(PLATFORM_LOCALES in @objectstack/spec/system): one pack per tag, keyed by
the tag (zh-Hans, zh-Hant, pt-BR). zh-Hant is generated from zh-Hans by
scripts/gen-zh-hant.mjs (OpenCC s2twp, pinned at 1.3.1, with the
www.objectos.ai overrides plus five straddle repairs found reading the pack).

Stored and browser codes keep resolving through the table's read aliases:
resolveBuiltInLanguage (new export) reads zh/zh-CN as zh-Hans, zh-TW/zh-HK
as zh-Hant, pt as pt-BR, then each shorter prefix; createI18n's fallbackLng
maps every alias to its tag. The app-shell splash, outbound agent text and
flow permission line read the snapshot by tag.

The parity, outbound, eager-catalogue and en-drift checks read the table;
en-drift reads a follower pack absent at the base as new.

Claude-Session: https://claude.ai/code/session_01L6bhgThgWPADcpU1xXCBsw
Co-authored-by: Claude <noreply@anthropic.com>
…pack, key packs by table tag

The Console's language packs are now the platform locale table
(PLATFORM_LOCALES in @objectstack/spec/system): one pack per tag, keyed by
the tag (zh-Hans, zh-Hant, pt-BR). zh-Hant is generated from zh-Hans by
scripts/gen-zh-hant.mjs (OpenCC s2twp, pinned at 1.3.1, with the
www.objectos.ai overrides, four straddle repairs and one phrase override
found reading the pack).

Stored and browser codes keep resolving through the table's read aliases:
resolveBuiltInLanguage (new export) reads zh/zh-CN as zh-Hans, zh-TW/zh-HK
as zh-Hant, pt as pt-BR, then each shorter prefix; createI18n's fallbackLng
maps every alias to its tag. The app-shell splash, outbound agent text and
flow permission line read the snapshot by tag.

The parity, outbound, eager-catalogue and en-drift checks read the table;
en-drift reads a follower pack absent at the base as new.

Claude-Session: https://claude.ai/code/session_01L6bhgThgWPADcpU1xXCBsw
Co-authored-by: Claude <noreply@anthropic.com>
…package

The suites outside ui/packages/i18n that hard-coded ten packs, a zh / pt
key, a 'zh' pack lookup or the menu's 中文 entry now read the platform
locale table or the tag.

Three readers keyed by the old codes are fixed with them. The provider
wrote a booted alias's catalogue (zh, zh-TW, pt) a second time under the
alias, an added event that re-rendered every reader and refetched every
chart for a catalogue already resident; it now writes under the tag. A
stored zh-TW locale took the first offered zh variant (zh-Hans);
pickRenderableLocale now takes the first offered code the table reads as
the same language first. The page-tab label dictionary served its
Simplified rows to every non-exact zh session; a zh-Hant, zh-HK or zh-MO
session reads its Traditional rows.

The en-drift fixtures write a const that is not named by the tag, and a
new describe pins the base-absent follower rule. The generated zh-Hant
pack is recorded as not driver-managed in the merge-driver ledger, and
the converter's declaration twin joins the package's test program. The
changeset declares the change.

Claude-Session: https://claude.ai/code/session_01L6bhgThgWPADcpU1xXCBsw
Co-authored-by: Claude <noreply@anthropic.com>
Main's pack edits to zh.ts and pt.ts land in zh-Hans.ts and pt-BR.ts;
zh-Hant.ts is regenerated from the merged zh-Hans.ts. The two conflicted
tests take main's CJK-Latin spacing cases under the zh-Hans tag, and a
case pins the spacing for both Chinese packs booted by their tags.

Claude-Session: https://claude.ai/code/session_01L6bhgThgWPADcpU1xXCBsw
Co-authored-by: Claude <noreply@anthropic.com>
The suite types its language by the keys of builtInLocales, which are now
the platform locale table's tags, so its zh cases name the zh-Hans pack.

Claude-Session: https://claude.ai/code/session_01L6bhgThgWPADcpU1xXCBsw
Co-authored-by: Claude <noreply@anthropic.com>
…er's twin

A doc link and a test comment named the old zh.ts path; they name
zh-Hans.ts. The nested-layout drift case pinned that a pack missing at
the base throws; under the follower rule only en does, and a follower the
base lacks reads as empty and is named, so the case pins both.

The lint-rule-coverage ledger declares the converter's declaration twin,
packages/i18n/scripts/zh-hant.d.mts, beside the two script twins it
already declares, and its tests accept an objectstack card on a row.

Claude-Session: https://claude.ai/code/session_01L6bhgThgWPADcpU1xXCBsw
Co-authored-by: Claude <noreply@anthropic.com>
34c9ccb is the first commit as pushed before its message was reworded
(c857810 has the same tree). Merged with the ours strategy so the branch
fast-forwards from what is already on the remote; no file changes.

Claude-Session: https://claude.ai/code/session_01L6bhgThgWPADcpU1xXCBsw
Co-authored-by: Claude <noreply@anthropic.com>
This was referenced Nov 5, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#2 · os-tesla · session_01L9qibw7DekEe3wx5f3zphf

Superseded by PR #30902 (claude/project-thread-f2dhrw, head f7e9df2d), which carries this diff resumed on that thread's own branch, merged with origin/main (including PR #30640's lazy marketplace namespace and PR #30840's install-preview keys) and with zh-Hant regenerated. Closed by domain:ui#2 (claim 6677798360 on #30511) so the PR-body guard on #30902 clears; the branch is kept.

@objectstack-fleet objectstack-fleet Bot closed this Nov 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant