绝对路径与本机目录绑定,换机器即失效,改为相对表述。 - docs/theme-multi-platform.md:删掉 cd 到绝对路径的命令行, 改为注释说明「在项目根目录(含 package.json)下执行」 - docs/i18n-audit-report.md:审计范围改为「项目根目录」 - docs/ai-agent-integration.md:适用前端改为「本仓库」; WB Cloud SDK 协议约束矩阵原指向外部 refs 仓的绝对路径, 改为相对文件名并标注「WB Cloud SDK 参考仓内」
223 lines
15 KiB
Markdown
223 lines
15 KiB
Markdown
# 国际化(i18n)全量审计报告
|
||
|
||
> 审计范围:项目根目录下所有 `.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` 包进行校验,确认全部可解析。
|