Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -720,7 +720,7 @@
},
{
"file": "src/renderer/settings/main.ts",
"specifier": "@/i18n"
"specifier": "@/i18n/bootstrap"
}
],
"settingsToChatAppImportCount": 170
Expand Down
88 changes: 88 additions & 0 deletions docs/architecture/settings-locale-lazy-loading/plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# 实施计划

## 模块边界

```text
config.getLanguage
renderer i18n bootstrap ──────── load en-US fallback
│ │
├──── normalize locale ────────┤
│ ▼
└────────────────────── load current locale
createI18n + app.mount

config.language.changed / config.setLanguage
language store revision token → load → register → publish state
```

### 1. Locale registry 与复数规则

将复数规则移到无 messages 依赖的模块。`i18n/index.ts` 改为只提供支持 locale 类型、规范化函数、显式
dynamic import registry 和带 Promise 缓存的 `loadLocaleMessages()`。别名规范化到完整 locale,未知值
回退到 `en-US`。

每个 registry entry 使用固定 import 路径,确保 Vite 为 locale 生成可预测的异步 chunk,且不会通过
变量路径打包额外模块。

### 2. Renderer bootstrap

新增共享 `createRendererI18n()`:

- 尝试调用注入的 `getLanguageState`;失败时使用 `en-US` 默认状态;
- 必定加载 `en-US`,当前 locale 不同时并行加载当前 locale;
- 当前 locale 加载失败时回退到 `en-US`;
- 只把实际加载成功的 messages 传给 `createI18n`;
- 返回 i18n 实例及最终启动状态,供测试和后续 store 初始化复用。

主窗口、Settings 和 floating 的入口都改为 async bootstrap,在挂载前创建 i18n。必须迁移所有静态
聚合入口,否则任一多入口 renderer 都可能把 locale 重新提升到共享同步 chunk。floating 的运行时语言
事件使用相同的 load/register/publish 顺序和 revision 保护。

### 3. Language store

store 使用 `setLocaleMessage()` 注册动态结果。所有来自初始化、事件和设置 action 的状态统一进入
`applyLanguageState()`:

1. 分配递增 revision;
2. 加载规范化后的 locale;
3. 检查 revision 是否仍为最新;
4. 注册 messages;
5. 原子更新 locale、requested language 与方向。

`setLanguage` 的 route 返回值也进入同一路径,避免依赖事件到达顺序。加载失败保持上一个可用状态并
记录错误;较早加载即使后完成,也不能覆盖较新状态。

### 4. 测试策略

- i18n loader:所有 locale 可解析、别名/未知值规范化、相同 locale Promise 缓存。
- bootstrap:读取成功、IPC 失败、当前 locale import 失败时的 fallback messages 和最终 locale。
- language store:先注册再切换、快速切换 last-write-wins、加载失败保持旧 locale、RTL 方向。
- production build:检查 Settings 与主窗口同步入口不包含 20 份 locale,并存在独立 locale chunk。

## 风险与缓解

| 风险 | 缓解 |
| --- | --- |
| 首屏等待动态 import | 只加载当前 locale 与 fallback;应用内本地 chunk,无网络依赖 |
| 语言事件与 action response 重复 | loader Promise 缓存,统一 revision 应用路径 |
| import 失败导致白屏 | bootstrap 始终可退回静态可加载的 `en-US` 动态 chunk |
| 旧异步请求覆盖新选择 | revision token 在注册和状态发布前校验 |
| 构建器重新合并全部 locale | 两个 renderer 入口同时移除静态聚合,并检查 production manifest/chunks |

## 验证命令

```bash
pnpm exec vitest --config vitest.config.renderer.ts test/renderer/i18n
pnpm run format
pnpm run i18n
pnpm run lint
pnpm run typecheck
pnpm run test:renderer
pnpm run build
```
53 changes: 53 additions & 0 deletions docs/architecture/settings-locale-lazy-loading/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Renderer 语言包按需加载

## 背景

`src/renderer/src/i18n/index.ts` 静态导入全部 20 个语言包,主窗口和 Settings
renderer 的入口又都同步导入该聚合对象。翻译资源约 5.4 MB,因此打开 Settings 时,即使用户只使用
一个语言,也要解析所有语言资源;同时主窗口的静态导入会让构建器保留共享同步 chunk,单独修改
Settings 入口无法稳定拆包。

