---
name: "i18n-modular-lazyload"
version: "1.0.0"
origin: "captured"
generation: 0
parent_skill_ids: []
status: "stable"
description: "IMEX EOMS 前端 i18n 分模块懒加载规范。语言资源按 locale + subsystem 入口切分于 src/lang/modules/<locale>/<subsystem>.js，并允许在 src/lang/modules/<locale>/<subsystem>/** 下继续按实体或页面做 shard；通过 src/lang/subsystems.js 的路由前缀注册表与 ensureI18nModulesForRoute 在路由切换时按需 import()。禁止静态聚合引入、禁止单侧语言 loader、禁止 core 包混业务 key。"
trigger_phases: ["ux-architecture", "architecture", "implementation", "code-review", "drift-check", "qa"]
applicable_agents: ["Copilot Orchestrator", "Copilot Architect", "Copilot UX Architect", "Copilot Product Manager", "Copilot Frontend Developer", "Copilot Implementation", "Copilot Code Review", "Copilot Drift Check", "Copilot Documentation", "Copilot Solution Adversary", "Copilot Topic Analysis"]
priority: 10
---

# i18n 分模块懒加载 (i18n-modular-lazyload)

> **适用范围**: 所有影响 `mes-enreach-mom-web` 用户可见文案、页面新增、子系统扩充、语言切换的切片。
> **核心原则**: i18n 消息 **按 locale + subsystem 切分**；仅在进入命中路由时通过动态 `import()` 加载对应子系统包；**核心包只放全站通用 key**；**双语 loader 必须同步对称**。
> **权威事实源**: `mes-enreach-mom-web/src/lang/index.js`、`src/lang/subsystems.js`、`src/lang/modules/<locale>/<subsystem>.js`、`src/lang/modules/<locale>/<subsystem>/**`（以运行时代码为准，不以既有文档表述为准）。
> **关联 Skill**: `dict-biz-integration/SKILL.md`（字典标签 i18n）/ `industrial-page-standard/SKILL.md`（页面文案）/ `agent-hook-lifecycle/SKILL.md`（PreAction 钩子预留）。

---

## 1. 架构事实锚点

### 1.1 目录结构（不得改变）

```
mes-enreach-mom-web/src/lang/
├── index.js                    # 唯一入口：createI18n({}) + loadLanguageAsync + ensureI18nModulesForRoute
├── subsystems.js               # 注册表：subsystemLocaleLoaders + subsystemRoutePrefixes + resolveSubsystemsForRoute
├── modules/
│   ├── zh-cn/
│   │   ├── core.js             # 全站通用 key（登录、全局菜单、通用按钮、全局空/错/成功态）
│   │   ├── platform.js         # 平台模块入口（可继续拆到 platform/**）
│   │   ├── partner.js          # /partner 入口（可继续拆到 partner/**）
│   │   ├── crm.js              # /crm /sd 入口（可继续拆到 crm/**）
│   │   ├── plm.js              # /plm 入口（可继续拆到 plm/**）
│   │   ├── plm/
│   │   │   └── part.js         # 示例 shard：PLM 零件文案
│   │   └── scm.js              # /scm 入口（可继续拆到 scm/**）
│   └── en-us/
│       └── （与 zh-cn 镜像对称，键路径 1:1 对齐）
├── zh.js / en.js / vi.js       # LEGACY COMPAT — 仅平台公共与历史兼容桥接层，禁止业务代码新增静态引入
```

### 1.2 运行时装载链路

1. `src/main.js` 启动时：`loadLanguageAsync(language, window.location.pathname)`
2. `src/permission.js` 的 `router.beforeEach`：`await ensureI18nModulesForRoute(to.path)`
3. `src/page/index/top/top-lang.vue` 切换语言时：`loadLanguageAsync(lang, $route.path)`
4. `index.js` 内部：
   - `ensureCoreBundle(locale)` — 首次加载目标 locale 的 core + avue locale + element-plus locale，用 `setLocaleMessage` 合并
   - `ensureSubsystemBundles(locale, modules)` — `resolveSubsystemsForRoute(path)` 返回的子系统数组，逐个执行 `subsystemLocaleLoaders[locale][subsystem]()` 并合并
   - 已加载 subsystem 放入 `loadedSubsystems[locale]` Set，不重复加载

### 1.3 注册表契约（`src/lang/subsystems.js`）

