Files
lucky_shop/docs/i18n-audit-report.md
ZF sun 55a5a68517 refactor: 统一多语言key的组件命名格式为短横线分隔
根据i18n规范,将所有组件相关的多语言key从驼峰式改为短横线分隔格式,同步更新文档中的示例说明。
2026-06-30 10:28:36 +08:00

223 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 国际化(i18n)全量审计报告
> 审计范围:`z:\D\projects\shop-projects\frontend` 下所有 `.vue` / `.js` 文件(排除 `node_modules`、`uni_modules`、`unpackage`、`docs`、`scripts` 等)
> 审计时间:2026-06-29
> 当前静态校验:`node scripts/i18n-check.js` 通过(zh-cn / en-us key 完全一致)
> 修复状态:P0 / P1 / P2(兜底 + 验证)已修复并验证 ✅
## 一、核心结论
项目已大规模使用 `$lang()`,但仍存在 **三类问题**:
1. **Resolver/代码不匹配导致的 raw key(用户可见)**:组件/页面调用的 key 与语言包实际结构不一致,英文环境下会直接显示 key 名。
2. **硬编码中文文本(英文环境下仍显示中文)**:`.vue` 模板和 `.js` 脚本中存在未替换为 `$lang()` 的静态中文。
3. **动态 key 无法静态校验**:9 处依赖运行时数据拼接 key,需人工确认对应 key 是否完整。
---
## 二、P0 - 会显示 raw key 的调用错误(已修复 ✅)
> 修复文件:
> - `components-diy/diy-article.vue`
> - `pages_promotion/point/goods_list.vue`
> - `pages_tool/member/cancelstatus.vue`
> - `pages_tool/member/card_buy.vue`
>
> 验证:自定义脚本验证所有修改后的 key 均可解析,`npm run i18n:check` 通过。
### 1. `components-diy/diy-article.vue`
| 行 | 错误调用 | 语言包实际 key | 说明 |
|---|---|---|---|
| 93 | `$lang('components-diy.diy-article.viewMore')` | `components-diy.diy-article.viewMore` | 组件名大小写不一致,`normalizeSharedLangPack` 将 `diy-article` 转为 `diyArticle` |
| 98 | `$lang('components-diy.diy-article.noArticles')` | `components-diy.diy-article.noArticles` | 同上 |
**修复建议**:统一改为 `$lang('components-diy.diy-article.viewMore')` / `$lang('components-diy.diy-article.noArticles')`。
### 2. `pages_promotion/point/goods_list.vue`
| 行 | 错误调用 | 语言包实际 key | 说明 |
|---|---|---|---|
| 9, 17, 20, 29, 38, 62, 75, 86, 94, 98, 101, 104, 109, 112, 123, 124, 286, 292, 299, 305 | `$lang('goods_list.xxx')` | 扁平 key(如 `searchPlaceholder`) | `normalizePageLangPack` 将文件名 `goods_list` camelCase 为 `goodsList`,导致 `goods_list.xxx` 无法解析 |
**修复建议**:页面内所有 `$lang('goods_list.xxx')` 改为 `$lang('xxx')`(该页面语言包为扁平结构),或改为 `$lang('goodsList.xxx')`。
### 3. `pages_tool/member/cancelstatus.vue`
| 行 | 错误调用 | 语言包实际 key | 说明 |
|---|---|---|---|
| 8, 9, 16, 17, 26, 27, 32, 33, 84, 85 | `$lang('member.cancelStatus.xxx')` | `member.cancelstatus.xxx`(小写 s) | 文件名 `cancelstatus` camelCase 后为 `cancelstatus`,页面调用为 `cancelStatus` |
**修复建议**:统一改为 `$lang('member.cancelstatus.xxx')`。
### 4. `pages_tool/member/card_buy.vue`
| 行 | 错误调用 | 语言包实际 key |
|---|---|---|
| 353 | `$lang('common.tip')` | `customerService.tip`(位于 `common.customerService.tip`) |
**修复建议**:改为 `$lang('customerService.tip')`,或根据业务语义使用其他 key。
---
## 三、P1 - 模板中硬编码中文(已修复 ✅)
共 **59 处**硬编码中文已替换为 `$lang()` 调用,并补充了对应的中/英文语言包。
- 验证:自定义 P1 验证脚本 `verify_p1_keys.js` 全部通过。
- 静态校验:`npm run i18n:check` 通过(zh-cn / en-us key 完全一致)。
- 语法检查:相关 `.js` 文件均通过 `node --check`。
原始 59 处按模块归类如下(实际落地 key 与最初建议略有差异,详见文末“命名空间修正记录”):
### 3.1 公共组件(`components/`)
| 文件 | 行 | 中文文本 | 建议 key |
|---|---|---|---|
| `components/chat-message/chat-message.vue` | 60 | 客服不在线 | `components.chat-message.customerOffline` |
| `components/chat-message/chat-message.vue` | 63 | 客服在线 | `components.chat-message.customerOnline` |
| `components/pick-regions/pick-regions.vue` | 17 | 取消 | `components.pick-regions.cancel` |
| `components/pick-regions/pick-regions.vue` | 18 | 完成 | `components.pick-regions.confirm` |
| `components/ns-goods-sku/ns-goods-sku.vue` | 22 | `{point}积分` | `components.ns-goods-sku.pointSuffix`(使用命名参数) |
| `components/ns-chat/ns-chat-receiveGoods.vue` | 8 | 库存: | `components.ns-chat-receiveGoods.stockPrefix` |
| `components/ns-chat/ns-chat-receiveGoods.vue` | 9 | 销量: | `components.ns-chat-receiveGoods.salesPrefix` |
| `components/ns-chat/ns-chat-receiveGoods.vue` | 17 | 查看商品 | `components.ns-chat-receiveGoods.viewGoods` |
| `components/ns-chat/ns-chat-goods.vue` | 12 | 已发送 | `components.ns-chat-goods.sent` |
| `components/ns-chat/ns-chat-goods.vue` | 25 | 发送 | `components.ns-chat-goods.send` |
| `components/ns-video-player-popup/ns-video-player-popup.vue` | 17 | 视频地址不存在 | `components.ns-video-player-popup.videoUrlNotExist` |
### 3.2 装修组件(`components-diy/`)
| 文件 | 行 | 中文文本 | 建议 key |
|---|---|---|---|
| `components-diy/diy-search.vue` | 31 | 搜索 | `components-diy.diy-search.searchBtn` |
| `components-diy/diy-map.vue` | 16 | 一键导航 | `components-diy.diy-map.navigate` |
| `components-diy/diy-kefu.vue` | 17 | 立即添加 | `components-diy.diy-kefu.addNow` |
| `components-diy/diy-channel-video.vue` | 20, 32, 40 | `{count}次观看` | `components-diy.diy-channel-video.viewCountSuffix`(使用命名参数) |
| `components-diy/diy-live.vue` | 29 | 主播: | `components-diy.diy-live.anchorPrefix` |
| `components-diy/diy-live.vue` | 33 | 直播商品: | `components-diy.diy-live.goodsPrefix` |
| `components-diy/diy-listmenu.vue` | 15 | 查看 | `components-diy.diy-listmenu.view` |
| `components-diy/diy-text.vue` | 211 | 专区 | `components-diy.diy-text.zone` |
| `components-diy/diy-goods-recommend.vue` | 56 | 售 / 件 | `components-diy.diy-goods-recommend.soldPrefix` / `unit.piece` |
| `components-diy/diy-notes.vue` | 42 | 阅读 | `components-diy.diy-notes.readPrefix` |
### 3.3 页面级(`pages_tool/`、`pages_promotion/`)
| 文件 | 行 | 中文文本 | 建议 key |
|---|---|---|---|
| `pages_tool/login/login.vue` | 396 | 请输入手机号 | `login.login.mobileRequired` |
| `pages_tool/login/login.vue` | 400 | 请输入正确的手机号 | `login.login.mobileFormatError` |
| `pages_tool/login/login.vue` | 419 | `{seconds}s后可重新获取` | `login.login.resendAfterSeconds` |
| `pages_tool/login/login.vue` | 449 | 获取动态码 | `login.login.getDynacode` |
| `pages_tool/member/index.vue` | 61 | 我知道了 | `member.index.iKnow` |
| `pages_tool/index/diy.vue` | 46 | 我知道了 | `index.index.iKnow` |
| `pages_tool/member/cancelsuccess.vue` | 7 | 您已成功注销账号 | `member.cancelsuccess.title` |
| `pages_tool/member/cancelsuccess.vue` | 8 | 待下次与您更好的相遇... | `member.cancelsuccess.desc` |
| `pages_tool/member/cancelsuccess.vue` | 9 | 完成 | `member.cancelsuccess.done` |
| `pages_tool/member/cancelrefuse.vue` | 5 | 您的申请已拒绝 | `member.cancelrefuse.title` |
| `pages_tool/member/cancelrefuse.vue` | 6 | 拒绝理由: | `member.cancelrefuse.reasonPrefix` |
| `pages_tool/member/cancelrefuse.vue` | 8 | 返回 | `member.cancelrefuse.back` |
| `pages_tool/member/cancelrefuse.vue` | 9 | 重新申请 | `member.cancelrefuse.reapply` |
| `pages_tool/member/public/js/member-public.js` | 63 | 风险提示 | `member.public.riskWarningTitle` |
| `pages_tool/member/public/js/member-public.js` | 64 | 确定要注销当前账号吗? | `member.public.cancelAccountConfirm` |
| `pages_tool/goods/not_exist.vue` | 5 | 您查看的商品不存在... | `goods.notExist.message` |
| `pages_tool/file-preview/file-preview.vue` | 15, 21 | 文件地址不存在 | `filePreview.fileUrlNotExist` |
| `pages_tool/file-preview/file-preview.vue` | 27 | 视频地址不存在 | `filePreview.videoUrlNotExist` |
| `pages_tool/file-preview/file-preview.vue` | 32 | 不支持的文件类型 | `filePreview.unsupportedFileType` |
| `pages_tool/agreement/contenr.vue` | 11 | 暂无内容 | `agreement.noContent` |
| `pages_tool/agreement/contenr.vue` | 12 | 该协议内容暂未设置 | `agreement.noContentDesc` |
| `pages_tool/agreement/contenr.vue` | 13 | 重新加载 | `agreement.reload` |
| `pages_tool/notice/list.vue` | 10 | 置顶 | `notice.list.topTag` |
| `pages_tool/notice/list.vue` | 18 | 暂无公告 | `notice.list.noNotice` |
| `pages_tool/notice/detail.vue` | 5 | 发表时间: | `notice.detail.publishTimePrefix` |
| `pages_tool/help/list.vue` | 9 | 暂无帮助信息 | `help.list.noHelp` |
| `pages_tool/help/detail.vue` | 6 | 发表时间: | `help.detail.publishTimePrefix` |
| `pages_tool/goods/brand.vue` | 14 | 暂无更多品牌,去首页看看吧 | `goods.brand.noMoreBrand` |
| `pages_tool/recharge/order_list.vue` | 16 | 充值成功 | `recharge.orderList.rechargeSuccess` |
| `pages_tool/recharge/order_list.vue` | 34 | 暂无充值记录 | `recharge.orderList.noRecord` |
| `pages_tool/seal/structure.vue` | 6 | 当前选择: / 请选择 | `seal.structure.currentChoicePrefix` / `seal.structure.pleaseSelect` |
| `pages_tool/seal/structure.vue` | 12 | 选型结果: | `seal.structure.resultPrefix` |
| `pages_tool/seal/structure.vue` | 23 | 结果文件 | `seal.structure.resultFile` |
| `pages_promotion/fenxiao/bill.vue` | 11 | 账单编号: | `fenxiao.bill.billNoPrefix` |
| `pages_promotion/fenxiao/bill.vue` | 19 | 暂无账单信息 | `fenxiao.bill.noBillData` |
### 3.4 公共 JS
| 文件 | 行 | 中文文本 | 建议 key |
|---|---|---|---|
| `common/js/auth.js` | 36, 77 | 小程序配置错误 | `common.auth.miniProgramConfigError` |
| `common/js/map/openMap.js` | 11 | 打开地图失败,请稍后重试 | `common.map.openMapFailed` |
| `common/js/goods_detail_base.js` | 144, 202 | 件 | `unit.piece`(`common.js` 中位于顶层 `unit.piece`) |
| `pages_order/public/js/refundMethod.js` | 10 | 撤销之后本次申请将会关闭... | `order.refund.cancelRefundContent` |
| `pages_order/public/js/refundMethod.js` | 11 | 暂不撤销 | `order.refund.cancelRefundCancelText` |
| `pages_goods/public/js/list.js` | 40 | 购买 | `goods.list.buyText` |
### 3.5 文档示例(可忽略)
| 文件 | 行 | 中文文本 |
|---|---|---|
| `docs/example/wechat-channel-demo.vue` | 4-23 | 微信视频号集成示例、单个视频号、视频号列表、使用说明等 |
> 该文件为文档示例,是否国际化可单独评估。
---
## 四、P2 - 动态 key 调用(已兜底 + 验证 ✅)
以下 8 处 `$lang()` 使用运行时变量拼接 key,无法被 `scripts/i18n-check.js` 静态覆盖。已采取以下措施:
1. **运行时兜底**:对可获取默认文案的场景,为 `$lang()` 增加第二参数 fallback,避免 key 缺失时暴露 raw key。
2. **专项验证脚本**:新增 `scripts/verify-p2-keys.js`,枚举所有已知取值并模拟 resolver 校验 zh-cn / en-us 解析结果。
3. **DIY 数据侧**:`components-diy/diy-tab.vue` 的 title 来自运行时配置,已在组件内增加 `this.$lang(title, title)` 兜底。
| 文件 | 行 | 调用片段 | 处理 |
|---|---|---|---|
| `components/payment/payment.vue` | 27 | `$lang('components.payment.' + item.type)` | 增加 `item.name` fallback |
| `components-diy/diy-tab.vue` | 907 | `this.$lang ? this.$lang(title) : title` | 改为 `this.$lang(title, title)` |
| `pages_order/list.vue` | 15 | `$lang('status.' + statusItem.status)` | 增加 `statusItem.name` fallback |
| `pages_promotion/fenxiao/order.vue` | 6 | `$lang(item.nameKey)` | 取值本地枚举,已在 `verify-p2-keys.js` 验证 |
| `pages_promotion/fenxiao/team.vue` | 7 | `$lang(item.nameKey)` | 取值本地枚举,已在 `verify-p2-keys.js` 验证 |
| `pages_promotion/point/order_list.vue` | 8 | `$lang('point.orderList.' + statusItem.nameKey)` | 取值本地枚举,已在 `verify-p2-keys.js` 验证 |
| `pages_tool/goods/evaluate.vue` | 6 | `$lang(item.langKey)` | 取值本地枚举,已在 `verify-p2-keys.js` 验证 |
| `pages_tool/pay/cashier.vue` | 16 | `$lang('pay.cashier.' + item.type)` | 增加 `item.name` fallback |
验证结果:`node scripts/verify-p2-keys.js` 全部 ✅;`npm run i18n:check` 通过。
---
## 五、修复优先级建议
1. **P0(已修复)**:4 个文件共 30+ 处错误调用已在英文环境下直接显示 raw key,已全部修正。
2. **P1(已修复)**:59 处硬编码中文已按模块补充语言包并替换为 `$lang()`。
3. **P2(已兜底 + 验证)**:8 处动态 key 已增加运行时 fallback 并建立枚举值验证脚本。
---
## 六、命名空间规范提醒
根据项目约定和 `common/js/lang.js` 的解析逻辑:
- 页面语言包:优先使用扁平 key(如 `$lang('searchPlaceholder')`),必要时使用 camelCase 路径(如 `$lang('member.cancelstatus.step1Name')`)。
- 共享组件语言包:统一按 `components.{ComponentCamelCase}.key` 或 `components-diy.{ComponentCamelCase}.key` 调用。
- 公共 JS 文案:统一放入 `lang/zh-cn/common.js` / `lang/en-us/common.js`,使用 `common.*` 命名空间。
- 新增 key 后务必在 `lang/zh-cn/` 和 `lang/en-us/` 同步,并运行 `npm run i18n:check`。
## 七、P1 实施过程中的命名空间修正记录
在按审计建议替换 P1 硬编码中文时,发现若干组件/页面实际调用的 key 与语言包结构不匹配,已一并修正:
| 文件 | 问题 | 修正 |
|---|---|---|
| `components-diy/diy-listmenu.vue` | 调用 `components-diy.diy-listmenu.view`,但 `normalizeSharedLangPack` 生成的命名空间为 `diyListmenu`(小写 m) | 改为 `components-diy.diy-listmenu.view` |
| `lang/zh-cn/components/ns-goods-sku.js`、`lang/en-us/components/ns-goods-sku.js` | 语言包使用 `components.ns-goods-sku.pointSuffix`,与文件路径 `components/ns-goods-sku` 生成的 `components.nsGoodsSku` 不一致 | 改为 `components.ns-goods-sku.pointSuffix` |
| `lang/zh-cn/notice/list.js`、`lang/en-us/notice/list.js` | 包内存在嵌套 `notice: { ... }`,导致完整命名空间 `notice.list.topTag` 无法解析 | 拍平为扁平结构 |
| `lang/zh-cn/notice/detail.js`、`lang/en-us/notice/detail.js` | 同上,嵌套 `notice` 对象 | 拍平为扁平结构 |
| `lang/zh-cn/help/list.js`、`lang/en-us/help/list.js` | 同上,嵌套 `help` 对象 | 拍平为扁平结构 |
| `lang/zh-cn/help/detail.js`、`lang/en-us/help/detail.js` | 同上,嵌套 `help` 对象 | 拍平为扁平结构 |
| `lang/zh-cn/login/login.js`、`lang/en-us/login/login.js` | 新增 key 放在 `login: { ... }` 下,导致 `login.login.mobileRequired` 解析失败 | 拍平为扁平结构 |
| `lang/zh-cn/member/public.js`、`lang/en-us/member/public.js` | 包内为 `{ public: { ... } }`,但源码调用 `member.public.riskWarningTitle` | 包装为 `{ member: { public: { ... } } }` |
| `common/js/goods_detail_base.js`、`components-diy/diy-goods-recommend.vue` | 调用 `common.unit.piece`,但 `common.js` 中该 key 位于顶层 `unit.piece` | 改为 `unit.piece` |
| `common/js/lang.js` | 新增组件语言包未注入共享上下文 | 补充 `SHARED_LANG_PACKS` 列表 |
> 说明:`components/ns-goods-sku/*.vue` 中大量使用了 `goods.detail.*` 形式的 key。这些 key 实际定义在页面语言包 `lang/zh-cn/goods/detail.js` 中,运行时通过 `tryResolveQualifiedPageField` 动态加载并解析;P1 验证脚本已按此逻辑加载 `goods/detail` 包进行校验,确认全部可解析。