语言 store 当前还会先切换 `locale`,再由现有全量 messages 隐式保证文案存在。改为动态加载后,必须
明确保证 messages 注册完成后才发布 locale,并处理快速连续切换产生的异步竞态。

## 目标

1. 主窗口、Settings 和 floating renderer 启动时只加载当前 locale 与 `en-US` fallback,不再同步加载
全部语言。
2. 其余语言通过明确的 loader registry 生成独立异步 chunk,并在首次使用后复用加载结果。
3. renderer 挂载前读取主进程解析后的语言状态并完成首屏语言注册,避免默认语言闪烁或缺失 key。
4. 运行时切换语言时先加载、注册 messages,再切换全局 locale;较早请求不得覆盖较新的请求。
5. IPC 或当前语言加载失败时仍能使用本地 fallback 启动,不阻塞应用挂载。
6. 保留现有复数规则、RTL 方向和 `system` 语言语义。

## 非目标

- 不拆分单个 locale 内部的功能域 JSON。
- 不更改翻译内容、支持语言清单、主进程语言解析规则或设置界面布局。
- 不新增网络请求;语言 chunk 仍来自应用打包资源。
- 不修改主进程 IPC contract 或持久化格式。

## 验收标准

1. `src/renderer/src/i18n` 不再存在静态聚合所有 locale messages 的入口。
2. 主窗口、Settings 和 floating renderer 使用同一套异步 bootstrap,并在 `app.mount()` 前得到可用
i18n 实例。
3. 当前 locale 与 `en-US` fallback 在首屏可用;读取语言状态失败时至少以 `en-US` 正常启动。
4. 每个支持 locale 都可由 registry 加载;语言别名和未知 locale 有确定的规范化/fallback 行为。
5. 快速连续切换时只有最后一次状态可以更新 locale、requested language 与方向。
6. locale 加载失败不会把全局 locale 切到未注册 messages 的值。
7. 单元测试覆盖 loader 缓存、bootstrap fallback、切换竞态与 RTL;production build 显示 locale 为异步 chunk,
Settings 同步入口不再包含全部语言资源。
8. format、i18n、lint、typecheck 与相关 renderer tests 通过。

## 约束

- 保持 Vue 3 Composition API、Pinia 和 `vue-i18n` legacy false 模式。
- loader registry 必须使用可被 Vite/Rollup 静态分析的显式动态 import。
- startup fallback 不依赖 IPC,也不能重新静态导入完整 messages 聚合。
- 不创建或同步 GitHub issue;此工作通过独立 PR 评审。

## 兼容性与回滚

语言设置仍由现有 `config.getLanguage` / `config.setLanguage` route 提供,数据格式不变。回滚时可恢复静态
messages 聚合和同步 `createI18n`;不会留下用户数据迁移或版本兼容负担。
14 changes: 14 additions & 0 deletions docs/architecture/settings-locale-lazy-loading/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# 任务清单

- [x] 拆分 plural rules,并实现显式 locale loader registry、规范化与 Promise 缓存。
- [x] 实现容错的 renderer i18n bootstrap,只注册当前 locale 和 `en-US` fallback。
- [x] 将主窗口、Settings 与 floating renderer 入口迁移为挂载前异步 bootstrap。
- [x] 更新 language store,保证 load → register → locale 的顺序并防止异步竞态。
- [x] 删除 Settings App 中重复获取/设置 locale 的 watcher,保留方向同步 owner。
- [x] 补 loader、bootstrap、language store fallback/竞态/RTL 单元测试(9 个用例通过)。
- [x] production build 已生成 20 个独立 locale chunk;Settings 同步 JS 为 917,504 B,gzip
300,498 B,未包含翻译正文。
- [x] format、i18n、lint、typecheck、build 和定向 renderer tests 通过。完整 renderer suite 为
1333 passed / 16 failed:15 个失败来自 `origin/dev` 未改动的 `App.startup.test.ts` Promise mock,
1 个全量并发超时的 `MemorySettings` 用例单跑 11/11 通过。
- [x] 已提交、推送并创建以 `dev` 为 base 的独立 PR #2003。
Loading
Loading