```js
export const subsystemLocaleLoaders = {
  'zh-cn': { <subsystem>: () => import('./modules/zh-cn/<subsystem>'), ... },
  'en-us': { <subsystem>: () => import('./modules/en-us/<subsystem>'), ... },
};

const subsystemRoutePrefixes = {
  <subsystem>: ['/路由前缀1', '/路由前缀2', ...],
};
```

**不变量**:
- `subsystemLocaleLoaders['zh-cn']` 与 `subsystemLocaleLoaders['en-us']` 的 key 集合必须**严格相等**
- `subsystemRoutePrefixes` 的 key 集合必须是 `subsystemLocaleLoaders[*]` 的子集（即每个声明的 subsystem 至少有双语 loader）
- 注册表中的每个 `import()` 路径必须形如 `./modules/<locale>/<subsystem>`，不得直接指向 shard 子文件
- shard 文件仅允许由对应的 subsystem 入口文件二次聚合，例如 `src/lang/modules/zh-cn/plm.js -> ./plm/part.js`

---

## 2. 新增/修改文案标准流程（6 步）

### Step 1 — 定位子系统归属

根据页面路由前缀对照 `subsystemRoutePrefixes`：

| 子系统 | 路由前缀 | 适用场景 |
|--------|---------|---------|
| platform | `/authority` `/base` `/data` `/desk` `/flow` `/job` `/monitor` `/report` `/resource` `/system` `/tool` `/util` `/wel` `/work` | BladeX 平台基础管理、工作流、日志、任务、资源等系统域 |
| partner | `/partner` | 伙伴主数据（企业、联系人、客户） |
| crm | `/crm` `/sd` | CRM 前链、销售/服务域 |
| plm | `/plm` | 产品生命周期 |
| scm | `/scm` | 供应链 |

如路由未被任一前缀命中 → 新增子系统（见 Step 3 中的"新建子系统分支"）。

### Step 2 — 在归属子系统文件同步加 key（双语）

```
src/lang/modules/zh-cn/<subsystem>.js         ← 中文 subsystem 入口
src/lang/modules/en-us/<subsystem>.js         ← 英文 subsystem 入口（键路径与 zh-cn 完全对称）
src/lang/modules/zh-cn/<subsystem>/**         ← 可选 shard 子目录
src/lang/modules/en-us/<subsystem>/**         ← 可选 shard 子目录（与 zh-cn 结构镜像对称）
```

**严禁**：
- 把新 key 写入 `core.js`（除非确认是全站通用文案，例如「保存」「取消」「请输入」）
- 只写 zh-cn 不写 en-us
- 把 key 放入与路由归属不一致的子系统（如 `/partner` 路由的文案写到 `crm.js`）
- 绕过 subsystem 入口，直接在 `subsystems.js` 注册 shard 子文件

### Step 3 — 新建子系统分支（仅当新业务域出现）

1. 创建 `src/lang/modules/zh-cn/<newSubsystem>.js` 与 `src/lang/modules/en-us/<newSubsystem>.js`（两个文件必须同时出现在同一次提交）
2. 在 `src/lang/subsystems.js` 的 `subsystemLocaleLoaders` 两个 locale 节点同时添加 loader
3. 在 `subsystemRoutePrefixes` 中添加该子系统的路由前缀数组，覆盖该域所有顶层路由
4. 自检：`node -e "console.log(require('./src/lang/subsystems.js').resolveSubsystemsForRoute('/<newPath>'))"` 应返回 `['<newSubsystem>']` 或包含它

### Step 4 — 页面使用 `t()` 读取

- 组件内统一使用 `t()`（`useI18n()` 或 `this.$t(...)`），不得硬编码中文/英文到模板
- 动态表格/表单列的标签通过 `createTableOption(t)` 工厂注入，不得在 option 文件内直接写中文字面量

### Step 5 — 语言切换联调

切换语言后：
- 当前路由所属子系统包应在 Network 面板出现 `modules/<locale>/<subsystem>.js` 新的 chunk 请求（冷状态）
- 已加载子系统不应再次请求
- 未进入过的子系统不应预先加载

### Step 6 — 提交前本地校验（见 §4 校验命令）

---

## 3. 反模式清单（RED FLAGS，出现即 BLOCKED）

