Skip to content

feat(i18n): ship the Console's ten locales and follow the phone language - #169

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

os-zhuang merged 5 commits into
mainfrom
claude/issue-159-ten-locales

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #159

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:手机端原生页面(选服务器、确认服务器、加载页、离线页)从 3 种语言扩到与 Console 一一对应的 10 种(en zh ja ko de fr es pt ru ar),按手机系统语言自动选择;阿拉伯语下原生页从右到左排版;没有 App 内语言切换。
  • 为什么改:维护者要求「应该根据手机的语言识别」「要能对应上」,即同一部手机在原生页和 Console 里落到同一种语言。
  • 风险与回滚:只动原生页文案与语言选择逻辑,不碰 WebView 和 Console;7 份新译文由 agent 起草,可能有不地道的措辞;回滚即还原本 PR,无数据迁移。
  • 先看:lib/i18n.ts(语言匹配规则,照搬 Console 的 matchLanguageTag;删掉了一个无人写入的旧语言覆盖值),lib/rtl.ts(useLayoutDirection),app/_layout.tsx(回到前台时重新识别手机语言)。
  • 两分钟验证:npx jest __tests__/lib/i18n.test.ts __tests__/components/RtlLayout.test.tsx,预期 2 个测试文件、73 个测试全部通过。
  • 需要你做的:安排一次真机加母语验收:手机语言依次设为日语、阿拉伯语、繁体中文(zh-TW),回到 App 确认原生页跟随(繁中按 Console 规则显示简体中文,阿拉伯语从右到左),同时请母语者审校 7 份新译文(ja ko de fr es pt ru)。

