refactor(i18n): 完成全量硬编码中文文案替换为多语言方案
1. 替换所有页面/组件中的硬编码中文提示文案为$lang调用,包括支付、帮助中心、公告等页面 2. 重构diy.js加载失败文案处理逻辑,支持参数化替换 3. 新增多语言混入逻辑,实现导航标题随语种动态切换 4. 重构支付组件初始化逻辑,优化平台适配代码 5. 添加多语言单元测试和硬编码扫描脚本 6. 更新语言包,新增缺失的翻译词条
This commit is contained in:
58
scripts/check-title-keys.js
Normal file
58
scripts/check-title-keys.js
Normal file
@@ -0,0 +1,58 @@
|
||||
// 检查 20 个硬编码导航标题的页面,其语言包是否都有 title key
|
||||
const paths = [
|
||||
['pages_goods/category','商品分类'],
|
||||
['pages_goods/cart','购物车'],
|
||||
['pages_goods/detail','商品详情'],
|
||||
['pages_goods/list','商品列表'],
|
||||
['pages_promotion/merch/merchcategory','入驻商户'],
|
||||
['pages_promotion/merch/detail','商户首页'],
|
||||
['pages_promotion/fenxiao/ranking_list','排行榜'],
|
||||
['pages_promotion/fenxiao/child_fenxiao','分销商'],
|
||||
['pages_tool/ai-chat/index','AI智能客服'],
|
||||
['pages_tool/contact/contact','电子名片'],
|
||||
['pages_tool/member/info','个人资料'],
|
||||
['pages_tool/order/activist','退款'],
|
||||
['pages_tool/login/login','登录'],
|
||||
['pages_tool/weapp/order_shipping','小程序发货'],
|
||||
['pages_tool/seal/structure','结构选型'],
|
||||
['pages_tool/seal/medium/search','介质材料搜索'],
|
||||
['pages_tool/seal/medium/detail','搜索结果'],
|
||||
['pages_tool/cases/index','案例展示'],
|
||||
['pages_tool/files/list','查看文件'],
|
||||
['pages_tool/file-preview/file-preview','文件预览'],
|
||||
];
|
||||
|
||||
function toLang(p) {
|
||||
const parts = p.split('/');
|
||||
if (parts[0] === 'pages_tool') return parts.slice(1).join('/');
|
||||
if (parts[0] === 'pages_goods') return 'goods/' + parts.slice(1).join('/');
|
||||
if (parts[0] === 'pages_member') return 'member/' + parts.slice(1).join('/');
|
||||
if (parts[0] === 'pages_order') return 'order/' + parts.slice(1).join('/');
|
||||
if (parts[0] === 'pages_promotion') {
|
||||
if (['point','fenxiao','merch'].includes(parts[1])) return parts.slice(1).join('/');
|
||||
return parts.slice(1).join('/');
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
for (const [p, hardcoded] of paths) {
|
||||
const lp = toLang(p);
|
||||
for (const loc of ['zh-cn','en-us']) {
|
||||
const fp = path.join('lang', loc, lp + '.js');
|
||||
if (!fs.existsSync(fp)) {
|
||||
console.log(`MISSING ${loc}/${lp}`);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const m = require(path.resolve(fp)).lang;
|
||||
const title = m && m.title;
|
||||
const status = title === undefined ? 'NO title' : (title === hardcoded ? 'ZH-only?' : 'OK');
|
||||
console.log(`${loc}/${lp}.title = ${JSON.stringify(title)} [${status}]`);
|
||||
} catch (e) {
|
||||
console.log(`ERR ${loc}/${lp}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
264
scripts/hardcoded_cn_real.json
Normal file
264
scripts/hardcoded_cn_real.json
Normal file
@@ -0,0 +1,264 @@
|
||||
{
|
||||
"uni_modules/uni-calendar/components/uni-calendar/uni-calendar.vue": [
|
||||
{
|
||||
"kind": "template",
|
||||
"tag": "text",
|
||||
"text": "取消",
|
||||
"line": 6
|
||||
},
|
||||
{
|
||||
"kind": "template",
|
||||
"tag": "text",
|
||||
"text": "确定",
|
||||
"line": 7
|
||||
},
|
||||
{
|
||||
"kind": "template",
|
||||
"tag": "text",
|
||||
"text": "{{ (nowDate.year || '') + '年' + (nowDate.month || '') + '月' }}",
|
||||
"line": 15
|
||||
},
|
||||
{
|
||||
"kind": "template",
|
||||
"tag": "text",
|
||||
"text": "日",
|
||||
"line": 27
|
||||
},
|
||||
{
|
||||
"kind": "template",
|
||||
"tag": "text",
|
||||
"text": "一",
|
||||
"line": 28
|
||||
},
|
||||
{
|
||||
"kind": "template",
|
||||
"tag": "text",
|
||||
"text": "二",
|
||||
"line": 29
|
||||
},
|
||||
{
|
||||
"kind": "template",
|
||||
"tag": "text",
|
||||
"text": "三",
|
||||
"line": 30
|
||||
},
|
||||
{
|
||||
"kind": "template",
|
||||
"tag": "text",
|
||||
"text": "四",
|
||||
"line": 31
|
||||
},
|
||||
{
|
||||
"kind": "template",
|
||||
"tag": "text",
|
||||
"text": "五",
|
||||
"line": 32
|
||||
},
|
||||
{
|
||||
"kind": "template",
|
||||
"tag": "text",
|
||||
"text": "六",
|
||||
"line": 33
|
||||
}
|
||||
],
|
||||
"pages_promotion/fenxiao/withdraw_list.vue": [
|
||||
{
|
||||
"kind": "js-text",
|
||||
"match": "已转账",
|
||||
"line": 45
|
||||
},
|
||||
{
|
||||
"kind": "js-text",
|
||||
"match": "待审核",
|
||||
"line": 49
|
||||
},
|
||||
{
|
||||
"kind": "js-text",
|
||||
"match": "已审核",
|
||||
"line": 53
|
||||
},
|
||||
{
|
||||
"kind": "js-text",
|
||||
"match": "已拒绝",
|
||||
"line": 57
|
||||
}
|
||||
],
|
||||
"uni_modules/mp-html/components/mp-html/node/node.vue": [
|
||||
{
|
||||
"kind": "js-text",
|
||||
"match": "uni.showActionSheet({\n itemList: ['保存图片'],\n success: () => {\n const save = path => {\n uni.saveImageToPhotosAlbum({\n ",
|
||||
"line": 289
|
||||
},
|
||||
{
|
||||
"kind": "js-text",
|
||||
"match": "uni.showToast({\n title: '链接已复制'\n })",
|
||||
"line": 368
|
||||
},
|
||||
{
|
||||
"kind": "js-text",
|
||||
"match": "保存成功",
|
||||
"line": 297
|
||||
},
|
||||
{
|
||||
"kind": "js-text",
|
||||
"match": "链接已复制",
|
||||
"line": 369
|
||||
}
|
||||
],
|
||||
"components-diy/diy-form.vue": [
|
||||
{
|
||||
"kind": "js-text",
|
||||
"match": "uni.showToast({\n\t\t\t\t\t\t\ttitle: '提交成功',\n\t\t\t\t\t\t\ticon: 'success'\n\t\t\t\t\t\t})",
|
||||
"line": 305
|
||||
},
|
||||
{
|
||||
"kind": "js-text",
|
||||
"match": "uni.showToast({\n\t\t\t\t\t\t\ttitle: res.message || '提交失败',\n\t\t\t\t\t\t\ticon: 'none'\n\t\t\t\t\t\t})",
|
||||
"line": 314
|
||||
},
|
||||
{
|
||||
"kind": "js-text",
|
||||
"match": "提交成功",
|
||||
"line": 306
|
||||
}
|
||||
],
|
||||
"uni_modules/uni-count-down/components/uni-count-down/uni-count-down.vue": [
|
||||
{
|
||||
"kind": "template",
|
||||
"tag": "view",
|
||||
"text": "{{ showColon ? ':' : '时' }}",
|
||||
"line": 19
|
||||
},
|
||||
{
|
||||
"kind": "template",
|
||||
"tag": "view",
|
||||
"text": "{{ showColon ? ':' : '分' }}",
|
||||
"line": 27
|
||||
},
|
||||
{
|
||||
"kind": "template",
|
||||
"tag": "view",
|
||||
"text": "秒",
|
||||
"line": 35
|
||||
}
|
||||
],
|
||||
"components/ns-goods-recommend/ns-goods-recommend.vue": [
|
||||
{
|
||||
"kind": "js-text",
|
||||
"match": "猜你喜欢",
|
||||
"line": 13
|
||||
},
|
||||
{
|
||||
"kind": "js-text",
|
||||
"match": "购买",
|
||||
"line": 20
|
||||
}
|
||||
],
|
||||
"components-diy/diy-channel-list.vue": [
|
||||
{
|
||||
"kind": "js-text",
|
||||
"match": "uni.showToast({\n\t\t\t\t\t\t\t\ttitle: '视频号仅支持在微信小程序环境中播放',\n\t\t\t\t\t\t\t\ticon: 'none',\n\t\t\t\t\t\t\t\tduration: 2000\n\t\t\t\t\t\t\t})",
|
||||
"line": 207
|
||||
},
|
||||
{
|
||||
"kind": "js-text",
|
||||
"match": "视频号仅支持在微信小程序环境中播放",
|
||||
"line": 208
|
||||
}
|
||||
],
|
||||
"pages/index/public/js/index.js": [
|
||||
{
|
||||
"kind": "js-text",
|
||||
"match": "uni.showModal({\n\t\t\t\t\ttitle: '版本信息',\n\t\t\t\t\tcontent: `提交 ID: ${v.commitId}\\n构建时间: ${v.buildTime}\\n${Config.baseUrl}`,\n\t\t\t\t\tshowCancel: false\n\t\t\t\t})",
|
||||
"line": 160
|
||||
},
|
||||
{
|
||||
"kind": "js-text",
|
||||
"match": "版本信息",
|
||||
"line": 161
|
||||
}
|
||||
],
|
||||
"uni_modules/uni-datetime-picker/components/uni-datetime-picker/calendar.vue": [
|
||||
{
|
||||
"kind": "template",
|
||||
"tag": "text",
|
||||
"text": "{{ (nowDate.year||'') + ' 年 ' + ( nowDate.month||'') +' 月'}}",
|
||||
"line": 13
|
||||
},
|
||||
{
|
||||
"kind": "template",
|
||||
"tag": "view",
|
||||
"text": "确认",
|
||||
"line": 87
|
||||
}
|
||||
],
|
||||
"uni_modules/uni-number-box/components/uni-number-box/uni-number-box.vue": [
|
||||
{
|
||||
"kind": "js-text",
|
||||
"match": "商品库存不足",
|
||||
"line": 131
|
||||
},
|
||||
{
|
||||
"kind": "js-text",
|
||||
"match": "商品最少购买",
|
||||
"line": 135
|
||||
}
|
||||
],
|
||||
"common/js/mp-share-default.js": [
|
||||
{
|
||||
"kind": "js-text",
|
||||
"match": "小程序分享",
|
||||
"line": 85
|
||||
}
|
||||
],
|
||||
"common/js/splash-state.js": [
|
||||
{
|
||||
"kind": "js-text",
|
||||
"match": "uni.showLoading({ title: opts.title || '加载中...', mask: false })",
|
||||
"line": 111
|
||||
}
|
||||
],
|
||||
"pages/index/index.vue": [
|
||||
{
|
||||
"kind": "template",
|
||||
"tag": "text",
|
||||
"text": "{{ diyLoadErrorMsg || '网络异常,请稍后重试' }}",
|
||||
"line": 8
|
||||
}
|
||||
],
|
||||
"pages_goods/public/js/list.js": [
|
||||
{
|
||||
"kind": "js-text",
|
||||
"match": "购买",
|
||||
"line": 40
|
||||
}
|
||||
],
|
||||
"pages_tool/help/list.vue": [
|
||||
{
|
||||
"kind": "js-text",
|
||||
"match": "帮助中心",
|
||||
"line": 82
|
||||
}
|
||||
],
|
||||
"pages_tool/notice/list.vue": [
|
||||
{
|
||||
"kind": "js-text",
|
||||
"match": "公告",
|
||||
"line": 80
|
||||
}
|
||||
],
|
||||
"pages_tool/pay/index.vue": [
|
||||
{
|
||||
"kind": "js-text",
|
||||
"match": "未获取到支付信息!",
|
||||
"line": 54
|
||||
}
|
||||
],
|
||||
"uni_modules/uni-datetime-picker/components/uni-datetime-picker/time-picker.vue": [
|
||||
{
|
||||
"kind": "js-text",
|
||||
"match": "日期和时间",
|
||||
"line": 112
|
||||
}
|
||||
]
|
||||
}
|
||||
7804
scripts/hardcoded_cn_scan.json
Normal file
7804
scripts/hardcoded_cn_scan.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,14 +1,77 @@
|
||||
{
|
||||
"bundle_version": "1782901586",
|
||||
"bundle_version": "1784767380",
|
||||
"bundle_lang": "zh-cn",
|
||||
"bundle_leaves": 2649,
|
||||
"code_calls_total": 2677,
|
||||
"code_unique_keys": 2083,
|
||||
"found_in_bundle": 2082,
|
||||
"bundle_leaves": 2659,
|
||||
"code_calls_total": 2693,
|
||||
"code_unique_keys": 2097,
|
||||
"found_in_bundle": 2086,
|
||||
"missing_summary": {
|
||||
"TRULY_MISSING": 12,
|
||||
"SYNTAX_BUG": 1
|
||||
},
|
||||
"missing_classified": {
|
||||
"TRULY_MISSING": [
|
||||
{
|
||||
"key": "common.buyNow",
|
||||
"file": "components\\ns-goods-recommend\\ns-goods-recommend.vue",
|
||||
"reason": "not in bundle, not local"
|
||||
},
|
||||
{
|
||||
"key": "wechatChannel.wechatMpOnly",
|
||||
"file": "components-diy\\diy-channel-list.vue",
|
||||
"reason": "not in bundle, not local"
|
||||
},
|
||||
{
|
||||
"key": "common.submitSuccess",
|
||||
"file": "components-diy\\diy-form.vue",
|
||||
"reason": "not in bundle, not local"
|
||||
},
|
||||
{
|
||||
"key": "submitFail",
|
||||
"file": "components-diy\\diy-form.vue",
|
||||
"reason": "not in bundle, not local"
|
||||
},
|
||||
{
|
||||
"key": "diy.networkError",
|
||||
"file": "pages\\index\\index.vue",
|
||||
"reason": "not in bundle, not local"
|
||||
},
|
||||
{
|
||||
"key": "versionInfo",
|
||||
"file": "pages\\index\\public\\js\\index.js",
|
||||
"reason": "not in bundle, not local"
|
||||
},
|
||||
{
|
||||
"key": "commitId",
|
||||
"file": "pages\\index\\public\\js\\index.js",
|
||||
"reason": "not in bundle, not local"
|
||||
},
|
||||
{
|
||||
"key": "buildTime",
|
||||
"file": "pages\\index\\public\\js\\index.js",
|
||||
"reason": "not in bundle, not local"
|
||||
},
|
||||
{
|
||||
"key": "diy.networkError",
|
||||
"file": "pages_goods\\category.vue",
|
||||
"reason": "not in bundle, not local"
|
||||
},
|
||||
{
|
||||
"key": "goods.detail.formEmptyTip",
|
||||
"file": "pages_goods\\detail.vue",
|
||||
"reason": "not in bundle, not local"
|
||||
},
|
||||
{
|
||||
"key": "common.buyNow",
|
||||
"file": "pages_goods\\public\\js\\list.js",
|
||||
"reason": "not in bundle, not local"
|
||||
},
|
||||
{
|
||||
"key": "pay.index.payInfoNotFound",
|
||||
"file": "pages_tool\\pay\\index.vue",
|
||||
"reason": "not in bundle, not local"
|
||||
}
|
||||
],
|
||||
"SYNTAX_BUG": [
|
||||
{
|
||||
"key": "order.list.status.",
|
||||
@@ -19,18 +82,8 @@
|
||||
},
|
||||
"placeholder_issues": [],
|
||||
"data_lang_issues": {
|
||||
"components\\ns-payment\\ns-payment.vue": [
|
||||
"components.nsPayment.alipay"
|
||||
],
|
||||
"pages_promotion\\fenxiao\\apply.vue": [
|
||||
"fenxiao.apply.pleaseEnterName"
|
||||
],
|
||||
"pages_promotion\\merch\\public\\js\\list.js": [
|
||||
"merch.list.buyText"
|
||||
],
|
||||
"pages_tool\\ai-chat\\ai-chat-message.vue": [
|
||||
"ai-chat.index.loadMoreHistory",
|
||||
"ai-chat.index.defaultNickname"
|
||||
]
|
||||
},
|
||||
"hardcoded_navbar_titles": [
|
||||
@@ -121,6 +174,11 @@
|
||||
"common.tabBar.cart",
|
||||
"common.tabBar.member"
|
||||
],
|
||||
"local_only_keys": [],
|
||||
"local_only_keys": [
|
||||
"buildTime",
|
||||
"commitId",
|
||||
"submitFail",
|
||||
"versionInfo"
|
||||
],
|
||||
"bundle_only_keys_in_unique": []
|
||||
}
|
||||
80
scripts/scan-hardcoded-cn.py
Normal file
80
scripts/scan-hardcoded-cn.py
Normal file
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env python3
|
||||
# 精筛 v2:剥离 HTML 注释和 JS 注释后,再扫硬编码中文(只关注真实会被渲染的)
|
||||
import os, re, json
|
||||
from collections import defaultdict
|
||||
|
||||
ROOT = r'D:\projects\shop-projects\frontend'
|
||||
SKIP_DIR = {'node_modules', '.unpackage', 'unpackage', '.git', 'dist', 'stub-app-projects', 'lang', 'tests', 'docs', 'scripts', 'common/mixin'}
|
||||
|
||||
CN = re.compile(r'[\u4e00-\u9fff]')
|
||||
LANG_RE = re.compile(r'\$lang\(')
|
||||
|
||||
TPL_RE = re.compile(r'<template[^>]*>([\s\S]*?)</template>', re.I)
|
||||
TAG_RE = re.compile(r'<([a-zA-Z][\w-]*)\b[^>]*>([^<>\n]+)</\1>')
|
||||
HTML_COMMENT_RE = re.compile(r'<!--[\s\S]*?-->')
|
||||
|
||||
JS_TRIGGERS = [
|
||||
re.compile(r'uni\.show(?:Toast|Modal|ActionSheet|Loading)\s*\(\s*\{[\s\S]*?\}\s*\)', re.S),
|
||||
re.compile(r'wx\.show(?:Toast|Modal|ActionSheet|Loading)\s*\(\s*\{[\s\S]*?\}\s*\)', re.S),
|
||||
re.compile(r'\btitle\s*:\s*[\'"]([^\'"]*?)[\'"]'),
|
||||
re.compile(r'\bcontent\s*:\s*[\'"]([^\'"]*?)[\'"]'),
|
||||
re.compile(r'\btext\s*:\s*[\'"]([^\'"]*?)[\'"]'),
|
||||
re.compile(r'\bplaceholder\s*:\s*[\'"]([^\'"]*?)[\'"]'),
|
||||
re.compile(r'\balert\s*\(\s*[\'"]([^\'"]*?)[\'"]'),
|
||||
re.compile(r'\bconfirm\s*\(\s*[\'"]([^\'"]*?)[\'"]'),
|
||||
]
|
||||
|
||||
def strip_all_comments(text, is_vue=False):
|
||||
text = HTML_COMMENT_RE.sub(lambda m: ' ' * len(m.group()), text)
|
||||
text = re.sub(r'/\*[\s\S]*?\*/', lambda m: ' ' * len(m.group()), text)
|
||||
text = re.sub(r'//[^\n]*', lambda m: ' ' * len(m.group()), text)
|
||||
return text
|
||||
|
||||
results = defaultdict(list)
|
||||
|
||||
for dirpath, dirnames, filenames in os.walk(ROOT):
|
||||
if any(s in dirpath.replace('\\', '/') for s in SKIP_DIR):
|
||||
continue
|
||||
for fn in filenames:
|
||||
if not fn.endswith(('.vue', '.js')):
|
||||
continue
|
||||
fp = os.path.join(dirpath, fn)
|
||||
rel = os.path.relpath(fp, ROOT).replace('\\', '/')
|
||||
try:
|
||||
content = open(fp, 'r', encoding='utf-8').read()
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
|
||||
stripped = strip_all_comments(content)
|
||||
|
||||
if fn.endswith('.vue'):
|
||||
for m in TPL_RE.finditer(stripped):
|
||||
tpl = m.group(1)
|
||||
for tag in TAG_RE.finditer(tpl):
|
||||
inner = tag.group(2).strip()
|
||||
if CN.search(inner) and not LANG_RE.search(inner):
|
||||
line_no = stripped[:m.start(1) + tpl.find(inner)].count('\n') + 1
|
||||
results[rel].append({'kind': 'template', 'tag': tag.group(1), 'text': inner[:160], 'line': line_no})
|
||||
|
||||
for trig in JS_TRIGGERS:
|
||||
for m in trig.finditer(stripped):
|
||||
text = (m.group(1) if m.lastindex else m.group(0))
|
||||
if not CN.search(text):
|
||||
continue
|
||||
if LANG_RE.search(text):
|
||||
continue
|
||||
line_no = stripped[:m.start()].count('\n') + 1
|
||||
results[rel].append({'kind': 'js-text', 'match': text[:160], 'line': line_no})
|
||||
|
||||
ranked = sorted(results.items(), key=lambda kv: -len(kv[1]))
|
||||
print(f'total files: {len(ranked)}')
|
||||
print(f'total hits: {sum(len(v) for v in ranked)}')
|
||||
|
||||
out = []
|
||||
for f, hits in ranked[:60]:
|
||||
out.append({'file': f, 'hits': len(hits), 'samples': hits[:3]})
|
||||
print(json.dumps(out, ensure_ascii=False, indent=2))
|
||||
|
||||
with open(os.path.join(ROOT, 'scripts', 'hardcoded_cn_real.json'), 'w', encoding='utf-8') as f:
|
||||
json.dump({k: v for k, v in ranked}, f, ensure_ascii=False, indent=2)
|
||||
print('saved hardcoded_cn_real.json')
|
||||
Reference in New Issue
Block a user