feat: 新增多语言文案并修复动态国际化调用问题

1. 新增多语言文件覆盖组件、页面、公共模块的翻译文案
2. 修复多处动态拼接lang key的调用,增加兜底逻辑避免空值或原始key暴露
3. 补全DIY组件、业务页面的国际化适配
4. 新增P2动态key国际化处理方案文档
This commit is contained in:
2026-06-29 15:29:24 +08:00
parent c3956f7bcd
commit c7810f16f7
115 changed files with 1343 additions and 153 deletions

View File

@@ -33,8 +33,8 @@ export default {
} else {
fallbackFunc && fallbackFunc(res);
this.$util.showToast({
title: res.message ? res.message : '小程序配置错误'
});
title: res.message ? res.message : this.$lang('common.auth.miniProgramConfigError')
});
}
}
})
@@ -74,8 +74,8 @@ export default {
} else {
fallbackFunc && fallbackFunc(res);
this.$util.showToast({
title: res.message ? res.message : '小程序配置错误'
});
title: res.message ? res.message : this.$lang('common.auth.miniProgramConfigError')
});
}
}
})

View File

@@ -141,7 +141,7 @@ export default {
});
this.goodsSkuDetail.swiperHeight = maxHeight + 'px';
this.goodsSkuDetail.unit = this.goodsSkuDetail.unit || "件";
this.goodsSkuDetail.unit = this.goodsSkuDetail.unit || this.$lang('unit.piece');
// 当前商品SKU规格
if (this.goodsSkuDetail.sku_spec_format) this.goodsSkuDetail.sku_spec_format = JSON.parse(this.goodsSkuDetail.sku_spec_format);
@@ -199,7 +199,7 @@ export default {
if (this.$refs.goodsDetailView) {
// 初始化商品详情视图数据
this.goodsSkuDetail.unit = this.goodsSkuDetail.unit || "件";
this.goodsSkuDetail.unit = this.goodsSkuDetail.unit || this.$lang('unit.piece');
// 解决轮播图数量不一致时,切换到第一个
if (this.swiperCurrent > this.goodsSkuDetail.sku_images.length) {

View File

@@ -42,6 +42,12 @@ const SHARED_LANG_PACKS = [
'components/register-reward',
'components/wxwork-contact',
'components/yuyue-date',
'components/chat-message',
'components/pick-regions',
'components/ns-goods-sku',
'components/ns-chat-receiveGoods',
'components/ns-chat-goods',
'components/ns-video-player-popup',
'components-diy/diy-article',
'components-diy/diy-bargain',
'components-diy/diy-category',
@@ -58,6 +64,16 @@ const SHARED_LANG_PACKS = [
'components-diy/diy-seckill',
'components-diy/diy-store',
'components-diy/diy-video',
'components-diy/diy-search',
'components-diy/diy-map',
'components-diy/diy-kefu',
'components-diy/diy-channel-video',
'components-diy/diy-live',
'components-diy/diy-listmenu',
'components-diy/diy-text',
'components-diy/diy-goods-recommend',
'components-diy/diy-notes',
'member/public',
];
function toCamelCaseName(name) {

View File

@@ -1,5 +1,6 @@
import util from '@/common/js/util.js'
import TransformCoordinate from './transformCoordinate.js'
import Lang from '@/common/js/lang.js'
function openMapByDefault(latitude, longitude, name) {
uni.openLocation({
@@ -8,7 +9,7 @@ function openMapByDefault(latitude, longitude, name) {
name: name,
fail: (e) => {
uni.showModal({
content: '打开地图失败,请稍后重试'
content: Lang.lang('common.map.openMapFailed')
})
},
})

View File

@@ -17,7 +17,7 @@
<view class="video-title" :style="[{ '--title-line-clamp': titleLineClamp }]">{{ value.videoTitle }}
</view>
<!-- 视频统计信息 -->
<view class="video-stats" v-if="showViewCount && value.showViewCount">{{ value.viewCount }}次观看</view>
<view class="video-stats" v-if="showViewCount && value.showViewCount">{{ $lang('components-diy.diyChannelVideo.viewCountSuffix', { count: value.viewCount }) }}</view>
</view>
</view>
@@ -29,7 +29,7 @@
<view class="play-icon-css"></view>
</view>
<view class="video-stats-overlay" v-if="showViewCount && value.showViewCount">
{{ value.viewCount }}次观看
{{ $lang('components-diy.diyChannelVideo.viewCountSuffix', { count: value.viewCount }) }}
</view>
</view>
<view class="video-info">
@@ -37,7 +37,7 @@
<view class="video-title" :style="[{ '--title-line-clamp': titleLineClamp }]">{{ value.videoTitle }}
</view>
<!-- 视频统计信息,非列表模式下显示 -->
<view class="video-stats" v-if="showViewCount && value.showViewCount && !listMode">{{ value.viewCount }}次观看</view>
<view class="video-stats" v-if="showViewCount && value.showViewCount && !listMode">{{ $lang('components-diy.diyChannelVideo.viewCountSuffix', { count: value.viewCount }) }}</view>
</view>
</view>
</view>

View File

@@ -53,7 +53,7 @@
¥{{ showMarketPrice(dataItem) }}</view>
<view class="sale" v-if="goodsValue.saleStyle.control"
:style="{ color: goodsValue.theme == 'diy' ? goodsValue.saleStyle.color : '' }">
售{{ dataItem.sale_num }}{{ dataItem.unit ? dataItem.unit : '件' }}
{{ $lang('components-diy.diyGoodsRecommend.soldPrefix') }}{{ dataItem.sale_num }}{{ dataItem.unit ? dataItem.unit : $lang('unit.piece') }}
</view>
</view>
</view>

View File

@@ -14,7 +14,7 @@
</view>
<view class="fui-remark jump" style="padding-right: 20rpx; text-align: center; line-height: 140rpx;">
<span style="font-size:24rpx;padding: 14rpx 18rpx;border-radius:8rpx"
:style="{ background: item.BtBgColor, color: item.BtColor }" @tap.stop="previewSqs()">立即添加</span>
:style="{ background: item.BtBgColor, color: item.BtColor }" @tap.stop="previewSqs()">{{ $lang('components-diy.diyKefu.addNow') }}</span>
</view>
</view>
</view>

View File

@@ -12,7 +12,7 @@
style="border-radius:6rpx;width: 60rpx;"></image>
</view>
<view class="fui-cell-text" style="color:#333;">{{ item.title }}</view>
<view class="fui-cell-remark" style="font-size: 24rpx;">{{ lang == 'en-us' ? 'view' : '查看' }}</view>
<view class="fui-cell-remark" style="font-size: 24rpx;">{{ $lang('components-diy.diyListmenu.view') }}</view>
</view>
</view>
<ns-login ref="login"></ns-login>
@@ -33,7 +33,6 @@ export default {
data() {
return {
loading: true,
lang: uni.getStorageSync("lang")//en-us 英文
};
},
created() {

View File

@@ -26,11 +26,11 @@
<image
:src="liveInfo.anchor_img != '' ? $util.img(liveInfo.anchor_img) : $util.getDefaultImage().head"
class="anchor-img" @error="liveInfo.anchor_img = $util.getDefaultImage().head" show-menu-by-longpress/>
<text class="anchor-name">主播:{{ liveInfo.anchor_name }}</text>
<text class="anchor-name">{{ $lang('components-diy.diyLive.anchorPrefix') }}{{ liveInfo.anchor_name }}</text>
</block>
<text class="separate" v-if="value.isShowAnchorInfo && value.isShowLiveGood">|</text>
<block v-if="value.isShowLiveGood">
<text class="goods-text">直播商品:{{ liveInfo.goods.length }}</text>
<text class="goods-text">{{ $lang('components-diy.diyLive.goodsPrefix') }}{{ liveInfo.goods.length }}</text>
</block>
</view>
</view>

View File

@@ -13,7 +13,7 @@
<div
style="position:absolute;right:12rpx;bottom:48rpx;z-index:1;background:#4390FF;padding:0rpx 20rpx;border-radius:8rpx;color: #fff;"
@tap.stop="handlerClick(item)">
<span style="font-size:24rpx;color: #fff;">一键导航</span>
<span style="font-size:24rpx;color: #fff;">{{ $lang('components-diy.diyMap.navigate') }}</span>
</div>
</map>
</view>
@@ -71,7 +71,7 @@ export default {
uni.openLocation({
latitude: parseFloat(item.lat),
longitude: parseFloat(item.lng),
name: "一键导航",
name: this.$lang('components-diy.diyMap.navigate'),
})
},
async handlerClick(item) {

View File

@@ -39,14 +39,14 @@
item.update_time_day }}</text>
</view>
<view class="notes-num">
<text v-show="value.readNum == 1 && item.is_show_read_num == 1">阅读 {{
item.initial_read_num + item.read_num }}</text>
</view>
<text v-show="value.readNum == 1 && item.is_show_read_num == 1">{{ $lang('components-diy.diyNotes.readPrefix') }} {{
item.initial_read_num + item.read_num }}</text>
</view>
</view>
</view>
</view>
</scroll-view>
</view>
</scroll-view>
<ns-login ref="login"></ns-login>
</view>

View File

@@ -28,7 +28,8 @@
@tap.stop="handlerSearchClick"
:placeholderStyle="placeholderStyle" />
<text class="search-content-btn" @tap.stop="handlerSearchClick"
:style="{ 'backgroundColor': value.pageBgColor ? value.pageBgColor : 'rgba(0,0,0,0)' }">搜索</text>
:style="{ 'backgroundColor': value.pageBgColor ? value.pageBgColor : 'rgba(0,0,0,0)' }"
>{{ $lang('components-diy.diySearch.searchBtn') }}</text>
</view>
<view class="img" v-if="value.iconType == 'img'"
@tap.stop="handlerRedirectToClick(value.searchLink)">

View File

@@ -903,8 +903,8 @@ export default {
// 如果 title 是字符串,保持原有逻辑
if (typeof title === 'string' && title.includes('.')) {
// 包含点号的标题视为国际化键,使用全局挂载的 $lang 方法翻译
return this.$lang ? this.$lang(title) : title;
// 包含点号的标题视为国际化键,使用全局挂载的 $lang 方法翻译;缺失时回退 title 本身
return this.$lang ? this.$lang(title, title) : title;
}
// 不包含点号的标题直接返回

View File

@@ -208,7 +208,7 @@
{{ value.text }}
</text>
<text class="zone"
:style="{ fontSize: value.fontSize * 2 + 'rpx', fontWeight: value.fontWeight }">专区</text>
:style="{ fontSize: value.fontSize * 2 + 'rpx', fontWeight: value.fontWeight }">{{ $lang('components-diy.diyText.zone') }}</text>
</view>
<text class="iconfont icon-danxuan-xuanzhong"
:style="{ color: value.textColor, fontWeight: value.fontWeight }"></text>

View File

@@ -57,10 +57,10 @@
<view v-else-if="message.contentType == 'order'"><ns-chat-order :isCanSend="false" :orderId="message.order_id"></ns-chat-order></view>
<view class="no-connect-box" v-if="message.contentType == 'noline'">
<view class="no-connect">客服不在线</view>
<view class="no-connect">{{ $lang('components.chatMessage.customerOffline') }}</view>
</view>
<view class="no-connect-box" v-if="message.contentType == 'online'">
<view class="no-connect">客服在线</view>
<view class="no-connect">{{ $lang('components.chatMessage.customerOnline') }}</view>
</view>
<uni-popup ref="imgPopup" type="center">
<view class="imagePop">

View File

@@ -9,7 +9,7 @@
<text class="goods-price-sign color-base-text">¥</text>
<text class="color-base-text">{{ goodsInfo.price }}</text>
</view>
<view class="goods-option font-size-goods-tag disabled">已发送</view>
<view class="goods-option font-size-goods-tag disabled">{{ $lang('components.chatGoods.sent') }}</view>
</view>
</view>
</view>
@@ -22,7 +22,7 @@
<text class="goods-price-sign color-base-text">¥</text>
<text class="color-base-text">{{ goodsDetail.price }}</text>
</view>
<view class="goods-option font-size-goods-tag color-base-bg" @click="sendMsg('goods')">发送</view>
<view class="goods-option font-size-goods-tag color-base-bg" @click="sendMsg('goods')">{{ $lang('components.chatGoods.send') }}</view>
</view>
</view>
</view>

View File

@@ -5,8 +5,8 @@
<view class="goods-item">
<view class="title">{{ isEnEnv ? goodsInfo.en_goods_name : goodsInfo.goods_name }}</view>
<view class="goods-sku">
库存:{{ goodsInfo.stock }}
<text>销量:{{ goodsInfo.sale_num }}</text>
{{ $lang('components.chatReceiveGoods.stockPrefix') }}{{ goodsInfo.stock }}
<text>{{ $lang('components.chatReceiveGoods.salesPrefix') }}{{ goodsInfo.sale_num }}</text>
</view>
<view class="goods-price">
<view class="price color-base-text">
@@ -14,7 +14,7 @@
<text class="price-num">{{ goodsInfo.price }}</text>
</view>
<view class="see-shop color-base-text" @click="go_shop()">
查看商品
{{ $lang('components.chatReceiveGoods.viewGoods') }}
<text class="iconfont icon-right"></text>
</view>
</view>

View File

@@ -19,7 +19,7 @@
<block v-if="type == 'point'">
<block v-if="goodsDetail.type == 1">
<view class="price-wrap">
<text class="price price-color font-size-toolbar">{{ goodsDetail.point }}积分</text>
<text class="price price-color font-size-toolbar">{{ $lang('components.nsGoodsSku.pointSuffix', { point: goodsDetail.point }) }}</text>
<block v-if="goodsDetail.exchange_price != '0.00' && goodsDetail.pay_type > 0">
<text class="unit price-color font-size-tag">+</text>
<text class="price price-color font-size-toolbar price-font">{{ $lang('common.currencySymbol') }}{{ goodsDetail.exchange_price }}</text>

View File

@@ -14,7 +14,7 @@
autoplay
class="video-player"
></video>
<view v-else class="video-error">视频地址不存在</view>
<view v-else class="video-error">{{ $lang('components.videoPlayerPopup.videoUrlNotExist') }}</view>
</view>
</view>
</uni-popup>

View File

@@ -24,7 +24,7 @@
<block v-if="payTypeList.length">
<view class="payment-item" v-for="(item, index) in payTypeList" :key="index" @click="payIndex = index">
<view class="iconfont" :class="item.icon"></view>
<text class="name">{{ $lang('components.payment.' + item.type) }}</text>
<text class="name">{{ $lang('components.payment.' + item.type, item.name) }}</text>
<text class="iconfont" :class="payIndex == index ? 'icon-yuan_checked color-base-text' : 'icon-checkboxblank'"></text>
</view>
</block>

View File

@@ -14,8 +14,8 @@
<uni-popup type="bottom" ref="regionsRef">
<view class="regions-picker-container">
<view class="picker-header">
<view class="picker-action cancel" @click="$refs.regionsRef.close()">取消</view>
<view class="picker-action confirm" @click="confirmRegions">完成</view>
<view class="picker-action cancel" @click="$refs.regionsRef.close()">{{ $lang('components.pickRegions.cancel') }}</view>
<view class="picker-action confirm" @click="confirmRegions">{{ $lang('components.pickRegions.confirm') }}</view>
</view>
<picker-view :value="multiIndex" @change="pickerViewColumnChange" class="picker-view">
<picker-view-column v-if="multiArray[0]">

222
i18n-audit-report.md Normal file
View File

@@ -0,0 +1,222 @@
# 国际化(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.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` 静态覆盖。已采取以下措施:
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.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` 包进行校验,确认全部可解析。

View File

@@ -1,4 +1,13 @@
export const lang = {
//title为每个页面的标题
title: 'Agreement',
// agreement/contenr.vue page new translation items
agreement: {
// No content
noContent: 'No content',
// This agreement content has not been set
noContentDesc: 'This agreement content has not been set',
// Reload
reload: 'Reload',
},
}

View File

@@ -262,4 +262,17 @@ export const lang = {
locationParamFormatError: 'Invalid location parameter format',
keyCannotBeEmpty: 'Key cannot be empty',
},
// Common JS text
auth: {
miniProgramConfigError: 'Mini program configuration error',
},
map: {
openMapFailed: 'Failed to open map, please try again later',
},
unit: {
piece: 'pc',
},
}

View File

@@ -0,0 +1,5 @@
export const lang = {
// diy-channel-video channel video component
// View count suffix: {count} is the view count placeholder
viewCountSuffix: '{count} views',
}

View File

@@ -0,0 +1,5 @@
export const lang = {
// diy-goods-recommend product recommendation component
// Sold prefix
soldPrefix: 'Sold ',
}

View File

@@ -0,0 +1,5 @@
export const lang = {
// diy-kefu customer service component
// Add now
addNow: 'Add Now',
}

View File

@@ -0,0 +1,5 @@
export const lang = {
// diy-listmenu list menu component
// View
view: 'View',
}

View File

@@ -0,0 +1,7 @@
export const lang = {
// diy-live live streaming component
// Anchor prefix
anchorPrefix: 'Anchor: ',
// Live goods prefix
goodsPrefix: 'Live Products: ',
}

View File

@@ -0,0 +1,5 @@
export const lang = {
// diy-map map component
// One-click navigation
navigate: 'Navigate',
}

View File

@@ -0,0 +1,5 @@
export const lang = {
// diy-notes notes component
// Read prefix
readPrefix: 'Reads',
}

View File

@@ -0,0 +1,5 @@
export const lang = {
// diy-search search component
// Search button
searchBtn: 'Search',
}

View File

@@ -0,0 +1,5 @@
export const lang = {
// diy-text text component
// Zone label
zone: 'Zone',
}

View File

@@ -0,0 +1,11 @@
export const lang = {
// chat-message component
components: {
chatMessage: {
// Customer service offline hint
customerOffline: 'Customer service is offline',
// Customer service online hint
customerOnline: 'Customer service is online',
},
},
}

View File

@@ -0,0 +1,11 @@
export const lang = {
// ns-chat-goods chat goods component
components: {
chatGoods: {
// Sent status
sent: 'Sent',
// Send button
send: 'Send',
},
},
}

View File

@@ -0,0 +1,13 @@
export const lang = {
// ns-chat-receiveGoods chat received goods component
components: {
chatReceiveGoods: {
// Stock prefix
stockPrefix: 'Stock:',
// Sales prefix
salesPrefix: 'Sales:',
// View goods button
viewGoods: 'View Product',
},
},
}

View File

@@ -0,0 +1,9 @@
export const lang = {
// ns-goods-sku product SKU component
components: {
nsGoodsSku: {
// Point suffix: {point} is the points amount placeholder
pointSuffix: '{point} points',
},
},
}

View File

@@ -0,0 +1,9 @@
export const lang = {
// ns-video-player-popup video popup component
components: {
videoPlayerPopup: {
// Video URL does not exist hint
videoUrlNotExist: 'Video URL does not exist',
},
},
}

View File

@@ -0,0 +1,11 @@
export const lang = {
// pick-regions region picker component
components: {
pickRegions: {
// Cancel button
cancel: 'Cancel',
// Confirm button
confirm: 'Done',
},
},
}

View File

@@ -1,4 +1,16 @@
export const lang = {
//title为每个页面的标题
title: 'Bill'
title: 'Bill',
// fenxiao/bill.vue page new translation items
fenxiao: {
// Bill
bill: {
// Bill number prefix
billNoPrefix: 'Bill No.:',
// No bill information
noBill: 'No bill information',
// Distribution is not enabled by merchant
addonClosed: 'Distribution is not enabled by merchant',
},
},
}

View File

@@ -1,4 +1,13 @@
export const lang = {
//title为每个页面的标题
title: 'File Preview',
// file-preview/file-preview.vue page new translation items
filePreview: {
// File URL does not exist
fileUrlNotExist: 'File URL does not exist',
// Video URL does not exist
videoUrlNotExist: 'Video URL does not exist',
// This file type preview is not supported
unsupportedFileType: 'This file type preview is not supported',
},
}

View File

@@ -1,4 +1,9 @@
export const lang = {
//title为每个页面的标题
title: 'Brand Zone'
title: 'Brand Zone',
// goods/brand.vue page new translation items
brand: {
// No more brands
noMoreBrand: 'No more brands, go to the homepage',
},
}

View File

@@ -20,4 +20,6 @@ export const lang = {
minPriceGreaterThanMax: 'Minimum price cannot be greater than maximum price',
maxPriceLessThanMin: 'Maximum price cannot be less than minimum price',
// pages_goods/public/js/list.js new translation items
buyText: 'Buy',
}

View File

@@ -1,4 +1,9 @@
export const lang = {
//title为每个页面的标题
title: 'Product Not Found',
// goods/not_exist.vue page new translation items
notExist: {
// The product you are viewing does not exist, it may have been removed or deleted
message: 'The product you are viewing does not exist, it may have been removed or deleted',
},
}

View File

@@ -1,4 +1,6 @@
export const lang = {
//title为每个页面的标题
title: 'Help Detail'
// Page title
title: 'Help Detail',
// Publish time prefix
publishTimePrefix: 'Published at: ',
}

View File

@@ -1,5 +1,7 @@
export const lang = {
//title为每个页面的标题
// Page title
title: 'Help Center',
emptyText: 'No help information available'
emptyText:"No help information",
// No help
noHelp: 'No help',
}

View File

@@ -1,3 +1,8 @@
export const lang = {
title: ''
title: '',
// index/diy.vue page new translation items
diy: {
// I got it
iKnow: 'Got it',
},
};

View File

@@ -34,5 +34,10 @@ export const lang = {
enterMobileTip: 'Please enter mobile phone number',
enterCorrectMobileTip: 'Please enter correct mobile phone number',
enterCaptchaTip: 'Please enter verification code',
requestFailTip: 'request:fail'
requestFailTip: 'request:fail',
// Login validation and countdown text
mobileRequired: 'Please enter mobile phone number',
mobileFormatError: 'Please enter correct mobile phone number',
resendAfterSeconds: '{seconds}s before resend',
}

View File

@@ -1,3 +1,14 @@
export const lang = {
title: 'Account Cancellation',
// member/cancelrefuse.vue page new translation items
cancelrefuse: {
// Your application has been rejected
title: 'Your application has been rejected',
// Reason for rejection:
reasonPrefix: 'Reason for rejection: ',
// Back
back: 'Back',
// Reapply
reapply: 'Reapply',
},
}

View File

@@ -1,3 +1,12 @@
export const lang = {
title: 'Account Cancellation',
// member/cancelsuccess.vue page new translation items
cancelsuccess: {
// You have successfully cancelled your account
title: 'You have successfully cancelled your account',
// Looking forward to seeing you again. To use again, please re-register
desc: 'Looking forward to seeing you again. To use again, please re-register',
// Done
done: 'Done',
},
}

View File

@@ -58,4 +58,10 @@ export const lang = {
// 协议
privacyPolicy: 'Privacy Policy',
registrationAgreement: 'Registration Agreement',
// member/index.vue page new translation items
index: {
// I got it
iKnow: 'Got it',
},
}

View File

@@ -0,0 +1,11 @@
export const lang = {
// member/public member common mixin
member: {
public: {
// Risk warning
riskWarningTitle: 'Risk Warning',
// Confirm account cancellation
cancelAccountConfirm: 'Are you sure you want to cancel the current account?',
},
},
}

View File

@@ -1,4 +1,6 @@
export const lang = {
//title为每个页面的标题
title: 'Notice Detail'
// Page title
title: 'Announcement Detail',
// Publish time prefix
publishTimePrefix: 'Published at: ',
}

View File

@@ -1,6 +1,10 @@
export const lang = {
//title为每个页面的标题
title: 'Notice List',
emptyText:"No more information available",
contentTitle:'Upgrade Notice'
// Page title
title: 'Announcement List',
emptyText:"No more information",
contentTitle:'Upgrade Announcement',
// Top tag
topTag: 'Top',
// No announcements
noNotice: 'No announcements',
}

View File

@@ -33,5 +33,14 @@ export const lang = {
// ====== Form Validation ======
validate: {
pleaseSelectRefundReason: 'Please select a refund reason'
}
},
// ====== Refund Method Popup ======
// pages_order/public/js/refundMethod.js new translation items
refund: {
// After cancellation, this application will be closed; you can reapply later if needed
cancelRefundContent: 'After cancellation, this application will be closed; you can reapply later if needed.',
// Do not cancel for now
cancelRefundCancelText: 'Do not cancel for now',
},
}

View File

@@ -1,4 +1,11 @@
export const lang = {
//title为每个页面的标题
title: 'Recharge Records',
// recharge/order_list.vue page new translation items
recharge: {
// Recharge successful
rechargeSuccess: 'Recharge Successful',
// No recharge records
noRecord: 'No recharge records',
},
}

View File

@@ -1,4 +1,27 @@
export const lang = {
//title为每个页面的标题
title: 'Structure Selection',
// seal/structure.vue page new translation items
structure: {
// Current choice prefix
currentChoicePrefix: 'Current choice: ',
// Please select
pleaseSelect: 'Please select',
// Result prefix
resultPrefix: 'Selection result: ',
// Result file
resultFile: 'Result file',
// Please select material
selectMaterial: 'Please select material',
// Please select structure
selectStructure: 'Please select structure',
// Upload image
uploadImage: 'Upload Image',
// Please upload image
pleaseUploadImage: 'Please upload image',
// Submit requirement
submitRequirement: 'Submit Requirement',
// Submit successful
submitSuccess: 'Submitted Successfully',
},
}

View File

@@ -1,4 +1,13 @@
export const lang = {
//title为每个页面的标题
title: '协议',
// agreement/contenr.vue 页面新增翻译项
agreement: {
// 暂无内容
noContent: '暂无内容',
// 该协议内容暂未设置
noContentDesc: '该协议内容暂未设置',
// 重新加载
reload: '重新加载',
},
}

View File

@@ -260,4 +260,17 @@ export const lang = {
keyCannotBeEmpty: 'key值不能为空',
},
// 公共 JS 文案
auth: {
miniProgramConfigError: '小程序配置错误',
},
map: {
openMapFailed: '打开地图失败,请稍后重试',
},
unit: {
piece: '件',
},
}

View File

@@ -0,0 +1,5 @@
export const lang = {
// diy-channel-video 视频号组件
// 观看次数后缀: {count} 为次数占位符
viewCountSuffix: '{count}次观看',
}

View File

@@ -0,0 +1,5 @@
export const lang = {
// diy-goods-recommend 商品推荐组件
// 销量前缀
soldPrefix: '售',
}

View File

@@ -0,0 +1,5 @@
export const lang = {
// diy-kefu 客服组件
// 立即添加
addNow: '立即添加',
}

View File

@@ -0,0 +1,5 @@
export const lang = {
// diy-listmenu 列表菜单组件
// 查看
view: '查看',
}

View File

@@ -0,0 +1,7 @@
export const lang = {
// diy-live 直播组件
// 主播前缀
anchorPrefix: '主播:',
// 直播商品前缀
goodsPrefix: '直播商品:',
}

View File

@@ -0,0 +1,5 @@
export const lang = {
// diy-map 地图组件
// 一键导航
navigate: '一键导航',
}

View File

@@ -0,0 +1,5 @@
export const lang = {
// diy-notes 笔记组件
// 阅读前缀
readPrefix: '阅读',
}

View File

@@ -0,0 +1,5 @@
export const lang = {
// diy-search 搜索组件
// 搜索按钮
searchBtn: '搜索',
}

View File

@@ -0,0 +1,5 @@
export const lang = {
// diy-text 文本组件
// 专区标签
zone: '专区',
}

View File

@@ -0,0 +1,11 @@
export const lang = {
// chat-message 组件
components: {
chatMessage: {
// 客服不在线提示
customerOffline: '客服不在线',
// 客服在线提示
customerOnline: '客服在线',
},
},
}

View File

@@ -0,0 +1,11 @@
export const lang = {
// ns-chat-goods 聊天商品组件
components: {
chatGoods: {
// 已发送状态
sent: '已发送',
// 发送按钮
send: '发送',
},
},
}

View File

@@ -0,0 +1,13 @@
export const lang = {
// ns-chat-receiveGoods 聊天收到商品组件
components: {
chatReceiveGoods: {
// 库存前缀
stockPrefix: '库存:',
// 销量前缀
salesPrefix: '销量:',
// 查看商品按钮
viewGoods: '查看商品',
},
},
}

View File

@@ -0,0 +1,9 @@
export const lang = {
// ns-goods-sku 商品 SKU 组件
components: {
nsGoodsSku: {
// 积分后缀: {point} 为积分数量占位符
pointSuffix: '{point}积分',
},
},
}

View File

@@ -0,0 +1,9 @@
export const lang = {
// ns-video-player-popup 视频弹窗组件
components: {
videoPlayerPopup: {
// 视频地址不存在提示
videoUrlNotExist: '视频地址不存在',
},
},
}

View File

@@ -0,0 +1,11 @@
export const lang = {
// pick-regions 地区选择组件
components: {
pickRegions: {
// 取消按钮
cancel: '取消',
// 完成按钮
confirm: '完成',
},
},
}

View File

@@ -1,4 +1,16 @@
export const lang = {
//title为每个页面的标题
title: '账单'
title: '账单',
// fenxiao/bill.vue 页面新增翻译项
fenxiao: {
// 账单
bill: {
// 账单编号前缀
billNoPrefix: '账单编号:',
// 暂无账单信息
noBill: '暂无账单信息',
// 商家未开启分销
addonClosed: '商家未开启分销',
},
},
}

View File

@@ -1,4 +1,13 @@
export const lang = {
//title为每个页面的标题
title: '文件预览',
// file-preview/file-preview.vue 页面新增翻译项
filePreview: {
// 文件地址不存在
fileUrlNotExist: '文件地址不存在',
// 视频地址不存在
videoUrlNotExist: '视频地址不存在',
// 暂不支持该类型文件预览
unsupportedFileType: '暂不支持该类型文件预览',
},
}

View File

@@ -1,4 +1,9 @@
export const lang = {
//title为每个页面的标题
title: '品牌专区'
title: '品牌专区',
// goods/brand.vue 页面新增翻译项
brand: {
// 没有更多品牌了
noMoreBrand: '暂无更多品牌,去首页看看吧',
},
}

View File

@@ -20,4 +20,6 @@ export const lang = {
minPriceGreaterThanMax: '最低价不能大于最高价',
maxPriceLessThanMin: '最高价不能小于最低价',
// pages_goods/public/js/list.js 新增翻译项
buyText: '购买',
}

View File

@@ -1,4 +1,9 @@
export const lang = {
//title为每个页面的标题
title: '商品找不到',
// goods/not_exist.vue 页面新增翻译项
notExist: {
// 您查看的商品不存在,可能已下架或被删除
message: '您查看的商品不存在,可能已下架或被删除',
},
}

View File

@@ -1,4 +1,6 @@
export const lang = {
//title为每个页面的标题
title: '帮助详情'
title: '帮助详情',
// 发布时间前缀
publishTimePrefix: '发布时间:',
}

View File

@@ -1,5 +1,7 @@
export const lang = {
//title为每个页面的标题
title: '帮助中心',
emptyText:"当前暂无帮助信息"
emptyText:"当前暂无帮助信息",
// 暂无帮助
noHelp: '暂无帮助',
}

View File

@@ -1,3 +1,8 @@
export const lang = {
title: ''
title: '',
// index/diy.vue 页面新增翻译项
diy: {
// 我知道了
iKnow: '我知道了',
},
};

View File

@@ -17,7 +17,7 @@ export const lang = {
registerTips1: '注册',
registerTips2: '吧',
newUserRegister: '新用户注册',
// login.vue页面新增翻译项
// login.vue 页面新增翻译项
welcomeText: '您好,<br />欢迎使用',
getDynacode: '获取动态码',
mobileInputPlaceholder: '请输入您的手机号',
@@ -34,5 +34,10 @@ export const lang = {
enterMobileTip: '请输入手机号',
enterCorrectMobileTip: '请输入正确的手机号',
enterCaptchaTip: '请输入验证码',
requestFailTip: 'request:fail'
requestFailTip: 'request:fail',
// 登录校验与倒计时文案
mobileRequired: '请输入手机号',
mobileFormatError: '请输入正确的手机号',
resendAfterSeconds: '{seconds}s后可重新获取',
}

View File

@@ -1,3 +1,14 @@
export const lang = {
title: '账号注销',
// member/cancelrefuse.vue 页面新增翻译项
cancelrefuse: {
// 您的申请已拒绝
title: '您的申请已拒绝',
// 拒绝理由:
reasonPrefix: '拒绝理由:',
// 返回
back: '返回',
// 重新申请
reapply: '重新申请',
},
}

View File

@@ -1,3 +1,12 @@
export const lang = {
title: '账号注销',
// member/cancelsuccess.vue 页面新增翻译项
cancelsuccess: {
// 您已成功注销账号
title: '您已成功注销账号',
// 待下次与您更好的相遇,如需再次使用,请重新注册
desc: '待下次与您更好的相遇,如需再次使用,请重新注册',
// 完成
done: '完成',
},
}

View File

@@ -61,4 +61,10 @@ export const lang = {
// 协议
privacyPolicy: '隐私协议',
registrationAgreement: '注册协议',
// member/index.vue 页面新增翻译项
index: {
// 我知道了
iKnow: '我知道了',
},
}

View File

@@ -0,0 +1,11 @@
export const lang = {
// member/public 会员公共 mixin
member: {
public: {
// 风险提示
riskWarningTitle: '风险提示',
// 注销账号确认
cancelAccountConfirm: '确定要注销当前账号吗?',
},
},
}

View File

@@ -1,4 +1,6 @@
export const lang = {
//title为每个页面的标题
title: '公告详情'
title: '公告详情',
// 发布时间前缀
publishTimePrefix: '发布时间:',
}

View File

@@ -2,5 +2,9 @@ export const lang = {
//title为每个页面的标题
title: '公告列表',
emptyText:"当前暂无更多信息",
contentTitle:'升级公告'
contentTitle:'升级公告',
// 置顶标签
topTag: '置顶',
// 暂无公告
noNotice: '暂无公告',
}

View File

@@ -33,5 +33,14 @@ export const lang = {
// ====== 表单验证 ======
validate: {
pleaseSelectRefundReason: '请选择退款原因'
}
},
// ====== 退款方式弹窗 ======
// pages_order/public/js/refundMethod.js 新增翻译项
refund: {
// 撤销之后本次申请将会关闭,如后续仍有问题可再次发起申请。
cancelRefundContent: '撤销之后本次申请将会关闭,如后续仍有问题可再次发起申请。',
// 暂不撤销
cancelRefundCancelText: '暂不撤销',
},
}

View File

@@ -1,4 +1,11 @@
export const lang = {
//title为每个页面的标题
title: '充值记录',
// recharge/order_list.vue 页面新增翻译项
recharge: {
// 充值成功
rechargeSuccess: '充值成功',
// 暂无充值记录
noRecord: '暂无充值记录',
},
}

View File

@@ -1,4 +1,27 @@
export const lang = {
//title为每个页面的标题
title: '结构选型',
// seal/structure.vue 页面新增翻译项
structure: {
// 当前选择前缀
currentChoicePrefix: '当前选择:',
// 请选择
pleaseSelect: '请选择',
// 选型结果前缀
resultPrefix: '选型结果:',
// 结果文件
resultFile: '结果文件',
// 请选择材质
selectMaterial: '请选择材质',
// 请选择结构
selectStructure: '请选择结构',
// 上传图片
uploadImage: '上传图片',
// 请上传图片
pleaseUploadImage: '请上传图片',
// 提交需求
submitRequirement: '提交需求',
// 提交成功
submitSuccess: '提交成功',
},
}

119
p2-solution-plan.md Normal file
View File

@@ -0,0 +1,119 @@
# P2 动态 key 国际化处理方案
> 范围:审计报告 P2 章节列出的 8 处运行时拼接 `$lang()` key
> 日期:2026-06-29
> 结论:**所有已知取值目前在 zh-cn / en-us 中均已存在对应 key,核心问题是无法被静态扫描覆盖;已通过“枚举值兜底 + 运行时 fallback + 专项验证脚本”解决。**
>
> 实施状态:✅ 已完成(2026-06-29)
---
## 一、8 处动态 key 现状盘点
| # | 文件 | 调用方式 | 运行时取值 | 对应语言包 | key 是否齐全 |
|---|---|---|---|---|---|
| 1 | `components/payment/payment.vue:27` | `$lang('components.payment.' + item.type)` | `alipay`、`wechatpay` | `lang/xx/components/payment.js` | ✅ |
| 2 | `components-diy/diy-tab.vue:907` | `$lang(title)`(仅当 title 含 `.`) | 由 DIY 数据或业务配置决定,常见如 `goods.list.xxx`、`member.index.xxx` | 取决于具体 title | ⚠️ 需数据侧保证 |
| 3 | `pages_order/list.vue:15` | `$lang('status.' + statusItem.status)` | `all`、`waitpay`、`waitsend`、`waitconfirm`、`waitrate` | `lang/xx/order/list.js` 中 `status` 对象 | ✅ |
| 4 | `pages_promotion/fenxiao/order.vue:6` | `$lang(item.nameKey)` | `all`、`pendingSettlement`、`settled`、`refunded` | `lang/xx/fenxiao/order.js` | ✅ |
| 5 | `pages_promotion/fenxiao/team.vue:7` | `$lang(item.nameKey)` | `levelOne`、`levelTwo` | `lang/xx/fenxiao/team.js` | ✅ |
| 6 | `pages_promotion/point/order_list.vue:8` | `$lang('point.orderList.' + statusItem.nameKey)` | `statusAll`、`statusPendingPayment`、`statusCompleted` | `lang/xx/point/order_list.js` | ✅ |
| 7 | `pages_tool/goods/evaluate.vue:6` | `$lang(item.langKey)` | `tabAll`、`tabPositive`、`tabNeutral`、`tabNegative` | `lang/xx/goods/evaluate.js` | ✅ |
| 8 | `pages_tool/pay/cashier.vue:16` | `$lang('pay.cashier.' + item.type)` | `alipay`、`wechatpay` | `lang/xx/pay/cashier.js` | ✅ |
---
## 二、核心风险分析
1. **静态扫描盲区**:`scripts/i18n-check.js` 只检查 zh-cn / en-us 两包 key 一致性,不会验证 `$lang('a.' + b)` 形式的 key 在运行时是否真实存在。
2. **后端新增取值**:#1、#8 的 `item.type` 来自 `/api/pay/type`,理论上后端可能新增支付方式(如 `unionpay`、`applepay`),前端当前只处理了 `alipay` / `wechatpay`。
3. **DIY 配置不可控**:#2 `diy-tab.vue` 的 title 来自装修组件数据,如果运营人员填写了含 `.` 的字符串且未配置对应 key,会显示 raw key 或空串。
4. **本地枚举安全**:#3 ~ #7 的取值由前端本地数组硬编码,只要语言包同步就不会出现缺失;风险最低。
---
## 三、推荐处理方案
### 3.1 立即执行:为动态调用增加 fallback 兜底
对于能拿到原始中文/默认文案的场景,给 `$lang()` 增加第二参数作为兜底,避免生产环境出现 raw key 或空串。
| # | 文件 | 建议修改 |
|---|---|---|
| 1 | `components/payment/payment.vue:27` | `{{ $lang('components.payment.' + item.type, item.name) }}` |
| 3 | `pages_order/list.vue:15` | `{{ $lang('status.' + statusItem.status, statusItem.name) }}` |
| 8 | `pages_tool/pay/cashier.vue:16` | `{{ $lang('pay.cashier.' + item.type, item.name) }}` |
> 说明:`item.name` / `statusItem.name` 本身可能是中文,兜底仅在对应 key 缺失时生效,符合“宁可显示中文也不显示 raw key”的保守策略。
### 3.2 立即执行:DIY tab 组件兜底增强
`components-diy/diy-tab.vue:907` 已有对象型 title 的兜底逻辑,但字符串型 title 在 key 缺失时会返回 raw key。建议改为:
```js
return this.$lang ? this.$lang(title, title) : title;
```
这样当 key 不存在时,至少返回原始 title 字符串本身,不会暴露 raw key。
### 3.3 可选执行:扩展支付方式 key
在 `lang/zh-cn/components/payment.js`、`lang/en-us/components/payment.js`、`lang/zh-cn/pay/cashier.js`、`lang/en-us/pay/cashier.js` 中预置常见支付方式 key:
```js
// components/payment.js
unionpay: '银联支付',
applepay: 'Apple Pay',
// ...
// pay/cashier.js
unionpay: '银联支付',
applepay: 'Apple Pay',
// ...
```
这样即使后端新增支付方式,也不会出现 raw key。建议与产品/后端确认当前系统支持的全部支付方式后补充。
### 3.4 立即执行:建立 P2 专项验证脚本
新增 `scripts/verify-p2-keys.js`,枚举所有已知动态取值,模拟 resolver 检查是否能解析:
- 读取 `common/js/lang.js` 的 `normalizeSharedLangPack` / `normalizePageLangPack`。
- 对 #1、#3、#4、#5、#6、#7、#8 的取值列表逐一验证。
- 对 #2 的 diy-tab,收集项目内所有可能作为 title 传入的 i18n key(可通过扫描 `diy-tab` 调用方或装修数据 schema 获得),建立白名单。
脚本输出示例:
```text
✅ components/payment.alipay => 支付宝支付
✅ components/payment.wechatpay => 微信支付
✅ status.waitpay => 待付款
...
```
### 3.5 长期建议:减少动态 key 模式
- **支付方式**:建议后端接口直接返回支付方式名称的 i18n key 或多语言 map,而不是让前端用 `type` 去拼 key。
- **订单状态**:`pages_order/list.vue` 已经在 `statusList` 里做了 `name` 兜底,后续可考虑直接用 `statusItem.name` 渲染,不再走动态 key。
- **分销/团队/积分 tab**:这些本地枚举 tab 可以直接写死 `$lang('xxx')`,不需要通过 `nameKey` / `langKey` 间接取值,虽然改动略多,但能彻底消除 P2 风险。
---
## 四、实施优先级
| 优先级 | 事项 | 预计改动量 | 收益 | 状态 |
|---|---|---|---|---|
| P0 | 为 #1、#3、#8 增加 `item.name` / `statusItem.name` fallback | 3 处模板 | 高:防止后端新增取值时暴露 raw key | ✅ 已完成 |
| P0 | `diy-tab.vue` 改为 `this.$lang(title, title)` | 1 处 JS | 高:防止 DIY 配置错误导致 raw key | ✅ 已完成 |
| P1 | 建立 `scripts/verify-p2-keys.js` 并接入 CI / pre-commit | 1 个脚本 | 高:持续守护动态 key | ✅ 已完成 |
| P1 | 扩展支付方式 key(确认后端全部支持类型后) | 4 个语言包 | 中:提升扩展性 | ⏸️ 可选,待与后端确认 |
| P2 | 逐步将本地枚举 tab 改为直接 `$lang()` 调用 | 5 ~ 6 个文件 | 低:彻底消除动态 key | ⏸️ 可选,收益较低 |
---
## 五、验证清单
- [x] 修改后 `npm run i18n:check` 通过。
- [x] `node --check` 检查新增脚本通过;`.vue` 文件 script 块提取后语法检查通过。
- [x] `scripts/verify-p2-keys.js` 输出全部 ✅。
- [ ] 在英文环境下手动检查支付方式弹窗、订单列表 tab、分销/团队/积分 tab、商品评价 tab 显示正常(建议发布后人工复核)。

View File

@@ -67,6 +67,8 @@ export default {
},
onLoad(options) {
this.config.text = this.$lang('goods.list.buyText');
this.$api.sendRequest({
url: '/api/lucky/status',
data: {},

View File

@@ -12,7 +12,7 @@
:id="statusItem.id" :data-current="statusIndex" @click="ontabtap">
<text class="uni-tab-item-title"
:class="statusItem.status == orderStatus ? 'uni-tab-item-title-active color-base-text' : ''">
{{ $lang('status.' + statusItem.status) }}
{{ $lang('status.' + statusItem.status, statusItem.name) }}
</text>
</view>
</view>

View File

@@ -7,8 +7,8 @@ export default {
*/
cancleRefund(order_goods_id, callback) {
uni.showModal({
content: '撤销之后本次申请将会关闭,如后续仍有问题可再次发起申请。',
cancelText: '暂不撤销',
content: this.$lang('order.refund.cancelRefundContent'),
cancelText: this.$lang('order.refund.cancelRefundCancelText'),
cancelColor: '#898989',
success: res => {
if (res.confirm) {

View File

@@ -8,7 +8,7 @@
<image v-else :src="$util.img('public/uniapp/fenxiao/bill/withdraw.png')" mode="widthFix" show-menu-by-longpress></image>
<view class="balances-info">
<text>{{ item.type_name }}</text>
<text>账单编号: {{ item.account_no }}</text>
<text>{{ $lang('fenxiao.bill.billNoPrefix') }}{{ item.account_no }}</text>
<text>{{ $util.timeStampTurnTime(item.create_time) }}</text>
</view>
<view class="balances-num">
@@ -16,7 +16,7 @@
item.money }}</text>
</view>
</view>
<ns-empty v-if="!accountList.length && showEmpty" text="暂无账单信息" :isIndex="false"></ns-empty>
<ns-empty v-if="!accountList.length && showEmpty" :text="$lang('fenxiao.bill.noBill')" :isIndex="false"></ns-empty>
</block>
<loading-cover ref="loadingCover"></loading-cover>
</mescroll-uni>
@@ -35,7 +35,7 @@ export default {
setTimeout(() => {
if (!this.addonIsExist.fenxiao) {
this.$util.showToast({
title: '商家未开启分销',
title: this.$lang('fenxiao.bill.addonClosed'),
mask: true,
duration: 2000
});

View File

@@ -8,9 +8,9 @@
<!-- 空状态 -->
<view v-else-if="!content" class="empty-state">
<view class="empty-icon">📄</view>
<text class="empty-title">暂无内容</text>
<text class="empty-desc">该协议内容暂未设置</text>
<button class="empty-button" @click="getcontent">重新加载</button>
<text class="empty-title">{{ $lang('agreement.noContent') }}</text>
<text class="empty-desc">{{ $lang('agreement.noContentDesc') }}</text>
<button class="empty-button" @click="getcontent">{{ $lang('agreement.reload') }}</button>
</view>
<!-- 内容状态 -->

View File

@@ -12,24 +12,24 @@
<!-- PDF 文件预览 -->
<view v-if="fileType === 'pdf'" class="file-viewer">
<web-view v-if="fileUrl" :src="fileUrl"></web-view>
<view v-else class="error-message">文件地址不存在</view>
<view v-else class="error-message">{{ $lang('filePreview.fileUrlNotExist') }}</view>
</view>
<!-- Word 文件预览 -->
<view v-else-if="fileType === 'word'" class="file-viewer">
<web-view v-if="fileUrl" :src="fileUrl"></web-view>
<view v-else class="error-message">文件地址不存在</view>
<view v-else class="error-message">{{ $lang('filePreview.fileUrlNotExist') }}</view>
</view>
<!-- 视频文件预览 -->
<view v-else-if="fileType === 'video'" class="video-viewer">
<video v-if="fileUrl" :src="fileUrl" controls autoplay class="video-player"></video>
<view v-else class="error-message">视频地址不存在</view>
<view v-else class="error-message">{{ $lang('filePreview.videoUrlNotExist') }}</view>
</view>
<!-- 其他文件类型 -->
<view v-else class="file-viewer">
<view class="error-message">不支持的文件类型</view>
<view class="error-message">{{ $lang('filePreview.unsupportedFileType') }}</view>
</view>
</view>
</view>

View File

@@ -11,7 +11,7 @@
</uni-grid-item>
</uni-grid>
</view>
<view v-if="brandList.length == 0"><ns-empty text="暂无更多品牌,去首页看看吧"></ns-empty></view>
<view v-if="brandList.length == 0"><ns-empty :text="$lang('goods.brand.noMoreBrand')"></ns-empty></view>
</block>
</mescroll-uni>
<loading-cover ref="loadingCover"></loading-cover>

Some files were not shown because too many files have changed in this diff Show More