feat: 新增多语言配置并优化语言加载逻辑

1. 新增多个页面的中英文多语言词条,包括会员卡协议、积分商品、收银台等
2. 新增退款商品选择、微信支付、电子名片等页面的语言包
3. 优化lang.js语言加载逻辑,支持蛇形命名转换和多路径匹配,完善$lang调用兼容处理
This commit is contained in:
2026-06-27 09:07:51 +08:00
parent 84f5cd4568
commit bf4fe97aa3
29 changed files with 245 additions and 8 deletions

View File

@@ -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;

View File

@@ -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',
}

View File

@@ -0,0 +1,3 @@
export const lang = {
title: ''
};

View File

@@ -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
View File

@@ -0,0 +1,3 @@
export const lang = {
title: ''
};

View File

@@ -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

View File

@@ -0,0 +1,3 @@
export const lang = {
title: 'Member Card Agreement'
};

View File

@@ -36,4 +36,9 @@ export const lang = {
reset: 'Reset',
confirm: 'Confirm',
},
modal: {
title: 'Tip',
merchantNotExist: 'Merchant does not exist',
},
}

View 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'
};

View File

@@ -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
View 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'
}
}
};

View File

@@ -1,4 +1,6 @@
export const lang = {
title: 'Point Products',
// Search
searchPlaceholder: 'Please enter product keywords',

View File

@@ -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

View File

@@ -0,0 +1,3 @@
export const lang = {
title: ''
};

View File

@@ -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: '复制失败',
}

View File

@@ -0,0 +1,3 @@
export const lang = {
title: ''
};

View File

@@ -3,6 +3,7 @@ export const lang = {
// 表单页面 - formdata
form: {
formdata: {
title: '表单详情',
// 空状态提示
submitSuccess: '提交成功',
noFormInfo: '未获取到表单信息',

3
lang/zh-cn/index/diy.js Normal file
View File

@@ -0,0 +1,3 @@
export const lang = {
title: ''
};

View File

@@ -1,4 +1,8 @@
export const lang = {
title: '找回密码',
accountPlaceholder: '请输入手机号',
captchaPlaceholder: '请输入验证码',
next: '下一步',
// 找回密码
find: {
// 步骤标题

View File

@@ -0,0 +1,3 @@
export const lang = {
title: '会员卡协议'
};

View File

@@ -36,4 +36,9 @@ export const lang = {
reset: '重置',
confirm: '确定',
},
modal: {
title: '提示',
merchantNotExist: '商家不存在',
},
}

View File

@@ -0,0 +1,7 @@
export const lang = {
title: '选择退款商品',
totalItems: '共{count}件商品',
next: '下一步',
selectGoods: '请选择商品',
orderInfoNotFound: '未查找到订单信息'
};

View File

@@ -1,6 +1,7 @@
export const lang = {
pay: {
cashier: {
title: '收银台',
// 支付按钮
confirmPay: '确认支付',
// 空状态

10
lang/zh-cn/pay/wx_pay.js Normal file
View File

@@ -0,0 +1,10 @@
export const lang = {
title: '微信支付',
openInBrowserTip: '点击右上角跳转到浏览器打开',
pay: {
wxPay: {
title: '微信支付',
openInBrowserTip: '点击右上角跳转到浏览器打开'
}
}
};

View File

@@ -1,4 +1,6 @@
export const lang = {
title: '积分商品',
// 搜索
searchPlaceholder: '请输入商品关键词',

View File

@@ -4,6 +4,7 @@ export const lang = {
seal: {
medium: {
search: {
title: '介质材料搜索',
// 搜索框占位符
placeholder: '请输入关键词搜索',
// 搜索结果标题

View File

@@ -0,0 +1,3 @@
export const lang = {
title: ''
};

View File

@@ -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(()=>{

View File

@@ -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>