What changes

  • Ten locales, the Console's codes. SUPPORTED_LANGUAGES is now en, zh, ja, ko, de, fr, es, pt, ru, ar, in the Console's order, with the Console language menu's native names. Seven new bundles: locales/{ja,ko,de,fr,es,pt,ru}.json, with the same keys as en.json.
  • The Console's matching rule, ported. matchLanguageTag (exact tag, else its base language) is a port of objectstack ui/packages/i18n/src/locales/registry.ts:112-117. detectDeviceLanguage walks the phone's preferred languages in order (getLocales(), languageTag, else languageCode) and takes the first one that resolves to a bundled locale, else en.
  • Traditional Chinese resolves to zh, because the Console does. The Console has no catalogue keyed by a full tag, so zh-TW, zh-HK and zh-Hant* reach zh through the base-language step: registry.ts:112-117 (matchLanguageTag) and :136-138 (resolveBuiltInLanguage), and the Console's own test says so at ui/packages/i18n/src/__tests__/region-locale-fallback-11326.test.tsx:109-112. Read at objectstack origin/main 47718699.
  • Live change on next foreground. followDeviceLanguageOnForeground() (an AppState listener started in app/_layout.tsx) re-detects when the app comes back to active and calls i18n.changeLanguage when the result differs. syncRTL now also runs on every languageChanged.
  • No in-app picker, and no stored override. loadInitialLanguage read a language from MMKV (objectstack-ui / language) that nothing has written since the 2.0 shell rebuild. It would pin the language against the phone and block the live change, so it is removed, together with the store.
  • RTL under ar, from the first render. lib/rtl.ts adds layoutDirection(lang) and useLayoutDirection(). The server picker root (both steps, so the confirm step inherits it), the offline page root and the app root carry Yoga's direction style. Unlike I18nManager.forceRTL, which applies only after the next launch, this flips rows, start/end insets and natural text alignment at once. syncRTL keeps I18nManager in step for whatever reads it, such as the stack transition direction, which therefore follows on the next launch.
  • Docs. docs/HYBRID-SHELL.md gains a ## Language section. It says the shell's locale set mirrors the Console UI's, gives the matching rule, says there is no picker, and says that once signed in the user's Console locale (sys_user.locale) wins inside the Console.
  • components/shell/ConsoleWebView.tsx is untouched (Bridge: stable install id and pushTokenChanged for device registration; document the no-Service-Worker rule #163 and feat(shell): act on the Console's ready and routeChanged signals; the shell owns the safe area #166 are in flight there). The WebView's content direction is the Console's own dir.

Translations: Console wording vs fresh

Matched to the Console's wording at the same concept (objectstack ui/packages/i18n/src/locales/CODE.ts, key in brackets):

  • common.ok (common.ok), common.cancel (common.cancel), common.retry (common.retry), common.error (chatbot.toolState.error)
  • errorBoundary.title (console.errors.somethingWentWrong), errorBoundary.message (errors.unknown), errorBoundary.retry and shell.retry (console.errors.tryAgain)
  • serverConfig.connect (auth.remediation.mfa.continue), serverConfig.error.unknown (organization.errors.unknown), shell.loading (common.loading)
  • Vocabulary only: serverConfirm.signIn.emailPassword is built from auth.login.emailLabel + auth.login.passwordLabel, and the sign-in verb in serverConfirm.signInLabel / serverConfirm.confirm follows auth.login.submitButton (ja サインイン, de Anmelden, fr Se connecter, es Iniciar sesión, pt Entrar, ru Войти, ko 로그인). The register follows the Console's: es uses usted, pt is Brazilian.

Translated fresh: every other key, meaning all of serverConfig.* except connect and error.unknown, serverConfirm.* apart from the vocabulary above, and shell.offline*, shell.serverError* and shell.changeServer. These need native-speaker review (see 维护者速读). The zh and ar bundles are unchanged.

Tests

  • __tests__/lib/i18n.test.ts:
  • __tests__/components/RtlLayout.test.tsx:
    • The server picker and the offline page render direction: rtl under ar and ltr under en.
    • The picker flips live on a language change while mounted.
    • The warm-up is the established beforeAll cold-load pattern.
Gates on head 878238e (after merging origin/main 29c25df)
npx tsc --noEmit                    EXIT=0
npx eslint . --ext .ts,.tsx         EXIT=0
npx jest --ci --coverage            EXIT=0
  Test Suites: 31 passed, 31 total
  Tests:       433 passed, 433 total
npx jest --ci --no-cache --maxWorkers=2 --verbose __tests__/components/RtlLayout.test.tsx   EXIT=0
  (cold transform cache) 5 passed; slowest test 14 ms; Time: 7.471 s
Reverse checks (each mutation landed on disk, then restored to the HEAD blob with an empty git diff HEAD)
lib/i18n.ts, base-language fallback line removed     -> i18n.test.ts: 23 failed, 45 passed
locales/ko.json, "changeServer" renamed to "zzExtra"  -> "carries every native string key": 1 failed (missing + extra listed)
app/server-config.tsx, direction forced to "ltr"     -> RtlLayout.test.tsx: 2 failed (the ar cases)

Acceptance notes

  • Platform locale regime ([Decision] One locale list, one completeness rule — the long-term i18n regime across Console, platform metadata, cloud and mobile objectstack#29349). Read once before opening this PR via REST: open, no comments, no maintainer ruling. Per hotlong's heads-up, this PR keeps the ten Console short codes. If the ruling is D, the set shrinks to the four full locales. If it is E, the codes move to the new platform table once it lands.
  • One deliberate difference from the Console's browser rung. The Console's browser detection reads only navigator.language, the first language (ui/packages/i18n/src/i18n.ts:170). The card asks for the first supported language from the ordered list, and its acceptance requires an unsupported first language to fall through. So the shell walks the list and applies the Console's per-tag rule to each entry.
  • The card's "switcher sheet" and "sign-in bar" do not exist on main yet. New native screens should put useLayoutDirection() on their root, as the picker and offline page do.
  • NOT MEASURED: what language the WebView states. The shell does not set Accept-Language. On iOS, WKWebView may derive it from the app's declared localizations, and the app declares none (expo-localization's supportedLocales option is not configured). A signed-out Console could therefore see en while the native screens show the phone's language. The device check under 维护者速读 should look at the Console too. Not filed: this is an unmeasured inference.
  • Test-infra note (not filed). jest.setup.ts mocks react-native/Libraries/AppState/AppState without a default, and react-native's AppState getter reads default, so AppState is undefined in every test. i18n.test.ts supplies its own mock with a default. Carrier: none.
  • Device check: not performed. It is the maintainer item above.

Verification

  • npx jest __tests__/lib/i18n.test.ts __tests__/components/RtlLayout.test.tsx → 73 passed.
  • Diff stays inside the claimed file surface: lib/i18n.ts, lib/rtl.ts, locales/*.json (seven new), app/_layout.tsx, app/server-config.tsx, components/shell/OfflineFallback.tsx, __tests__/**, docs/HYBRID-SHELL.md, .changeset/159-ten-locales.md.

Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

repo:mobile#1 · os-justin · session_01QSeKYnW1Xf6HDSWz1eGAqv

Seat verdict: ACCEPT — Head-sha: 878238ef3ad120d12cd81d644d823a01240cb89a

维护者速读

  • 改了什么: App 的原生界面现在支持和 Console 完全相同的 10 种语言,代码也一样:en zh ja ko de fr es pt ru ar。
    • 按手机系统语言的先后顺序挑第一种支持的语言,匹配规则逐行照搬 Console。比如 zh-Hans-CN、zh-TW、zh-HK 都映射到 zh,pt-BR 到 pt,es-MX 到 es,都不支持时用英文。
    • 手机改了语言,App 回到前台就跟着变。
    • 阿拉伯语下原生页面从右到左排版。
    • 不提供 App 内的语言选择器。
  • 为什么改: 落实你的要求「根据手机的语言识别」「要能对应上」。之前原生界面只有中、英、阿三种。
  • 风险与回滚: 去掉了一个读取 MMKV「语言覆盖」的旧逻辑。全仓没有任何地方写这个值(席位已核),删掉不影响任何用户。新增 7 种语言的文案是 AI 翻译的,能和 Console 对上的地方沿用了 Console 的措辞。回滚就是 revert 本 PR。
  • 先看: lib/i18n.ts(matchLanguageTag / resolveLanguage 和回前台重新检测)→ locales/*.json → app/server-config.tsx 的 RTL。
  • 两分钟验证: 运行 npx jest --ci __tests__/lib/i18n.test.ts __tests__/components/RtlLayout.test.tsx。
  • 需要你做的: ① 真机把系统语言分别切到 日文、阿拉伯文、繁体中文(台湾),原生页面应跟着切换,阿拉伯文从右到左。顺便看看未登录时 Console 是否也跟着系统语言(见下方「开放问题」)。② 请懂这些语言的同事过一遍新的 7 个语言包。③ 审批并合并。

席位意见: 范围与卡面一致,语言代码与 Console 一一对应,并有测试钉死。objectstack#29349(全项目语言体系的裁决卡)还没裁,按 hotlong 卡上的说明先用这 10 种,PR 正文里写明了。dev 留了一个开放问题:iOS 上 WebView 的 Accept-Language 可能取决于 App 声明了哪些本地化,而 App 目前一种都没声明。如果是这样,未登录时 Console 可能停在英文。这一点没实测,席位选 A,先在上面第 ① 项真机检查里验证,复现了再立卡。建议合并。

席位核对

  1. Shape: draft → main. The first line is Fixes #159. Clause-②: no and Metadata-protocol: none are present, with the changeset .changeset/159-ten-locales.md.
  2. Premise, measured:
  3. Gates: check-runs on head 878238e: Code Quality success, Lint & Type Check success. mergeable_state is clean. Local run: jest --ci --coverage 31/31 suites, 433 tests. The RTL component tests were also run cold.
  4. Accepted deviations:
    • __tests__/lib/i18n.test.ts carries its own AppState mock, because the shared jest.setup.ts mock lacks default. That shared mock is outside the surface.
    • The merge commit from main carries git's default message.
  5. Out of scope:
  6. Landing:
    • The path surface is NOT governed. +969 / −63.
    • The maintainer merges.

Stack #168's connect-by-QR work with the ten-locale shell: the picker keeps
the scan button, link prefill and way-back from #168 and the RTL direction
from #169; #168's new keys (serverConfig.scan, serverConfig.backTo, scanner.*,
permissions.camera) are carried into ja ko de fr es pt ru; app.config.ts
localizes the iOS camera prompt in all ten languages.

Claude-Session: https://claude.ai/code/session_01QSeKYnW1Xf6HDSWz1eGAqv
Co-authored-by: Claude <noreply@anthropic.com>
@os-zhuang
os-zhuang merged commit 13d5b01 into main Nov 2, 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 2, 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: follow the phone's language in the platform's 11 locales (ruling F of objectstack#29349)

2 participants