| # | 反模式 | 危害 | 处置 |
|---|-------|------|------|
| R1 | 任何业务代码出现 `import ... from '@/lang/zh'` 或 `'@/lang/en'` | 一次性打入所有子系统，懒加载失效，并重新耦合到 legacy 兼容桥接层 | 改走 `loadLanguageAsync` / `ensureI18nModulesForRoute` 或直接用 `t()` |
| R2 | 业务代码出现 `import ... from '@/lang/modules/<locale>/<subsystem>'` | 绕过注册表与路由驱动，子系统被强制进入首屏 chunk | 仅 `src/lang/index.js` 通过注册表里的 `() => import(...)` 引用允许 |
| R3 | 新子系统只注册 `zh-cn` loader 不注册 `en-us`（或相反） | 切英文时 key 缺失、页面退化为 key 字面量 | 两侧 loader 必须同时存在 |
| R4 | 新子系统只创建一侧语言文件 | 同 R3 | `modules/zh-cn/<sub>.js` 与 `modules/en-us/<sub>.js` 同次提交 |
| R5 | 业务 key 写入 `core.js`（如 `crm.lead.*` 进入 core） | 核心包膨胀、跨子系统耦合、失去按域懒加载意义 | 迁至归属子系统文件 |
| R6 | 新增路由但未在 `subsystemRoutePrefixes` 注册 | `resolveSubsystemsForRoute(path)` 返回空，子系统文案不会装载 | 补充 prefix 或修正路由归属 |
| R7 | 组件 `<template>` 中出现中文字面量（非 `t()` 调用） | 单语交付，违反双语义务 | 改走 `t('xxx.yyy')` |
| R8 | option 文件（`src/option/<module>/<entity>.js`）直接写中文 label | 无法国际化 | 改为 `createTableOption(t)` 工厂返回列定义 |
| R9 | 新文案只在 `zh-cn/*.js` 加，不在 `en-us/*.js` 同步 | 英文缺 key | 同次改动双侧对称 |
| R10 | 在 `subsystemLocaleLoaders` 使用非 `() => import(...)` 的同步 require | 被打包器视为静态依赖，丧失代码分割 | 必须为动态 import 箭头函数 |

---

## 4. 校验命令（Implementation / Code Review / Drift Check 必跑）

### 4.1 静态聚合引入基线（应为空）

```bash
# 从仓库根执行
grep -rn --include='*.js' --include='*.vue' "from '@/lang/zh'\|from '@/lang/en'\|require('@/lang/zh\|require('@/lang/en" mes-enreach-mom-web/src
```

- 期望：**无输出**
- 若有命中 → R1 违规，BLOCKED

### 4.2 禁止绕过注册表直接引 modules

```bash
grep -rn --include='*.js' --include='*.vue' "from '@/lang/modules" mes-enreach-mom-web/src
```

- 期望：**无输出**（注册表内部是相对路径 `./modules/...`，不会命中 `@/lang/modules`）
- 若有命中 → R2 违规，BLOCKED

### 4.3 双语 loader 对称性

```bash
node -e "const s=require('./mes-enreach-mom-web/src/lang/subsystems.js');const z=Object.keys(s.subsystemLocaleLoaders['zh-cn']).sort().join(',');const e=Object.keys(s.subsystemLocaleLoaders['en-us']).sort().join(',');if(z!==e){console.error('ASYMMETRIC',z,'vs',e);process.exit(1);}console.log('OK',z);"
```

- 期望：`OK <subsystem-list>`
- 若 `ASYMMETRIC` → R3 违规，BLOCKED

### 4.4 路由前缀可解析（新增路由时）

```bash
node -e "console.log(require('./mes-enreach-mom-web/src/lang/subsystems.js').resolveSubsystemsForRoute('/<新路由>'))"
```

- 期望：返回非空数组
- 若返回 `[]` → R6 违规，需补充 `subsystemRoutePrefixes`

### 4.5 双语文件存在性（新增子系统时）

```bash
test -f mes-enreach-mom-web/src/lang/modules/zh-cn/<new>.js && test -f mes-enreach-mom-web/src/lang/modules/en-us/<new>.js && echo OK
```

- 期望：`OK`
- 否则 → R4 违规

### 4.6 首屏/按需构建（整体验证）

```bash
# 使用工作区 task
tools/blade/cli.sh frontend-build
```

- 期望：构建成功，且构建产物中可见子系统级 chunk（Vite 动态 import 的默认代码分割）
- 失败（尤其是运行时 `throw` 被触发）→ R1 / R2 违规残留

