fix(i18n): 语言选择写入 localStorage 并在启动时恢复,刷新不再退回 en - #3376
Merged
Conversation
The console language switch only lived in the i18next instance: any reload or new tab rebuilt the provider and reverted the whole UI to `en`, which made every non-`en` locale demo-only (objectstack#5406). I18nProvider now writes every language change to localStorage and boots the next session in that language. The restore happens at instance creation, not in a post-mount changeLanguage, so `<html lang>` — and therefore the Accept-Language header on the first API calls (#1319) — is correct on the first render. A stored choice outranks browser detection; a stored value the app no longer offers is ignored and purged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes objectstack-ai/objectstack#5406
问题
控制台切换语言(头像菜单 → Preferences → Language)只改动内存里的 i18next 实例:
packages/i18n/src/provider.tsx与packages/app-shell/src/layout/LocaleSwitcher.tsx全无持久化写入。任何一次刷新或新开标签页都会重建 provider,UI 整个退回en—— 非en语言只能演示,无法真正使用。前提已在origin/main复核成立(i18n 面零 localStorage 命中)。方案(PM 裁定:localStorage 持久化 + 启动时恢复)
languageChanged监听,不是 LocaleSwitcher 的点击回调。 i18next 对「context 的 changeLanguage」和「直接调 i18n.changeLanguage()」都会触发该事件,所以这是唯一收口点 —— 换任何一种写法的切换器都不会漏写偏好。bootstrap 不触发该事件,因此「恢复」不会反过来重写存储。据此LocaleSwitcher.tsx本身无需改动(只补了一条端到端测试)。createI18n的入参),而不是挂载后再changeLanguage。provider 依据实例语言设置 html 的 lang 属性,createAuthenticatedFetch又拿它拼Accept-Language(chore: release packages #1319):晚一步切换会让首屏那批服务端解析的标签用错语言取回,再靠 remount 补救。defaultLanguage。 显式选择必须压过浏览器探测,否则「在 ja 浏览器上选了中文」的用户每次刷新都会被塞回 ja,偏好对最需要它的人反而失效。config.resources」内(app 换了 locale 集),回退默认语言并删除该键,一条陈旧记录不会把 UI 锁死在没有翻译的 locale 上。判定用hasOwnProperty而非in,免得constructor这类垃圾值蒙混过关。新增公开面(
@object-ui/i18n):persistLanguage(默认true;预览、demo、截图夹具等固定语言场景传false,既不恢复也不写入)、LOCALE_STORAGE_KEY(objectui-locale,循objectui-favorites/objectui-recent-items一族命名)、readStoredLanguage()(自带 i18next 实例的 app 用它沿用同一偏好;这类 app 的 bootstrap 语言仍归自己决定)。反向验证(方向先定,再跑)
预判:两条肢都是常规「红」方向 —— 新测试钉的是此前根本不存在的行为,不存在 #5018 那种反转。逐条关掉后实测与预判集合完全一致:
7 failed | 5 passed—— 红的是跨重载恢复、优先级压过浏览器探测、RTL 恢复、config.resourceslocale、脏值清理(语言回退这半截仍绿,清理那半截红)、垃圾值清理,以及 LocaleSwitcher 的重载断言;写入类断言全绿。5 failed | 7 passed—— 红的是切换写入、跨重载恢复(无可恢复之物)、直接调实例 API 的写入、readStoredLanguage回读,以及 LocaleSwitcher;手工塞存储的那批恢复用例全绿。测试
packages/i18n/src/__tests__/provider-locale-persistence.test.tsx(11 例):切换写入、重载恢复且落在首帧(不是异步补正)、直接调i18n.changeLanguage也持久化、显式选择压过浏览器探测(先用无存储的基线钉出探测确实赢过defaultLanguage)、RTL 方向、config.resources提供的 locale、脏值/垃圾值回退并清理、persistLanguage={false}两头都不动、存储被禁时不崩。packages/i18n/src/__tests__/locale-persistence-ssr.test.ts:刻意用.test.ts落进unit项目的 node 环境 —— 这里真的没有 window,不靠 stub,SSR 读取必须返回 null 而非抛错。packages/app-shell/src/layout/__tests__/LocaleSwitcher.persistence.test.tsx:issue 报告的那条路径端到端 —— 点 中文 → 写入 → 重载后 UI 仍是中文。将来若有人把切换器改到绕过 i18n context 的 API 上,这里会红,而不是又悄悄退回en。packages/i18n/src/__tests__/provider.test.tsx:加beforeEach清 localStorage。持久化默认开启后,那条changeLanguage用例会把zh递给它后面的每个用例(包括断言 config 指定 bootstrap 语言的 ar 用例)—— 每条用例都该是一次全新的浏览器。命令与结果(全部持容器验证锁跑):
pnpm exec vitest run --maxWorkers=2 --reporter=verbose四个相关文件 →Test Files 4 passed (4) / Tests 25 passed (25),输出里逐条列出文件名(本仓--filter ... test -- --run path会静默忽略路径过滤,故走根 vitest)。pnpm exec vitest run --maxWorkers=2 packages/i18n→19 passed / 171 passed。pnpm exec vitest run --maxWorkers=2 packages/app-shell→275 passed / 2382 passed | 1 skipped(含 metadata-admin 那条直接切实例语言的 studio-locale 用例)。pnpm exec vitest run --maxWorkers=2 apps/console→20 passed / 184 passed。pnpm --filter @object-ui/i18n type-check、pnpm --filter @object-ui/app-shell type-check(先pnpm --workspace-concurrency=2 --filter "@object-ui/app-shell^..." build备齐依赖 d.ts)→ 均通过。eslint→ 0 error(仅测试 mock 的any与 provider 早已存在的 fast-refresh / exhaustive-deps 警告)。pnpm changeset:check→ fixed 组齐全、无major声明。影响半径
持久化默认开启,意味着所有
I18nProvider消费方(预览画廊、各 plugin demo)也会跟随同源下用户选过的语言;需要固定语言的表面传persistLanguage={false}即可。已按「规则的消费半径」而非「改动的包」清点:全仓仅 3 个测试文件调用过changeLanguage,其中 2 个经由 provider(已处理/已复跑),另一个在packages/i18n内部直接操作实例;app-shell、console 两个项目全量跑绿。顺带的文档:
packages/i18n/README.md补了一节「Language persistence」(优先级、脏值语义、persistLanguage与自带实例的用法)。顺手记下、未在本 PR 修的
GET /api/v1/i18n/locales是好用的)。本 PR 的脏值校验边界(内置包 +config.resources)今天正确,恰恰因为菜单只给这十个;菜单一旦扩到 app 真实 locale 列表,校验必须同步扩,否则「用户选的 app locale 活不过刷新」会成为下一个同款 bug。locale(/auth/me/localization) is fetched but never seeds the UI language — first login is always browser-or-enobjectstack#5419:/auth/me/localization已经把租户侧locale取回来了,却只喂给货币/数字格式化,从不参与 UI 语言 —— 新设备首次登录仍是浏览器语言或en。这是契约问题(优先级、存哪、写入端点都缺),按 PM 的裁定另立,不在本 PR 猜。Generated by Claude Code