# 国际化(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` 包进行校验,确认全部可解析。