@agentscope-ai/i18n 1.0.0 → 1.0.2
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/README.md +57 -135
- package/i18n.config.example.js +106 -0
- package/lib/cli.js +112 -221
- package/lib/core/ast-processor.js +17 -21
- package/lib/core/checker.js +544 -0
- package/lib/core/file-processor.js +45 -205
- package/lib/core/translator.js +96 -937
- package/lib/index.js +2 -8
- package/lib/parse-jsx.js +33 -23
- package/lib/utils/ast-utils.js +48 -25
- package/lib/utils/excel-utils.js +7 -4
- package/lib/utils/file-utils.js +50 -11
- package/package.json +10 -5
- package/skills/i18n-helper/SKILL.md +186 -82
package/lib/core/translator.js
CHANGED
|
@@ -4,35 +4,24 @@ const find = require('find');
|
|
|
4
4
|
const { parse } = require('@babel/parser');
|
|
5
5
|
const traverse = require('@babel/traverse').default;
|
|
6
6
|
const generate = require('@babel/generator').default;
|
|
7
|
+
const t = require('@babel/types');
|
|
7
8
|
const { scanFiles, resolveFiles, injectImportStatement } = require('../utils/file-utils');
|
|
8
|
-
const {
|
|
9
|
-
processFiles,
|
|
10
|
-
translateAndUpdateFiles,
|
|
11
|
-
singleTranslateAndUpdateFiles,
|
|
12
|
-
replaceKeysInFiles,
|
|
13
|
-
} = require('./file-processor');
|
|
9
|
+
const { processFiles, translateAndUpdateFiles, replaceKeysInFiles } = require('./file-processor');
|
|
14
10
|
const { generateMdsPayload } = require('../utils/mds-payload');
|
|
15
11
|
const { singleKeyTranslate, singleTranslate } = require('../utils/translation-utils');
|
|
16
|
-
const {
|
|
17
|
-
extractI18nKey,
|
|
18
|
-
chnRegExp,
|
|
19
|
-
onlyContainsChinesePunctuation,
|
|
20
|
-
onlyContainsEmoji,
|
|
21
|
-
isI18nCall,
|
|
22
|
-
isInConsoleCall,
|
|
23
|
-
} = require('../utils/ast-utils');
|
|
24
|
-
const { askQuestion } = require('../utils/cli-utils');
|
|
25
|
-
const {
|
|
26
|
-
shouldIgnoreLineByComment,
|
|
27
|
-
shouldIgnoreBlockByComment,
|
|
28
|
-
shouldIgnoreByComment,
|
|
29
|
-
shouldIgnoreFile,
|
|
30
|
-
} = require('../utils/file-utils');
|
|
12
|
+
const { extractI18nKey } = require('../utils/ast-utils');
|
|
31
13
|
const { generateMedusaExcel } = require('../utils/excel-utils');
|
|
32
|
-
|
|
33
|
-
|
|
14
|
+
const {
|
|
15
|
+
scanFilesAndCollectKeys,
|
|
16
|
+
scanUntranslatedTexts,
|
|
17
|
+
readTranslationFiles,
|
|
18
|
+
analyzeMissingAndUnusedKeys,
|
|
19
|
+
generateCheckOutput,
|
|
20
|
+
handleOutputAndJsonGeneration,
|
|
21
|
+
handleUnusedKeysDeletion,
|
|
22
|
+
handleExcelGeneration,
|
|
23
|
+
} = require('./checker');
|
|
34
24
|
|
|
35
|
-
// 解析AST
|
|
36
25
|
const parseAST = (code) => {
|
|
37
26
|
return parse(code, {
|
|
38
27
|
sourceType: 'module',
|
|
@@ -40,15 +29,11 @@ const parseAST = (code) => {
|
|
|
40
29
|
});
|
|
41
30
|
};
|
|
42
31
|
|
|
43
|
-
// 生成代码
|
|
44
32
|
const generateCode = (ast, originalCode) => {
|
|
45
33
|
return generate(
|
|
46
34
|
ast,
|
|
47
35
|
{
|
|
48
|
-
jsescOption: {
|
|
49
|
-
minimal: true,
|
|
50
|
-
quotes: 'single',
|
|
51
|
-
},
|
|
36
|
+
jsescOption: { minimal: true, quotes: 'single' },
|
|
52
37
|
retainLines: false,
|
|
53
38
|
compact: 'auto',
|
|
54
39
|
},
|
|
@@ -56,840 +41,109 @@ const generateCode = (ast, originalCode) => {
|
|
|
56
41
|
).code;
|
|
57
42
|
};
|
|
58
43
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
try {
|
|
62
|
-
const {
|
|
63
|
-
fileType = '.tsx,.js,.ts,.jsx',
|
|
64
|
-
localesFilePath,
|
|
65
|
-
keyPrefix,
|
|
66
|
-
doNotTranslateFiles,
|
|
67
|
-
functionImportPath,
|
|
68
|
-
i18nFilePath,
|
|
69
|
-
dashScope,
|
|
70
|
-
medusa,
|
|
71
|
-
skipConfirm,
|
|
72
|
-
} = params;
|
|
73
|
-
|
|
74
|
-
if (!keyPrefix) {
|
|
75
|
-
throw new Error('keyPrefix is required in params');
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const files = scanFiles(sourcePath, fileType, doNotTranslateFiles);
|
|
79
|
-
const { i18n, fileCodeMap, needImportFiles } = await processFiles(files, params);
|
|
80
|
-
await translateAndUpdateFiles(
|
|
81
|
-
i18n,
|
|
82
|
-
sourcePath,
|
|
83
|
-
localesFilePath,
|
|
84
|
-
fileType,
|
|
85
|
-
fileCodeMap,
|
|
86
|
-
needImportFiles,
|
|
87
|
-
functionImportPath,
|
|
88
|
-
false,
|
|
89
|
-
dashScope,
|
|
90
|
-
medusa,
|
|
91
|
-
skipConfirm,
|
|
92
|
-
);
|
|
44
|
+
const copyI18nFile = (i18nFilePath, isUpdate) => {
|
|
45
|
+
if (!i18nFilePath) return;
|
|
93
46
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const sourceI18nPath = path.join(__dirname, '../i18n.ts');
|
|
97
|
-
const targetI18nPath = path.join(i18nFilePath, 'index.ts');
|
|
47
|
+
const sourceI18nPath = path.join(__dirname, '../i18n.ts');
|
|
48
|
+
const targetI18nPath = path.join(i18nFilePath, 'index.ts');
|
|
98
49
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
// 复制文件
|
|
103
|
-
fs.copyFileSync(sourceI18nPath, targetI18nPath);
|
|
104
|
-
console.log(` i18n.ts文件已复制到 ${targetI18nPath}`);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
console.log(` init: ${sourcePath} 目录下文件中文文案提取、替换和翻译成功!`);
|
|
108
|
-
} catch (error) {
|
|
109
|
-
console.error(' [错误] 初始化失败:', error);
|
|
110
|
-
throw error;
|
|
50
|
+
if (isUpdate && fs.existsSync(targetI18nPath)) {
|
|
51
|
+
console.log(` [提示] i18n.ts文件已存在于 ${targetI18nPath},跳过复制`);
|
|
52
|
+
return;
|
|
111
53
|
}
|
|
54
|
+
|
|
55
|
+
fs.ensureDirSync(i18nFilePath);
|
|
56
|
+
fs.copyFileSync(sourceI18nPath, targetI18nPath);
|
|
57
|
+
console.log(` [完成] i18n.ts文件已复制到 ${targetI18nPath}`);
|
|
112
58
|
};
|
|
113
59
|
|
|
114
|
-
|
|
115
|
-
|
|
60
|
+
/**
|
|
61
|
+
* 统一的翻译入口。合并了原来的 init / initMT / patch / patchMT 四个函数。
|
|
62
|
+
* @param {string} sourcePath - 源文件目录
|
|
63
|
+
* @param {object} config - 完整的配置对象
|
|
64
|
+
* @param {object} options - { isUpdate, engine }
|
|
65
|
+
*/
|
|
66
|
+
const runTranslation = async (sourcePath, config, { isUpdate = false, engine = 'mt' } = {}) => {
|
|
116
67
|
try {
|
|
117
68
|
const {
|
|
118
69
|
fileType = '.tsx,.js,.ts,.jsx',
|
|
119
|
-
localesFilePath,
|
|
120
70
|
keyPrefix,
|
|
121
71
|
doNotTranslateFiles,
|
|
122
|
-
functionImportPath,
|
|
123
|
-
i18nFilePath,
|
|
124
72
|
dashScope,
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
} = params;
|
|
73
|
+
i18nFilePath,
|
|
74
|
+
} = config;
|
|
128
75
|
|
|
129
76
|
if (!keyPrefix) {
|
|
130
77
|
throw new Error('keyPrefix is required in params');
|
|
131
78
|
}
|
|
132
79
|
|
|
133
|
-
if (!dashScope || !dashScope.apiKey) {
|
|
80
|
+
if (engine === 'mt' && (!dashScope || !dashScope.apiKey)) {
|
|
134
81
|
throw new Error('dashScope.apiKey is required for MT translation');
|
|
135
82
|
}
|
|
136
83
|
|
|
137
|
-
|
|
84
|
+
const engineLabel = engine === 'agent' ? 'Agent' : 'MT';
|
|
85
|
+
const modeLabel = isUpdate ? '增量翻译' : '初始化翻译';
|
|
86
|
+
console.log(` [信息] 使用${engineLabel}方式进行${modeLabel}...`);
|
|
138
87
|
|
|
139
88
|
const files = scanFiles(sourcePath, fileType, doNotTranslateFiles);
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
i18n,
|
|
143
|
-
sourcePath,
|
|
144
|
-
localesFilePath,
|
|
145
|
-
fileType,
|
|
146
|
-
fileCodeMap,
|
|
147
|
-
needImportFiles,
|
|
148
|
-
functionImportPath,
|
|
149
|
-
false,
|
|
150
|
-
dashScope,
|
|
151
|
-
medusa,
|
|
152
|
-
skipConfirm,
|
|
153
|
-
);
|
|
154
|
-
|
|
155
|
-
// 如果配置了i18nFilePath,复制i18n.ts文件
|
|
156
|
-
if (i18nFilePath) {
|
|
157
|
-
const sourceI18nPath = path.join(__dirname, '../i18n.ts');
|
|
158
|
-
const targetI18nPath = path.join(i18nFilePath, 'index.ts');
|
|
159
|
-
|
|
160
|
-
// 确保目标目录存在
|
|
161
|
-
fs.ensureDirSync(i18nFilePath);
|
|
162
|
-
|
|
163
|
-
// 复制文件
|
|
164
|
-
fs.copyFileSync(sourceI18nPath, targetI18nPath);
|
|
165
|
-
console.log(` i18n.ts文件已复制到 ${targetI18nPath}`);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
console.log(` initMT: ${sourcePath} 目录下文件中文文案提取、替换和MT翻译成功!`);
|
|
169
|
-
} catch (error) {
|
|
170
|
-
console.error(' [错误] MT初始化失败:', error);
|
|
171
|
-
throw error;
|
|
172
|
-
}
|
|
173
|
-
};
|
|
174
|
-
|
|
175
|
-
// 补丁更新
|
|
176
|
-
const patch = async function (sourcePath, params) {
|
|
177
|
-
const {
|
|
178
|
-
fileType = '.tsx,.js,.ts,.jsx',
|
|
179
|
-
localesFilePath,
|
|
180
|
-
keyPrefix,
|
|
181
|
-
doNotTranslateFiles,
|
|
182
|
-
functionImportPath,
|
|
183
|
-
i18nFilePath,
|
|
184
|
-
dashScope,
|
|
185
|
-
medusa,
|
|
186
|
-
skipConfirm,
|
|
187
|
-
} = params;
|
|
188
|
-
|
|
189
|
-
if (!keyPrefix) {
|
|
190
|
-
throw new Error('keyPrefix is required in params');
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const files = scanFiles(sourcePath, fileType, doNotTranslateFiles);
|
|
194
|
-
console.log(` # 检查${files.length}个${fileType}文件`);
|
|
195
|
-
|
|
196
|
-
const { i18n, fileCodeMap, needImportFiles } = await processFiles(files, params);
|
|
197
|
-
const newKeysCount = await translateAndUpdateFiles(
|
|
198
|
-
i18n,
|
|
199
|
-
sourcePath,
|
|
200
|
-
localesFilePath,
|
|
201
|
-
fileType,
|
|
202
|
-
fileCodeMap,
|
|
203
|
-
needImportFiles,
|
|
204
|
-
functionImportPath,
|
|
205
|
-
true,
|
|
206
|
-
dashScope,
|
|
207
|
-
medusa,
|
|
208
|
-
skipConfirm,
|
|
209
|
-
);
|
|
210
|
-
|
|
211
|
-
// 如果配置了i18nFilePath,检查并复制i18n.ts文件
|
|
212
|
-
if (i18nFilePath) {
|
|
213
|
-
const sourceI18nPath = path.join(__dirname, '../i18n.ts');
|
|
214
|
-
const targetI18nPath = path.join(i18nFilePath, 'index.ts');
|
|
215
|
-
|
|
216
|
-
// 检查目标文件是否已存在
|
|
217
|
-
if (!fs.existsSync(targetI18nPath)) {
|
|
218
|
-
// 确保目标目录存在
|
|
219
|
-
fs.ensureDirSync(i18nFilePath);
|
|
220
|
-
|
|
221
|
-
// 复制文件
|
|
222
|
-
fs.copyFileSync(sourceI18nPath, targetI18nPath);
|
|
223
|
-
console.log(` [完成] i18n.ts文件已复制到 ${targetI18nPath}`);
|
|
224
|
-
} else {
|
|
225
|
-
console.log(` [提示] i18n.ts文件已存在于 ${targetI18nPath},跳过复制`);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
console.log(` 新增文案补充翻译完成!新增${newKeysCount}条翻译文案`);
|
|
230
|
-
};
|
|
231
|
-
|
|
232
|
-
// 使用MT方式的补丁更新
|
|
233
|
-
const patchMT = async function (sourcePath, params) {
|
|
234
|
-
const {
|
|
235
|
-
fileType = '.tsx,.js,.ts,.jsx',
|
|
236
|
-
localesFilePath,
|
|
237
|
-
keyPrefix,
|
|
238
|
-
doNotTranslateFiles,
|
|
239
|
-
functionImportPath,
|
|
240
|
-
i18nFilePath,
|
|
241
|
-
dashScope,
|
|
242
|
-
medusa,
|
|
243
|
-
skipConfirm,
|
|
244
|
-
} = params;
|
|
245
|
-
|
|
246
|
-
if (!keyPrefix) {
|
|
247
|
-
throw new Error('keyPrefix is required in params');
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
if (!dashScope || !dashScope.apiKey) {
|
|
251
|
-
throw new Error('dashScope.apiKey is required for MT translation');
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
console.log(' [信息] 使用MT方式进行单条翻译补丁更新...');
|
|
255
|
-
|
|
256
|
-
const files = scanFiles(sourcePath, fileType, doNotTranslateFiles);
|
|
257
|
-
console.log(` # 检查${files.length}个${fileType}文件`);
|
|
258
|
-
|
|
259
|
-
const { i18n, fileCodeMap, needImportFiles } = await processFiles(files, params);
|
|
260
|
-
const newKeysCount = await singleTranslateAndUpdateFiles(
|
|
261
|
-
i18n,
|
|
262
|
-
sourcePath,
|
|
263
|
-
localesFilePath,
|
|
264
|
-
fileType,
|
|
265
|
-
fileCodeMap,
|
|
266
|
-
needImportFiles,
|
|
267
|
-
functionImportPath,
|
|
268
|
-
true,
|
|
269
|
-
dashScope,
|
|
270
|
-
medusa,
|
|
271
|
-
skipConfirm,
|
|
272
|
-
);
|
|
273
|
-
|
|
274
|
-
// 如果配置了i18nFilePath,检查并复制i18n.ts文件
|
|
275
|
-
if (i18nFilePath) {
|
|
276
|
-
const sourceI18nPath = path.join(__dirname, '../i18n.ts');
|
|
277
|
-
const targetI18nPath = path.join(i18nFilePath, 'index.ts');
|
|
278
|
-
|
|
279
|
-
// 检查目标文件是否已存在
|
|
280
|
-
if (!fs.existsSync(targetI18nPath)) {
|
|
281
|
-
// 确保目标目录存在
|
|
282
|
-
fs.ensureDirSync(i18nFilePath);
|
|
283
|
-
|
|
284
|
-
// 复制文件
|
|
285
|
-
fs.copyFileSync(sourceI18nPath, targetI18nPath);
|
|
286
|
-
console.log(` [完成] i18n.ts文件已复制到 ${targetI18nPath}`);
|
|
287
|
-
} else {
|
|
288
|
-
console.log(` [提示] i18n.ts文件已存在于 ${targetI18nPath},跳过复制`);
|
|
89
|
+
if (isUpdate) {
|
|
90
|
+
console.log(` # 检查${files.length}个${fileType}文件`);
|
|
289
91
|
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
console.log(` 新增文案MT翻译补充完成!新增${newKeysCount}条翻译文案`);
|
|
293
|
-
};
|
|
294
92
|
|
|
295
|
-
|
|
296
|
-
const scanFilesAndCollectKeys = (files) => {
|
|
297
|
-
const usedKeys = new Set();
|
|
298
|
-
const usedKeysData = {}; // 存储key对应的dm值(中文默认值)
|
|
93
|
+
const { i18n, fileCodeMap, needImportFiles } = await processFiles(files, config);
|
|
299
94
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
traverse(ast, {
|
|
306
|
-
CallExpression(path) {
|
|
307
|
-
const result = extractI18nKey(path.node);
|
|
308
|
-
if (result && result.id) {
|
|
309
|
-
usedKeys.add(result.id);
|
|
310
|
-
|
|
311
|
-
// 如果有dm值,记录下来(dm是中文默认值)
|
|
312
|
-
if (result.dm && typeof result.dm === 'string') {
|
|
313
|
-
usedKeysData[result.id] = result.dm;
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
},
|
|
317
|
-
});
|
|
318
|
-
} catch (error) {
|
|
319
|
-
console.error(` # 处理文件失败: ${file}`, error);
|
|
320
|
-
if (error.loc) {
|
|
321
|
-
console.error(` # 错误位置: 第${error.loc.line}行, 第${error.loc.column}列`);
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
return { usedKeys, usedKeysData };
|
|
327
|
-
};
|
|
328
|
-
|
|
329
|
-
// 扫描未翻译的中文文案(未被 $i18n.get 包裹的中文字符串)
|
|
330
|
-
const scanUntranslatedTexts = (files, doNotTranslateFiles) => {
|
|
331
|
-
const untranslatedTexts = [];
|
|
332
|
-
|
|
333
|
-
const filteredFiles = files.filter((file) => {
|
|
334
|
-
if (shouldIgnoreFile(file, doNotTranslateFiles || [])) return false;
|
|
335
|
-
if (shouldIgnoreByComment(file)) return false;
|
|
336
|
-
return true;
|
|
337
|
-
});
|
|
338
|
-
|
|
339
|
-
filteredFiles.forEach((file) => {
|
|
340
|
-
try {
|
|
341
|
-
const code = fs.readFileSync(file, 'utf8');
|
|
342
|
-
const ast = parseAST(code);
|
|
343
|
-
|
|
344
|
-
const shouldIgnoreNode = (nodePath) => {
|
|
345
|
-
if (!nodePath.node.loc) return false;
|
|
346
|
-
const lineNumber = nodePath.node.loc.start.line;
|
|
347
|
-
return (
|
|
348
|
-
shouldIgnoreLineByComment(file, lineNumber) ||
|
|
349
|
-
shouldIgnoreBlockByComment(file, lineNumber)
|
|
350
|
-
);
|
|
351
|
-
};
|
|
352
|
-
|
|
353
|
-
traverse(ast, {
|
|
354
|
-
StringLiteral(astPath) {
|
|
355
|
-
const { value } = astPath.node;
|
|
356
|
-
if (
|
|
357
|
-
chnRegExp.test(value) &&
|
|
358
|
-
!isI18nCall(astPath.parent) &&
|
|
359
|
-
!astPath.findParent((p) => isI18nCall(p.node)) &&
|
|
360
|
-
!isInConsoleCall(astPath) &&
|
|
361
|
-
!shouldIgnoreNode(astPath) &&
|
|
362
|
-
!onlyContainsChinesePunctuation(value) &&
|
|
363
|
-
!onlyContainsEmoji(value)
|
|
364
|
-
) {
|
|
365
|
-
untranslatedTexts.push({
|
|
366
|
-
file,
|
|
367
|
-
line: astPath.node.loc?.start.line,
|
|
368
|
-
column: astPath.node.loc?.start.column,
|
|
369
|
-
type: 'StringLiteral',
|
|
370
|
-
text: value,
|
|
371
|
-
});
|
|
372
|
-
}
|
|
373
|
-
},
|
|
374
|
-
TemplateLiteral(astPath) {
|
|
375
|
-
if (
|
|
376
|
-
!isI18nCall(astPath.parent) &&
|
|
377
|
-
!astPath.findParent((p) => isI18nCall(p.node)) &&
|
|
378
|
-
!isInConsoleCall(astPath) &&
|
|
379
|
-
!shouldIgnoreNode(astPath)
|
|
380
|
-
) {
|
|
381
|
-
const staticParts = astPath.node.quasis.map((q) => q.value.raw).join('');
|
|
382
|
-
if (
|
|
383
|
-
chnRegExp.test(staticParts) &&
|
|
384
|
-
!onlyContainsChinesePunctuation(staticParts) &&
|
|
385
|
-
!onlyContainsEmoji(staticParts)
|
|
386
|
-
) {
|
|
387
|
-
const fullText = astPath.node.quasis
|
|
388
|
-
.map((q, i) => {
|
|
389
|
-
const expr = astPath.node.expressions[i];
|
|
390
|
-
const exprStr = expr ? `\${${generate(expr, { compact: true }).code}}` : '';
|
|
391
|
-
return q.value.raw + exprStr;
|
|
392
|
-
})
|
|
393
|
-
.join('');
|
|
394
|
-
untranslatedTexts.push({
|
|
395
|
-
file,
|
|
396
|
-
line: astPath.node.loc?.start.line,
|
|
397
|
-
column: astPath.node.loc?.start.column,
|
|
398
|
-
type: 'TemplateLiteral',
|
|
399
|
-
text: fullText,
|
|
400
|
-
});
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
},
|
|
404
|
-
JSXText(astPath) {
|
|
405
|
-
const { value } = astPath.node;
|
|
406
|
-
if (
|
|
407
|
-
chnRegExp.test(value) &&
|
|
408
|
-
!isI18nCall(astPath.parent) &&
|
|
409
|
-
!astPath.findParent((p) => isI18nCall(p.node)) &&
|
|
410
|
-
!isInConsoleCall(astPath) &&
|
|
411
|
-
!shouldIgnoreNode(astPath) &&
|
|
412
|
-
!onlyContainsChinesePunctuation(value) &&
|
|
413
|
-
!onlyContainsEmoji(value)
|
|
414
|
-
) {
|
|
415
|
-
const trimmedValue = value.replace(/^\s+|\s+$/g, '');
|
|
416
|
-
if (trimmedValue) {
|
|
417
|
-
untranslatedTexts.push({
|
|
418
|
-
file,
|
|
419
|
-
line: astPath.node.loc?.start.line,
|
|
420
|
-
column: astPath.node.loc?.start.column,
|
|
421
|
-
type: 'JSXText',
|
|
422
|
-
text: trimmedValue,
|
|
423
|
-
});
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
},
|
|
427
|
-
});
|
|
428
|
-
} catch (error) {
|
|
429
|
-
console.error(` # 扫描未翻译文案失败: ${file}`, error.message);
|
|
430
|
-
}
|
|
431
|
-
});
|
|
432
|
-
|
|
433
|
-
return untranslatedTexts;
|
|
434
|
-
};
|
|
435
|
-
|
|
436
|
-
// 读取翻译文件
|
|
437
|
-
const readTranslationFiles = (localesFilePath, dashScope, checkLanguages) => {
|
|
438
|
-
// 读取中文翻译文件
|
|
439
|
-
let zhCN = {};
|
|
440
|
-
try {
|
|
441
|
-
zhCN = fs.readJsonSync(`${localesFilePath}/zh-cn.json`);
|
|
442
|
-
} catch (error) {
|
|
443
|
-
console.log(' 未找到zh-cn.json文件');
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
// 确定需要检查的语言列表,优先级:check.languages > dashScope.valueTranslateAppId
|
|
447
|
-
let languages = [];
|
|
448
|
-
|
|
449
|
-
if (Array.isArray(checkLanguages) && checkLanguages.length > 0) {
|
|
450
|
-
// 从 check.languages 配置中获取,排除 zh-cn(已单独处理)
|
|
451
|
-
languages = checkLanguages.filter((lang) => lang !== 'zh-cn');
|
|
452
|
-
} else if (dashScope && dashScope.valueTranslateAppId) {
|
|
453
|
-
if (typeof dashScope.valueTranslateAppId === 'string') {
|
|
454
|
-
languages = ['en-us'];
|
|
455
|
-
} else if (typeof dashScope.valueTranslateAppId === 'object') {
|
|
456
|
-
languages = Object.keys(dashScope.valueTranslateAppId);
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
const languageFiles = {};
|
|
461
|
-
for (const language of languages) {
|
|
462
|
-
try {
|
|
463
|
-
languageFiles[language] = fs.readJsonSync(`${localesFilePath}/${language}.json`);
|
|
464
|
-
} catch (error) {
|
|
465
|
-
console.log(` 未找到${language}.json文件`);
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
return { zhCN, languageFiles };
|
|
470
|
-
};
|
|
471
|
-
|
|
472
|
-
// 分析缺失和未使用的key
|
|
473
|
-
const analyzeMissingAndUnusedKeys = (usedKeys, zhCN, languageFiles) => {
|
|
474
|
-
const zhCNKeys = new Set(Object.keys(zhCN));
|
|
475
|
-
const languageKeys = {};
|
|
476
|
-
for (const [language, content] of Object.entries(languageFiles)) {
|
|
477
|
-
languageKeys[language] = new Set(Object.keys(content));
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
// 找出缺失的key
|
|
481
|
-
const missingKeys = {
|
|
482
|
-
zhCN: Array.from(usedKeys).filter((key) => !zhCNKeys.has(key)),
|
|
483
|
-
};
|
|
484
|
-
|
|
485
|
-
for (const [language, keys] of Object.entries(languageKeys)) {
|
|
486
|
-
missingKeys[language] = Array.from(usedKeys).filter((key) => !keys.has(key));
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
// 找出多余的key
|
|
490
|
-
const unusedKeys = {
|
|
491
|
-
zhCN: Array.from(zhCNKeys).filter((key) => !usedKeys.has(key)),
|
|
492
|
-
};
|
|
493
|
-
|
|
494
|
-
for (const [language, keys] of Object.entries(languageKeys)) {
|
|
495
|
-
unusedKeys[language] = Array.from(keys).filter((key) => !usedKeys.has(key));
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
return { missingKeys, unusedKeys };
|
|
499
|
-
};
|
|
500
|
-
|
|
501
|
-
// 生成检查结果输出
|
|
502
|
-
const generateCheckOutput = (missingKeys, unusedKeys, untranslatedTexts) => {
|
|
503
|
-
const outputLines = [];
|
|
504
|
-
outputLines.push('=== 检查结果 ===');
|
|
505
|
-
outputLines.push('');
|
|
506
|
-
outputLines.push('1. 缺失的翻译key:');
|
|
507
|
-
|
|
508
|
-
if (missingKeys.zhCN.length > 0) {
|
|
509
|
-
outputLines.push('');
|
|
510
|
-
outputLines.push('zh-cn.json 缺失的key:');
|
|
511
|
-
missingKeys.zhCN.forEach((key) => outputLines.push(` ${key}`));
|
|
512
|
-
} else {
|
|
513
|
-
outputLines.push('');
|
|
514
|
-
outputLines.push('zh-cn.json 没有缺失的key');
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
for (const [language, missing] of Object.entries(missingKeys)) {
|
|
518
|
-
if (language === 'zhCN') continue;
|
|
519
|
-
|
|
520
|
-
if (missing.length > 0) {
|
|
521
|
-
outputLines.push('');
|
|
522
|
-
outputLines.push(`${language}.json 缺失的key:`);
|
|
523
|
-
missing.forEach((key) => outputLines.push(` ${key}`));
|
|
524
|
-
} else {
|
|
525
|
-
outputLines.push('');
|
|
526
|
-
outputLines.push(`${language}.json 没有缺失的key`);
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
outputLines.push('');
|
|
531
|
-
outputLines.push('2. 未使用的翻译key:');
|
|
532
|
-
|
|
533
|
-
if (unusedKeys.zhCN.length > 0) {
|
|
534
|
-
outputLines.push('');
|
|
535
|
-
outputLines.push('zh-cn.json 中未使用的key:');
|
|
536
|
-
unusedKeys.zhCN.forEach((key) => outputLines.push(` ${key}`));
|
|
537
|
-
} else {
|
|
538
|
-
outputLines.push('');
|
|
539
|
-
outputLines.push('zh-cn.json 没有未使用的key');
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
for (const [language, unused] of Object.entries(unusedKeys)) {
|
|
543
|
-
if (language === 'zhCN') continue;
|
|
544
|
-
|
|
545
|
-
if (unused.length > 0) {
|
|
546
|
-
outputLines.push('');
|
|
547
|
-
outputLines.push(`${language}.json 中未使用的key:`);
|
|
548
|
-
unused.forEach((key) => outputLines.push(` ${key}`));
|
|
549
|
-
} else {
|
|
550
|
-
outputLines.push('');
|
|
551
|
-
outputLines.push(`${language}.json 没有未使用的key`);
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
outputLines.push('');
|
|
556
|
-
outputLines.push('3. 未翻译的中文文案:');
|
|
557
|
-
|
|
558
|
-
if (untranslatedTexts.length > 0) {
|
|
559
|
-
outputLines.push('');
|
|
560
|
-
outputLines.push(`共发现 ${untranslatedTexts.length} 条未翻译的中文文案:`);
|
|
561
|
-
untranslatedTexts.forEach((item) => {
|
|
562
|
-
outputLines.push(` ${item.file}:${item.line}:${item.column} "${item.text}"`);
|
|
95
|
+
const newKeysCount = await translateAndUpdateFiles(i18n, config, {
|
|
96
|
+
isUpdate,
|
|
97
|
+
fileCodeMap,
|
|
98
|
+
needImportFiles,
|
|
99
|
+
engine,
|
|
563
100
|
});
|
|
564
|
-
} else {
|
|
565
|
-
outputLines.push('');
|
|
566
|
-
outputLines.push('没有发现未翻译的中文文案');
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
outputLines.push('');
|
|
570
|
-
outputLines.push('=== 检查完成 ===');
|
|
571
101
|
|
|
572
|
-
|
|
573
|
-
};
|
|
102
|
+
copyI18nFile(i18nFilePath, isUpdate);
|
|
574
103
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
unusedKeys,
|
|
580
|
-
untranslatedTexts,
|
|
581
|
-
autoDeleteUnused,
|
|
582
|
-
) => {
|
|
583
|
-
// 判断是否需要生成JSON文件(自动删除模式下不生成日志文件)
|
|
584
|
-
if (outputLines.length > 100 && !autoDeleteUnused) {
|
|
585
|
-
// 生成JSON文件
|
|
586
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
587
|
-
const jsonFileName = `check-result-${timestamp}.json`;
|
|
588
|
-
|
|
589
|
-
const jsonResult = {
|
|
590
|
-
timestamp: new Date().toISOString(),
|
|
591
|
-
summary: {
|
|
592
|
-
totalMissingKeys: Object.values(missingKeys).reduce((sum, keys) => sum + keys.length, 0),
|
|
593
|
-
totalUnusedKeys: Object.values(unusedKeys).reduce((sum, keys) => sum + keys.length, 0),
|
|
594
|
-
totalUntranslatedTexts: untranslatedTexts.length,
|
|
595
|
-
totalOutputLines: outputLines.length,
|
|
596
|
-
},
|
|
597
|
-
missingKeys,
|
|
598
|
-
unusedKeys,
|
|
599
|
-
untranslatedTexts,
|
|
600
|
-
};
|
|
104
|
+
const resultMsg = isUpdate
|
|
105
|
+
? `新增文案${engineLabel}翻译补充完成!新增${newKeysCount}条翻译文案`
|
|
106
|
+
: `${modeLabel}: ${sourcePath} 目录下文件中文文案提取、替换和${engineLabel}翻译成功!`;
|
|
107
|
+
console.log(` ${resultMsg}`);
|
|
601
108
|
|
|
602
|
-
|
|
603
|
-
fs.writeJsonSync(jsonFileName, jsonResult, { spaces: 2 });
|
|
604
|
-
|
|
605
|
-
// 输出简化版本
|
|
606
|
-
console.log('\n=== 检查结果 ===');
|
|
607
|
-
console.log(`\n⚠️ 输出内容较多(${outputLines.length}行),详细结果已保存到: ${jsonFileName}`);
|
|
608
|
-
console.log('\n📊 概要信息:');
|
|
609
|
-
console.log(` • 缺失的翻译key总数: ${jsonResult.summary.totalMissingKeys}`);
|
|
610
|
-
console.log(` • 未使用的翻译key总数: ${jsonResult.summary.totalUnusedKeys}`);
|
|
611
|
-
console.log(` • 未翻译的中文文案总数: ${jsonResult.summary.totalUntranslatedTexts}`);
|
|
612
|
-
|
|
613
|
-
// 显示各语言的统计
|
|
614
|
-
console.log('\n📋 各语言统计:');
|
|
615
|
-
console.log(' 缺失的key:');
|
|
616
|
-
for (const [language, missing] of Object.entries(missingKeys)) {
|
|
617
|
-
const displayLang = language === 'zhCN' ? 'zh-cn' : language;
|
|
618
|
-
console.log(` ${displayLang}.json: ${missing.length}个`);
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
console.log(' 未使用的key:');
|
|
622
|
-
for (const [language, unused] of Object.entries(unusedKeys)) {
|
|
623
|
-
const displayLang = language === 'zhCN' ? 'zh-cn' : language;
|
|
624
|
-
console.log(` ${displayLang}.json: ${unused.length}个`);
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
if (untranslatedTexts.length > 0) {
|
|
628
|
-
console.log(` 未翻译的中文文案: ${untranslatedTexts.length}条`);
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
console.log('\n=== 检查完成 ===\n');
|
|
632
|
-
} catch (error) {
|
|
633
|
-
console.error(`\n❌ 生成JSON文件失败: ${error.message}`);
|
|
634
|
-
if (!autoDeleteUnused) {
|
|
635
|
-
outputLines.forEach((line) => console.log(line));
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
} else {
|
|
639
|
-
// 正常输出(自动删除模式下不输出日志)
|
|
640
|
-
if (!autoDeleteUnused) {
|
|
641
|
-
console.log('');
|
|
642
|
-
outputLines.forEach((line) => console.log(line));
|
|
643
|
-
console.log('');
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
};
|
|
647
|
-
|
|
648
|
-
// 处理未使用key的删除
|
|
649
|
-
const handleUnusedKeysDeletion = async (
|
|
650
|
-
unusedKeys,
|
|
651
|
-
zhCN,
|
|
652
|
-
languageFiles,
|
|
653
|
-
localesFilePath,
|
|
654
|
-
autoDeleteUnused,
|
|
655
|
-
) => {
|
|
656
|
-
const hasUnusedKeys = Object.values(unusedKeys).some((keys) => keys.length > 0);
|
|
657
|
-
if (!hasUnusedKeys) return;
|
|
658
|
-
|
|
659
|
-
let shouldDelete = false;
|
|
660
|
-
|
|
661
|
-
if (autoDeleteUnused) {
|
|
662
|
-
// 自动删除模式
|
|
663
|
-
shouldDelete = true;
|
|
664
|
-
console.log(' # 自动删除未使用的key...');
|
|
665
|
-
} else {
|
|
666
|
-
// 询问用户是否删除
|
|
667
|
-
const answer = await askQuestion('是否删除未使用的key?(y/n): ');
|
|
668
|
-
shouldDelete = answer === 'y' || answer === 'yes';
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
if (shouldDelete) {
|
|
672
|
-
// 删除zh-cn.json中未使用的key
|
|
673
|
-
if (unusedKeys.zhCN.length > 0) {
|
|
674
|
-
unusedKeys.zhCN.forEach((key) => {
|
|
675
|
-
delete zhCN[key];
|
|
676
|
-
});
|
|
677
|
-
fs.writeJsonSync(`${localesFilePath}/zh-cn.json`, zhCN, { spaces: 2 });
|
|
678
|
-
console.log(` # 已从zh-cn.json中删除${unusedKeys.zhCN.length}个未使用的key`);
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
// 删除其他语言文件中未使用的key
|
|
682
|
-
for (const [language, unused] of Object.entries(unusedKeys)) {
|
|
683
|
-
if (language === 'zhCN') continue; // 跳过中文,已经处理过了
|
|
684
|
-
|
|
685
|
-
if (unused.length > 0) {
|
|
686
|
-
unused.forEach((key) => {
|
|
687
|
-
delete languageFiles[language][key];
|
|
688
|
-
});
|
|
689
|
-
fs.writeJsonSync(`${localesFilePath}/${language}.json`, languageFiles[language], {
|
|
690
|
-
spaces: 2,
|
|
691
|
-
});
|
|
692
|
-
console.log(` # 已从${language}.json中删除${unused.length}个未使用的key`);
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
} else {
|
|
696
|
-
console.log(' # 已取消删除操作');
|
|
697
|
-
}
|
|
698
|
-
};
|
|
699
|
-
|
|
700
|
-
// 处理Excel生成的交互逻辑
|
|
701
|
-
const handleExcelGeneration = async (missingKeys, usedKeysData, zhCN, params, autoDeleteUnused) => {
|
|
702
|
-
const hasAnyMissingKeys = Object.values(missingKeys).some((keys) => keys.length > 0);
|
|
703
|
-
|
|
704
|
-
if (!hasAnyMissingKeys || !params.medusa || autoDeleteUnused) {
|
|
705
|
-
if (hasAnyMissingKeys && !params.medusa && !autoDeleteUnused) {
|
|
706
|
-
console.log('\n=== 提示 ===');
|
|
707
|
-
console.log(' [提示] 检测到丢失的翻译key,但未配置medusa,无法生成Excel文件');
|
|
708
|
-
console.log(' [提示] 请在配置文件中添加medusa配置以启用Excel生成功能');
|
|
709
|
-
}
|
|
710
|
-
return;
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
console.log('\n=== Excel生成选项 ===');
|
|
714
|
-
const answer = await askQuestion('检测到丢失的翻译key,是否生成Excel文件用于翻译?(y/n): ');
|
|
715
|
-
|
|
716
|
-
if (answer !== 'y' && answer !== 'yes') {
|
|
717
|
-
console.log(' [取消] 已取消Excel文件生成');
|
|
718
|
-
return;
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
try {
|
|
722
|
-
// 让用户选择要生成Excel的语言
|
|
723
|
-
console.log('\n各语言缺失的翻译key数量:');
|
|
724
|
-
const languageOptions = [];
|
|
725
|
-
|
|
726
|
-
for (const [language, keys] of Object.entries(missingKeys)) {
|
|
727
|
-
if (keys.length > 0) {
|
|
728
|
-
const languageName = language === 'zhCN' ? 'zh-cn' : language;
|
|
729
|
-
console.log(` ${languageName}: ${keys.length}个缺失key`);
|
|
730
|
-
languageOptions.push({ code: languageName, keys: keys, originalCode: language });
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
if (languageOptions.length === 0) {
|
|
735
|
-
console.log(' 没有缺失的key');
|
|
736
|
-
return;
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
// 尝试使用inquirer进行交互式选择,如果不支持则回退到数字输入
|
|
740
|
-
let selectedLanguage;
|
|
741
|
-
|
|
742
|
-
try {
|
|
743
|
-
// 动态导入inquirer
|
|
744
|
-
const inquirer = await import('inquirer');
|
|
745
|
-
|
|
746
|
-
const choices = languageOptions.map((option) => ({
|
|
747
|
-
name: `${option.code} (${option.keys.length}个缺失key)`,
|
|
748
|
-
value: option,
|
|
749
|
-
}));
|
|
750
|
-
|
|
751
|
-
const result = await inquirer.default.prompt([
|
|
752
|
-
{
|
|
753
|
-
type: 'list',
|
|
754
|
-
name: 'selectedLanguage',
|
|
755
|
-
message: '请选择要生成Excel的语言(使用上下键选择,回车确认):',
|
|
756
|
-
choices: choices,
|
|
757
|
-
pageSize: 10,
|
|
758
|
-
},
|
|
759
|
-
]);
|
|
760
|
-
|
|
761
|
-
selectedLanguage = result.selectedLanguage;
|
|
762
|
-
} catch (error) {
|
|
763
|
-
if (error.isTtyError) {
|
|
764
|
-
// 回退到数字输入模式
|
|
765
|
-
console.log('\n当前环境不支持交互式选择,使用数字输入模式:');
|
|
766
|
-
languageOptions.forEach((option, index) => {
|
|
767
|
-
console.log(` ${index + 1}. ${option.code} (${option.keys.length}个缺失key)`);
|
|
768
|
-
});
|
|
769
|
-
|
|
770
|
-
const languageAnswer = await askQuestion(
|
|
771
|
-
`请输入数字选择语言 (1-${languageOptions.length}): `,
|
|
772
|
-
);
|
|
773
|
-
const selectedIndex = parseInt(languageAnswer) - 1;
|
|
774
|
-
|
|
775
|
-
if (selectedIndex < 0 || selectedIndex >= languageOptions.length) {
|
|
776
|
-
console.log(' [取消] 选择无效,已取消Excel文件生成');
|
|
777
|
-
return;
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
selectedLanguage = languageOptions[selectedIndex];
|
|
781
|
-
} else {
|
|
782
|
-
throw error;
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
const missingKeysArray = selectedLanguage.keys;
|
|
787
|
-
|
|
788
|
-
console.log(
|
|
789
|
-
`\n已选择语言: ${selectedLanguage.code},将为${missingKeysArray.length}个缺失key生成Excel`,
|
|
790
|
-
);
|
|
791
|
-
|
|
792
|
-
if (missingKeysArray.length > 0) {
|
|
793
|
-
// 构建自定义数据源,只包含缺失的key和对应的dm值
|
|
794
|
-
const customData = {};
|
|
795
|
-
|
|
796
|
-
console.log(` [信息] 准备为${missingKeysArray.length}个缺失的key生成Excel`);
|
|
797
|
-
|
|
798
|
-
for (const key of missingKeysArray) {
|
|
799
|
-
customData[key] = {};
|
|
800
|
-
|
|
801
|
-
// 为每种语言设置数据
|
|
802
|
-
for (const [languageFile, columnName] of Object.entries(params.medusa.keyMap)) {
|
|
803
|
-
if (languageFile === 'zh-cn') {
|
|
804
|
-
// Simple Chinese列的逻辑
|
|
805
|
-
if (selectedLanguage.originalCode === 'zhCN') {
|
|
806
|
-
// 如果选择的是中文语言,使用dm值
|
|
807
|
-
const dmValue = usedKeysData[key] || '';
|
|
808
|
-
customData[key][languageFile] = dmValue;
|
|
809
|
-
|
|
810
|
-
if (dmValue) {
|
|
811
|
-
console.log(` [数据] ${key} -> "${dmValue}"`);
|
|
812
|
-
} else {
|
|
813
|
-
console.log(` [数据] ${key} -> (无dm值)`);
|
|
814
|
-
}
|
|
815
|
-
} else {
|
|
816
|
-
// 如果选择的是其他语言,从现有的中文翻译文件中获取值
|
|
817
|
-
const existingZhValue = zhCN[key] || usedKeysData[key] || '';
|
|
818
|
-
customData[key][languageFile] = existingZhValue;
|
|
819
|
-
|
|
820
|
-
if (existingZhValue) {
|
|
821
|
-
console.log(` [数据] ${key} -> "${existingZhValue}" (来自zh-cn.json或dm)`);
|
|
822
|
-
} else {
|
|
823
|
-
console.log(` [数据] ${key} -> (无中文值)`);
|
|
824
|
-
}
|
|
825
|
-
}
|
|
826
|
-
} else {
|
|
827
|
-
// 其他语言列为空,等待翻译
|
|
828
|
-
customData[key][languageFile] = '';
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
const excelPath = await generateMedusaExcel({
|
|
834
|
-
localesFilePath: params.localesFilePath,
|
|
835
|
-
medusa: params.medusa,
|
|
836
|
-
newKeys: missingKeysArray,
|
|
837
|
-
customData,
|
|
838
|
-
});
|
|
839
|
-
|
|
840
|
-
console.log(` [完成] Excel文件已生成: ${excelPath}`);
|
|
841
|
-
console.log(' [提示] 请在Excel中完成翻译后,使用相应的导入功能更新翻译文件');
|
|
842
|
-
} else {
|
|
843
|
-
console.log(' [信息] 没有需要生成Excel的丢失key');
|
|
844
|
-
}
|
|
109
|
+
return newKeysCount;
|
|
845
110
|
} catch (error) {
|
|
846
|
-
console.error(
|
|
847
|
-
|
|
848
|
-
console.error(error.stack);
|
|
849
|
-
}
|
|
111
|
+
console.error(' [错误] 翻译失败:', error);
|
|
112
|
+
throw error;
|
|
850
113
|
}
|
|
851
114
|
};
|
|
852
115
|
|
|
853
|
-
// 检查翻译
|
|
854
116
|
const check = async function (sourcePath, params) {
|
|
855
117
|
const {
|
|
856
118
|
fileType = '.tsx,.js,.ts,.jsx',
|
|
857
119
|
localesFilePath,
|
|
858
120
|
dashScope,
|
|
859
|
-
medusa,
|
|
860
121
|
autoDeleteUnused,
|
|
861
122
|
summaryOnly,
|
|
862
123
|
doNotTranslateFiles,
|
|
863
124
|
check: checkConfig,
|
|
125
|
+
callExpression,
|
|
864
126
|
} = params;
|
|
865
127
|
|
|
866
128
|
const checkLanguages = checkConfig?.languages;
|
|
867
129
|
|
|
868
|
-
// 扫描文件
|
|
869
130
|
const fileTypes = fileType.split(',').map((s) => s.trim());
|
|
870
131
|
const re = new RegExp(`(${fileTypes.map((ft) => ft.replace('.', '\\.')).join('|')})$`);
|
|
871
132
|
const files = find.fileSync(re, sourcePath);
|
|
872
133
|
console.log(` # 检查${files.length}个${fileType}文件`);
|
|
873
134
|
|
|
874
|
-
|
|
875
|
-
const { usedKeys, usedKeysData } = scanFilesAndCollectKeys(files);
|
|
876
|
-
|
|
877
|
-
// 读取翻译文件
|
|
135
|
+
const { usedKeys, usedKeysData } = scanFilesAndCollectKeys(files, callExpression);
|
|
878
136
|
const { zhCN, languageFiles } = readTranslationFiles(localesFilePath, dashScope, checkLanguages);
|
|
879
|
-
|
|
880
|
-
// 分析缺失和未使用的key
|
|
881
137
|
const { missingKeys, unusedKeys } = analyzeMissingAndUnusedKeys(usedKeys, zhCN, languageFiles);
|
|
882
138
|
|
|
883
|
-
// 扫描未翻译的中文文案
|
|
884
139
|
console.log(` # 扫描未翻译的中文文案...`);
|
|
885
|
-
const untranslatedTexts = scanUntranslatedTexts(files, doNotTranslateFiles);
|
|
140
|
+
const untranslatedTexts = scanUntranslatedTexts(files, doNotTranslateFiles, callExpression);
|
|
886
141
|
if (untranslatedTexts.length > 0) {
|
|
887
142
|
console.log(` # 发现 ${untranslatedTexts.length} 条未翻译的中文文案`);
|
|
888
143
|
} else {
|
|
889
144
|
console.log(` # 未发现未翻译的中文文案`);
|
|
890
145
|
}
|
|
891
146
|
|
|
892
|
-
// summaryOnly 模式:只输出各语言缺失的key数量,不做删除/生成等操作
|
|
893
147
|
if (summaryOnly) {
|
|
894
148
|
console.log('');
|
|
895
149
|
console.log('=== 翻译缺失统计 ===');
|
|
@@ -932,44 +186,24 @@ const check = async function (sourcePath, params) {
|
|
|
932
186
|
return { missingKeys, unusedKeys, usedKeysData, untranslatedTexts };
|
|
933
187
|
}
|
|
934
188
|
|
|
935
|
-
// 完整模式:输出详情、删除未使用key、生成Excel等
|
|
936
189
|
const outputLines = generateCheckOutput(missingKeys, unusedKeys, untranslatedTexts);
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
outputLines,
|
|
940
|
-
missingKeys,
|
|
941
|
-
unusedKeys,
|
|
942
|
-
untranslatedTexts,
|
|
943
|
-
autoDeleteUnused,
|
|
944
|
-
);
|
|
945
|
-
|
|
946
|
-
await handleUnusedKeysDeletion(
|
|
947
|
-
unusedKeys,
|
|
948
|
-
zhCN,
|
|
949
|
-
languageFiles,
|
|
950
|
-
localesFilePath,
|
|
951
|
-
autoDeleteUnused,
|
|
952
|
-
);
|
|
953
|
-
|
|
190
|
+
handleOutputAndJsonGeneration(outputLines, missingKeys, unusedKeys, untranslatedTexts, autoDeleteUnused);
|
|
191
|
+
await handleUnusedKeysDeletion(unusedKeys, zhCN, languageFiles, localesFilePath, autoDeleteUnused);
|
|
954
192
|
await handleExcelGeneration(missingKeys, usedKeysData, zhCN, params, autoDeleteUnused);
|
|
955
193
|
|
|
956
|
-
|
|
957
|
-
missingKeys,
|
|
958
|
-
unusedKeys,
|
|
959
|
-
usedKeysData,
|
|
960
|
-
untranslatedTexts,
|
|
961
|
-
};
|
|
962
|
-
|
|
963
|
-
return result;
|
|
194
|
+
return { missingKeys, unusedKeys, usedKeysData, untranslatedTexts };
|
|
964
195
|
};
|
|
965
196
|
|
|
966
|
-
// 还原国际化代码为中文
|
|
967
197
|
const reverse = async function (sourcePath, params) {
|
|
968
|
-
const { fileType = '.tsx,.js,.ts,.jsx', localesFilePath, functionImportPath } =
|
|
198
|
+
const { fileType = '.tsx,.js,.ts,.jsx', localesFilePath, functionImportPath, callExpression } =
|
|
199
|
+
params;
|
|
200
|
+
const importName =
|
|
201
|
+
(callExpression && callExpression.importName) ||
|
|
202
|
+
(callExpression && callExpression.functionName) ||
|
|
203
|
+
'$i18n';
|
|
969
204
|
|
|
970
205
|
console.log(` [开始] 开始还原国际化代码为中文...`);
|
|
971
206
|
|
|
972
|
-
// 读取中文翻译文件
|
|
973
207
|
let zhCN = {};
|
|
974
208
|
try {
|
|
975
209
|
zhCN = fs.readJsonSync(`${localesFilePath}/zh-cn.json`);
|
|
@@ -979,42 +213,25 @@ const reverse = async function (sourcePath, params) {
|
|
|
979
213
|
|
|
980
214
|
const fileTypes = fileType.split(',').map((s) => s.trim());
|
|
981
215
|
const re = new RegExp(`(${fileTypes.map((ft) => ft.replace('.', '\\.')).join('|')})$`);
|
|
982
|
-
|
|
983
|
-
// 检索文件
|
|
984
216
|
const files = find.fileSync(re, sourcePath);
|
|
985
217
|
console.log(` [进度] 找到${files.length}个${fileType}文件需要处理`);
|
|
986
218
|
|
|
987
219
|
let totalReversed = 0;
|
|
988
220
|
let totalFilesModified = 0;
|
|
989
221
|
|
|
990
|
-
// 遍历所有文件
|
|
991
222
|
for (const file of files) {
|
|
992
223
|
try {
|
|
993
224
|
const code = fs.readFileSync(file, 'utf8');
|
|
994
|
-
|
|
995
|
-
// 解析文件
|
|
996
225
|
const ast = parseAST(code);
|
|
997
226
|
let hasChanges = false;
|
|
998
227
|
let reversedCount = 0;
|
|
999
228
|
|
|
1000
|
-
// 遍历AST查找$i18n.get调用并替换
|
|
1001
229
|
traverse(ast, {
|
|
1002
230
|
CallExpression(path) {
|
|
1003
|
-
const result = extractI18nKey(path.node);
|
|
231
|
+
const result = extractI18nKey(path.node, callExpression);
|
|
1004
232
|
if (result && result.id) {
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
// 优先使用dm字段(defaultMessage)
|
|
1008
|
-
if (result.dm) {
|
|
1009
|
-
chineseText = result.dm;
|
|
1010
|
-
}
|
|
1011
|
-
// 如果没有dm字段,从zh-cn.json中查找
|
|
1012
|
-
else if (zhCN[result.id]) {
|
|
1013
|
-
chineseText = zhCN[result.id];
|
|
1014
|
-
}
|
|
1015
|
-
|
|
233
|
+
const chineseText = result.dm || zhCN[result.id] || null;
|
|
1016
234
|
if (chineseText) {
|
|
1017
|
-
// 替换为中文文本
|
|
1018
235
|
path.replaceWith(t.stringLiteral(chineseText));
|
|
1019
236
|
hasChanges = true;
|
|
1020
237
|
reversedCount++;
|
|
@@ -1023,7 +240,6 @@ const reverse = async function (sourcePath, params) {
|
|
|
1023
240
|
},
|
|
1024
241
|
});
|
|
1025
242
|
|
|
1026
|
-
// 如果有改动,写回文件
|
|
1027
243
|
if (hasChanges) {
|
|
1028
244
|
const newCode = generateCode(ast, code);
|
|
1029
245
|
fs.writeFileSync(file, newCode);
|
|
@@ -1039,13 +255,7 @@ const reverse = async function (sourcePath, params) {
|
|
|
1039
255
|
}
|
|
1040
256
|
}
|
|
1041
257
|
|
|
1042
|
-
// 删除导入语句
|
|
1043
258
|
console.log(` [进度] 开始删除导入语句...`);
|
|
1044
|
-
if (functionImportPath) {
|
|
1045
|
-
console.log(` [信息] 匹配的导入路径: ${functionImportPath}`);
|
|
1046
|
-
} else {
|
|
1047
|
-
console.log(` [信息] 未配置functionImportPath,将删除所有$i18n导入语句`);
|
|
1048
|
-
}
|
|
1049
259
|
let importRemovedCount = 0;
|
|
1050
260
|
|
|
1051
261
|
for (const file of files) {
|
|
@@ -1054,56 +264,38 @@ const reverse = async function (sourcePath, params) {
|
|
|
1054
264
|
const ast = parseAST(code);
|
|
1055
265
|
let hasImportChanges = false;
|
|
1056
266
|
|
|
1057
|
-
// 查找并删除 $i18n 的导入语句
|
|
1058
267
|
traverse(ast, {
|
|
1059
268
|
ImportDeclaration(path) {
|
|
1060
269
|
const source = path.node.source.value;
|
|
1061
270
|
const specifiers = path.node.specifiers;
|
|
1062
271
|
|
|
1063
|
-
// 检查是否是来自指定路径的 $i18n 导入(支持默认导入和命名导入)
|
|
1064
272
|
const hasI18nImport = specifiers.some(
|
|
1065
273
|
(spec) =>
|
|
1066
|
-
(spec.type === 'ImportSpecifier' && spec.imported.name ===
|
|
1067
|
-
(spec.type === 'ImportDefaultSpecifier' && spec.local.name ===
|
|
274
|
+
(spec.type === 'ImportSpecifier' && spec.imported.name === importName) ||
|
|
275
|
+
(spec.type === 'ImportDefaultSpecifier' && spec.local.name === importName),
|
|
1068
276
|
);
|
|
1069
277
|
|
|
1070
|
-
// 添加调试信息
|
|
1071
|
-
if (hasI18nImport) {
|
|
1072
|
-
console.log(` [调试] 找到$i18n导入语句:`);
|
|
1073
|
-
console.log(` - 导入路径: "${source}"`);
|
|
1074
|
-
console.log(` - 配置的functionImportPath: "${functionImportPath}"`);
|
|
1075
|
-
}
|
|
1076
|
-
|
|
1077
|
-
// 检查导入路径是否匹配(如果未配置functionImportPath,则匹配所有)
|
|
1078
278
|
const isMatchingPath =
|
|
1079
279
|
!functionImportPath ||
|
|
1080
280
|
source === functionImportPath ||
|
|
1081
281
|
source.endsWith(functionImportPath.replace(/^@\//, '')) ||
|
|
1082
282
|
functionImportPath.endsWith(source.replace(/^@\//, ''));
|
|
1083
283
|
|
|
1084
|
-
if (hasI18nImport) {
|
|
1085
|
-
console.log(` - 是否匹配路径: ${isMatchingPath}`);
|
|
1086
|
-
}
|
|
1087
|
-
|
|
1088
284
|
if (hasI18nImport && isMatchingPath) {
|
|
1089
|
-
// 移除 $i18n 的导入(支持默认导入和命名导入)
|
|
1090
285
|
const remainingSpecifiers = specifiers.filter(
|
|
1091
286
|
(spec) =>
|
|
1092
287
|
!(
|
|
1093
|
-
(spec.type === 'ImportSpecifier' && spec.imported.name ===
|
|
1094
|
-
(spec.type === 'ImportDefaultSpecifier' && spec.local.name ===
|
|
288
|
+
(spec.type === 'ImportSpecifier' && spec.imported.name === importName) ||
|
|
289
|
+
(spec.type === 'ImportDefaultSpecifier' && spec.local.name === importName)
|
|
1095
290
|
),
|
|
1096
291
|
);
|
|
1097
292
|
|
|
1098
293
|
if (remainingSpecifiers.length === 0) {
|
|
1099
|
-
// 如果没有其他导入,删除整个导入语句
|
|
1100
294
|
path.remove();
|
|
1101
295
|
} else {
|
|
1102
|
-
// 更新导入语句,移除 $i18n
|
|
1103
296
|
path.node.specifiers = remainingSpecifiers;
|
|
1104
297
|
}
|
|
1105
298
|
hasImportChanges = true;
|
|
1106
|
-
console.log(` [删除] 从 ${file} 中删除 $i18n 导入: ${source}`);
|
|
1107
299
|
}
|
|
1108
300
|
},
|
|
1109
301
|
});
|
|
@@ -1118,23 +310,18 @@ const reverse = async function (sourcePath, params) {
|
|
|
1118
310
|
}
|
|
1119
311
|
}
|
|
1120
312
|
|
|
1121
|
-
console.log(` [完成] 已从${importRemovedCount}个文件中删除$
|
|
313
|
+
console.log(` [完成] 已从${importRemovedCount}个文件中删除${importName}导入语句`);
|
|
1122
314
|
|
|
1123
315
|
console.log(`\n=== 还原完成 ===`);
|
|
1124
316
|
console.log(`共处理了${files.length}个文件`);
|
|
1125
317
|
console.log(`修改了${totalFilesModified}个文件`);
|
|
1126
318
|
console.log(`还原了${totalReversed}处国际化代码`);
|
|
1127
319
|
|
|
1128
|
-
return {
|
|
1129
|
-
totalFiles: files.length,
|
|
1130
|
-
modifiedFiles: totalFilesModified,
|
|
1131
|
-
reversedCount: totalReversed,
|
|
1132
|
-
};
|
|
320
|
+
return { totalFiles: files.length, modifiedFiles: totalFilesModified, reversedCount: totalReversed };
|
|
1133
321
|
};
|
|
1134
322
|
|
|
1135
323
|
/**
|
|
1136
|
-
* translate:
|
|
1137
|
-
* 不会修改源代码,是三步工作流的第一步。
|
|
324
|
+
* translate: 三步工作流 Step 1 — 只生成 locale 文件,不修改源码
|
|
1138
325
|
*/
|
|
1139
326
|
const translate = async function (sourcePath, params) {
|
|
1140
327
|
try {
|
|
@@ -1148,12 +335,8 @@ const translate = async function (sourcePath, params) {
|
|
|
1148
335
|
file: singleFile,
|
|
1149
336
|
} = params;
|
|
1150
337
|
|
|
1151
|
-
if (!keyPrefix)
|
|
1152
|
-
|
|
1153
|
-
}
|
|
1154
|
-
if (!dashScope || !dashScope.apiKey) {
|
|
1155
|
-
throw new Error('dashScope.apiKey is required for translation');
|
|
1156
|
-
}
|
|
338
|
+
if (!keyPrefix) throw new Error('keyPrefix is required in params');
|
|
339
|
+
if (!dashScope || !dashScope.apiKey) throw new Error('dashScope.apiKey is required for translation');
|
|
1157
340
|
|
|
1158
341
|
console.log('');
|
|
1159
342
|
console.log('=== [Step 1/3] translate ===');
|
|
@@ -1175,11 +358,7 @@ const translate = async function (sourcePath, params) {
|
|
|
1175
358
|
|
|
1176
359
|
const zhCNPath = `${localesFilePath}/zh-cn.json`;
|
|
1177
360
|
let existingZhCN = {};
|
|
1178
|
-
try {
|
|
1179
|
-
existingZhCN = fs.readJsonSync(zhCNPath);
|
|
1180
|
-
} catch {
|
|
1181
|
-
// first run
|
|
1182
|
-
}
|
|
361
|
+
try { existingZhCN = fs.readJsonSync(zhCNPath); } catch { /* first run */ }
|
|
1183
362
|
const finalZhCN = { ...existingZhCN, ...zhCN };
|
|
1184
363
|
fs.ensureDirSync(localesFilePath);
|
|
1185
364
|
fs.writeJsonSync(zhCNPath, finalZhCN, { spaces: 2 });
|
|
@@ -1191,32 +370,20 @@ const translate = async function (sourcePath, params) {
|
|
|
1191
370
|
for (const [language, translatedI18n] of Object.entries(translatedResults)) {
|
|
1192
371
|
const languagePath = `${localesFilePath}/${language}.json`;
|
|
1193
372
|
let existing = {};
|
|
1194
|
-
try {
|
|
1195
|
-
existing = fs.readJsonSync(languagePath);
|
|
1196
|
-
} catch {
|
|
1197
|
-
// first run
|
|
1198
|
-
}
|
|
373
|
+
try { existing = fs.readJsonSync(languagePath); } catch { /* first run */ }
|
|
1199
374
|
fs.writeJsonSync(languagePath, { ...existing, ...translatedI18n }, { spaces: 2 });
|
|
1200
375
|
console.log(` [完成] 更新 ${language}.json`);
|
|
1201
376
|
}
|
|
1202
377
|
|
|
1203
378
|
const newKeys = Object.keys(zhCN);
|
|
1204
379
|
|
|
1205
|
-
generateMdsPayload({
|
|
1206
|
-
localesFilePath,
|
|
1207
|
-
appName: medusa?.appName,
|
|
1208
|
-
filterKeys: newKeys,
|
|
1209
|
-
});
|
|
380
|
+
generateMdsPayload({ localesFilePath, appName: medusa?.appName, filterKeys: newKeys });
|
|
1210
381
|
|
|
1211
382
|
const statePath = path.resolve(localesFilePath, '.i18n-state.json');
|
|
1212
|
-
fs.writeJsonSync(
|
|
1213
|
-
statePath,
|
|
1214
|
-
{ keyMap, newKeys, sourcePath, timestamp: new Date().toISOString() },
|
|
1215
|
-
{ spaces: 2 },
|
|
1216
|
-
);
|
|
383
|
+
fs.writeJsonSync(statePath, { keyMap, newKeys, sourcePath, timestamp: new Date().toISOString() }, { spaces: 2 });
|
|
1217
384
|
console.log(` [完成] 翻译状态已保存到 ${statePath}`);
|
|
1218
385
|
|
|
1219
|
-
if (medusa && medusa.appName && medusa.
|
|
386
|
+
if (medusa && medusa.appName && medusa.keyMap) {
|
|
1220
387
|
try {
|
|
1221
388
|
await generateMedusaExcel({ localesFilePath, medusa, newKeys });
|
|
1222
389
|
} catch (error) {
|
|
@@ -1237,8 +404,7 @@ const translate = async function (sourcePath, params) {
|
|
|
1237
404
|
};
|
|
1238
405
|
|
|
1239
406
|
/**
|
|
1240
|
-
* replaceOnly:
|
|
1241
|
-
* 依赖 translate 步骤保存的 .i18n-state.json,是三步工作流的第三步。
|
|
407
|
+
* replaceOnly: 三步工作流 Step 3 — 用 .i18n-state.json 替换源码
|
|
1242
408
|
*/
|
|
1243
409
|
const replaceOnly = async function (sourcePath, params) {
|
|
1244
410
|
try {
|
|
@@ -1249,16 +415,20 @@ const replaceOnly = async function (sourcePath, params) {
|
|
|
1249
415
|
doNotTranslateFiles,
|
|
1250
416
|
functionImportPath,
|
|
1251
417
|
i18nFilePath,
|
|
418
|
+
callExpression,
|
|
1252
419
|
file: singleFile,
|
|
1253
420
|
} = params;
|
|
1254
421
|
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
422
|
+
const callDisplayName =
|
|
423
|
+
callExpression && callExpression.functionName
|
|
424
|
+
? `${callExpression.functionName}()`
|
|
425
|
+
: `${(callExpression && callExpression.objectName) || '$i18n'}.${(callExpression && callExpression.methodName) || 'get'}()`;
|
|
426
|
+
|
|
427
|
+
if (!keyPrefix) throw new Error('keyPrefix is required in params');
|
|
1258
428
|
|
|
1259
429
|
console.log('');
|
|
1260
430
|
console.log('=== [Step 3/3] replace ===');
|
|
1261
|
-
console.log(
|
|
431
|
+
console.log(` 将代码中的中文替换为 ${callDisplayName} 调用`);
|
|
1262
432
|
console.log('');
|
|
1263
433
|
|
|
1264
434
|
const statePath = path.resolve(localesFilePath, '.i18n-state.json');
|
|
@@ -1275,31 +445,23 @@ const replaceOnly = async function (sourcePath, params) {
|
|
|
1275
445
|
const { fileCodeMap, needImportFiles } = await processFiles(files, params);
|
|
1276
446
|
|
|
1277
447
|
console.log(` [进度] 开始替换文件中的key...`);
|
|
1278
|
-
await replaceKeysInFiles(sourcePath, keyMap, fileType, fileCodeMap);
|
|
448
|
+
await replaceKeysInFiles(sourcePath, keyMap, fileType, fileCodeMap, callExpression);
|
|
1279
449
|
|
|
1280
450
|
if (needImportFiles.size > 0 && functionImportPath) {
|
|
1281
451
|
console.log(` [进度] 开始添加导入语句...`);
|
|
1282
452
|
needImportFiles.forEach((file) => {
|
|
1283
|
-
injectImportStatement(file, functionImportPath);
|
|
453
|
+
injectImportStatement(file, functionImportPath, callExpression);
|
|
1284
454
|
});
|
|
1285
455
|
console.log(` [完成] 已为${needImportFiles.size}个文件添加导入语句`);
|
|
1286
456
|
}
|
|
1287
457
|
|
|
1288
|
-
|
|
1289
|
-
const sourceI18nPath = path.join(__dirname, '../i18n.ts');
|
|
1290
|
-
const targetI18nPath = path.join(i18nFilePath, 'index.ts');
|
|
1291
|
-
if (!fs.existsSync(targetI18nPath)) {
|
|
1292
|
-
fs.ensureDirSync(i18nFilePath);
|
|
1293
|
-
fs.copyFileSync(sourceI18nPath, targetI18nPath);
|
|
1294
|
-
console.log(` [完成] i18n.ts 已复制到 ${targetI18nPath}`);
|
|
1295
|
-
}
|
|
1296
|
-
}
|
|
458
|
+
copyI18nFile(i18nFilePath, true);
|
|
1297
459
|
|
|
1298
460
|
fs.removeSync(statePath);
|
|
1299
461
|
console.log(` [完成] 已清理临时状态文件`);
|
|
1300
462
|
|
|
1301
463
|
console.log('');
|
|
1302
|
-
console.log(
|
|
464
|
+
console.log(` ✅ replace 完成!代码中的中文已替换为 ${callDisplayName} 调用`);
|
|
1303
465
|
console.log('');
|
|
1304
466
|
} catch (error) {
|
|
1305
467
|
console.error(' [错误] replace 失败:', error);
|
|
@@ -1308,10 +470,7 @@ const replaceOnly = async function (sourcePath, params) {
|
|
|
1308
470
|
};
|
|
1309
471
|
|
|
1310
472
|
module.exports = {
|
|
1311
|
-
|
|
1312
|
-
initMT,
|
|
1313
|
-
patch,
|
|
1314
|
-
patchMT,
|
|
473
|
+
runTranslation,
|
|
1315
474
|
check,
|
|
1316
475
|
reverse,
|
|
1317
476
|
translate,
|