feat: 新增多语言配置并优化语言加载逻辑
1. 新增多个页面的中英文多语言词条,包括会员卡协议、积分商品、收银台等 2. 新增退款商品选择、微信支付、电子名片等页面的语言包 3. 优化lang.js语言加载逻辑,支持蛇形命名转换和多路径匹配,完善$lang调用兼容处理
This commit is contained in:
@@ -61,11 +61,35 @@ const SHARED_LANG_PACKS = [
|
||||
];
|
||||
|
||||
function toCamelCaseName(name) {
|
||||
return String(name || '').replace(/-([a-z])/g, function(_, letter) {
|
||||
return String(name || '').replace(/[-_]([a-z])/g, function(_, letter) {
|
||||
return letter.toUpperCase();
|
||||
});
|
||||
}
|
||||
|
||||
function toSnakeCaseName(name) {
|
||||
return String(name || '')
|
||||
.replace(/([A-Z])/g, '_$1')
|
||||
.replace(/-/g, '_')
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
function buildLangPathCandidates(parts) {
|
||||
const results = [];
|
||||
function walk(index, current) {
|
||||
if (index >= parts.length) {
|
||||
const path = current.join('/');
|
||||
if (!results.includes(path)) results.push(path);
|
||||
return;
|
||||
}
|
||||
const raw = parts[index];
|
||||
const snake = toSnakeCaseName(raw);
|
||||
walk(index + 1, current.concat(raw));
|
||||
if (snake !== raw) walk(index + 1, current.concat(snake));
|
||||
}
|
||||
walk(0, []);
|
||||
return results;
|
||||
}
|
||||
|
||||
function normalizeSharedLangPack(path, langData) {
|
||||
if (!langData || typeof langData !== 'object') return {};
|
||||
|
||||
@@ -98,15 +122,96 @@ function normalizePageLangPack(path, langData) {
|
||||
const segments = String(path).split('/').filter(Boolean);
|
||||
if (!segments.length) return langData;
|
||||
|
||||
let leaf = langData;
|
||||
let hasLeaf = true;
|
||||
for (const segment of segments) {
|
||||
const key = toCamelCaseName(segment);
|
||||
if (leaf && typeof leaf === 'object' && !Array.isArray(leaf) && leaf[key] !== undefined) {
|
||||
leaf = leaf[key];
|
||||
} else {
|
||||
hasLeaf = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const lastSegment = toCamelCaseName(segments[segments.length - 1]);
|
||||
const currentPageLeaf =
|
||||
hasLeaf && leaf && typeof leaf === 'object' && !Array.isArray(leaf)
|
||||
? leaf
|
||||
: (langData[lastSegment] && typeof langData[lastSegment] === 'object' && !Array.isArray(langData[lastSegment]) ? langData[lastSegment] : langData);
|
||||
let result = deepCloneLang(langData);
|
||||
if (currentPageLeaf !== langData) {
|
||||
// 部分语言包本身就是完整路径结构,如 { pay: { result: { title } } }。
|
||||
// 页面内通常仍会调用 $lang('title') / $lang('xxx'),所以需要把当前页面 leaf
|
||||
// 也展开到顶层,避免运行时出现缺失 key 警告。
|
||||
result = deepMergeLang(deepCloneLang(currentPageLeaf), result);
|
||||
}
|
||||
if (!result[lastSegment]) {
|
||||
// 兼容页面内按文件名命名空间调用的写法,如 point/goods_list 页面调用
|
||||
// $lang('goods_list.searchPlaceholder')。
|
||||
result[lastSegment] = deepCloneLang(currentPageLeaf);
|
||||
}
|
||||
|
||||
let namespaced = {};
|
||||
let cursor = namespaced;
|
||||
for (let i = 0; i < segments.length; i++) {
|
||||
const segment = toCamelCaseName(segments[i]);
|
||||
cursor[segment] = i === segments.length - 1 ? deepCloneLang(langData) : {};
|
||||
cursor[segment] = i === segments.length - 1 ? deepCloneLang(currentPageLeaf) : {};
|
||||
cursor = cursor[segment];
|
||||
}
|
||||
|
||||
return deepMergeLang(deepCloneLang(langData), namespaced);
|
||||
return deepMergeLang(result, namespaced);
|
||||
}
|
||||
|
||||
function tryResolveQualifiedPageField(locale, field) {
|
||||
const arr = String(field || '').split('.');
|
||||
if (arr.length < 3) return undefined;
|
||||
|
||||
for (let i = arr.length - 1; i >= 2; i--) {
|
||||
const langPathCandidates = buildLangPathCandidates(arr.slice(0, i));
|
||||
for (const langPath of langPathCandidates) {
|
||||
const pack = normalizePageLangPack(langPath, loadLangPackSync(locale, langPath));
|
||||
const value = resolveLangField(pack, arr);
|
||||
if (value !== undefined) return value;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function resolveLangField(langData, arr) {
|
||||
let temp = langData;
|
||||
for (const key of arr) {
|
||||
if (temp && typeof temp === 'object' && temp[key] !== undefined) {
|
||||
temp = temp[key];
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
return typeof temp === 'string' ? temp : undefined;
|
||||
}
|
||||
|
||||
function tryResolveLoadedPageField(locale, field) {
|
||||
const arr = String(field || '').split('.');
|
||||
if (!arr.length) return undefined;
|
||||
|
||||
let matchedValue;
|
||||
let matchedCount = 0;
|
||||
const prefix = `${locale}_`;
|
||||
for (const cacheKey in loadedLangPacks) {
|
||||
if (!Object.prototype.hasOwnProperty.call(loadedLangPacks, cacheKey)) continue;
|
||||
if (cacheKey.indexOf(prefix) !== 0) continue;
|
||||
const langPath = cacheKey.slice(prefix.length);
|
||||
if (langPath === 'common' || SHARED_LANG_PACKS.includes(langPath)) continue;
|
||||
|
||||
const pack = normalizePageLangPack(langPath, loadedLangPacks[cacheKey]);
|
||||
const value = resolveLangField(pack, arr);
|
||||
if (value !== undefined) {
|
||||
matchedValue = value;
|
||||
matchedCount++;
|
||||
if (matchedCount > 1) return undefined;
|
||||
}
|
||||
}
|
||||
return matchedCount === 1 ? matchedValue : undefined;
|
||||
}
|
||||
|
||||
// 清除所有语言缓存(切语言/切租户/远端覆写更新时调用)
|
||||
@@ -848,6 +953,16 @@ export default {
|
||||
value = (raw !== undefined && typeof raw === 'string') ? raw : field;
|
||||
}
|
||||
|
||||
if (value === field && arr.length >= 3) {
|
||||
const qualifiedValue = tryResolveQualifiedPageField(locale, field);
|
||||
if (qualifiedValue !== undefined) value = qualifiedValue;
|
||||
}
|
||||
|
||||
if (value === field && field !== 'title') {
|
||||
const loadedPageValue = tryResolveLoadedPageField(locale, field);
|
||||
if (loadedPageValue !== undefined) value = loadedPageValue;
|
||||
}
|
||||
|
||||
// 将结果存入字段缓存(不含参数替换部分:参数每次可能不同)
|
||||
_fieldResultCache[fieldCacheKey] = value;
|
||||
|
||||
|
||||
@@ -1,11 +1,27 @@
|
||||
export const lang = {
|
||||
title: 'Business Card',
|
||||
|
||||
// Section titles
|
||||
enterpriseFiles: 'Enterprise Files',
|
||||
enterpriseVideos: 'Enterprise Videos',
|
||||
onlineMessage: 'Online Message',
|
||||
exclusiveCustomerService: 'Exclusive Customer Service',
|
||||
|
||||
// Share
|
||||
contactUs: 'Contact Us',
|
||||
share: 'Share',
|
||||
call: 'Call',
|
||||
copy: 'Copy',
|
||||
oneClickNavigation: 'Navigation',
|
||||
|
||||
// Message form
|
||||
name: 'Name',
|
||||
pleaseEnterName: 'Please enter your name',
|
||||
contactInfo: 'Contact Info',
|
||||
pleaseEnterMobile: 'Please enter your mobile number',
|
||||
messageContent: 'Message',
|
||||
pleaseEnterMessage: 'Please enter your message',
|
||||
submit: 'Submit',
|
||||
|
||||
// Toast messages
|
||||
videoUrlNotExist: 'Video URL does not exist',
|
||||
@@ -14,5 +30,7 @@ export const lang = {
|
||||
pleaseFillMessage: 'Please fill in the message content',
|
||||
submitSuccess: 'Submitted successfully',
|
||||
submitFailed: 'Submission failed, please try again',
|
||||
copySuccess: 'Copied successfully',
|
||||
copyFailed: 'Copy failed',
|
||||
|
||||
}
|
||||
|
||||
3
lang/en-us/empty/index.js
Normal file
3
lang/en-us/empty/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export const lang = {
|
||||
title: ''
|
||||
};
|
||||
@@ -3,6 +3,7 @@ export const lang = {
|
||||
// Form page - formdata
|
||||
form: {
|
||||
formdata: {
|
||||
title: 'Form Details',
|
||||
// Empty state messages
|
||||
submitSuccess: 'Submitted successfully',
|
||||
noFormInfo: 'Form information not available',
|
||||
|
||||
3
lang/en-us/index/diy.js
Normal file
3
lang/en-us/index/diy.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export const lang = {
|
||||
title: ''
|
||||
};
|
||||
@@ -1,4 +1,8 @@
|
||||
export const lang = {
|
||||
title: 'Find Password',
|
||||
accountPlaceholder: 'Please enter mobile number',
|
||||
captchaPlaceholder: 'Please enter verification code',
|
||||
next: 'Next',
|
||||
// Find Password
|
||||
find: {
|
||||
// Step titles
|
||||
|
||||
3
lang/en-us/member/card_agreement.js
Normal file
3
lang/en-us/member/card_agreement.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export const lang = {
|
||||
title: 'Member Card Agreement'
|
||||
};
|
||||
@@ -36,4 +36,9 @@ export const lang = {
|
||||
reset: 'Reset',
|
||||
confirm: 'Confirm',
|
||||
},
|
||||
|
||||
modal: {
|
||||
title: 'Tip',
|
||||
merchantNotExist: 'Merchant does not exist',
|
||||
},
|
||||
}
|
||||
|
||||
7
lang/en-us/order/refund_goods_select.js
Normal file
7
lang/en-us/order/refund_goods_select.js
Normal file
@@ -0,0 +1,7 @@
|
||||
export const lang = {
|
||||
title: 'Select Refund Items',
|
||||
totalItems: '{count} items total',
|
||||
next: 'Next',
|
||||
selectGoods: 'Please select goods',
|
||||
orderInfoNotFound: 'Order information not found'
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
export const lang = {
|
||||
pay: {
|
||||
cashier: {
|
||||
title: 'Cashier',
|
||||
// Pay button
|
||||
confirmPay: 'Confirm Payment',
|
||||
// Empty states
|
||||
|
||||
10
lang/en-us/pay/wx_pay.js
Normal file
10
lang/en-us/pay/wx_pay.js
Normal file
@@ -0,0 +1,10 @@
|
||||
export const lang = {
|
||||
title: 'WeChat Pay',
|
||||
openInBrowserTip: 'Tap the top-right menu and open in browser',
|
||||
pay: {
|
||||
wxPay: {
|
||||
title: 'WeChat Pay',
|
||||
openInBrowserTip: 'Tap the top-right menu and open in browser'
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,4 +1,6 @@
|
||||
export const lang = {
|
||||
title: 'Point Products',
|
||||
|
||||
// Search
|
||||
searchPlaceholder: 'Please enter product keywords',
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ export const lang = {
|
||||
seal: {
|
||||
medium: {
|
||||
search: {
|
||||
title: 'Medium Material Search',
|
||||
// Search input placeholder
|
||||
placeholder: 'Enter keywords to search',
|
||||
// Search results title
|
||||
|
||||
3
lang/en-us/webview/webview.js
Normal file
3
lang/en-us/webview/webview.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export const lang = {
|
||||
title: ''
|
||||
};
|
||||
@@ -1,11 +1,27 @@
|
||||
export const lang = {
|
||||
title: '电子名片',
|
||||
|
||||
// 区块标题
|
||||
enterpriseFiles: '企业文件',
|
||||
enterpriseVideos: '企业视频',
|
||||
onlineMessage: '在线留言',
|
||||
exclusiveCustomerService: '专属客服',
|
||||
|
||||
// 分享
|
||||
contactUs: '联系我们',
|
||||
share: '分享',
|
||||
call: '拨打',
|
||||
copy: '复制',
|
||||
oneClickNavigation: '一键导航',
|
||||
|
||||
// 留言表单
|
||||
name: '姓名',
|
||||
pleaseEnterName: '请输入姓名',
|
||||
contactInfo: '联系方式',
|
||||
pleaseEnterMobile: '请输入手机号',
|
||||
messageContent: '留言内容',
|
||||
pleaseEnterMessage: '请输入留言内容',
|
||||
submit: '提交',
|
||||
|
||||
// 提示信息
|
||||
videoUrlNotExist: '视频地址不存在',
|
||||
@@ -14,5 +30,7 @@ export const lang = {
|
||||
pleaseFillMessage: '请填写留言内容',
|
||||
submitSuccess: '提交成功',
|
||||
submitFailed: '提交失败,请重试',
|
||||
copySuccess: '复制成功',
|
||||
copyFailed: '复制失败',
|
||||
|
||||
}
|
||||
|
||||
3
lang/zh-cn/empty/index.js
Normal file
3
lang/zh-cn/empty/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export const lang = {
|
||||
title: ''
|
||||
};
|
||||
@@ -3,6 +3,7 @@ export const lang = {
|
||||
// 表单页面 - formdata
|
||||
form: {
|
||||
formdata: {
|
||||
title: '表单详情',
|
||||
// 空状态提示
|
||||
submitSuccess: '提交成功',
|
||||
noFormInfo: '未获取到表单信息',
|
||||
|
||||
3
lang/zh-cn/index/diy.js
Normal file
3
lang/zh-cn/index/diy.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export const lang = {
|
||||
title: ''
|
||||
};
|
||||
@@ -1,4 +1,8 @@
|
||||
export const lang = {
|
||||
title: '找回密码',
|
||||
accountPlaceholder: '请输入手机号',
|
||||
captchaPlaceholder: '请输入验证码',
|
||||
next: '下一步',
|
||||
// 找回密码
|
||||
find: {
|
||||
// 步骤标题
|
||||
|
||||
3
lang/zh-cn/member/card_agreement.js
Normal file
3
lang/zh-cn/member/card_agreement.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export const lang = {
|
||||
title: '会员卡协议'
|
||||
};
|
||||
@@ -36,4 +36,9 @@ export const lang = {
|
||||
reset: '重置',
|
||||
confirm: '确定',
|
||||
},
|
||||
|
||||
modal: {
|
||||
title: '提示',
|
||||
merchantNotExist: '商家不存在',
|
||||
},
|
||||
}
|
||||
|
||||
7
lang/zh-cn/order/refund_goods_select.js
Normal file
7
lang/zh-cn/order/refund_goods_select.js
Normal file
@@ -0,0 +1,7 @@
|
||||
export const lang = {
|
||||
title: '选择退款商品',
|
||||
totalItems: '共{count}件商品',
|
||||
next: '下一步',
|
||||
selectGoods: '请选择商品',
|
||||
orderInfoNotFound: '未查找到订单信息'
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
export const lang = {
|
||||
pay: {
|
||||
cashier: {
|
||||
title: '收银台',
|
||||
// 支付按钮
|
||||
confirmPay: '确认支付',
|
||||
// 空状态
|
||||
|
||||
10
lang/zh-cn/pay/wx_pay.js
Normal file
10
lang/zh-cn/pay/wx_pay.js
Normal file
@@ -0,0 +1,10 @@
|
||||
export const lang = {
|
||||
title: '微信支付',
|
||||
openInBrowserTip: '点击右上角跳转到浏览器打开',
|
||||
pay: {
|
||||
wxPay: {
|
||||
title: '微信支付',
|
||||
openInBrowserTip: '点击右上角跳转到浏览器打开'
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,4 +1,6 @@
|
||||
export const lang = {
|
||||
title: '积分商品',
|
||||
|
||||
// 搜索
|
||||
searchPlaceholder: '请输入商品关键词',
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ export const lang = {
|
||||
seal: {
|
||||
medium: {
|
||||
search: {
|
||||
title: '介质材料搜索',
|
||||
// 搜索框占位符
|
||||
placeholder: '请输入关键词搜索',
|
||||
// 搜索结果标题
|
||||
|
||||
3
lang/zh-cn/webview/webview.js
Normal file
3
lang/zh-cn/webview/webview.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export const lang = {
|
||||
title: ''
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<view class="goods-select" :style="themeColor">
|
||||
<view class="top">
|
||||
共<text class="color-base-text">{{refund_data.length}}</text>件商品
|
||||
{{ $lang('totalItems', { count: refund_data.length }) }}
|
||||
</view>
|
||||
|
||||
<view class="body">
|
||||
@@ -24,8 +24,8 @@
|
||||
<text v-else class="iconfont icon-yuan_checkbox"></text> -->
|
||||
</view>
|
||||
|
||||
<view v-if="nexthover" class="next" @click="next">下一步</view>
|
||||
<view v-else class="next nexthover">请选择商品</view>
|
||||
<view v-if="nexthover" class="next" @click="next">{{ $lang('next') }}</view>
|
||||
<view v-else class="next nexthover">{{ $lang('selectGoods') }}</view>
|
||||
</view>
|
||||
|
||||
<!-- <loading-cover ref="loadingCover"></loading-cover> -->
|
||||
@@ -50,7 +50,7 @@
|
||||
this.getGoodsInfo()
|
||||
} else{
|
||||
uni.showToast({
|
||||
title:'未查找到订单信息',
|
||||
title: this.$lang('orderInfoNotFound'),
|
||||
icon:'none'
|
||||
})
|
||||
setTimeout(()=>{
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<view class="mask-img">
|
||||
<image :src="$util.img('public/uniapp/pay/invite_friends_share.png')" mode="aspectFit" show-menu-by-longpress></image>
|
||||
</view>
|
||||
<view class="mask-word">点击右上角跳转到浏览器打开</view>
|
||||
<view class="mask-word">{{ $lang('pay.wxPay.openInBrowserTip') }}</view>
|
||||
</view>
|
||||
</template>
|
||||
<script>
|
||||
|
||||
Reference in New Issue
Block a user