1. 新增多语言文件覆盖组件、页面、公共模块的翻译文案 2. 修复多处动态拼接lang key的调用,增加兜底逻辑避免空值或原始key暴露 3. 补全DIY组件、业务页面的国际化适配 4. 新增P2动态key国际化处理方案文档
15 KiB
国际化(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(),但仍存在 三类问题:
- Resolver/代码不匹配导致的 raw key(用户可见):组件/页面调用的 key 与语言包实际结构不一致,英文环境下会直接显示 key 名。
- 硬编码中文文本(英文环境下仍显示中文):
.vue模板和.js脚本中存在未替换为$lang()的静态中文。 - 动态 key 无法静态校验:9 处依赖运行时数据拼接 key,需人工确认对应 key 是否完整。
二、P0 - 会显示 raw key 的调用错误(已修复 ✅)
修复文件:
components-diy/diy-article.vuepages_promotion/point/goods_list.vuepages_tool/member/cancelstatus.vuepages_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.diyArticle.viewMore |
组件名大小写不一致,normalizeSharedLangPack 将 diy-article 转为 diyArticle |
| 98 | $lang('components-diy.diy-article.noArticles') |
components-diy.diyArticle.noArticles |
同上 |
修复建议:统一改为 $lang('components-diy.diyArticle.viewMore') / $lang('components-diy.diyArticle.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.chatMessage.customerOffline |
components/chat-message/chat-message.vue |
63 | 客服在线 | components.chatMessage.customerOnline |
components/pick-regions/pick-regions.vue |
17 | 取消 | components.pickRegions.cancel |
components/pick-regions/pick-regions.vue |
18 | 完成 | components.pickRegions.confirm |
components/ns-goods-sku/ns-goods-sku.vue |
22 | {point}积分 |
components.goodsSku.pointSuffix(使用命名参数) |
components/ns-chat/ns-chat-receiveGoods.vue |
8 | 库存: | components.chatReceiveGoods.stockPrefix |
components/ns-chat/ns-chat-receiveGoods.vue |
9 | 销量: | components.chatReceiveGoods.salesPrefix |
components/ns-chat/ns-chat-receiveGoods.vue |
17 | 查看商品 | components.chatReceiveGoods.viewGoods |
components/ns-chat/ns-chat-goods.vue |
12 | 已发送 | components.chatGoods.sent |
components/ns-chat/ns-chat-goods.vue |
25 | 发送 | components.chatGoods.send |
components/ns-video-player-popup/ns-video-player-popup.vue |
17 | 视频地址不存在 | components.videoPlayerPopup.videoUrlNotExist |
3.2 装修组件(components-diy/)
| 文件 | 行 | 中文文本 | 建议 key |
|---|---|---|---|
components-diy/diy-search.vue |
31 | 搜索 | components-diy.diySearch.searchBtn |
components-diy/diy-map.vue |
16 | 一键导航 | components-diy.diyMap.navigate |
components-diy/diy-kefu.vue |
17 | 立即添加 | components-diy.diyKefu.addNow |
components-diy/diy-channel-video.vue |
20, 32, 40 | {count}次观看 |
components-diy.diyChannelVideo.viewCountSuffix(使用命名参数) |
components-diy/diy-live.vue |
29 | 主播: | components-diy.diyLive.anchorPrefix |
components-diy/diy-live.vue |
33 | 直播商品: | components-diy.diyLive.goodsPrefix |
components-diy/diy-listmenu.vue |
15 | 查看 | components-diy.diyListMenu.view |
components-diy/diy-text.vue |
211 | 专区 | components-diy.diyText.zone |
components-diy/diy-goods-recommend.vue |
56 | 售 / 件 | components-diy.diyGoodsRecommend.soldPrefix / unit.piece |
components-diy/diy-notes.vue |
42 | 阅读 | components-diy.diyNotes.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 静态覆盖。已采取以下措施:
- 运行时兜底:对可获取默认文案的场景,为
$lang()增加第二参数 fallback,避免 key 缺失时暴露 raw key。 - 专项验证脚本:新增
scripts/verify-p2-keys.js,枚举所有已知取值并模拟 resolver 校验 zh-cn / en-us 解析结果。 - 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 通过。
五、修复优先级建议
- P0(已修复):4 个文件共 30+ 处错误调用已在英文环境下直接显示 raw key,已全部修正。
- P1(已修复):59 处硬编码中文已按模块补充语言包并替换为
$lang()。 - 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.diyListMenu.view,但 normalizeSharedLangPack 生成的命名空间为 diyListmenu(小写 m) |
改为 components-diy.diyListmenu.view |
lang/zh-cn/components/ns-goods-sku.js、lang/en-us/components/ns-goods-sku.js |
语言包使用 components.goodsSku.pointSuffix,与文件路径 components/ns-goods-sku 生成的 components.nsGoodsSku 不一致 |
改为 components.nsGoodsSku.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包进行校验,确认全部可解析。