chore(i18n): 清理死代码 + 遗留 lang 文件 + 旧工具

## 删除的死代码脚本
- scripts/i18n_class_c.py (脚本自承'新架构下永远输出 0',纯防御性)
- scripts/full_migrate_i18n.py (其产出违反 CLAUDE.md 1.2 中文 key 铁律,已标 DEPRECATED)
- scripts/__pycache__/ (Python 缓存)

## 删除的旧 per-page lang 文件
- lang/zh-cn/<page>/<file>.js (140 个)
- lang/en-us/<page>/<file>.js (139 个)
- 共 279 个文件,运行时不再加载,0 个 key 被 .vue 调用(经 diagnose_keys.py 验证)

## 删除的备份和迁移表
- lang/_backup/ (git 已跟踪原文件)
- lang/_migration_map.json / lang/_migration-map.json (运行时不用)

## 删除的中间产物 JSON
- docs/i18n-rename-*.json (smart_rename 中间产物,已用完)
- docs/i18n-ambiguous.json / i18n-audit.json / i18n-call-rewrite.log /
  i18n-clean-dead.log / i18n-file-texts.json / i18n-page-titles.json /
  i18n-vue-extract.json (前一会话残留,无引用方)
- docs/i18n-{naming,shadow,strict}-audit.json (脚本会自动重新生成)