---

## 5. Closure Gate（前端文案切片关闭前必答）

| # | 检查点 | 通过条件 |
|---|-------|---------|
| G1 | 新增/修改 key 归属的子系统文件 | zh-cn / en-us 双侧同次提交，键路径对称 |
| G2 | 是否新增了子系统 | 若是，`subsystemLocaleLoaders` 双侧注册 + `subsystemRoutePrefixes` 覆盖完整 |
| G3 | 路由前缀覆盖 | `resolveSubsystemsForRoute(<相关路由>)` 返回预期子系统 |
| G4 | 页面零硬编码 | `<template>` 与 option 文件无中英文字面量，统一通过 `t()` |
| G5 | core 未被污染 | 本次变更的 `core.js` 未新增业务域 key |
| G6 | 校验命令 §4.1–§4.3 全部通过 | 无输出 / `OK` |
| G7 | 若结构改变，构建通过 | §4.6 `frontend-build` 成功 |

**任一项 FAIL → 切片不得关闭。**

---

## 6. 常见场景示例

### 6.1 给已有页面（如 `/crm/lead`）加一条空态文案

1. 识别子系统：`/crm` → `crm`
2. `src/lang/modules/zh-cn/crm.js` 增加 `crm.lead.empty: '暂无线索'`
3. `src/lang/modules/en-us/crm.js` 增加 `crm.lead.empty: 'No leads yet.'`
4. 页面使用 `t('crm.lead.empty')`
5. 运行 §4.1–§4.3 → 关闭

### 6.2 新模块 `/eam/asset-ledger` 第一次出现

1. 创建 `src/lang/modules/zh-cn/eam.js` 与 `src/lang/modules/en-us/eam.js`
2. `subsystems.js`：
   - `subsystemLocaleLoaders['zh-cn'].eam = () => import('./modules/zh-cn/eam');`
   - `subsystemLocaleLoaders['en-us'].eam = () => import('./modules/en-us/eam');`
   - `subsystemRoutePrefixes.eam = ['/eam'];`
3. 验证：`resolveSubsystemsForRoute('/eam/asset-ledger')` 返回 `['eam']`
4. 页面文案按 Step 2 同步双语
5. 运行 §4.3 / §4.5 / §4.6 → 关闭

### 6.3 全站通用新按钮文案（如「导出为 CSV」）

1. 子系统：无（通用）→ 放 `core.js`
2. `modules/zh-cn/core.js` 与 `modules/en-us/core.js` 双侧同步
3. 不影响 `subsystems.js`

---

## 7. 与其他 Skill 的交互

- **dict-biz-integration**: 字典标签也在 i18n 文件中维护可读文案映射；字典 code 本身归属由 `blade_dict_biz.tenant_id` 决定，文案 i18n 归属由使用该字典的页面路由决定
- **industrial-page-standard**: 页面骨架生成时必须经 `t()` 注入 label；option 文件必须导出 `createTableOption(t)` 工厂
- **agent-hook-lifecycle**: 预留 PreAction 钩子扩展点 —— 当实现切片新增 `src/views/<newModule>/` 且 `subsystemRoutePrefixes` 未同步时应 BLOCKED（当前未强制，列为后续切片）
- **fk-field-detection**: FK 选择器的 label 源自关联实体的 i18n key，必须落在 FK 引用实体所属子系统包中

---

## 8. 引用锚点（权威代码位置）

- 入口：`mes-enreach-mom-web/src/lang/index.js`
  - `createI18n({ messages: {} })`
  - `coreLocaleLoaders`
  - `ensureCoreBundle(locale)`
  - `ensureSubsystemBundles(locale, modules)`
  - `ensureI18nModulesForRoute(routePath, locale)`
  - `loadLanguageAsync(locale, routePath)`
- 注册表：`mes-enreach-mom-web/src/lang/subsystems.js`
  - `subsystemLocaleLoaders`
  - `subsystemRoutePrefixes`
  - `resolveSubsystemsForRoute(routePath)`
- 触发点：
  - `mes-enreach-mom-web/src/main.js` — 启动初始化
  - `mes-enreach-mom-web/src/permission.js` — `router.beforeEach`
  - `mes-enreach-mom-web/src/page/index/top/top-lang.vue` — 语言切换

**变更上述文件结构或契约时，必须同步更新本 Skill（版本号递增）。**
