Skip to content

feat(i18n): ship the platform's 11 language tags, resolved by script - #189

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-182-eleven-locales
Nov 5, 2026
Merged

os-zhuang merged 2 commits into
mainfrom
claude/issue-182-eleven-locales

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #182

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:手机原生页面(选服务器、确认、加载、离线页)从控制台的 10 种语言改为平台统一的 11 种语言;新增繁體中文,zh 改名 zh-Hans、pt 改名 pt-BR,iOS 相机/相册/面容 ID 权限弹窗的繁体也有了自己的文案。
  • 为什么改:平台裁决 F(objectstack#29349)定了全平台只用一张 11 种语言的表,维护者原话「mobile 也需要支持这些语言」;此前台湾、香港、澳门用户看到的是简体。
  • 风险与回滚:只影响原生页面和 iOS 权限弹窗的语言,控制台内部不受影响;繁体文本是机器转换的,可能残留大陆用词,不影响功能;回滚就是 revert 本 PR。
  • 先看:lib/i18n.ts 的 resolveLanguageTag(脚本优先于地区的判定)、locales/zh-Hant.json(机器转换文本)、app.config.ts 的 PERMISSION_LOCALES(葡语写成不带地区的 pt)。
  • 两分钟验证:npx jest --ci __tests__/lib/i18n.test.ts,预期 Tests: 90 passed, 90 total。
  • 需要你做的:请一位繁体母语者拿一台切到「繁體中文(台灣)」的手机,审一遍原生页面和相机权限弹窗的文案(候选改词见 Acceptance notes),同一台手机再切到「Português (Brasil)」确认显示葡语;这项真机检查尚未做过。

What changes

The shell's native strings move from the Console's ten short codes to the platform's one list of 11 tags (objectstack-ai/objectstack#29428, ruling F of #29349): en zh-Hans zh-Hant es ar fr pt-BR ru de ja ko, in the table's order.

  • Bundles. locales/zh.json → zh-Hans.json and locales/pt.json → pt-BR.json, both pure renames (git similarity 100%). New locales/zh-Hant.json, converted from zh-Hans.json (see Acceptance notes for the recipe). Every key on main carries over to all 11, and the key-parity test now covers all 11.
  • Resolution by script. matchLanguageTag, the port of the Console's exact-then-base rule that collapsed both scripts to zh, is removed (nothing else imported it). resolveLanguageTag(tag) replaces it.
  • OS-level strings. app.config.ts PERMISSION_LOCALES gives zh-Hant.lproj the zh-Hant bundle's camera, photo-library and Face ID text instead of the Simplified text. Portuguese stays written as the unqualified pt lproj, now holding the pt-BR text, so every Portuguese variant falls back to it on iOS, matching the shell's pt-* → pt-BR rule.
  • A locale with no languageTag. detectDeviceLanguage builds the tag from languageCode + languageScriptCode + regionCode, so the script is not lost (zh + Hant + TW → zh-Hant-TW).
  • Docs. docs/HYBRID-SHELL.md (Language, the camera-permission paragraph, the file map) and docs/design/connection-flow.md (Languages and layout) say 11 and name the table.

The contract of resolveLanguageTag(tag): SupportedLanguage | null

One BCP 47 tag in, one bundled language or null out. Subtags compare case-insensitively. Only the language, script and region subtags are read, and scanning stops at the first singleton (-u-, -x-), so an extension value never reads as a region.

Input Result
zh with script Hant (zh-Hant, zh-Hant-TW, zh-Hant-CN) zh-Hant
zh with script Hans (zh-Hans, zh-Hans-CN, zh-Hans-HK) zh-Hans
zh with no script and region TW, HK or MO zh-Hant
any other zh (zh, zh-CN, zh-SG) zh-Hans
any pt (pt, pt-PT, pt-BR, pt-AO) pt-BR
any other tag whose primary subtag is bundled (en-US, es-MX) the primary subtag
anything else (it-IT, yue-HK, empty) null

resolveLanguage / detectDeviceLanguage walk the phone's preferred languages in order, return the first non-null result, and otherwise return en.

Tests

All three CI families were run locally at a4a903a; their commands are the same as ci.yml and lint-typecheck.yml.

  • npx tsc --noEmit: exit 0. --listFiles includes lib/i18n.ts, app.config.ts, locales/zh-Hant.json and both changed __tests__/lib files.
  • npx eslint . --ext .ts,.tsx: exit 0, no output.
  • npx jest --ci --coverage: exit 0, Test Suites: 49 passed, 49 total, Tests: 1070 passed, 1070 total; lib/i18n.ts 92.06% statements.
  • No component test file is new, so the cold variant does not apply.

New or changed pins in __tests__/lib/i18n.test.ts:

  • each read alias #29428 declares;
  • the card's acceptance cases: zh-Hans-CN/zh-CN/zh-SG → zh-Hans, zh-TW/zh-HK/zh-Hant → zh-Hant, pt-PT/pt-BR → pt-BR, fall-through to the next preferred language, empty → en;
  • script over region;
  • case folding;
  • extension subtags;
  • a locale without a languageTag;
  • translation through the zh-Hant and pt-BR bundles.

__tests__/lib/app-config.test.ts pins that each Chinese script carries its own bundle's text, that the zh-Hant text differs from the zh-Hans text, and that the pt lproj carries the pt-BR text.

Ablation: map zh-TW to zh-Hans

The implementation was committed first. The mutation went through scripts/ablation-replace.mjs (objectstack): new Set(["tw", "hk", "mo"]) → new Set(["hk", "mo"]) in lib/i18n.ts, anchor x1 → x0, blob 3da779ed305e → c60ec6d6b0be. Jest reads the source directly; there is no dist/ in this repo.

Tests:       3 failed, 96 passed, 99 total
  ● detectDeviceLanguage › resolves Traditional Chinese zh-TW to zh-Hant
  ● resolveLanguageTag — the platform's alias rule, by script › compares subtags case-insensitively, as BCP 47 defines them
  ● resolveLanguageTag — the platform's alias rule, by script › resolves the platform alias zh-TW to zh-Hant
ablation-replace: ok restored: blob == HEAD (3da779ed305e) and `git diff HEAD` is empty

The tests went red as expected. zh-Hant-TW stayed green, correctly, because its script subtag decides before the region is read.

Acceptance notes

  • Base. The branch starts from 13b7e32, the origin/main at worktree creation, not from 427cb49 as dispatched. 13b7e32 contains 427cb49, and the five commits in between (feat(eas): link the app to the EAS project so CI can build it #185) touch app.config.ts, so branching from the newer head avoids a conflict there.
  • #29428 against the card: no disagreement. #29428's alias list is in its body, under Phases, item 1, second bullet: zh, zh-CN, zh-SG → zh-Hans; zh-TW, zh-HK, zh-MO → zh-Hant; ja-JP → ja; es-ES → es; pt → pt-BR; en-US → en. The card's rule maps every one of those the same way, and a test pins each. The card's rule also covers inputs that #29428 does not list (pt-PT, zh-Hant-TW, zh-Hans-HK, es-MX). #29428 is silent on those, so they are not conflicts.
  • No merged table on objectstack main yet. At 4b71a82b76, git grep -n PLATFORM_LOCALES origin/main -- packages/spec/src ui/packages/i18n/src finds nothing. The control word BUILT_IN_LANGUAGE_CODES hits in ui/packages/i18n/src, so the empty result is not a broken grep. When phase 1 lands, this shell's list and rule should be re-read against the exported table.
  • The Console still ships ten short codes. The shell never hands its language to the Console (it does not set Accept-Language; see HYBRID-SHELL "Language"), so the tag rename reaches only native screens.
  • How zh-Hant was made. The card's precedent, objectstack-ai/www.objectos.ai scripts/gen-zh-hant.mjs, was read at 4ed1dfe through git (the REST API refuses that repo in this session). It converts with opencc-js 1.3.1, preset s2twp (Simplified to Taiwan, phrase conversion), and its src/lib/zhconvert.ts places eight phrase overrides at the head of the TWPhrases group: 權限, 實例, 全域, 擴充, 配接器, 控制項, 映像, 腳本. The bundle here was converted with the same version, preset and overrides, values only, keys untouched. A second run reproduces locales/zh-Hant.json byte for byte. Both locative 里 came out as 裡 (哪裡, 這裡).
  • No generator was committed. Committing one would add opencc-js to package.json and a scripts/ file, both outside this card's file surface. So when zh-Hans.json changes, zh-Hant.json is reconverted by the recipe above. HYBRID-SHELL and CONTRIBUTING now say so, and the key-parity test catches a missing key, though not stale text.
  • Native-speaker review candidates. These are mainland terms the converter keeps. They are not applied here, because the file is kept byte-exact to the converter output: 二維碼 (QR Code / 二維條碼), 證書 (憑證), 響應 (回應), 郵箱 (電子郵件), 賬號 (帳號), 面容 ID / 觸控 ID (Apple's Taiwan names are Face ID / Touch ID), 應用 (App), 伺服器地址 (位址), 鎖屏 (螢幕鎖定), 錄屏 (螢幕錄影).
  • File surface: two additions beyond the claim, with reasons. Both lines would be false after this PR, so they are corrected here:
    • docs/DEVICE-TESTING.md:78 said zh-TW shows Simplified Chinese. It now carries the 繁體中文 (Taiwan) and Português (Brasil) device check.
    • docs/CONTRIBUTING.md:26 named locales/{en,zh,ar}.json, and zh.json no longer exists.
  • Left as is. ROADMAP.md:23 "i18n (en / zh / ar)" is a historical status line, not a file reference.
  • Concurrency. MDM lock: while lockServer is set, the connection switcher shows only the managed connection #187 (MDM lock scope) also edits docs/HYBRID-SHELL.md, in a different section. Whichever PR lands second reconciles.

Verification

  • Local: tsc, eslint and jest, as above, at a4a903a.
  • Device check: not verified. This needs a phone set to 繁體中文 (Taiwan) and to Português (Brasil). See 需要你做的.

Generated by Claude Code

Rename zh -> zh-Hans and pt -> pt-BR, add a zh-Hant bundle converted from
zh-Hans (opencc-js s2twp with the www.objectos.ai phrase overrides), and
replace the Console's exact-then-base matcher with resolveLanguageTag:
Hant/Hans script first, TW/HK/MO -> zh-Hant, other zh -> zh-Hans,
pt-* -> pt-BR, else the primary subtag. zh-Hant gets its own iOS
permission text.

Claude-Session: https://claude.ai/code/session_01QSeKYnW1Xf6HDSWz1eGAqv
Co-authored-by: Claude <[email protected]>
HYBRID-SHELL and connection-flow say 11 and name the table; the device
check and the contributing note stop naming zh.json and stop saying zh-TW
shows Simplified Chinese. Changeset for the locale move.

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

Copy link
Copy Markdown
Contributor Author

repo:mobile#1 · os-justin · session_01QSeKYnW1Xf6HDSWz1eGAqv

Seat verdict: ACCEPT, maintainer merges. Seat landing is suspended because EXPO_TOKEN is set (see #160). Head-sha: a4a903a6e41e815fd919244044414f8d3b6dae68

维护者速读

  • 改了什么: 原生界面从 10 种语言代码改为平台统一的 11 个标签:en zh-Hans zh-Hant es ar fr pt-BR ru de ja ko。
    • zh.json 改名为 zh-Hans.json,pt.json 改名为 pt-BR.json,两个都是纯改名。
    • 新增繁体包 zh-Hant.json。它用 opencc-js s2twp 加上 www.objectos.ai 的 8 条短语修正,从简体生成。
    • 繁体手机(zh-Hant、zh-TW、zh-HK、zh-MO)从此显示繁体中文。
    • iOS 相机和相册的权限提示也有了繁体版本。
  • 为什么改: 平台已裁定 objectstack#29349 选 F,全平台统一使用 11 种语言。
  • 风险与回滚: 繁体文案是机器转换的,需要人工过一遍。回滚就是 revert 本 PR。
  • 先看: lib/i18n.ts 的 resolveLanguageTag(正文里有规则表)和 locales/zh-Hant.json。
  • 两分钟验证: npx jest --ci __tests__/lib/i18n.test.ts __tests__/lib/app-config.test.ts
  • 需要你做的:
    1. 请懂繁体的同事过一遍 zh-Hant.json。
    2. 手机分别设为「繁體中文(台灣)」和「Português (Brasil)」,确认原生页面跟着切换。
    3. 审批并合并。和 feat(mdm): list only the managed connection while the server is locked #188 先合哪个都行,席位已经试合过,没有冲突。

席位核对

  1. Shape: draft → main, first line Fixes #182. Clause-②: no and Metadata-protocol: none, plus a patch changeset. Size +421 / −135.
  2. Premise: Platform locale table (ruling F of #29349): one list of 11 languages in spec, complete on every surface objectstack#29428 is open. Every alias it names maps identically and is pinned by a test, so there is no disagreement with the card. No merged table exists yet on objectstack main.
  3. Gates:
    • check-runs on head a4a903a: Code Quality success, Lint & Type Check success;
    • mergeable_state: clean;
    • local jest --ci --coverage: 49/49 suites, 1070 tests.
  4. Ablation: dropping tw from the Traditional regions turns 3 tests red.
  5. Cross-PR check: the seat merged main + feat(mdm): list only the managed connection while the server is locked #188 + feat(i18n): ship the platform's 11 language tags, resolved by script #189 in a scratch worktree. The result is clean, and no changeLanguage("zh") call is left in the merged tests.
  6. Accepted deviations:
    • docs/DEVICE-TESTING.md and docs/CONTRIBUTING.md are corrected, because the change made them false.
    • The zh-Hant generator is not committed (it would need a new dependency); the recipe is documented.
  7. Out of scope: no drift test yet ties zh-Hant to zh-Hans. Recorded in Acceptance notes.
  8. Landing: the maintainer merges; the seat does not enable auto-merge.

@objectstack-fleet objectstack-fleet Bot added the needs-user-decision Blocked on a maintainer decision — do not dispatch; on a PR: awaiting the maintainer's review label Nov 3, 2026
@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 3, 2026 08:42
@os-zhuang
os-zhuang added this pull request to the merge queue Nov 5, 2026
Merged via the queue into main with commit 8e4235f Nov 5, 2026
2 checks passed
@objectstack-fleet objectstack-fleet Bot removed the needs-user-decision Blocked on a maintainer decision — do not dispatch; on a PR: awaiting the maintainer's review label Nov 5, 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.

Languages: move from the Console's ten codes to the platform's 11 tags (split zh into zh-Hans / zh-Hant, pt → pt-BR)

2 participants