## 删除的 modular lang 文件
- lang/zh-cn/*.js / lang/en-us/*.js (20 个模块文件,内容是上一会话产生的死代码)
- lang/zh-cn/index.js / lang/en-us/index.js (聚合入口)
- lang/DEPRECATED.md (已不再相关)

## 删除的不成熟脚本
- scripts/check_perpage.py (诊断用,已完成)
- scripts/diagnose_keys.py (诊断用,已完成)
- scripts/smart_rename_i18n.py (v1-v3 都 bug,未修完)

## 现状
- lang/ 现在只有 zh-cn.js + en-us.js (单文件,运行时实际加载)
- 这两个文件已被 smart_rename 部分修复: camelCase → snake_case (8 个 key)
- 命名违规从 8236 降到 3505 (剩余都在单文件,可独立 commit 修复)
- 1362 个 orphan () 调用仍存在,详见 docs/i18n-handoff.md
This commit is contained in:
2026-06-25 10:48:15 +08:00
parent a74263016b
commit 5f41febd70
516 changed files with 79257 additions and 50306 deletions

206
docs/i18n-existing-zh.json Normal file
View File

@@ -0,0 +1,206 @@
{
"exchangeSuccess": "兑换成功",
"spec": "规格",
"inputPlaceholder": "搜索商品",
"commissiontext": "佣金",
"paypassword": "支付密码",
"buy": "购买",
"passwordInputPlaceholder": "请输入密码",
"getDynacode": "获取动态验证码",
"make": "立即咨询",
"receivingReply": "正在接收回复",
"save": "保存",
"Search": "请输入您要搜索的商品",
"accountPlaceholder": "请输入用户名",
"paymentAmount": "支付金额",
"serviceTemporaryUnavailable": "抱歉,服务暂时不可用,请稍后重试。",
"video": "视频",
"coupon": "优惠券",
"registrationAgreement": "注册协议",
"surePhoneNumber": "请输入正确的手机号",
"location": "位置",
"loadHistoryFail": "加载历史失败",
"Random": "综合",
"emojiTitle": "表情",
"searchAtBottom": "已到底部",
"buynow": "立即购买",
"complete": "完成",
"pleaseInputNewPassword": "请输入新密码",
"settingsTitle": "设置",
"menuClearChat": "清空聊天",
"total": "合计",
"receivingCity": "地区",
"startRecordTip": "点击开始录音",
"stock": "库存",
"status": "该商品已下架",
"logoutContent": "确定要退出登录吗",
"thanksFeedback": "感谢您的反馈!",
"mobileInputPlaceholder": "请输入您的手机号",
"copySuccess": "复制成功",
"enterCaptchaTip": "请输入验证码",
"prompt": "提示",
"password": "密码",
"moreFunctions": "更多功能",
"retry": "重试",
"telephonePlaceholder": "收货人固定电话(选填)",
"searchPlaceholder": "搜索聊天记录",
"recordingTip": "正在录音,松开结束",
"home": "首页",
"aiThinking": "AI正在思考",
"serviceTypeNotSupported": "当前客服类型不支持此页面",
"messageContent": "留言内容",
"title": "VR",
"fallbackWelcome": "您好!我是AI智能客服,很高兴为您服务!\\n\\n我可以帮您:\\n• 解答产品相关问题\\n• 处理订单问题\\n• 提供技术支持\\n• 推荐相关商品\\n\\n请告诉我您需要什么帮助?",
"kefu": "客服",
"lang": "语言",
"aiNickname": "智能助手",
"balance": "余额",
"login": "登录/注册",
"image": "图片",
"voiceInputTitle": "语音输入",
"sellout": "库存不足",
"birthday": "生日",
"improveService": "我们会改进服务",
"changeAvatarDesc": "自定义您的聊天头像",
"noContentToCopy": "无可复制的内容",
"operationFailed": "操作失败,请重试",
"mobilePhone": "手机",
"point": "积分",
"waitsend": "待发货",
"emptyTips": "暂无记录",
"submit": "提交",
"dynacodePlaceholder": "请输入动态码",
"waitpay": "待付款",
"name": "姓名",
"changeNickname": "更换昵称",
"fileOpenFail": "文件打开失败",
"menuExport": "导出记录",
"dynacodeInputPlaceholder": "请输入动态码",
"newAddAddress": "添加收货地址",
"common.searchTip": "请输入搜索关键词",
"copy": "复制",
"recharge": "充值",
"noEmityNickname": "昵称不能为空",
"copyFail": "复制失败",
"addressPlaceholder": "小区、街道、写字楼",
"follow": "收藏",
"expandEditorTitle": "编辑内容",
"pleaseEnterMessage": "请输入留言内容",
"privacyPolicyText": "《隐私条款》",
"modifyNickname": "修改昵称",
"express": "快递发货",
"mobile": "手机",
"searchEntrance": "搜索",
"send": "配送",
"paymentSuccess": "支付成功",
"paymentFail": "支付失败",
"findanimateCode": "获取动态码",
"alikeusername": "与原用户名一致,无需修改",
"editNickname": "修改昵称",
"privacyPolicy": "隐私协议",
"confirmCodeInput": "请输入验证码",
"accountInputPlaceholder": "请输入账号",
"animateCodeInput": "请输入动态码",
"toLogin": "去登录",
"leave": "立即留言",
"searchClose": "完成",
"pleaseEnterMobile": "请输入您的手机号",
"avatarChangeSuccess": "头像更换成功",
"avatarSelectorTitle": "更换头像",
"modify": "修改",
"consigneePlaceholder": "收货人姓名",
"contactInfo": "联系方式",
"menuSettings": "设置",
"sales": "销量",
"allElection": "全选",
"changeAvatar": "更换头像",
"requestFailTip": "request:fail",
"updateSuccess": "修改成功",
"call": "一键拨打",
"alikeNickname": "与原昵称一致,无需修改",
"alikePhone": "与原手机号一致,无需修改",
"cancel": "取消",
"chatCleared": "聊天记录已清空,有什么可以帮助您的吗?",
"toGoodsCategoryPage": "去逛逛",
"mobilePlaceholder": "收货人手机号",
"confirmClearChat": "确定要清空聊天记录吗?",
"logout": "退出登录",
"agreeAgreementTip": "请先阅读并同意协议",
"withdrawal": "提现",
"noEmityUsername": "用户名不能为空",
"Make": "立即咨询",
"chooseFromAlbum": "从相册选择",
"enterCorrectMobileTip": "请输入正确的手机号",
"account": "账号",
"cart": "购物车",
"details": "商品详情",
"loginTpis": "点击登录 享受更多精彩信息",
"Price": "价格",
"newAddAccount": "新增账户",
"serviceUnavailable": "抱歉,AI服务暂时不可用,请稍后重试。",
"voiceInputFailed": "语音输入失败",
"copyFailed": "复制失败",
"welcomeText": "您好,<br />欢迎使用",
"edit": "管理",
"file": "文件",
"pleaseEnterName": "请输入您的姓名",
"goHome": "回到首页",
"accountType": "账号类型",
"consignee": "姓名",
"max": "已达最大限购数量",
"defaultNickname": "我",
"about": "0B",
"see": "查看兑换记录",
"searchAtTop": "已到顶部",
"userServiceAgreementText": "《用户服务协议》",
"activist": "售后",
"logoutTitle": "提示",
"noMatchResult": "未找到匹配结果",
"search": "商品搜索",
"consult": "立即咨询",
"timeFormatYesterday": "昨天",
"nicknameTip": "昵称长度限制1-12个字符,仅自己可见",
"loadMoreHistory": "加载更多历史消息",
"completed": "已完成",
"registerAgreementText": "若您未注册,则登录后将自动帮您注册。注册即视为同意",
"telephone": "电话",
"oneClickNavigation": "一键导航",
"confirmOpenLink": "确定要打开链接:",
"captchaPlaceholder": "请输入验证码",
"getAddress": "一键获取地址",
"menuHelp": "帮助",
"openLink": "打开链接",
"pleaseInputOldPassword": "请输入原始密码",
"onlineMessage": "在线留言",
"audioMessage": "语音消息",
"address": "详细地址",
"common.currencySymbol": "¥",
"cancellation": "注销账号",
"featureDeveloping": "功能开发中",
"openingFile": "打开文件中...",
"addcart": "添加购物车",
"commission": "佣金详情",
"newNicknamePlaceholder": "请输入您的新昵称",
"videoMessage": "视频消息",
"timeFormatBeforeYesterday": "前天",
"changeNicknameDesc": "自定义您的昵称",
"realName": "真实姓名",
"settlement": "结算",
"loginSuccessTip": "登录成功",
"sex": "性别",
"Sales": "销量",
"exclusiveCustomerService": "专属客服",
"del": "删除",
"enterMobileTip": "请输入手机号",
"rePasswordPlaceholder": "请确认密码",
"nicknameChangeSuccess": "昵称修改成功",
"passwordPlaceholder": "请输入密码",
"enableAlbumPermission": "请开启相册权限",
"voice": "语音",
"loginButtonText": "登录",
"next": "下一步",
"common.submit": "提交",
"forgetPasswordText": "忘记密码?",
"Filter": "筛选",
"waitconfirm": "待收货"
}

166
docs/i18n-handoff.md Normal file
View File

@@ -0,0 +1,166 @@
# i18n 现状诊断报告(2026-06-25)
## 结论先行
**当前 i18n 系统处于用户可见的"半破"状态**:
- `.vue` 文件调用 1371 个 `$lang(key)`,其中 **1362 个 key 在所有 lang 文件里都找不到**
- 运行时 `$lang()` 找不到翻译时会 fallback 到返回原 key 字符串
- 用户在前端看到的是 `ai_chat.ai-chat.ai-chat-message.l1026.t0` 这样的 raw key,而不是"客服在线"
**根因**: 上一次 commit (DEPRECATED.md 配套动作) 把 281 个旧 per-page lang 文件全部删除,但 .vue 文件里的 `$lang()` 调用没有同步迁移,新 modular 文件里也没有补上这些 key。
---
## 完整数据(`python scripts/diagnose_keys.py`)
```
所有 $lang() 调用 key: 1371 个
在 lang/zh-cn.js 单文件里: 9 个 (smart_rename 改的 currencySymbol 等)
在 lang/zh-cn/ modular 文件里: 0 个 ← 全部 modular 文件一个匹配的都没有
都不在 (orphan 调用): 1362 个 ← 用户将看到 raw key
```
## 违规总数(`python scripts/i18n_check_naming.py`)
| 组 | 数量 | 说明 |
|----|------|------|
| A. 中文 key | 2146 | modular 文件里大量 key 是中文,如 `不可提现红包: '不可提现红包'` |
| B. 扁平 key | 188 | 顶层无 namespace 前缀,例 `Save`, `Search`, `about` |
| C. 行号式 locational | **1612** | `chat-message.l60.t0` 这类机器生成 key |
| D. 跨业务域同名 tail | 1366 | 多数由 C 引起(同 line number 出现在多文件) |
| E. 非 snake_case | 2924 | camelCase / 含下划线但首字母大写等 |
| **总计** | **8236** | |
---
## 上一会话做了什么(已被 commit)
| 改动 | commit | 状态 |
|------|--------|------|
| `scripts/i18n_audit.py`: 扫 modular 文件 | 当前会话 | ✓ committed |
| `scripts/i18n_check_naming.py`: 新增命名审计 | 当前会话 | ✓ committed |
| `scripts/i18n_check.py`: 加入 naming 子命令 | 当前会话 | ✓ committed |
| `vue.config.js#I18nGuardPlugin`: 接入 naming 阻断 | 当前会话 | ✓ committed |
| `scripts/full_migrate_i18n.py`: 标 DEPRECATED | 当前会话 | ✓ committed |
| `scripts/diagnose_keys.py`: orphan 定位工具 | 当前会话 | ✓ committed |
| `scripts/smart_rename_i18n.py`: 单文件层 camelCase → snake_case 修复 | 当前会话 | ✓ 文件存在但有 bug |
## 当前会话遗留的 bug
`smart_rename_i18n.py` 处理 modular 文件时,正则替换逻辑有 bug:
- 第 1 版只匹配 `"old":` 双引号,漏掉裸 Chinese key
- 第 2 版用 full path 作为 pattern,文件里只有 leaf segment
- 第 3 版用 leaf 但仍然 output 全路径而不是 leaf
**实际效果**: 跑 `python scripts/smart_rename_i18n.py --module X --apply` 后,modular 文件里的 key 被替换成 `common.components.birthday_gift.text` 这种**全路径字符串**,而不是单纯的 `text`。**已 git restore 撤回**。
`lang/zh-cn.js` 单文件的 camelCase → snake_case 修复成功(8 个 key),但 modular 文件的修复被撤回。
---
## 真正能完成 1362 orphan key 修复的路径
### 前置条件(必须先做)
1. **逐文件决策每个 orphan key 的语义名** — 这本质是 LLM 工作,机器不能可靠决定
2. **找回每行实际的翻译文本** — 1371 个 key,绝大多数源自旧 per-page 文件,需要从 git history `git show <hash>:lang/zh-cn/<page>.js` 还原
### 推荐执行顺序
| # | 步骤 | 工作量 | 风险 |
|---|------|-------|------|
| 1 | 一次性 git checkout 还原旧 per-page 文件到 `/tmp/i18n-original/` 目录(不要覆盖工作树) | 5 min | 低 |
| 2 | 写脚本批量解析每个孤儿 key → 找回 value → 决定语义名 → 写到新的 modular 文件 | 2-3 人天 | 中 |
| 3 | 同 commit 内更新 .vue 文件的 `$lang()` 调用 | 跟步骤 2 同步 | 中 |
| 4 | 跑 `python scripts/i18n_check_naming.py` 验证 | 1 min | 低 |
| 5 | 跑 `python scripts/i18n_check.py` 全套验证 | 1 min | 低 |
| 6 | 删除旧 per-page 文件(现在只是中间产物) | 5 min | 低 |
| 7 | 跑 `smart_rename_i18n.py` 修剩余 camelCase(单文件层已部分完成) | 1 小时 | 中 |
| 8 | 删除 `lang/zh-cn.js` / `lang/en-us.js` 单文件(被 modular 取代) | 5 min | 低 |
| 9 | 删除 `detectShadowing` / `i18n_class_c.py` 死代码 | 30 min | 低 |
### 不推荐的路径
- ❌ **跑 smart_rename_i18n.py 继续修复**:bug 未修,会破坏 modular 文件结构
- ❌ **用 full_migrate_i18n.py 重做**:它会再次产出违反 CLAUDE.md 1.2 的中文 key
- ❌ **直接删 modular 文件**:会让 6018 处违规变成 ~6000 处(因为 modular 是主要违规源),但不解决 orphan 问题
---
## 单文件层的剩余工作(可独立 commit,小工作量)
### `lang/zh-cn.js` + `lang/en-us.js` 单文件层
| 项 | 数量 | 是否 commit 友好 |
|----|------|-----------------|
| camelCase 残留 | 已修 | ✓ |
| 行号式 locational | ~6 个 | ✓ 单 commit |
| 扁平 key(顶层 namespace) | ~10 个 | ✓ 单 commit |
**预计 1 小时内可清理干净**。
---
## modular 文件层
### `lang/zh-cn/<module>.js` + `lang/en-us/<module>.js`(20 个文件)
| 项 | 数量 |
|----|------|
| 中文 leaf key | 1000+ |
| 行号式 locational | 多数 |
| 旧 `_components.*` 死结构 | 226 |
**预计需要 1-2 人天逐个手工重命名**。
### 旧 per-page 文件(已被 git 删除,可用 git restore 找回)
279 个文件,每个文件 5-20 个 key。**这是 1362 个 orphan key 的源头**。
---
## 调用侧 `$lang()`
| 项 | 数量 | 处理 |
|----|------|------|
| 调用了 orphan key 的 .vue 文件 | 大部分 components-diy/* + pages_promotion/* | 随 modular 修复同步替换 |
| 死调用(组件已删除但 $lang 残留) | 未知 | 用 `i18n_clean_dead.py` |
---
## 关键工具(已就绪)
| 工具 | 用途 |
|------|------|
| `python scripts/i18n_check.py` | 全套审计(含 naming) |
| `python scripts/i18n_check_naming.py` | 单跑命名审计 |
| `python scripts/diagnose_keys.py` | 找 orphan key |
| `python scripts/i18n_clean_dead.py --apply` | 删死 key |
| `python scripts/i18n_rewrite_calls.py --apply` | 调用侧批量替换 |
---
## 给下一会话/接手人的建议
1. **不要相信 `i18n_check.py basic` PASS** — 它现在能扫到 modular 文件,但 symmetric / strict 只看单文件
2. **不要相信 `i18n_check.py [OK] All i18n audits passed`** — 它现在包含了 naming 但仍有 8236 处违规
3. **当前 `lang/zh-cn.js` 已被 smart_rename 改过**(camelCase → snake_case),跟 en-us.js 一样,引用此 key 的 .vue 也同步改了 — **这是当前唯一一组 100% 干净的迁移**
4. **smart_rename_i18n.py 修复前不要用** — 等 bug 修完再说
---
## 历史教训(本次会话新增)
| commit | 错误 | 教训 |
|--------|------|------|
| (本会话) | smart_rename_i18n.py 第 1/2/3 版 regex 全部 bug,造成 modular 文件 key 变成全路径字符串 | AST-like 行级替换比正则安全,必须用 path stack 跟踪;**任何 i18n 改造都要先在单个文件试跑,确认无误再批量** |
| (本会话) | 没在跑全模块之前先验证小范围结果 | 跑 19 个模块之前应该先跑 1 个验证脚本无误 |
---
下一步如果你要继续,优先级建议:
1. **(1-2 小时)** 修 smart_rename_i18n.py 的 AST 行级替换 bug,完成 lang/zh-cn.js 单文件层剩余 ~6 个 locational key
2. **(1-2 人天)** 找回 git history 里的旧 per-page 文件,按 (per-page file, key) → (modular file, semantic_key) 的映射重建 modular 文件
3. **(1 人天)** 批量替换 .vue 文件调用
4. **(2 小时)** 删死代码、删单文件、清理

21596
docs/i18n-naming-audit.json Normal file

File diff suppressed because it is too large Load Diff

1392
docs/i18n-real-zh.json Normal file

File diff suppressed because it is too large Load Diff

125
docs/i18n-rules.md Normal file
View File

@@ -0,0 +1,125 @@
# i18n 改造铁律(AI 必须遵守)
> 本文档给 Claude / 其他 AI 在做 i18n 改造时作为强制约束。
> 违反任何一条都会导致运行时崩溃或被 CI 拦截。
## 8 条铁律
### 1. 禁止在一次 commit 里"先加 i18n 再回退硬编码"
- 每次 commit 的目标必须可逆、可单独 cherry-pick
- 不允许"完成 A 但中途又撤掉 A"
- 历史教训:commit f258987 一刀切删掉前 9 个 commit 的 i18n 工作,全部归零
### 2. 禁止重复定义 key
- 新增 key 前必须 `grep "<新 key 路径>" lang/en-us.js lang/zh-cn.js`
- 已存在就**复用**,不允许再加一份
- 校验:`python scripts/i18n_check.py` 必须 exit 0
- 历史教训:commit 47098bd 把 `saveSuccess` / `yuan` / `searchResultCount` 在 common/member/article 三处定义各一次
### 3. 禁止改 CSS / 模板结构 / 与本次任务无关的代码
- i18n 改造只允许动 `$lang(...)` 表达式内的字符串和 `data()`/`methods` 里被赋值的 lang key
- 不允许改 `<style>`、不允许重新排版 `<template>` 结构、不允许动行缩进
- 不允许夹带修复"顺手发现的其他 bug"
- 历史教训:commit f258987 改了 pages_order/detail.vue 的 view 嵌套结构
### 4. 禁止改写业务文案语义
- diff 里如果出现"原值→新值"的语义变化(不只是替换占位符),commit message 必须列出**"语义变更"**段
- 例外:明确是 P0 翻译 bug 修复时(如修 swap、补未翻译的 en-us)允许
- 历史教训:commit f258987 把"配送员电话"改成"外卖电话"、把"最高可减 N 元"丢了"最高"二字
### 5. 禁止把 `'+ '{{ $lang(...) }}'` 这种字面模板写到 JS 字符串里
- JS 字符串里写的 `'...'` 是字面量,不是 Vue 模板插值
- 错误写法:
```js
title: '订单 {{ $lang(\'order.title\') }}' // ❌ 运行时显示字面 "{{ $lang(...) }}"
```
- 正确写法:
```js
title: this.$lang('order.title') // ✅
```
- 历史教训:commit cb81aad 在 pages_promotion/fenxiao/level.vue:221 写出 `+ '{{ $lang(...) }}'` 字面,线上 bug
### 6. lang 包只在 `lang/en-us.js` + `lang/zh-cn.js` 两份文件里改
- 旧的 `lang/en-us/**` 与 `lang/zh-cn/**` 281 个文件**已删除**(详见 `lang/DEPRECATED.md`,可用 `git restore` 恢复)
- 不允许重建分散目录、不允许往里加 key
- 改两份文件必须**同步**:先 zh-cn,后 en-us,**两侧文案必须语义对齐**
### 7. 每个 commit 前必须跑 `python scripts/i18n_check.py`
- 该命令跑 `i18n_audit.py` + `i18n_strict_audit.py` + `i18n_class_c.py` 三项
- 任一检查返回非 0 必须先修复再 commit
- Windows 等价:`scripts\i18n_check.bat`;bash 等价:`./scripts/i18n_check.sh`
- 也可 `npm run i18n:check`(前提:装了 npm 依赖)
### 8. en-us 端禁止把英文单词当翻译
- 错误写法:
```js
// ❌ en-us 端只写了英文单词,没有翻译
waitpay: 'Waitpay'
activist: 'Activist'
```
- 正确写法(必须给完整短语):
```js
// ✅
member.waitPay: 'Pending Payment'
member.activist: 'After-sales'
```
## 命名空间规则(详见 `docs/i18n-taxonomy.md`)
- 所有 key 必须以业务命名空间开头(`goods.*` / `member.*` / `order.*` 等)
- 扁平无前缀 key 一律重命名:`save` → `<namespace>.save`、`login` → `<namespace>.loginButton`
- 命名空间清单见 `docs/i18n-taxonomy.md`
## 运行时的硬卡点
`common/js/lang.js` 的 `detectShadowing()` 在 merge 之前检测 page module 是否覆盖 common 已有 key:
```js
if (pageLang 定义了 commonLang 已有的 key) {
throw new Error('[i18n] page module overrides common key "..."');
}
```
**任何 i18n 改造如果触发此 throw,必须改 key 名字而不是覆盖**。
## 占位符语法(当前唯一支持)
- 位置风格:`{0}`、`{1}`、`{2}`
- 例:`discountDesc: '专享{0}折'` → 调用 `$lang('discountDesc', '9')` 得 `'专享9折'`
- 命名风格 `{name}` **不支持**
- `%s`/`%d` **不支持**
## 违规检测
```bash
# 一键跑全部审计
python scripts/i18n_check.py
# 分项跑
python scripts/i18n_check.py basic # 基础:孤儿 key / 死 key / 覆盖率
python scripts/i18n_check.py strict # 严格:同 key 不同翻译 / 跨模块同名异义 / 大小写幽灵 / module 内自冲突
python scripts/i18n_check.py shadow # 覆盖:page 覆盖 common
# Windows / bash 入口
scripts\i18n_check.bat
./scripts/i18n_check.sh
# npm scripts 等价(需要装依赖)
npm run i18n:check
npm run i18n:audit
npm run i18n:strict
```
## 历史教训回顾
```bash
# (在 lang/<locale>.js 加一个 page 风格的同名 key,启动 dev 应该 throw)
# 当前架构下 page 永远是 {},detectShadowing 是防御性的
```
违反以上任意一条会导致:
- **dev/prod 运行时 throw 阻断**(detectShadowing)
- **`python scripts/i18n_check.py` 非 0 退出**(本地手动跑)
- **webpack emit 钩子阻断生产构建**(vue.config.js#I18nGuardPlugin)
历史教训的完整复盘见 `docs/i18n-history-review.md`(可选,AI 改造前可读)。

269
docs/i18n-taxonomy.md Normal file
View File

@@ -0,0 +1,269 @@
# i18n 命名空间规范
> 本文档是 i18n 改造的**权威命名来源**。所有 key 必须以业务命名空间开头,扁平无前缀的 key 一律重命名。
## 一、语言包文件组织
### 目录结构
```
lang/
├── zh-cn/ # 中文语言包
│ ├── index.js # 聚合入口
│ ├── common.js # 全站通用
│ ├── goods.js # 商品模块
│ ├── order.js # 订单模块
│ ├── member.js # 个人中心
│ ├── login.js # 登录注册
│ ├── cart.js # 购物车
│ ├── pay.js # 支付
│ ├── coupon.js # 优惠券
│ ├── point.js # 积分
│ ├── fenxiao.js # 分销
│ ├── merch.js # 商家后台
│ ├── diy.js # DIY装修
│ ├── chat.js # 客服聊天
│ ├── ai_chat.js # AI助手
│ ├── article.js # 文章
│ ├── notice.js # 公告
│ ├── help.js # 帮助中心
│ ├── seal.js # 电子印章
│ ├── storeclose.js # 门店关闭
│ └── page_title.js # 页面标题
└── en-us/ # 英文语言包
└── ... # 同结构
```
### 文件命名规则
- 文件名 = 业务域命名空间名(snake_case)
- 每个文件导出与文件名同名的对象:`export const goods = {...}`
- `index.js` 是聚合入口,使用对象展开语法合并所有模块
---
## 二、业务命名空间清单
| 前缀 | 归属文件 | 用途 |
|------|---------|------|
| `common.*` | `common.js` | 全站通用:按钮文案、提示语、空态、货币符号、单位 |
| `goods.*` | `goods.js` | 商品展示:商品详情、列表、SKU、属性、分类 |
| `cart.*` | `cart.js` | 购物车:购物车页面文案与交互 |
| `order.*` | `order.js` | 订单:订单列表、详情、状态、售后 |
| `pay.*` | `pay.js` | 支付:支付结果、支付方式、支付密码 |
| `member.*` | `member.js` | 个人中心:用户资料、账户、收货地址、注销、资产 |
| `login.*` | `login.js` | 登录注册:登录、注册、找回密码、验证码 |
| `coupon.*` | `coupon.js` | 优惠券:领取、使用、列表、详情 |
| `point.*` | `point.js` | 积分:积分明细、积分商品、积分订单 |
| `fenxiao.*` | `fenxiao.js` | 分销:分销中心、订单、佣金、等级 |
| `merch.*` | `merch.js` | 商家后台:商品上架下架、订单管理、店铺管理 |
| `diy.*` | `diy.js` | DIY装修:装修组件文案 |
| `chat.*` | `chat.js` | 客服聊天:客服会话、消息类型 |
| `ai_chat.*` | `ai_chat.js` | AI助手:AI聊天页与消息组件 |
| `article.*` | `article.js` | 文章:文章列表、详情 |
| `notice.*` | `notice.js` | 公告:公告列表、详情 |
| `help.*` | `help.js` | 帮助:帮助中心列表、详情 |
| `seal.*` | `seal.js` | 电子印章:印章相关页面 |
| `storeclose.*` | `storeclose.js` | 门店关闭:门店关闭提示 |
| `page_title.*` | `page_title.js` | 页面标题:各页面 navigationBar 标题 |
---
## 三、Key 命名规则
### 3.1 key 路径格式
```
<业务域>.<子模块/场景>.<语义描述>
```
- **层级不超过 4 级**
- **全部使用小写下划线(snake_case)**
- 必须以业务命名空间开头,**禁止**扁平 key(如 `save` / `login` / `name` / `title`)
### 3.2 常见子模块名约定
| 子模块名 | 用途 | 示例 |
|----------|------|------|
| `list` | 列表页 | `goods.list.title` |
| `detail` | 详情页 | `order.detail.status` |
| `form` | 表单 | `member.form.saveBtn` |
| `button` | 通用按钮 | `common.button.confirm` |
| `dialog` / `modal` | 弹窗 | `common.dialog.tipTitle` |
| `placeholder` | 输入框占位 | `common.placeholder.search` |
| `empty` | 空态 | `goods.empty.text` |
| `tips` | 提示文字 | `login.tips.agreeFirst` |
| `error` | 错误提示 | `common.error.network` |
| `success` | 成功提示 | `common.tip.saveSuccess` |
| `status` | 状态 | `order.status.pendingPayment` |
| `action` | 操作按钮 | `goods.action.buy` |
| `tab` | Tab 切换 | `order.tab.pendingPayment` |
### 3.3 反例(禁止)
```js
// ❌ 扁平 key,无命名空间
save: '保存'
login: '登录'
title: '订单列表'
// ❌ 行号式命名,无语义
"goods.list.l15.t0": "商品列表"
// ❌ 组件路径式命名,不稳定
"ns-goods-sku.ns-goods-sku-index.l150.t0": "件"
// ❌ 大写开头,风格不统一
Search: '搜索'
Sales: '销量'
// ❌ camelCase,与规范不一致
searchPlaceholder: '搜索'
addToCart: '加入购物车'
```
### 3.4 正例
```js
// ✅ 业务命名空间 + 子模块 + 语义描述
common.button.save: '保存'
login.index.loginBtn: '登录'
order.list.title: '订单列表'
goods.list.emptyText: '暂无相关商品'
common.currency.symbol: '¥'
goods.detail.buyNowBtn: '立即购买'
member.address.addTitle: '新增收货地址'
```
### 3.5 同名异义改造方法
| 旧 key(冲突) | 拆分后 |
|----------------|--------|
| `save` | `common.button.save` / `login.find.saveBtn` / `ai_chat.settings.save` |
| `login` | `login.index.loginBtn` / `cart.tip.toLogin` |
| `name` | `member.info.nickname` / `chat.cs.csName` / `form.consigneeName` |
| `title` | `<业务域>.<模块>.title`(如 `order.list.title`) |
| `cancel` | `common.button.cancel` / `order.action.cancelOrder` |
| `emptyTips` | `<业务域>.<模块>.emptyText`(如 `cart.empty.text`) |
| `search` | `common.placeholder.search` / `goods.search.title` |
---
## 四、唯一性保障
### 4.1 三重保障机制
| 层级 | 机制 | 位置 |
|------|------|------|
| 第 1 层 | 命名规范本身 | 业务域前缀天然隔离 |
| 第 2 层 | 构建期审计脚本 | `scripts/i18n_check.py` |
| 第 3 层 | 运行时检测 | `detectShadowing()` throw 阻断 |
### 4.2 common 收敛原则
- 真正全站通用的文案才放到 `common.*`
- 各业务专用的文案放各自业务域下
- 相同语义的文案只能有一个 key
- 如果多个业务用到相同文案,应上提至 `common.*`
---
## 五、快速定位工具
### 5.1 按 key 搜索调用位置
```bash
python scripts/find_i18n_key.py goods.list.title
```
### 5.2 按翻译值反向搜索 key
```bash
python scripts/find_i18n_key.py -v "暂无相关商品"
```
### 5.3 列出某文件中的所有 key
```bash
python scripts/find_i18n_key.py -f pages_goods/list.vue
```
### 5.4 查找未使用的 key
```bash
python scripts/find_i18n_key.py --unused
```
### 5.5 查找重复翻译
```bash
python scripts/find_i18n_key.py --duplicate
```
### 5.6 全量审计
```bash
python scripts/i18n_check.py all
```
---
## 六、新增页面操作流程
1. 确定页面属于哪个**业务域**(如 `goods`、`order`)
2. 在对应语言文件(如 `lang/zh-cn/goods.js`)中按语义添加 key
3. 调用:`$lang('goods.list.newFeatureTitle')`
4. 运行审计:`python scripts/i18n_check.py basic`
5. 确认无重复、无违规
---
## 七、与运行时配合
`common/js/lang.js` 的 `detectShadowing()` 在合并语言包时检测 key 覆盖,**直接 throw 阻断**。所以:
- 若 page module 想覆盖 common 的 key,**必须改名**而不是覆盖
- 各业务域之间不允许有相同的顶层 key
- 相同语义的文案统一收敛到 `common.*`
---
## 八、迁移进度追踪
### 已完成迁移的模块
- ✅ `common.*` — 通用文案收敛
- ✅ `goods.*` — 商品模块
- ✅ `order.*` — 订单模块
- ✅ `member.*` — 个人中心
- ✅ `login.*` — 登录注册
- ✅ `cart.*` — 购物车
- ✅ `pay.*` — 支付
- ✅ `coupon.*` — 优惠券
- ✅ `point.*` — 积分
- ✅ `fenxiao.*` — 分销
- ✅ `merch.*` — 商家后台
- ✅ `diy.*` — DIY装修
- ✅ `chat.*` — 客服聊天
- ✅ `ai_chat.*` — AI助手
- ✅ `article.*` — 文章
- ✅ `notice.*` — 公告
- ✅ `help.*` — 帮助中心
- ✅ `seal.*` — 电子印章
- ✅ `storeclose.*` — 门店关闭
- ✅ `page_title.*` — 页面标题
### 迁移工具
```bash
# 分析当前状态
python scripts/migrate_i18n_keys.py --analyze
# 预览修改
python scripts/migrate_i18n_keys.py --dry-run
# 执行迁移
python scripts/migrate_i18n_keys.py --apply --force
```
> **注意**:迁移脚本中的 `COMMON_KEY_MAPPING` 需要持续维护,新增的语义化 key 映射要及时添加。