feat: add multi-language page configs and fix i18n issues
1. 新增多套多语言页面翻译文件,覆盖文件列表、案例展示、商户相关、印章相关、分销、小程序发货等页面 2. 修复语言包拼写错误:loginTpis → loginTips 3. 重构语言包合并逻辑,使用深度合并替代浅合并避免子key丢失 4. 优化导航栏标题设置,统一使用$langConfig.title替代硬编码标题 5. 修复底部导航栏语言切换不响应的问题,改为响应式获取当前语言 6. 完善英文通用语言包,补充订单、购物车、会员相关翻译 7. 修复商品详情页默认标题为空的问题,设置为"商品详情"
This commit is contained in:
@@ -86,6 +86,81 @@ function deepCloneLang(obj) {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 语言包深度合并:将 source 合并到 target 中,返回 target
|
||||
* - 对象类型递归合并(嵌套 key 不会整体覆盖)
|
||||
* - 数组/字符串/数字等基本类型直接覆盖
|
||||
* - 危险 key 过滤:__proto__ / constructor / prototype
|
||||
*
|
||||
* 为什么需要深合并:
|
||||
* 之前用 Object.assign 做顶层浅合并,如果 page 语言包定义了 common: { submit: 'x' },
|
||||
* 会整体替换 common.js 中的 common 对象,导致 common.currencySymbol 等子 key 丢失。
|
||||
* 深合并可以保留两边的 key,只在 leaf 节点覆盖。
|
||||
*/
|
||||
function deepMergeLang(target, source) {
|
||||
if (target === null || typeof target !== 'object') return source;
|
||||
if (source === null || typeof source !== 'object') return source;
|
||||
if (Array.isArray(target) || Array.isArray(source)) return source;
|
||||
|
||||
for (const key in source) {
|
||||
if (!Object.prototype.hasOwnProperty.call(source, key)) continue;
|
||||
if (FORBIDDEN_KEYS.has(key)) continue;
|
||||
|
||||
const srcVal = source[key];
|
||||
const tgtVal = target[key];
|
||||
|
||||
// 两边都是纯对象 → 递归合并
|
||||
if (
|
||||
srcVal !== null && typeof srcVal === 'object' && !Array.isArray(srcVal) &&
|
||||
tgtVal !== null && typeof tgtVal === 'object' && !Array.isArray(tgtVal)
|
||||
) {
|
||||
target[key] = deepMergeLang(tgtVal, srcVal);
|
||||
} else {
|
||||
// 基本类型 / 数组 / 类型不一致 → 直接覆盖
|
||||
target[key] = deepCloneLang(srcVal);
|
||||
}
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
/**
|
||||
* 翻译参数替换:支持位置参数和命名参数
|
||||
*
|
||||
* 位置参数: $lang('key', 'a', 'b') → {0}=a, {1}=b
|
||||
* 命名参数: $lang('key', { name: 'Tom', count: 3 }) → {name}=Tom, {count}=3
|
||||
*
|
||||
* 规则:
|
||||
* - 第 2 个参数起如果是对象(非 null 非数组),则当作命名参数 map
|
||||
* - 其他情况当作位置参数
|
||||
* - 全局替换(同一个占位符出现多次都能替换)
|
||||
* - 字面量花括号可以用 {{ 和 }} 转义(暂不支持,有需要再加)
|
||||
*/
|
||||
function applyLangParams(value, args) {
|
||||
if (args.length === 0) return value;
|
||||
let result = String(value);
|
||||
|
||||
const firstArg = args[0];
|
||||
const isNamed = firstArg !== null && typeof firstArg === 'object' && !Array.isArray(firstArg);
|
||||
|
||||
if (isNamed) {
|
||||
// 命名参数:遍历对象的每个 key
|
||||
const map = firstArg;
|
||||
for (const key in map) {
|
||||
if (!Object.prototype.hasOwnProperty.call(map, key)) continue;
|
||||
if (FORBIDDEN_KEYS.has(key)) continue;
|
||||
const safeKey = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
result = result.replace(new RegExp('\\{' + safeKey + '\\}', 'g'), String(map[key]));
|
||||
}
|
||||
} else {
|
||||
// 位置参数: {0}, {1}, {2}...
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
result = result.replace(new RegExp('\\{' + i + '\\}', 'g'), String(args[i]));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// 远端覆写:内存缓存
|
||||
// shape: { [app_lang]: { version, map, ts, global_version } }
|
||||
// key 格式 `${appType}_${lang}`,天然按平台 + 语种二维隔离,避免多端 cache 串味
|
||||
@@ -543,17 +618,30 @@ export default {
|
||||
const fieldCacheKey = `${locale}__${langPath}__${_overrideVersion}__${field}`;
|
||||
const cachedFieldResult = _fieldResultCache[fieldCacheKey];
|
||||
const hasDefault = arguments.length > 1 && typeof arguments[1] === 'string' && arguments[1] !== '';
|
||||
const __DEV__ = (typeof process !== 'undefined' && process.env && process.env.NODE_ENV !== 'production');
|
||||
if (cachedFieldResult !== undefined) {
|
||||
let value = cachedFieldResult;
|
||||
// 没找到翻译且提供了默认值 → 使用默认值
|
||||
if (value === field && hasDefault) {
|
||||
return arguments[1];
|
||||
}
|
||||
// 带参数的翻译(如 hello {0})需要在缓存后仍做参数替换
|
||||
if (arguments.length > 1) {
|
||||
for (let i = 1; i < arguments.length; i++) {
|
||||
value = String(value).replace("{" + (i - 1) + "}", arguments[i]);
|
||||
// 没找到翻译
|
||||
if (value === field) {
|
||||
// 有默认值 → 使用默认值
|
||||
if (hasDefault) {
|
||||
return arguments[1];
|
||||
}
|
||||
// 生产环境无默认值 → 返回空串(避免用户看到 raw key)
|
||||
if (!__DEV__) {
|
||||
return '';
|
||||
}
|
||||
// 开发环境 → 返回 raw key(便于发现问题)
|
||||
return value;
|
||||
}
|
||||
// 带参数的翻译:支持位置参数({0})和命名参数({name})
|
||||
if (arguments.length > 1) {
|
||||
// 从第 2 个参数开始提取参数数组
|
||||
const args = new Array(arguments.length - 1);
|
||||
for (let i = 1; i < arguments.length; i++) {
|
||||
args[i - 1] = arguments[i];
|
||||
}
|
||||
value = applyLangParams(value, args);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
@@ -565,7 +653,7 @@ export default {
|
||||
try {
|
||||
const lang = loadLangPackSync(locale, 'common');
|
||||
const currentPageLang = langPath ? loadLangPackSync(locale, langPath) : {};
|
||||
mergedLang = Object.assign(deepCloneLang(lang), deepCloneLang(currentPageLang));
|
||||
mergedLang = deepMergeLang(deepCloneLang(lang), deepCloneLang(currentPageLang));
|
||||
const ovr = overrideCache[getOverrideMemKey(locale)];
|
||||
if (ovr && ovr.map) applyOverrides(mergedLang, ovr.map);
|
||||
} catch (e) {
|
||||
@@ -604,20 +692,28 @@ export default {
|
||||
// 将结果存入字段缓存(不含参数替换部分:参数每次可能不同)
|
||||
_fieldResultCache[fieldCacheKey] = value;
|
||||
|
||||
// 没找到翻译且提供了默认值 → 使用默认值
|
||||
if (value === field && hasDefault) {
|
||||
value = arguments[1];
|
||||
} else if (arguments.length > 1) {
|
||||
// 带参数的翻译
|
||||
for (let i = 1; i < arguments.length; i++) {
|
||||
value = String(value).replace("{" + (i - 1) + "}", arguments[i]);
|
||||
// 没找到翻译
|
||||
if (value === field) {
|
||||
// 有默认值 → 使用默认值
|
||||
if (hasDefault) {
|
||||
value = arguments[1];
|
||||
} else if (!__DEV__) {
|
||||
// 生产环境无默认值 → 返回空串(避免用户看到 raw key)
|
||||
value = '';
|
||||
}
|
||||
// 开发环境无默认值 → 保留 raw key(便于发现问题)
|
||||
} else if (arguments.length > 1) {
|
||||
// 带参数的翻译:支持位置参数({0})和命名参数({name})
|
||||
const args = new Array(arguments.length - 1);
|
||||
for (let i = 1; i < arguments.length; i++) {
|
||||
args[i - 1] = arguments[i];
|
||||
}
|
||||
value = applyLangParams(value, args);
|
||||
}
|
||||
|
||||
if (value == undefined || (value == 'title' && field == 'title')) value = '';
|
||||
if (value == undefined) value = '';
|
||||
|
||||
// 多语言调试
|
||||
const __DEV__ = (typeof process !== 'undefined' && process.env && process.env.NODE_ENV !== 'production');
|
||||
if (__DEV__ && field == value) {
|
||||
console.warn(`[i18n] missing key "${field}" in ${langPath || '<root>'} (${locale})`);
|
||||
}
|
||||
|
||||
@@ -28,12 +28,12 @@
|
||||
<view class="label"
|
||||
v-if="(tabBarList.type == 1 || tabBarList.type == 3) && tabBarList.theme == 'diy'"
|
||||
:style="{ color: verify(item.link) ? tabBarList.textHoverColor : tabBarList.textColor }">
|
||||
{{ lang == 'en-us' ? item.en_text : item.text }}
|
||||
{{ currentLang == 'en-us' ? item.en_text : item.text }}
|
||||
</view>
|
||||
<view class="label"
|
||||
v-if="(tabBarList.type == 1 || tabBarList.type == 3) && tabBarList.theme == 'default'"
|
||||
:style="{ color: verify(item.link) ? 'var(--base-color)' : '#333333' }">
|
||||
{{ lang == 'en-us' ? item.en_text : item.text }}
|
||||
{{ currentLang == 'en-us' ? item.en_text : item.text }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -61,7 +61,6 @@ export default {
|
||||
mixins: [DiyMinx],
|
||||
data() {
|
||||
return {
|
||||
lang: uni.getStorageSync("lang"),
|
||||
currentRoute: '', //当前页面路径
|
||||
jumpFlag: true, //是否可以跳转,防止重复点击
|
||||
cartAnimation: {}
|
||||
@@ -90,6 +89,14 @@ export default {
|
||||
});
|
||||
},
|
||||
computed: {
|
||||
// 当前语言(响应式):通过 componentRefresh 建立依赖,切语言时自动更新
|
||||
// 不能直接用 uni.getStorageSync 作为 data,因为它不是响应式的
|
||||
currentLang() {
|
||||
// 通过 componentRefresh 建立响应式依赖
|
||||
// 切语言时 store 中的 componentRefresh 会 +1,触发重新计算
|
||||
this.componentRefresh;
|
||||
return uni.getStorageSync("lang") || 'zh-cn';
|
||||
},
|
||||
cartChange() {
|
||||
return this.$store.state.cartChange;
|
||||
},
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
</view>
|
||||
<view class="info">
|
||||
<view class="nickname"><text class="name">{{ $lang('login') }}</text></view>
|
||||
<view class="desc">{{ $lang('loginTpis') }}</view>
|
||||
<view class="desc">{{ $lang('loginTips') }}</view>
|
||||
</view>
|
||||
|
||||
<view v-if="ischina == 1"
|
||||
|
||||
4
lang/en-us/agreement/contenr.js
Normal file
4
lang/en-us/agreement/contenr.js
Normal file
@@ -0,0 +1,4 @@
|
||||
export const lang = {
|
||||
//title为每个页面的标题
|
||||
title: 'Agreement',
|
||||
}
|
||||
@@ -34,7 +34,10 @@ export const lang = {
|
||||
copySuccess: 'Copied',
|
||||
copySelected: 'Copied selected content',
|
||||
copyFail: 'Copy failed',
|
||||
copyFailWithReason: 'Copy failed: {reason}',
|
||||
openingFile: 'Opening file...',
|
||||
openingFileName: 'Opening file: {name}',
|
||||
openingFileTitle: 'Opening: {title}',
|
||||
fileOpenFail: 'Failed to open file',
|
||||
loadHistoryFail: 'Failed to load history',
|
||||
about: '0B',
|
||||
@@ -64,6 +67,7 @@ export const lang = {
|
||||
improveService: 'We will improve our service',
|
||||
openLink: 'Open Link',
|
||||
confirmOpenLink: 'Are you sure you want to open this link: ',
|
||||
confirmOpenLinkText: 'Are you sure you want to open this link: {url}?',
|
||||
operationFailed: 'Operation failed, please try again',
|
||||
prompt: 'Prompt',
|
||||
serviceTypeNotSupported: 'Current service type does not support this page',
|
||||
|
||||
4
lang/en-us/cases/index.js
Normal file
4
lang/en-us/cases/index.js
Normal file
@@ -0,0 +1,4 @@
|
||||
export const lang = {
|
||||
//title为每个页面的标题
|
||||
title: 'Case Studies',
|
||||
}
|
||||
@@ -34,4 +34,34 @@ export const lang = {
|
||||
loginTips: 'Click to login and enjoy more exciting information',
|
||||
toLogin: 'Go to login',
|
||||
toGoodsCategoryPage: 'Go shopping',
|
||||
|
||||
// 订单状态
|
||||
waitpay: 'Pending Payment',
|
||||
waitsend: 'Pending Shipment',
|
||||
waitconfirm: 'Pending Receipt',
|
||||
activist: 'After-sales',
|
||||
completed: 'Completed',
|
||||
|
||||
// 购买/购物车
|
||||
buy: 'Buy',
|
||||
buyNow: 'Buy Now',
|
||||
addToCart: 'Add to Cart',
|
||||
addToCartSuccess: 'Added to Cart',
|
||||
addToCartFail: 'Failed to Add to Cart',
|
||||
addToCartFailMax: 'Maximum purchase quantity reached',
|
||||
addToCartFailMin: 'Minimum purchase quantity not met',
|
||||
addToCartFailStock: 'Insufficient Stock',
|
||||
|
||||
// 咨询
|
||||
consult: 'Consult Now',
|
||||
consultSuccess: 'Consultation Successful',
|
||||
consultFail: 'Consultation Failed',
|
||||
|
||||
// 会员相关
|
||||
privacyPolicy: 'Privacy Policy',
|
||||
registrationAgreement: 'Registration Agreement',
|
||||
cancellation: 'Account Cancellation',
|
||||
logout: 'Log Out',
|
||||
logoutTitle: 'Tip',
|
||||
logoutContent: 'Are you sure you want to log out?',
|
||||
}
|
||||
4
lang/en-us/fenxiao/child_fenxiao.js
Normal file
4
lang/en-us/fenxiao/child_fenxiao.js
Normal file
@@ -0,0 +1,4 @@
|
||||
export const lang = {
|
||||
//title为每个页面的标题
|
||||
title: 'Distributors',
|
||||
}
|
||||
4
lang/en-us/fenxiao/ranking_list.js
Normal file
4
lang/en-us/fenxiao/ranking_list.js
Normal file
@@ -0,0 +1,4 @@
|
||||
export const lang = {
|
||||
//title为每个页面的标题
|
||||
title: 'Ranking',
|
||||
}
|
||||
4
lang/en-us/file-preview/file-preview.js
Normal file
4
lang/en-us/file-preview/file-preview.js
Normal file
@@ -0,0 +1,4 @@
|
||||
export const lang = {
|
||||
//title为每个页面的标题
|
||||
title: 'File Preview',
|
||||
}
|
||||
4
lang/en-us/files/list.js
Normal file
4
lang/en-us/files/list.js
Normal file
@@ -0,0 +1,4 @@
|
||||
export const lang = {
|
||||
//title为每个页面的标题
|
||||
title: 'View Files',
|
||||
}
|
||||
@@ -2,5 +2,6 @@ export const lang = {
|
||||
//title为每个页面的标题
|
||||
title: 'Goods Category',
|
||||
search: 'Search',
|
||||
seeMore : 'See More'
|
||||
seeMore : 'See More',
|
||||
Make: 'Consult Now'
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ export const lang = {
|
||||
leave:'leave a message',
|
||||
make:'make an appointment',
|
||||
makebtn:'make',
|
||||
commissiontext: 'Commission',
|
||||
|
||||
sales:'sales',
|
||||
stock:'stock',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export const lang = {
|
||||
//title为每个页面的标题
|
||||
title: 'Edit Account',
|
||||
addTitle: 'Add Account',
|
||||
name: 'Name',
|
||||
namePlaceholder: 'Please enter real name',
|
||||
mobilePhone: 'Mobile Number',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export const lang = {
|
||||
//title为每个页面的标题
|
||||
title: 'Edit Shipping Address',
|
||||
addTitle: 'Add Shipping Address',
|
||||
consignee: 'Name',
|
||||
consigneePlaceholder: 'Consignee Name',
|
||||
mobile: 'Mobile',
|
||||
|
||||
@@ -2,7 +2,7 @@ export const lang = {
|
||||
//title为每个页面的标题
|
||||
title: 'Member Center',
|
||||
login: 'Login',
|
||||
loginTpis: 'Login experience',
|
||||
loginTips: 'Login experience',
|
||||
memberLevel: 'Member Level',
|
||||
moreAuthority: 'More Privileges',
|
||||
allOrders: 'All Orders',
|
||||
@@ -11,6 +11,7 @@ export const lang = {
|
||||
readyDelivery: 'Pending Shipment',
|
||||
waitDelivery: 'Pending Receipt',
|
||||
refunding: 'Refunding',
|
||||
getDynacode: 'Get Verification Code',
|
||||
// 会员中心入口
|
||||
sign: 'Check-in',
|
||||
personInfo: 'Personal Information',
|
||||
|
||||
4
lang/en-us/merch/detail.js
Normal file
4
lang/en-us/merch/detail.js
Normal file
@@ -0,0 +1,4 @@
|
||||
export const lang = {
|
||||
//title为每个页面的标题
|
||||
title: 'Merchant Home',
|
||||
}
|
||||
5
lang/en-us/merch/edit.js
Normal file
5
lang/en-us/merch/edit.js
Normal file
@@ -0,0 +1,5 @@
|
||||
export const lang = {
|
||||
//title为每个页面的标题
|
||||
title: 'Edit Product',
|
||||
addTitle: 'Add Product',
|
||||
}
|
||||
4
lang/en-us/merch/merchcategory.js
Normal file
4
lang/en-us/merch/merchcategory.js
Normal file
@@ -0,0 +1,4 @@
|
||||
export const lang = {
|
||||
//title为每个页面的标题
|
||||
title: 'Merchants',
|
||||
}
|
||||
4
lang/en-us/seal/medium/detail.js
Normal file
4
lang/en-us/seal/medium/detail.js
Normal file
@@ -0,0 +1,4 @@
|
||||
export const lang = {
|
||||
//title为每个页面的标题
|
||||
title: 'Search Results',
|
||||
}
|
||||
4
lang/en-us/seal/medium/search.js
Normal file
4
lang/en-us/seal/medium/search.js
Normal file
@@ -0,0 +1,4 @@
|
||||
export const lang = {
|
||||
//title为每个页面的标题
|
||||
title: 'Material Search',
|
||||
}
|
||||
4
lang/en-us/seal/structure.js
Normal file
4
lang/en-us/seal/structure.js
Normal file
@@ -0,0 +1,4 @@
|
||||
export const lang = {
|
||||
//title为每个页面的标题
|
||||
title: 'Structure Selection',
|
||||
}
|
||||
4
lang/en-us/weapp/order_shipping.js
Normal file
4
lang/en-us/weapp/order_shipping.js
Normal file
@@ -0,0 +1,4 @@
|
||||
export const lang = {
|
||||
//title为每个页面的标题
|
||||
title: 'Mini Program Shipping',
|
||||
}
|
||||
@@ -1 +1,4 @@
|
||||
export const lang = {}
|
||||
export const lang = {
|
||||
//title为每个页面的标题
|
||||
title: '协议',
|
||||
}
|
||||
|
||||
@@ -34,7 +34,10 @@ export const lang = {
|
||||
copySuccess: '已复制',
|
||||
copySelected: '已复制选中内容',
|
||||
copyFail: '复制失败',
|
||||
copyFailWithReason: '复制失败:{reason}',
|
||||
openingFile: '打开文件中...',
|
||||
openingFileName: '正在打开文件:{name}',
|
||||
openingFileTitle: '正在打开:{title}',
|
||||
fileOpenFail: '文件打开失败',
|
||||
loadHistoryFail: '加载历史失败',
|
||||
about: '0B',
|
||||
@@ -64,6 +67,7 @@ export const lang = {
|
||||
improveService: '我们会改进服务',
|
||||
openLink: '打开链接',
|
||||
confirmOpenLink: '确定要打开链接:',
|
||||
confirmOpenLinkText: '确定要打开链接:{url}?',
|
||||
operationFailed: '操作失败,请重试',
|
||||
prompt: '提示',
|
||||
serviceTypeNotSupported: '当前客服类型不支持此页面',
|
||||
|
||||
4
lang/zh-cn/cases/index.js
Normal file
4
lang/zh-cn/cases/index.js
Normal file
@@ -0,0 +1,4 @@
|
||||
export const lang = {
|
||||
//title为每个页面的标题
|
||||
title: '案例展示',
|
||||
}
|
||||
@@ -29,7 +29,7 @@ export const lang = {
|
||||
},
|
||||
|
||||
login: '登录/注册',
|
||||
loginTpis: '点击登录 享受更多精彩信息',
|
||||
loginTips: '点击登录 享受更多精彩信息',
|
||||
toLogin: '去登录',
|
||||
toGoodsCategoryPage: '去逛逛',
|
||||
|
||||
|
||||
4
lang/zh-cn/fenxiao/child_fenxiao.js
Normal file
4
lang/zh-cn/fenxiao/child_fenxiao.js
Normal file
@@ -0,0 +1,4 @@
|
||||
export const lang = {
|
||||
//title为每个页面的标题
|
||||
title: '分销商',
|
||||
}
|
||||
4
lang/zh-cn/fenxiao/ranking_list.js
Normal file
4
lang/zh-cn/fenxiao/ranking_list.js
Normal file
@@ -0,0 +1,4 @@
|
||||
export const lang = {
|
||||
//title为每个页面的标题
|
||||
title: '排行榜',
|
||||
}
|
||||
4
lang/zh-cn/file-preview/file-preview.js
Normal file
4
lang/zh-cn/file-preview/file-preview.js
Normal file
@@ -0,0 +1,4 @@
|
||||
export const lang = {
|
||||
//title为每个页面的标题
|
||||
title: '文件预览',
|
||||
}
|
||||
4
lang/zh-cn/files/list.js
Normal file
4
lang/zh-cn/files/list.js
Normal file
@@ -0,0 +1,4 @@
|
||||
export const lang = {
|
||||
//title为每个页面的标题
|
||||
title: '查看文件',
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
export const lang = {
|
||||
//title为每个页面的标题
|
||||
title: '',
|
||||
title: '商品详情',
|
||||
select: '选择',
|
||||
details:'商品详情',
|
||||
document:'文档详情',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export const lang = {
|
||||
//title为每个页面的标题
|
||||
title: '编辑账户',
|
||||
addTitle: '新增账户',
|
||||
name: '姓名',
|
||||
namePlaceholder: '请输入真实姓名',
|
||||
mobilePhone: '手机号码',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export const lang = {
|
||||
//title为每个页面的标题
|
||||
title: '编辑收货地址',
|
||||
addTitle: '新增收货地址',
|
||||
consignee: '姓名',
|
||||
consigneePlaceholder: '收货人姓名',
|
||||
mobile: '手机',
|
||||
|
||||
@@ -2,7 +2,7 @@ export const lang = {
|
||||
//title为每个页面的标题
|
||||
title: '会员中心',
|
||||
login: '登录/注册',
|
||||
loginTpis: '点击登录 享受更多精彩信息',
|
||||
loginTips: '点击登录 享受更多精彩信息',
|
||||
memberLevel: '会员等级',
|
||||
moreAuthority: '更多权限',
|
||||
allOrders: '全部订单',
|
||||
|
||||
4
lang/zh-cn/merch/detail.js
Normal file
4
lang/zh-cn/merch/detail.js
Normal file
@@ -0,0 +1,4 @@
|
||||
export const lang = {
|
||||
//title为每个页面的标题
|
||||
title: '商户首页',
|
||||
}
|
||||
5
lang/zh-cn/merch/edit.js
Normal file
5
lang/zh-cn/merch/edit.js
Normal file
@@ -0,0 +1,5 @@
|
||||
export const lang = {
|
||||
//title为每个页面的标题
|
||||
title: '编辑商品',
|
||||
addTitle: '添加商品',
|
||||
}
|
||||
4
lang/zh-cn/merch/merchcategory.js
Normal file
4
lang/zh-cn/merch/merchcategory.js
Normal file
@@ -0,0 +1,4 @@
|
||||
export const lang = {
|
||||
//title为每个页面的标题
|
||||
title: '入驻商户',
|
||||
}
|
||||
4
lang/zh-cn/seal/medium/detail.js
Normal file
4
lang/zh-cn/seal/medium/detail.js
Normal file
@@ -0,0 +1,4 @@
|
||||
export const lang = {
|
||||
//title为每个页面的标题
|
||||
title: '搜索结果',
|
||||
}
|
||||
4
lang/zh-cn/seal/medium/search.js
Normal file
4
lang/zh-cn/seal/medium/search.js
Normal file
@@ -0,0 +1,4 @@
|
||||
export const lang = {
|
||||
//title为每个页面的标题
|
||||
title: '介质材料搜索',
|
||||
}
|
||||
4
lang/zh-cn/seal/structure.js
Normal file
4
lang/zh-cn/seal/structure.js
Normal file
@@ -0,0 +1,4 @@
|
||||
export const lang = {
|
||||
//title为每个页面的标题
|
||||
title: '结构选型',
|
||||
}
|
||||
4
lang/zh-cn/weapp/order_shipping.js
Normal file
4
lang/zh-cn/weapp/order_shipping.js
Normal file
@@ -0,0 +1,4 @@
|
||||
export const lang = {
|
||||
//title为每个页面的标题
|
||||
title: '小程序发货',
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"scripts": {
|
||||
"mp-weixin": "node scripts/mp-weixin.patch.js"
|
||||
"mp-weixin": "node scripts/mp-weixin.patch.js",
|
||||
"i18n:check": "node scripts/i18n-check.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"dart-sass": "^1.25.0",
|
||||
|
||||
@@ -74,9 +74,7 @@ export default {
|
||||
this.langstatus = res.status
|
||||
if(this.langstatus == 1){
|
||||
// this.$langConfig.refresh();
|
||||
uni.setNavigationBarTitle({
|
||||
title:uni.getStorageSync('lang') == 'en-us'?'Goods List':'商品列表'
|
||||
})
|
||||
this.$langConfig.title(this.$lang('title'));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -122,16 +122,12 @@ export default {
|
||||
// this.getCategoryTreeFn();
|
||||
if (this.goodsData.id) {
|
||||
this.isAWait = true;
|
||||
uni.setNavigationBarTitle({
|
||||
title: '编辑商品'
|
||||
});
|
||||
this.$langConfig.title(this.$lang('title'));
|
||||
await this.editGetGoodsInfo();
|
||||
} else {
|
||||
|
||||
this.isAWait = false;
|
||||
uni.setNavigationBarTitle({
|
||||
title: '添加商品'
|
||||
});
|
||||
this.$langConfig.title(this.$lang('addTitle'));
|
||||
}
|
||||
|
||||
let date = new Date();
|
||||
|
||||
@@ -1712,7 +1712,7 @@ _handleStreamError(msg) {
|
||||
fail: (err) => {
|
||||
console.error('[AI Chat] 复制失败, 详细错误:', JSON.stringify(err))
|
||||
console.error('[AI Chat] 复制失败 errMsg:', err.errMsg || err.message || '无错误消息')
|
||||
uni.showToast({ title: this.$lang('copyFail') + ': ' + (err.errMsg || this.$lang('serviceUnavailable')), icon: 'none', duration: 3000 })
|
||||
uni.showToast({ title: this.$lang('copyFailWithReason', { reason: err.errMsg || this.$lang('serviceUnavailable') }), icon: 'none', duration: 3000 })
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
|
||||
@@ -461,7 +461,7 @@ export default {
|
||||
},
|
||||
|
||||
onFilePreview(message) {
|
||||
uni.showToast({ title: this.$lang('openingFile') + ' ' + message.fileName, icon: 'none' });
|
||||
uni.showToast({ title: this.$lang('openingFileName', { name: message.fileName }), icon: 'none' });
|
||||
},
|
||||
|
||||
onAudioPlay(message) {
|
||||
@@ -491,7 +491,7 @@ export default {
|
||||
onLinkOpen(message) {
|
||||
uni.showModal({
|
||||
title: this.$lang('openLink'),
|
||||
content: this.$lang('confirmOpenLink') + message.url + '?',
|
||||
content: this.$lang('confirmOpenLinkText', { url: message.url }),
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
// #ifdef H5
|
||||
@@ -506,7 +506,7 @@ export default {
|
||||
},
|
||||
|
||||
onProductView(message) {
|
||||
uni.showToast({ title: this.$lang('openingFile') + ': ' + message.title, icon: 'none' });
|
||||
uni.showToast({ title: this.$lang('openingFileTitle', { title: message.title }), icon: 'none' });
|
||||
},
|
||||
|
||||
onInputChange(value) {
|
||||
|
||||
@@ -63,13 +63,9 @@ export default {
|
||||
this.getTransferType();
|
||||
}
|
||||
if (this.formData.id) {
|
||||
uni.setNavigationBarTitle({
|
||||
title: '编辑账户'
|
||||
});
|
||||
this.$langConfig.title(this.$lang('title'));
|
||||
} else {
|
||||
uni.setNavigationBarTitle({
|
||||
title: '新增账户'
|
||||
});
|
||||
this.$langConfig.title(this.$lang('addTitle'));
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -132,17 +132,13 @@
|
||||
onBackPress() {
|
||||
uni.setStorageSync('addressInfo', '');
|
||||
},
|
||||
onShow() {
|
||||
if (this.formData.id) {
|
||||
uni.setNavigationBarTitle({
|
||||
title: '编辑收货地址'
|
||||
});
|
||||
} else {
|
||||
uni.setNavigationBarTitle({
|
||||
title: '新增收货地址'
|
||||
});
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
if (this.formData.id) {
|
||||
this.$langConfig.title(this.$lang('title'));
|
||||
} else {
|
||||
this.$langConfig.title(this.$lang('addTitle'));
|
||||
}
|
||||
},
|
||||
onReady() {
|
||||
this.$refs.loadingCover.hide();
|
||||
},
|
||||
|
||||
@@ -154,7 +154,7 @@ export default {
|
||||
}
|
||||
},
|
||||
setNavbarTitle() {
|
||||
let title = '个人资料';
|
||||
let title = this.$lang('title');
|
||||
switch (this.indent) {
|
||||
case 'name':
|
||||
title = this.$lang('modifyNickname');
|
||||
@@ -175,9 +175,7 @@ export default {
|
||||
title = this.$lang('mobile');
|
||||
break;
|
||||
}
|
||||
uni.setNavigationBarTitle({
|
||||
title: title
|
||||
});
|
||||
this.$langConfig.title(title);
|
||||
},
|
||||
// 初始化用户信息
|
||||
getInfo() {
|
||||
|
||||
280
scripts/i18n-check.js
Normal file
280
scripts/i18n-check.js
Normal file
@@ -0,0 +1,280 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* i18n 国际化检查脚本
|
||||
*
|
||||
* 功能:
|
||||
* 1. 检查 zh-cn / en-us 语言包文件结构是否一致
|
||||
* 2. 检查同文件下中英文 key 是否一致
|
||||
* 3. 统计 key 总数和差异数
|
||||
*
|
||||
* 用法:
|
||||
* node scripts/i18n-check.js
|
||||
*
|
||||
* 退出码:
|
||||
* 0 - 检查通过(无差异 或 只有警告级差异)
|
||||
* 1 - 存在错误级差异
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '..');
|
||||
const LANG_DIR = path.join(ROOT, 'lang');
|
||||
const LOCALES = ['zh-cn', 'en-us'];
|
||||
|
||||
// 递归获取目录下所有 .js 文件(相对路径)
|
||||
function getAllLangFiles(dir) {
|
||||
const result = [];
|
||||
function walk(current, rel) {
|
||||
const entries = fs.readdirSync(current, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(current, entry.name);
|
||||
const relPath = rel ? path.join(rel, entry.name) : entry.name;
|
||||
if (entry.isDirectory()) {
|
||||
walk(fullPath, relPath);
|
||||
} else if (entry.name.endsWith('.js')) {
|
||||
result.push(relPath.replace(/\\/g, '/'));
|
||||
}
|
||||
}
|
||||
}
|
||||
walk(dir, '');
|
||||
return result.sort();
|
||||
}
|
||||
|
||||
// 解析语言包 js 文件,提取所有 key(支持嵌套,用 . 连接)
|
||||
// 注意: 用正则粗略提取,不做完整 AST 解析,对本项目语言包格式足够
|
||||
function extractKeys(filePath) {
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
const keys = new Set();
|
||||
|
||||
// 简单的对象 key 提取:匹配 key: 'value' 或 key: { 形式
|
||||
// 用栈跟踪嵌套层级,记录路径
|
||||
const stack = [];
|
||||
const lines = content.split('\n');
|
||||
let braceDepth = 0;
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
// 跳过注释
|
||||
const trimmed = line.trim();
|
||||
if (trimmed.startsWith('//')) continue;
|
||||
if (trimmed.startsWith('/*')) continue;
|
||||
if (trimmed.startsWith('*')) continue;
|
||||
|
||||
// 匹配 key: 形式
|
||||
// 匹配: identifier: 或 'key': 或 "key":
|
||||
const keyMatch = line.match(/^\s*['"]?(\w[\w]*)['"]?\s*:/);
|
||||
if (keyMatch) {
|
||||
const key = keyMatch[1];
|
||||
// 跳过 export const lang = { 这种
|
||||
if (stack.length === 0 && key === 'lang') {
|
||||
// 往下找第一个 {
|
||||
if (line.includes('{')) braceDepth++;
|
||||
if (braceDepth > 0) stack.push('');
|
||||
continue;
|
||||
}
|
||||
|
||||
// 计算当前路径
|
||||
const pathParts = stack.filter(p => p);
|
||||
const fullKey = [...pathParts, key].join('.');
|
||||
|
||||
// 判断这是字符串值还是对象值
|
||||
const afterColon = line.substring(line.indexOf(':') + 1);
|
||||
const isObject = afterColon.includes('{');
|
||||
const isString = afterColon.match(/['"`]/);
|
||||
|
||||
if (isObject && !isString) {
|
||||
// 对象 key,入栈
|
||||
stack.push(key);
|
||||
keys.add(fullKey + '.__object__');
|
||||
} else {
|
||||
// 叶子 key
|
||||
keys.add(fullKey);
|
||||
}
|
||||
}
|
||||
|
||||
// 处理闭合 },出栈
|
||||
const closeBraces = (line.match(/\}/g) || []).length;
|
||||
const openBraces = (line.match(/\{/g) || []).length;
|
||||
const netClose = closeBraces - openBraces;
|
||||
|
||||
if (line.includes('{') && keyMatch && !line.includes('}')) {
|
||||
// 这行是 key: {,已经在上面处理了入栈
|
||||
}
|
||||
|
||||
// 简单处理:遇到 } 且栈非空就出栈
|
||||
if (netClose > 0 && stack.length > 0) {
|
||||
for (let j = 0; j < netClose && stack.length > 0; j++) {
|
||||
stack.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 去掉 __object__ 标记
|
||||
const result = new Set();
|
||||
for (const k of keys) {
|
||||
if (!k.endsWith('.__object__')) {
|
||||
result.add(k);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// 更可靠的 key 提取:用 Node.js require + 递归遍历对象
|
||||
function extractKeysByRequire(filePath) {
|
||||
try {
|
||||
// 清除 require 缓存
|
||||
delete require.cache[require.resolve(filePath)];
|
||||
const mod = require(filePath);
|
||||
const langObj = mod.lang || mod.default?.lang || mod.default;
|
||||
if (!langObj || typeof langObj !== 'object') return new Set();
|
||||
|
||||
const keys = new Set();
|
||||
function walk(obj, prefix) {
|
||||
for (const key of Object.keys(obj)) {
|
||||
const fullKey = prefix ? `${prefix}.${key}` : key;
|
||||
const val = obj[key];
|
||||
if (val !== null && typeof val === 'object' && !Array.isArray(val)) {
|
||||
walk(val, fullKey);
|
||||
} else if (typeof val === 'string' || typeof val === 'number' || typeof val === 'boolean') {
|
||||
keys.add(fullKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
walk(langObj, '');
|
||||
return keys;
|
||||
} catch (e) {
|
||||
console.error(` 解析失败: ${filePath} - ${e.message}`);
|
||||
return new Set();
|
||||
}
|
||||
}
|
||||
|
||||
function main() {
|
||||
console.log('========================================');
|
||||
console.log(' i18n 国际化检查');
|
||||
console.log('========================================\n');
|
||||
|
||||
const localeDirs = {};
|
||||
const localeFiles = {};
|
||||
|
||||
for (const locale of LOCALES) {
|
||||
const dir = path.join(LANG_DIR, locale);
|
||||
if (!fs.existsSync(dir)) {
|
||||
console.error(`错误: 找不到语言目录 ${dir}`);
|
||||
process.exit(1);
|
||||
}
|
||||
localeDirs[locale] = dir;
|
||||
localeFiles[locale] = new Set(getAllLangFiles(dir));
|
||||
}
|
||||
|
||||
// ---- 检查 1: 文件结构一致性 ----
|
||||
console.log('【1/2】文件结构检查\n');
|
||||
|
||||
const zhFiles = localeFiles['zh-cn'];
|
||||
const enFiles = localeFiles['en-us'];
|
||||
|
||||
const onlyZh = [...zhFiles].filter(f => !enFiles.has(f));
|
||||
const onlyEn = [...enFiles].filter(f => !zhFiles.has(f));
|
||||
const commonFiles = [...zhFiles].filter(f => enFiles.has(f));
|
||||
|
||||
console.log(` 中文语言包文件数: ${zhFiles.size}`);
|
||||
console.log(` 英文语言包文件数: ${enFiles.size}`);
|
||||
console.log(` 共同文件数: ${commonFiles.length}`);
|
||||
|
||||
if (onlyZh.length > 0) {
|
||||
console.log(`\n ⚠️ 只有中文没有英文的文件 (${onlyZh.length}):`);
|
||||
for (const f of onlyZh) {
|
||||
console.log(` - ${f}`);
|
||||
}
|
||||
}
|
||||
if (onlyEn.length > 0) {
|
||||
console.log(`\n ⚠️ 只有英文没有中文的文件 (${onlyEn.length}):`);
|
||||
for (const f of onlyEn) {
|
||||
console.log(` - ${f}`);
|
||||
}
|
||||
}
|
||||
if (onlyZh.length === 0 && onlyEn.length === 0) {
|
||||
console.log(' ✅ 文件结构完全一致');
|
||||
}
|
||||
|
||||
// ---- 检查 2: key 一致性 ----
|
||||
console.log('\n【2/2】Key 一致性检查\n');
|
||||
|
||||
let totalZhKeys = 0;
|
||||
let totalEnKeys = 0;
|
||||
let filesWithDiff = 0;
|
||||
const diffDetails = [];
|
||||
|
||||
for (const file of commonFiles) {
|
||||
const zhPath = path.join(localeDirs['zh-cn'], file);
|
||||
const enPath = path.join(localeDirs['en-us'], file);
|
||||
|
||||
const zhKeys = extractKeysByRequire(zhPath);
|
||||
const enKeys = extractKeysByRequire(enPath);
|
||||
|
||||
totalZhKeys += zhKeys.size;
|
||||
totalEnKeys += enKeys.size;
|
||||
|
||||
const missingInEn = [...zhKeys].filter(k => !enKeys.has(k));
|
||||
const missingInZh = [...enKeys].filter(k => !zhKeys.has(k));
|
||||
|
||||
if (missingInEn.length > 0 || missingInZh.length > 0) {
|
||||
filesWithDiff++;
|
||||
diffDetails.push({
|
||||
file,
|
||||
missingInEn,
|
||||
missingInZh,
|
||||
zhCount: zhKeys.size,
|
||||
enCount: enKeys.size,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
console.log(` 中文 key 总数: ${totalZhKeys}`);
|
||||
console.log(` 英文 key 总数: ${totalEnKeys}`);
|
||||
console.log(` 有差异的文件数: ${filesWithDiff} / ${commonFiles.length}`);
|
||||
|
||||
if (filesWithDiff > 0) {
|
||||
console.log(`\n ⚠️ Key 差异详情:`);
|
||||
for (const diff of diffDetails) {
|
||||
console.log(`\n 📄 ${diff.file}`);
|
||||
console.log(` 中文: ${diff.zhCount} 个, 英文: ${diff.enCount} 个`);
|
||||
if (diff.missingInEn.length > 0) {
|
||||
console.log(` 英文缺失 (${diff.missingInEn.length}):`);
|
||||
for (const k of diff.missingInEn.slice(0, 10)) {
|
||||
console.log(` - ${k}`);
|
||||
}
|
||||
if (diff.missingInEn.length > 10) {
|
||||
console.log(` ... 还有 ${diff.missingInEn.length - 10} 个`);
|
||||
}
|
||||
}
|
||||
if (diff.missingInZh.length > 0) {
|
||||
console.log(` 中文缺失 (${diff.missingInZh.length}):`);
|
||||
for (const k of diff.missingInZh.slice(0, 10)) {
|
||||
console.log(` - ${k}`);
|
||||
}
|
||||
if (diff.missingInZh.length > 10) {
|
||||
console.log(` ... 还有 ${diff.missingInZh.length - 10} 个`);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log(' ✅ 所有共同文件的 key 完全一致');
|
||||
}
|
||||
|
||||
// ---- 总结 ----
|
||||
console.log('\n========================================');
|
||||
console.log(' 检查完成');
|
||||
console.log('========================================');
|
||||
|
||||
const hasError = filesWithDiff > 0 || onlyZh.length > 0;
|
||||
if (hasError) {
|
||||
console.log('\n ❌ 存在差异,请修复后再提交');
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log('\n ✅ 全部通过');
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
Reference in New Issue
Block a user