td-octopus 0.2.4 → 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +15 -0
- package/package.json +4 -2
- package/src/back/index.js +9 -3
- package/src/pop/index.js +4 -4
- package/src/transLang/single.js +545 -22
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Bash(node -e \"\nconst { transformToChinese } = require\\('./src/transLang/single.js'\\);\n\nconst testCode = \\\\`\nimport I18N from '@/utils/I18N';\n\nconst immuneTimePicker = \\(field\\) => {\n let params = {\n dateMap: {\n cn: {\n [I18N.trade.eventdetail.zuiJinTian4]: [],\n [I18N.lang.timepicker.zuiJinTian4]: [],\n [I18N.lang.timepicker.zuiJinTian3]: [],\n [I18N.lang.timepicker.zuiJinTian2]: [],\n [I18N.lang.timepicker.zuiJinTian]: []\n },\n en: {\n 'Recent 1 days': [],\n 'Recent 7 days': [],\n 'Recent 30 days': [],\n 'Recent 90 days': [],\n 'Recent 180 days': []\n }\n }\n };\n return params[field]['cn'];\n};\n\\\\`;\n\nconst globalList = {};\ntransformToChinese\\(testCode, globalList\\);\nconsole.log\\('结果:', JSON.stringify\\(globalList, null, 2\\)\\);\n\")",
|
|
5
|
+
"Bash(node debug-trans.js)",
|
|
6
|
+
"Bash(node debug-switch.js)",
|
|
7
|
+
"Bash(node -e \"\nconst { transformToChinese } = require\\('./src/transLang/single.js'\\);\n\nconst testCode = \\\\`\nimport I18N from '@/utils/I18N';\n\nconst getDeleteInfo = \\(name\\) => {\n let cn = I18N.lang.common.queRenShanChu + name + I18N.lang.common.ma2;\n let en = 'Confirm to delete \\\\\"' + name + '\\\\\"?';\n\n return cn;\n};\n\\\\`;\n\nconst globalList = {};\ntransformToChinese\\(testCode, globalList\\);\nconsole.log\\('结果:', JSON.stringify\\(globalList, null, 2\\)\\);\n\")",
|
|
8
|
+
"Bash(node -e \"\nconst { transformToChinese } = require\\('./src/transLang/single.js'\\);\n\nconst testCode = \\\\`\nimport I18N from '@/utils/I18N';\n\nconst test1 = \\(\\) => {\n // 单个 I18N 引用的拼接\n let cn = I18N.lang.common.hello + name;\n let en = 'Hello ' + name;\n return cn;\n};\n\nconst test2 = \\(\\) => {\n // 多个 I18N 引用\n let cn = I18N.lang.common.start + value + I18N.lang.common.end;\n let en = 'Start: ' + value + ' End';\n return cn;\n};\n\\\\`;\n\nconst globalList = {};\ntransformToChinese\\(testCode, globalList\\);\nconsole.log\\('结果:', JSON.stringify\\(globalList, null, 2\\)\\);\n\")",
|
|
9
|
+
"Bash(ping -c 3 10.57.80.253)",
|
|
10
|
+
"Bash(git config --list | grep -i \"url\\\\|proxy\\\\|insteadof\")",
|
|
11
|
+
"Bash(nslookup ssh.github.com)",
|
|
12
|
+
"Bash(ssh -T git@github.com 2>&1)"
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "td-octopus",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
4
4
|
"description": "I18N tool",
|
|
5
5
|
"author": "Anthony Li",
|
|
6
6
|
"bin": {
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@babel/core": "^7.28.6",
|
|
12
12
|
"@babel/parser": "^7.28.6",
|
|
13
|
+
"@babel/plugin-proposal-class-properties": "^7.18.6",
|
|
13
14
|
"@babel/traverse": "^7.28.6",
|
|
14
15
|
"@babel/types": "^7.28.6",
|
|
15
16
|
"axios": "^1.6.5",
|
|
@@ -34,7 +35,8 @@
|
|
|
34
35
|
"update-notifier": "^4.1.0",
|
|
35
36
|
"vue-template-compiler": "^2.6.14",
|
|
36
37
|
"xlsx": "0.16.9",
|
|
37
|
-
"yargs": "^15.3.1"
|
|
38
|
+
"yargs": "^15.3.1",
|
|
39
|
+
"@babel/plugin-proposal-decorators": "^7.29.0"
|
|
38
40
|
},
|
|
39
41
|
"keywords": [
|
|
40
42
|
"I18N",
|
package/src/back/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* @Author: 郑泳健
|
|
4
4
|
* @Date: 2022-06-01 13:56:18
|
|
5
5
|
* @LastEditors: 郑泳健
|
|
6
|
-
* @LastEditTime:
|
|
6
|
+
* @LastEditTime: 2026-03-03 14:18:16
|
|
7
7
|
*/
|
|
8
8
|
const path = require('path')
|
|
9
9
|
const fs = require('fs')
|
|
@@ -63,6 +63,12 @@ function getFilePaths() {
|
|
|
63
63
|
*/
|
|
64
64
|
function getTemplateValue(str) {
|
|
65
65
|
try {
|
|
66
|
+
if (!str) return {};
|
|
67
|
+
|
|
68
|
+
// 移除外层的花括号,获取对象内容
|
|
69
|
+
const objStr = str.trim().replace(/^{/, '').replace(/}$/, '').trim();
|
|
70
|
+
if (!objStr) return {};
|
|
71
|
+
|
|
66
72
|
const result = {};
|
|
67
73
|
const regex = /(\w+)\s*:\s*/g;
|
|
68
74
|
let match;
|
|
@@ -77,8 +83,8 @@ function getTemplateValue(str) {
|
|
|
77
83
|
const char = objStr[i];
|
|
78
84
|
if (char === '(') bracketCount++;
|
|
79
85
|
if (char === ')') bracketCount--;
|
|
80
|
-
if ((char === ',' && bracketCount === 0) ||
|
|
81
|
-
valueEnd = i;
|
|
86
|
+
if ((char === ',' && bracketCount === 0) || i === objStr.length - 1) {
|
|
87
|
+
valueEnd = i === objStr.length - 1 ? i + 1 : i;
|
|
82
88
|
break;
|
|
83
89
|
}
|
|
84
90
|
}
|
package/src/pop/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* @Author: 郑泳健
|
|
4
4
|
* @Date: 2024-12-12 15:00:24
|
|
5
5
|
* @LastEditors: 郑泳健
|
|
6
|
-
* @LastEditTime:
|
|
6
|
+
* @LastEditTime: 2026-02-12 10:40:03
|
|
7
7
|
*/
|
|
8
8
|
const path = require('path');
|
|
9
9
|
const fs = require('fs');
|
|
@@ -45,7 +45,7 @@ function readJsFiles(folderPath) {
|
|
|
45
45
|
} else if (path.extname(fullPath) === '.js') {
|
|
46
46
|
// 如果是 .js 文件,读取文件内容
|
|
47
47
|
let content = fs.readFileSync(fullPath, 'utf-8');
|
|
48
|
-
const ast = parse(content, { sourceType: 'module', plugins: ['jsx', 'typescript'] });
|
|
48
|
+
const ast = parse(content, { sourceType: 'module', plugins: ['decorators-legacy', 'jsx', 'typescript'] });
|
|
49
49
|
const output = generate(ast, { comments: false });
|
|
50
50
|
content = output.code;
|
|
51
51
|
jsContent += `\n/* File: ${fullPath} */\n${content}\n`;
|
|
@@ -73,7 +73,7 @@ function main() {
|
|
|
73
73
|
|
|
74
74
|
if (!fs.existsSync(path.resolve(__dirname, '../octopus/zh-CN.js'))) {
|
|
75
75
|
spinner.succeed(`查询完毕,共计丢失${lostKey.length}个,请在lostI18N.js中查看`);
|
|
76
|
-
spinner.warn('请先执行otp
|
|
76
|
+
spinner.warn('请先执行otp stash');
|
|
77
77
|
return;
|
|
78
78
|
}
|
|
79
79
|
spinner.start(`查询完毕,共计丢失${lostKey.length}个,开始同步开始`);
|
|
@@ -105,7 +105,7 @@ function main() {
|
|
|
105
105
|
autoImportJSFiles(path.resolve(process.cwd(), '.octopus/zh-CN'), cnJSON);
|
|
106
106
|
autoImportJSFiles(path.resolve(process.cwd(), '.octopus/en-US'), enJSON);
|
|
107
107
|
fs.rmSync(path.resolve(__dirname, '../octopus'), { recursive: true, force: true });
|
|
108
|
-
spinner.succeed('同步完成,如果需要重新check,请先执行otp
|
|
108
|
+
spinner.succeed('同步完成,如果需要重新check,请先执行otp stash');
|
|
109
109
|
})();
|
|
110
110
|
}
|
|
111
111
|
|
package/src/transLang/single.js
CHANGED
|
@@ -24,6 +24,38 @@ function extractCnKeyDirect(node) {
|
|
|
24
24
|
return node.value;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
// 处理 I18N.template(I18N.lang.xxx.yyy, {...}) 形式
|
|
28
|
+
if (t.isCallExpression(node)) {
|
|
29
|
+
const { callee, arguments: args } = node;
|
|
30
|
+
|
|
31
|
+
// 检查是否为 I18N.template 调用
|
|
32
|
+
if (t.isMemberExpression(callee) &&
|
|
33
|
+
t.isIdentifier(callee.object, { name: 'I18N' }) &&
|
|
34
|
+
t.isIdentifier(callee.property, { name: 'template' }) &&
|
|
35
|
+
args.length >= 1) {
|
|
36
|
+
|
|
37
|
+
// 提取第一个参数(I18N.lang.xxx.yyy)
|
|
38
|
+
const firstArg = args[0];
|
|
39
|
+
if (t.isMemberExpression(firstArg)) {
|
|
40
|
+
let parts = [];
|
|
41
|
+
let current = firstArg;
|
|
42
|
+
|
|
43
|
+
while (t.isMemberExpression(current)) {
|
|
44
|
+
if (current.computed) return null;
|
|
45
|
+
const prop = current.property;
|
|
46
|
+
if (!t.isIdentifier(prop)) return null;
|
|
47
|
+
parts.unshift(prop.name);
|
|
48
|
+
current = current.object;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (t.isIdentifier(current) && current.name === 'I18N') {
|
|
52
|
+
// 不包含 I18N,直接返回 lang.xxx.yyy
|
|
53
|
+
return parts.join('.');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
27
59
|
if (t.isMemberExpression(node)) {
|
|
28
60
|
let parts = [];
|
|
29
61
|
let current = node;
|
|
@@ -50,20 +82,191 @@ function extractCnKeyDirect(node) {
|
|
|
50
82
|
return null;
|
|
51
83
|
}
|
|
52
84
|
|
|
53
|
-
// =====
|
|
85
|
+
// ===== 从 TemplateLiteral 中提取所有部分 =====
|
|
86
|
+
function extractTemplateParts(node) {
|
|
87
|
+
const parts = [];
|
|
88
|
+
|
|
89
|
+
for (let i = 0; i < node.quasis.length; i++) {
|
|
90
|
+
// 添加字符串部分
|
|
91
|
+
if (node.quasis[i].value.raw) {
|
|
92
|
+
parts.push({ type: 'string', value: node.quasis[i].value.raw });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 如果不是最后一个 quasi,添加表达式部分
|
|
96
|
+
if (i < node.expressions.length) {
|
|
97
|
+
const expr = node.expressions[i];
|
|
98
|
+
if (t.isIdentifier(expr)) {
|
|
99
|
+
parts.push({ type: 'variable', value: expr.name });
|
|
100
|
+
} else {
|
|
101
|
+
parts.push({ type: 'expr', value: expr });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return parts;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ===== 从 BinaryExpression 中提取所有部分(I18N引用、字符串、变量)=====
|
|
110
|
+
function extractBinaryParts(node) {
|
|
111
|
+
const parts = [];
|
|
112
|
+
|
|
113
|
+
function traverse(n) {
|
|
114
|
+
if (t.isBinaryExpression(n) && n.operator === '+') {
|
|
115
|
+
traverse(n.left);
|
|
116
|
+
traverse(n.right);
|
|
117
|
+
} else if (t.isMemberExpression(n)) {
|
|
118
|
+
// I18N 引用
|
|
119
|
+
const key = extractCnKeyDirect(n);
|
|
120
|
+
if (key && key.includes('.')) {
|
|
121
|
+
parts.push({ type: 'i18n', value: key });
|
|
122
|
+
} else {
|
|
123
|
+
parts.push({ type: 'expr', value: n });
|
|
124
|
+
}
|
|
125
|
+
} else if (t.isStringLiteral(n)) {
|
|
126
|
+
// 字符串字面量
|
|
127
|
+
parts.push({ type: 'string', value: n.value });
|
|
128
|
+
} else if (t.isIdentifier(n)) {
|
|
129
|
+
// 变量
|
|
130
|
+
parts.push({ type: 'variable', value: n.name });
|
|
131
|
+
} else {
|
|
132
|
+
// 其他表达式
|
|
133
|
+
parts.push({ type: 'expr', value: n });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
traverse(node);
|
|
138
|
+
return parts;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ===== 尝试匹配 cn 和 en 的表达式,返回多个映射 =====
|
|
142
|
+
function matchBinaryExpressions(cnNode, enNode) {
|
|
143
|
+
// 提取 cn 部分
|
|
144
|
+
let cnParts;
|
|
145
|
+
if (t.isBinaryExpression(cnNode) && cnNode.operator === '+') {
|
|
146
|
+
cnParts = extractBinaryParts(cnNode);
|
|
147
|
+
} else {
|
|
148
|
+
return [];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// 提取 en 部分(可能是 BinaryExpression 或 TemplateLiteral)
|
|
152
|
+
let enParts;
|
|
153
|
+
if (t.isBinaryExpression(enNode) && enNode.operator === '+') {
|
|
154
|
+
enParts = extractBinaryParts(enNode);
|
|
155
|
+
} else if (t.isTemplateLiteral(enNode)) {
|
|
156
|
+
enParts = extractTemplateParts(enNode);
|
|
157
|
+
} else {
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const mappings = [];
|
|
162
|
+
|
|
163
|
+
// 提取所有 I18N 引用
|
|
164
|
+
const i18nParts = cnParts.filter(p => p.type === 'i18n');
|
|
165
|
+
|
|
166
|
+
// 如果没有 I18N 引用,返回空
|
|
167
|
+
if (i18nParts.length === 0) {
|
|
168
|
+
return mappings;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// 如果只有一个 I18N 引用,将整个 en 表达式映射给它
|
|
172
|
+
if (i18nParts.length === 1) {
|
|
173
|
+
let enValue = '';
|
|
174
|
+
let varIndex = 1;
|
|
175
|
+
for (const part of enParts) {
|
|
176
|
+
if (part.type === 'string') {
|
|
177
|
+
enValue += part.value;
|
|
178
|
+
} else if (part.type === 'variable' || part.type === 'expr') {
|
|
179
|
+
enValue += `{val${varIndex}}`;
|
|
180
|
+
varIndex++;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
mappings.push({ key: i18nParts[0].value, value: enValue });
|
|
184
|
+
return mappings;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// 多个 I18N 引用:使用变量位置作为锚点进行匹配
|
|
188
|
+
// 策略:找到 cn 和 en 中的共同变量,根据变量前后的位置匹配字符串
|
|
189
|
+
|
|
190
|
+
// 构建位置索引:记录每个 I18N 和字符串前后的变量
|
|
191
|
+
const cnMap = [];
|
|
192
|
+
const enMap = [];
|
|
193
|
+
|
|
194
|
+
// 记录 cn 中每个 I18N 前后的变量
|
|
195
|
+
for (let i = 0; i < cnParts.length; i++) {
|
|
196
|
+
const part = cnParts[i];
|
|
197
|
+
if (part.type === 'i18n') {
|
|
198
|
+
const prevVar = i > 0 && cnParts[i - 1].type === 'variable' ? cnParts[i - 1].value : null;
|
|
199
|
+
const nextVar = i < cnParts.length - 1 && cnParts[i + 1].type === 'variable' ? cnParts[i + 1].value : null;
|
|
200
|
+
cnMap.push({
|
|
201
|
+
index: i,
|
|
202
|
+
key: part.value,
|
|
203
|
+
prevVar,
|
|
204
|
+
nextVar,
|
|
205
|
+
position: prevVar ? 'after' : (nextVar ? 'before' : 'standalone')
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// 记录 en 中每个字符串前后的变量
|
|
211
|
+
for (let i = 0; i < enParts.length; i++) {
|
|
212
|
+
const part = enParts[i];
|
|
213
|
+
if (part.type === 'string') {
|
|
214
|
+
const prevVar = i > 0 && enParts[i - 1].type === 'variable' ? enParts[i - 1].value : null;
|
|
215
|
+
const nextVar = i < enParts.length - 1 && enParts[i + 1].type === 'variable' ? enParts[i + 1].value : null;
|
|
216
|
+
enMap.push({
|
|
217
|
+
index: i,
|
|
218
|
+
value: part.value,
|
|
219
|
+
prevVar,
|
|
220
|
+
nextVar,
|
|
221
|
+
position: prevVar ? 'after' : (nextVar ? 'before' : 'standalone')
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// 匹配:根据前后变量的位置进行匹配
|
|
227
|
+
for (const cnItem of cnMap) {
|
|
228
|
+
// 查找具有相同前后变量的 en 字符串
|
|
229
|
+
const matchedEnItem = enMap.find(enItem => {
|
|
230
|
+
// 优先匹配前后都相同的
|
|
231
|
+
if (cnItem.prevVar && cnItem.nextVar) {
|
|
232
|
+
return enItem.prevVar === cnItem.prevVar && enItem.nextVar === cnItem.nextVar;
|
|
233
|
+
}
|
|
234
|
+
// 匹配前一个变量相同的
|
|
235
|
+
if (cnItem.prevVar) {
|
|
236
|
+
return enItem.prevVar === cnItem.prevVar;
|
|
237
|
+
}
|
|
238
|
+
// 匹配后一个变量相同的
|
|
239
|
+
if (cnItem.nextVar) {
|
|
240
|
+
return enItem.nextVar === cnItem.nextVar;
|
|
241
|
+
}
|
|
242
|
+
// 都没有变量的情况
|
|
243
|
+
return cnItem.position === enItem.position;
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
if (matchedEnItem) {
|
|
247
|
+
mappings.push({ key: cnItem.key, value: matchedEnItem.value });
|
|
248
|
+
// 标记已使用,避免重复匹配
|
|
249
|
+
enMap.splice(enMap.indexOf(matchedEnItem), 1);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return mappings;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ===== 提取 cn 的 key(支持:字符串字面量、I18N.a.b.c、BinaryExpression)=====
|
|
54
257
|
function extractCnKey(node, scope) {
|
|
55
258
|
// 情况1: 字符串字面量
|
|
56
259
|
if (t.isStringLiteral(node)) {
|
|
57
260
|
return node.value;
|
|
58
261
|
}
|
|
59
|
-
|
|
60
|
-
|
|
262
|
+
|
|
263
|
+
// 情况2: Identifier → 查找其定义
|
|
61
264
|
if (t.isIdentifier(node)) {
|
|
62
265
|
const binding = scope.getBinding(node.name);
|
|
63
|
-
|
|
266
|
+
|
|
64
267
|
if (binding && binding.path.isVariableDeclarator()) {
|
|
65
268
|
const init = binding.path.node.init;
|
|
66
|
-
|
|
269
|
+
|
|
67
270
|
if (init) {
|
|
68
271
|
// 递归解析初始化表达式(不再传 scope,避免无限递归)
|
|
69
272
|
return extractCnKeyDirect(init);
|
|
@@ -72,8 +275,15 @@ function extractCnKey(node, scope) {
|
|
|
72
275
|
return null;
|
|
73
276
|
}
|
|
74
277
|
|
|
75
|
-
// 情况3:
|
|
76
|
-
|
|
278
|
+
// 情况3: BinaryExpression (字符串拼接) - 返回第一个 I18N 引用
|
|
279
|
+
if (t.isBinaryExpression(node) && node.operator === '+') {
|
|
280
|
+
const parts = extractBinaryParts(node);
|
|
281
|
+
const i18nPart = parts.find(p => p.type === 'i18n');
|
|
282
|
+
return i18nPart ? i18nPart.value : null;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// 情况4: 检查是否为 I18N.a.b.c(必须是四层 MemberExpression,非 computed)
|
|
286
|
+
return extractCnKeyDirect(node);
|
|
77
287
|
}
|
|
78
288
|
|
|
79
289
|
function extractEnValue(node, scope) {
|
|
@@ -82,6 +292,51 @@ function extractEnValue(node, scope) {
|
|
|
82
292
|
return node.value;
|
|
83
293
|
}
|
|
84
294
|
|
|
295
|
+
// 处理模板字符串:`xxx ${a} yyy ${b}` -> "xxx {val1} yyy {val2}"
|
|
296
|
+
if (t.isTemplateLiteral(node)) {
|
|
297
|
+
let result = '';
|
|
298
|
+
let varIndex = 1;
|
|
299
|
+
|
|
300
|
+
for (let i = 0; i < node.quasis.length; i++) {
|
|
301
|
+
result += node.quasis[i].value.raw;
|
|
302
|
+
|
|
303
|
+
// 如果不是最后一个 quasi,说明后面有变量表达式
|
|
304
|
+
if (i < node.expressions.length) {
|
|
305
|
+
result += `{val${varIndex}}`;
|
|
306
|
+
varIndex++;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return result;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// 处理字符串拼接:'str1' + var + 'str2' -> "str1{val1}str2"
|
|
314
|
+
if (t.isBinaryExpression(node) && node.operator === '+') {
|
|
315
|
+
let result = '';
|
|
316
|
+
let varIndex = 1;
|
|
317
|
+
|
|
318
|
+
function traverseBinary(n) {
|
|
319
|
+
if (t.isBinaryExpression(n) && n.operator === '+') {
|
|
320
|
+
traverseBinary(n.left);
|
|
321
|
+
traverseBinary(n.right);
|
|
322
|
+
} else if (t.isStringLiteral(n)) {
|
|
323
|
+
// 字符串字面量,直接拼接
|
|
324
|
+
result += n.value;
|
|
325
|
+
} else if (t.isIdentifier(n)) {
|
|
326
|
+
// 变量,替换为占位符
|
|
327
|
+
result += `{val${varIndex}}`;
|
|
328
|
+
varIndex++;
|
|
329
|
+
} else {
|
|
330
|
+
// 其他表达式(如函数调用等),也用占位符
|
|
331
|
+
result += `{val${varIndex}}`;
|
|
332
|
+
varIndex++;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
traverseBinary(node);
|
|
337
|
+
return result;
|
|
338
|
+
}
|
|
339
|
+
|
|
85
340
|
// 是变量(Identifier),尝试回溯
|
|
86
341
|
if (t.isIdentifier(node)) {
|
|
87
342
|
const binding = scope.getBinding(node.name);
|
|
@@ -90,10 +345,25 @@ function extractEnValue(node, scope) {
|
|
|
90
345
|
if (init && t.isStringLiteral(init)) {
|
|
91
346
|
return init.value;
|
|
92
347
|
}
|
|
348
|
+
// 如果是模板字符串,递归处理
|
|
349
|
+
if (init && t.isTemplateLiteral(init)) {
|
|
350
|
+
let result = '';
|
|
351
|
+
let varIndex = 1;
|
|
352
|
+
|
|
353
|
+
for (let i = 0; i < init.quasis.length; i++) {
|
|
354
|
+
result += init.quasis[i].value.raw;
|
|
355
|
+
if (i < init.expressions.length) {
|
|
356
|
+
result += `{val${varIndex}}`;
|
|
357
|
+
varIndex++;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return result;
|
|
362
|
+
}
|
|
93
363
|
}
|
|
94
364
|
}
|
|
95
365
|
|
|
96
|
-
//
|
|
366
|
+
// 其他情况(表达式、多变量等)不支持
|
|
97
367
|
return null;
|
|
98
368
|
}
|
|
99
369
|
|
|
@@ -103,7 +373,7 @@ const transformToChinese = (code, globalList) => {
|
|
|
103
373
|
|
|
104
374
|
const ast = parser.parse(code, {
|
|
105
375
|
sourceType: 'module',
|
|
106
|
-
plugins: ['jsx', 'typescript']
|
|
376
|
+
plugins: ['decorators-legacy', 'jsx', 'typescript']
|
|
107
377
|
});
|
|
108
378
|
|
|
109
379
|
const langVariableNames = new Set();
|
|
@@ -125,6 +395,124 @@ const transformToChinese = (code, globalList) => {
|
|
|
125
395
|
|
|
126
396
|
// 第二阶段:处理对象和成员表达式
|
|
127
397
|
traverse(ast, {
|
|
398
|
+
// 处理函数或块作用域内的连续 cn/en 变量声明
|
|
399
|
+
BlockStatement(path) {
|
|
400
|
+
const body = path.node.body;
|
|
401
|
+
|
|
402
|
+
// 查找所有变量声明
|
|
403
|
+
for (let i = 0; i < body.length; i++) {
|
|
404
|
+
const node = body[i];
|
|
405
|
+
|
|
406
|
+
// 跳过非变量声明
|
|
407
|
+
if (!t.isVariableDeclaration(node)) continue;
|
|
408
|
+
|
|
409
|
+
// 支持 cn/cnName 变量
|
|
410
|
+
const cnDeclarator = node.declarations.find(
|
|
411
|
+
(decl) => t.isIdentifier(decl.id) &&
|
|
412
|
+
(decl.id.name === 'cn' || decl.id.name === 'cnName') &&
|
|
413
|
+
decl.init
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
if (!cnDeclarator) continue;
|
|
417
|
+
|
|
418
|
+
// 在当前和后续语句中查找 en/enName 变量
|
|
419
|
+
let enDeclarator = null;
|
|
420
|
+
const cnVarName = cnDeclarator.id.name;
|
|
421
|
+
const enVarName = cnVarName === 'cn' ? 'en' : 'enName';
|
|
422
|
+
|
|
423
|
+
// 先在同一个声明中查找
|
|
424
|
+
enDeclarator = node.declarations.find(
|
|
425
|
+
(decl) => t.isIdentifier(decl.id, { name: enVarName }) && decl.init
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
// 如果同一声明中没找到,查找后续语句
|
|
429
|
+
if (!enDeclarator) {
|
|
430
|
+
for (let j = i + 1; j < body.length; j++) {
|
|
431
|
+
const nextNode = body[j];
|
|
432
|
+
if (t.isVariableDeclaration(nextNode)) {
|
|
433
|
+
enDeclarator = nextNode.declarations.find(
|
|
434
|
+
(decl) => t.isIdentifier(decl.id, { name: enVarName }) && decl.init
|
|
435
|
+
);
|
|
436
|
+
if (enDeclarator) break;
|
|
437
|
+
}
|
|
438
|
+
// 遇到 switch、if 等控制结构,停止搜索
|
|
439
|
+
if (t.isSwitchStatement(nextNode) || t.isIfStatement(nextNode)) {
|
|
440
|
+
break;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// 如果找到了 cn/cnName 和 en/enName,提取值
|
|
446
|
+
if (enDeclarator) {
|
|
447
|
+
const cnInit = cnDeclarator.init;
|
|
448
|
+
const enInit = enDeclarator.init;
|
|
449
|
+
|
|
450
|
+
// 特殊处理:如果 cn 是 BinaryExpression,en 是 BinaryExpression 或 TemplateLiteral
|
|
451
|
+
if (t.isBinaryExpression(cnInit) && cnInit.operator === '+' &&
|
|
452
|
+
(t.isBinaryExpression(enInit) && enInit.operator === '+' || t.isTemplateLiteral(enInit))) {
|
|
453
|
+
|
|
454
|
+
const mappings = matchBinaryExpressions(cnInit, enInit);
|
|
455
|
+
for (const mapping of mappings) {
|
|
456
|
+
localList[mapping.key.replace('I18N.', '')] = mapping.value;
|
|
457
|
+
}
|
|
458
|
+
} else {
|
|
459
|
+
// 常规处理
|
|
460
|
+
const cnKey = extractCnKey(cnInit, path.scope);
|
|
461
|
+
const enStr = extractEnValue(enInit, path.scope);
|
|
462
|
+
|
|
463
|
+
if (cnKey !== null && enStr !== null) {
|
|
464
|
+
localList[cnKey.replace('I18N.', '')] = enStr;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
},
|
|
470
|
+
|
|
471
|
+
// 单独处理 SwitchStatement
|
|
472
|
+
SwitchStatement(path) {
|
|
473
|
+
path.node.cases.forEach((caseNode) => {
|
|
474
|
+
const consequent = caseNode.consequent;
|
|
475
|
+
|
|
476
|
+
let cnAssignment = null;
|
|
477
|
+
let enAssignment = null;
|
|
478
|
+
|
|
479
|
+
// 在每个 case 中查找 cnName = xxx 和 enName = xxx
|
|
480
|
+
for (const stmt of consequent) {
|
|
481
|
+
if (t.isExpressionStatement(stmt) && t.isAssignmentExpression(stmt.expression)) {
|
|
482
|
+
const { left, right } = stmt.expression;
|
|
483
|
+
|
|
484
|
+
if (t.isIdentifier(left, { name: 'cnName' }) || t.isIdentifier(left, { name: 'cn' })) {
|
|
485
|
+
cnAssignment = right;
|
|
486
|
+
}
|
|
487
|
+
if (t.isIdentifier(left, { name: 'enName' }) || t.isIdentifier(left, { name: 'en' })) {
|
|
488
|
+
enAssignment = right;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// 如果找到了 cnName 和 enName 的赋值,提取值
|
|
494
|
+
if (cnAssignment && enAssignment) {
|
|
495
|
+
// 特殊处理:如果 cn 是 BinaryExpression,en 是 BinaryExpression 或 TemplateLiteral
|
|
496
|
+
if (t.isBinaryExpression(cnAssignment) && cnAssignment.operator === '+' &&
|
|
497
|
+
(t.isBinaryExpression(enAssignment) && enAssignment.operator === '+' || t.isTemplateLiteral(enAssignment))) {
|
|
498
|
+
|
|
499
|
+
const mappings = matchBinaryExpressions(cnAssignment, enAssignment);
|
|
500
|
+
for (const mapping of mappings) {
|
|
501
|
+
localList[mapping.key.replace('I18N.', '')] = mapping.value;
|
|
502
|
+
}
|
|
503
|
+
} else {
|
|
504
|
+
// 常规处理
|
|
505
|
+
const cnKey = extractCnKey(cnAssignment, path.scope);
|
|
506
|
+
const enStr = extractEnValue(enAssignment, path.scope);
|
|
507
|
+
|
|
508
|
+
if (cnKey !== null && enStr !== null) {
|
|
509
|
+
localList[cnKey.replace('I18N.', '')] = enStr;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
},
|
|
515
|
+
|
|
128
516
|
ObjectExpression(path) {
|
|
129
517
|
const props = path.node.properties;
|
|
130
518
|
if (props.length === 0) return;
|
|
@@ -135,27 +523,119 @@ const transformToChinese = (code, globalList) => {
|
|
|
135
523
|
const enProp = props.find(
|
|
136
524
|
(prop) => t.isObjectProperty(prop) && !prop.computed && isKeyNamed(prop.key, 'en') && !prop.method
|
|
137
525
|
);
|
|
138
|
-
|
|
139
|
-
//
|
|
140
|
-
if (cnProp && enProp) {
|
|
526
|
+
|
|
527
|
+
// 情况1: 简单的 { cn: xxx, en: yyy } 结构
|
|
528
|
+
if (cnProp && enProp && !t.isObjectExpression(cnProp.value) && !t.isObjectExpression(enProp.value)) {
|
|
141
529
|
const enStr = extractEnValue(enProp.value, path.scope);
|
|
142
530
|
const cnKey = extractCnKey(cnProp.value, path.scope);
|
|
143
|
-
|
|
531
|
+
|
|
144
532
|
if (cnKey !== null && enStr !== null) {
|
|
145
533
|
localList[cnKey.replace('I18N.', '')] = enStr;
|
|
146
534
|
}
|
|
147
535
|
}
|
|
536
|
+
|
|
537
|
+
// 情况2: 嵌套对象结构 { cn: { [I18N.xxx]: value }, en: { 'key': value } }
|
|
538
|
+
if (cnProp && enProp && t.isObjectExpression(cnProp.value) && t.isObjectExpression(enProp.value)) {
|
|
539
|
+
const cnProps = cnProp.value.properties;
|
|
540
|
+
const enProps = enProp.value.properties;
|
|
541
|
+
|
|
542
|
+
// 提取 cn 对象的计算属性 key
|
|
543
|
+
const cnKeys = [];
|
|
544
|
+
for (const prop of cnProps) {
|
|
545
|
+
if (t.isObjectProperty(prop) && prop.computed) {
|
|
546
|
+
// 提取计算属性中的 I18N 引用
|
|
547
|
+
const cnKey = extractCnKeyDirect(prop.key);
|
|
548
|
+
if (cnKey) {
|
|
549
|
+
cnKeys.push({ key: cnKey, index: cnKeys.length });
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// 提取 en 对象的字符串 key(支持计算属性和普通属性)
|
|
555
|
+
const enKeys = [];
|
|
556
|
+
for (const prop of enProps) {
|
|
557
|
+
if (t.isObjectProperty(prop)) {
|
|
558
|
+
let enKeyValue = null;
|
|
559
|
+
|
|
560
|
+
// 计算属性: ['string']
|
|
561
|
+
if (prop.computed && t.isStringLiteral(prop.key)) {
|
|
562
|
+
enKeyValue = prop.key.value;
|
|
563
|
+
}
|
|
564
|
+
// 普通字符串属性: 'string' 或 "string"
|
|
565
|
+
else if (!prop.computed && t.isStringLiteral(prop.key)) {
|
|
566
|
+
enKeyValue = prop.key.value;
|
|
567
|
+
}
|
|
568
|
+
// 普通标识符属性: identifier (如 Today)
|
|
569
|
+
else if (!prop.computed && t.isIdentifier(prop.key)) {
|
|
570
|
+
enKeyValue = prop.key.name;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
if (enKeyValue) {
|
|
574
|
+
enKeys.push({ key: enKeyValue, index: enKeys.length });
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// 按索引位置匹配
|
|
580
|
+
for (let i = 0; i < Math.min(cnKeys.length, enKeys.length); i++) {
|
|
581
|
+
const cnKey = cnKeys[i].key;
|
|
582
|
+
const enKey = enKeys[i].key;
|
|
583
|
+
if (cnKey && enKey) {
|
|
584
|
+
localList[cnKey.replace('I18N.', '')] = enKey;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
},
|
|
589
|
+
|
|
590
|
+
// 处理三元表达式:getLanguage() === 'cn' ? cn : en
|
|
591
|
+
ConditionalExpression(path) {
|
|
592
|
+
const { test, consequent, alternate } = path.node;
|
|
593
|
+
|
|
594
|
+
// 检查是否为 getLanguage() === 'cn' 或 'cn' === getLanguage()
|
|
595
|
+
let isLanguageCheck = false;
|
|
596
|
+
if (t.isBinaryExpression(test) && test.operator === '===') {
|
|
597
|
+
const isLeftLangCall =
|
|
598
|
+
t.isCallExpression(test.left) &&
|
|
599
|
+
t.isIdentifier(test.left.callee) &&
|
|
600
|
+
['getLang', 'getLanguage'].includes(test.left.callee.name);
|
|
601
|
+
const isRightCn = t.isStringLiteral(test.right, { value: 'cn' });
|
|
602
|
+
|
|
603
|
+
const isRightLangCall =
|
|
604
|
+
t.isCallExpression(test.right) &&
|
|
605
|
+
t.isIdentifier(test.right.callee) &&
|
|
606
|
+
['getLang', 'getLanguage'].includes(test.right.callee.name);
|
|
607
|
+
const isLeftCn = t.isStringLiteral(test.left, { value: 'cn' });
|
|
608
|
+
|
|
609
|
+
isLanguageCheck = (isLeftLangCall && isRightCn) || (isRightLangCall && isLeftCn);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// 如果是语言检查,直接替换为 consequent(cn 分支)
|
|
613
|
+
if (isLanguageCheck) {
|
|
614
|
+
path.replaceWith(consequent);
|
|
615
|
+
}
|
|
148
616
|
},
|
|
149
617
|
|
|
150
618
|
MemberExpression(path) {
|
|
151
619
|
const node = path.node;
|
|
152
620
|
if (!node.computed) return;
|
|
153
621
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
622
|
+
// 检查是否为语言访问
|
|
623
|
+
let isLangAccess = false;
|
|
624
|
+
|
|
625
|
+
// 情况1: 已收集的 lang 变量名
|
|
626
|
+
if (t.isIdentifier(node.property) && langVariableNames.has(node.property.name)) {
|
|
627
|
+
isLangAccess = true;
|
|
628
|
+
}
|
|
629
|
+
// 情况2: 直接调用 getLang/getLanguage
|
|
630
|
+
else if (t.isCallExpression(node.property) &&
|
|
157
631
|
t.isIdentifier(node.property.callee) &&
|
|
158
|
-
['getLang', 'getLanguage'].includes(node.property.callee.name))
|
|
632
|
+
['getLang', 'getLanguage'].includes(node.property.callee.name)) {
|
|
633
|
+
isLangAccess = true;
|
|
634
|
+
}
|
|
635
|
+
// 情况3: 未定义的 lang 标识符(不管作用域)
|
|
636
|
+
else if (t.isIdentifier(node.property) && node.property.name === 'lang') {
|
|
637
|
+
isLangAccess = true;
|
|
638
|
+
}
|
|
159
639
|
|
|
160
640
|
if (isLangAccess) {
|
|
161
641
|
// 替换为 obj['cn']
|
|
@@ -165,6 +645,37 @@ const transformToChinese = (code, globalList) => {
|
|
|
165
645
|
}
|
|
166
646
|
},
|
|
167
647
|
|
|
648
|
+
// 处理可选链:obj?.[lang]
|
|
649
|
+
OptionalMemberExpression(path) {
|
|
650
|
+
const node = path.node;
|
|
651
|
+
if (!node.computed) return;
|
|
652
|
+
|
|
653
|
+
// 检查是否为语言访问
|
|
654
|
+
let isLangAccess = false;
|
|
655
|
+
|
|
656
|
+
// 情况1: 已收集的 lang 变量名
|
|
657
|
+
if (t.isIdentifier(node.property) && langVariableNames.has(node.property.name)) {
|
|
658
|
+
isLangAccess = true;
|
|
659
|
+
}
|
|
660
|
+
// 情况2: 直接调用 getLang/getLanguage
|
|
661
|
+
else if (t.isCallExpression(node.property) &&
|
|
662
|
+
t.isIdentifier(node.property.callee) &&
|
|
663
|
+
['getLang', 'getLanguage'].includes(node.property.callee.name)) {
|
|
664
|
+
isLangAccess = true;
|
|
665
|
+
}
|
|
666
|
+
// 情况3: 未定义的 lang 标识符(不管作用域)
|
|
667
|
+
else if (t.isIdentifier(node.property) && node.property.name === 'lang') {
|
|
668
|
+
isLangAccess = true;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
if (isLangAccess) {
|
|
672
|
+
// 替换为 obj?.['cn']
|
|
673
|
+
path.replaceWith(
|
|
674
|
+
t.optionalMemberExpression(node.object, t.stringLiteral('cn'), true, node.optional)
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
},
|
|
678
|
+
|
|
168
679
|
// 清理无用的 lang 变量声明
|
|
169
680
|
VariableDeclarator(path) {
|
|
170
681
|
const { id } = path.node;
|
|
@@ -256,14 +767,26 @@ const eslint = new ESLint({
|
|
|
256
767
|
});
|
|
257
768
|
|
|
258
769
|
// ===== 递归读取所有 .js 文件(排除 index.js)=====
|
|
259
|
-
const getAllJsFiles = (
|
|
770
|
+
const getAllJsFiles = (pathInput) => {
|
|
771
|
+
// 检查路径是文件还是目录
|
|
772
|
+
const stat = fs.statSync(pathInput);
|
|
773
|
+
|
|
774
|
+
// 如果是文件,直接返回(只要是 .js 文件)
|
|
775
|
+
if (stat.isFile()) {
|
|
776
|
+
if (pathInput.endsWith('.js')) {
|
|
777
|
+
return [pathInput];
|
|
778
|
+
}
|
|
779
|
+
return [];
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// 如果是目录,递归处理
|
|
260
783
|
let results = [];
|
|
261
|
-
const files = fs.readdirSync(
|
|
784
|
+
const files = fs.readdirSync(pathInput);
|
|
262
785
|
|
|
263
786
|
for (const file of files) {
|
|
264
|
-
const fullPath = path.join(
|
|
265
|
-
const
|
|
266
|
-
if (
|
|
787
|
+
const fullPath = path.join(pathInput, file);
|
|
788
|
+
const fileStat = fs.statSync(fullPath);
|
|
789
|
+
if (fileStat.isDirectory()) {
|
|
267
790
|
results = results.concat(getAllJsFiles(fullPath));
|
|
268
791
|
} else if (file.endsWith('.js') && file !== 'index.js') {
|
|
269
792
|
results.push(fullPath);
|