@agentscope-ai/i18n 1.0.1 → 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.
@@ -0,0 +1,544 @@
1
+ const fs = require('fs-extra');
2
+ const find = require('find');
3
+ const { parse } = require('@babel/parser');
4
+ const traverse = require('@babel/traverse').default;
5
+ const generate = require('@babel/generator').default;
6
+ const {
7
+ extractI18nKey,
8
+ chnRegExp,
9
+ onlyContainsChinesePunctuation,
10
+ onlyContainsEmoji,
11
+ isI18nCall,
12
+ isInConsoleCall,
13
+ } = require('../utils/ast-utils');
14
+ const { askQuestion } = require('../utils/cli-utils');
15
+ const {
16
+ shouldIgnoreLineByComment,
17
+ shouldIgnoreBlockByComment,
18
+ shouldIgnoreByComment,
19
+ shouldIgnoreFile,
20
+ } = require('../utils/file-utils');
21
+ const { generateMedusaExcel } = require('../utils/excel-utils');
22
+
23
+ const parseAST = (code) => {
24
+ return parse(code, {
25
+ sourceType: 'module',
26
+ plugins: ['jsx', 'typescript'],
27
+ });
28
+ };
29
+
30
+ const scanFilesAndCollectKeys = (files, callExpr) => {
31
+ const usedKeys = new Set();
32
+ const usedKeysData = {};
33
+
34
+ files.forEach((file) => {
35
+ try {
36
+ const code = fs.readFileSync(file, 'utf8');
37
+ const ast = parseAST(code);
38
+
39
+ traverse(ast, {
40
+ CallExpression(path) {
41
+ const result = extractI18nKey(path.node, callExpr);
42
+ if (result && result.id) {
43
+ usedKeys.add(result.id);
44
+ if (result.dm && typeof result.dm === 'string') {
45
+ usedKeysData[result.id] = result.dm;
46
+ }
47
+ }
48
+ },
49
+ });
50
+ } catch (error) {
51
+ console.error(` # 处理文件失败: ${file}`, error);
52
+ if (error.loc) {
53
+ console.error(` # 错误位置: 第${error.loc.line}行, 第${error.loc.column}列`);
54
+ }
55
+ }
56
+ });
57
+
58
+ return { usedKeys, usedKeysData };
59
+ };
60
+
61
+ const scanUntranslatedTexts = (files, doNotTranslateFiles, callExpr) => {
62
+ const untranslatedTexts = [];
63
+
64
+ const filteredFiles = files.filter((file) => {
65
+ if (shouldIgnoreFile(file, doNotTranslateFiles || [])) return false;
66
+ if (shouldIgnoreByComment(file)) return false;
67
+ return true;
68
+ });
69
+
70
+ filteredFiles.forEach((file) => {
71
+ try {
72
+ const code = fs.readFileSync(file, 'utf8');
73
+ const ast = parseAST(code);
74
+
75
+ const shouldIgnoreNode = (nodePath) => {
76
+ if (!nodePath.node.loc) return false;
77
+ const lineNumber = nodePath.node.loc.start.line;
78
+ return (
79
+ shouldIgnoreLineByComment(file, lineNumber) ||
80
+ shouldIgnoreBlockByComment(file, lineNumber)
81
+ );
82
+ };
83
+
84
+ traverse(ast, {
85
+ StringLiteral(astPath) {
86
+ const { value } = astPath.node;
87
+ if (
88
+ chnRegExp.test(value) &&
89
+ !isI18nCall(astPath.parent, callExpr) &&
90
+ !astPath.findParent((p) => isI18nCall(p.node, callExpr)) &&
91
+ !isInConsoleCall(astPath) &&
92
+ !shouldIgnoreNode(astPath) &&
93
+ !onlyContainsChinesePunctuation(value) &&
94
+ !onlyContainsEmoji(value)
95
+ ) {
96
+ untranslatedTexts.push({
97
+ file,
98
+ line: astPath.node.loc?.start.line,
99
+ column: astPath.node.loc?.start.column,
100
+ type: 'StringLiteral',
101
+ text: value,
102
+ });
103
+ }
104
+ },
105
+ TemplateLiteral(astPath) {
106
+ if (
107
+ !isI18nCall(astPath.parent, callExpr) &&
108
+ !astPath.findParent((p) => isI18nCall(p.node, callExpr)) &&
109
+ !isInConsoleCall(astPath) &&
110
+ !shouldIgnoreNode(astPath)
111
+ ) {
112
+ const staticParts = astPath.node.quasis.map((q) => q.value.raw).join('');
113
+ if (
114
+ chnRegExp.test(staticParts) &&
115
+ !onlyContainsChinesePunctuation(staticParts) &&
116
+ !onlyContainsEmoji(staticParts)
117
+ ) {
118
+ const fullText = astPath.node.quasis
119
+ .map((q, i) => {
120
+ const expr = astPath.node.expressions[i];
121
+ const exprStr = expr ? `\${${generate(expr, { compact: true }).code}}` : '';
122
+ return q.value.raw + exprStr;
123
+ })
124
+ .join('');
125
+ untranslatedTexts.push({
126
+ file,
127
+ line: astPath.node.loc?.start.line,
128
+ column: astPath.node.loc?.start.column,
129
+ type: 'TemplateLiteral',
130
+ text: fullText,
131
+ });
132
+ }
133
+ }
134
+ },
135
+ JSXText(astPath) {
136
+ const { value } = astPath.node;
137
+ if (
138
+ chnRegExp.test(value) &&
139
+ !isI18nCall(astPath.parent, callExpr) &&
140
+ !astPath.findParent((p) => isI18nCall(p.node, callExpr)) &&
141
+ !isInConsoleCall(astPath) &&
142
+ !shouldIgnoreNode(astPath) &&
143
+ !onlyContainsChinesePunctuation(value) &&
144
+ !onlyContainsEmoji(value)
145
+ ) {
146
+ const trimmedValue = value.replace(/^\s+|\s+$/g, '');
147
+ if (trimmedValue) {
148
+ untranslatedTexts.push({
149
+ file,
150
+ line: astPath.node.loc?.start.line,
151
+ column: astPath.node.loc?.start.column,
152
+ type: 'JSXText',
153
+ text: trimmedValue,
154
+ });
155
+ }
156
+ }
157
+ },
158
+ });
159
+ } catch (error) {
160
+ console.error(` # 扫描未翻译文案失败: ${file}`, error.message);
161
+ }
162
+ });
163
+
164
+ return untranslatedTexts;
165
+ };
166
+
167
+ const readTranslationFiles = (localesFilePath, dashScope, checkLanguages) => {
168
+ let zhCN = {};
169
+ try {
170
+ zhCN = fs.readJsonSync(`${localesFilePath}/zh-cn.json`);
171
+ } catch (error) {
172
+ console.log(' 未找到zh-cn.json文件');
173
+ }
174
+
175
+ let languages = [];
176
+ if (Array.isArray(checkLanguages) && checkLanguages.length > 0) {
177
+ languages = checkLanguages.filter((lang) => lang !== 'zh-cn');
178
+ } else if (dashScope && dashScope.valueTranslateAppId) {
179
+ if (typeof dashScope.valueTranslateAppId === 'string') {
180
+ languages = ['en-us'];
181
+ } else if (typeof dashScope.valueTranslateAppId === 'object') {
182
+ languages = Object.keys(dashScope.valueTranslateAppId);
183
+ }
184
+ }
185
+
186
+ const languageFiles = {};
187
+ for (const language of languages) {
188
+ try {
189
+ languageFiles[language] = fs.readJsonSync(`${localesFilePath}/${language}.json`);
190
+ } catch (error) {
191
+ console.log(` 未找到${language}.json文件`);
192
+ }
193
+ }
194
+
195
+ return { zhCN, languageFiles };
196
+ };
197
+
198
+ const analyzeMissingAndUnusedKeys = (usedKeys, zhCN, languageFiles) => {
199
+ const zhCNKeys = new Set(Object.keys(zhCN));
200
+ const languageKeys = {};
201
+ for (const [language, content] of Object.entries(languageFiles)) {
202
+ languageKeys[language] = new Set(Object.keys(content));
203
+ }
204
+
205
+ const missingKeys = {
206
+ zhCN: Array.from(usedKeys).filter((key) => !zhCNKeys.has(key)),
207
+ };
208
+ for (const [language, keys] of Object.entries(languageKeys)) {
209
+ missingKeys[language] = Array.from(usedKeys).filter((key) => !keys.has(key));
210
+ }
211
+
212
+ const unusedKeys = {
213
+ zhCN: Array.from(zhCNKeys).filter((key) => !usedKeys.has(key)),
214
+ };
215
+ for (const [language, keys] of Object.entries(languageKeys)) {
216
+ unusedKeys[language] = Array.from(keys).filter((key) => !usedKeys.has(key));
217
+ }
218
+
219
+ return { missingKeys, unusedKeys };
220
+ };
221
+
222
+ const generateCheckOutput = (missingKeys, unusedKeys, untranslatedTexts) => {
223
+ const outputLines = [];
224
+ outputLines.push('=== 检查结果 ===');
225
+ outputLines.push('');
226
+ outputLines.push('1. 缺失的翻译key:');
227
+
228
+ if (missingKeys.zhCN.length > 0) {
229
+ outputLines.push('');
230
+ outputLines.push('zh-cn.json 缺失的key:');
231
+ missingKeys.zhCN.forEach((key) => outputLines.push(` ${key}`));
232
+ } else {
233
+ outputLines.push('');
234
+ outputLines.push('zh-cn.json 没有缺失的key');
235
+ }
236
+
237
+ for (const [language, missing] of Object.entries(missingKeys)) {
238
+ if (language === 'zhCN') continue;
239
+ if (missing.length > 0) {
240
+ outputLines.push('');
241
+ outputLines.push(`${language}.json 缺失的key:`);
242
+ missing.forEach((key) => outputLines.push(` ${key}`));
243
+ } else {
244
+ outputLines.push('');
245
+ outputLines.push(`${language}.json 没有缺失的key`);
246
+ }
247
+ }
248
+
249
+ outputLines.push('');
250
+ outputLines.push('2. 未使用的翻译key:');
251
+
252
+ if (unusedKeys.zhCN.length > 0) {
253
+ outputLines.push('');
254
+ outputLines.push('zh-cn.json 中未使用的key:');
255
+ unusedKeys.zhCN.forEach((key) => outputLines.push(` ${key}`));
256
+ } else {
257
+ outputLines.push('');
258
+ outputLines.push('zh-cn.json 没有未使用的key');
259
+ }
260
+
261
+ for (const [language, unused] of Object.entries(unusedKeys)) {
262
+ if (language === 'zhCN') continue;
263
+ if (unused.length > 0) {
264
+ outputLines.push('');
265
+ outputLines.push(`${language}.json 中未使用的key:`);
266
+ unused.forEach((key) => outputLines.push(` ${key}`));
267
+ } else {
268
+ outputLines.push('');
269
+ outputLines.push(`${language}.json 没有未使用的key`);
270
+ }
271
+ }
272
+
273
+ outputLines.push('');
274
+ outputLines.push('3. 未翻译的中文文案:');
275
+
276
+ if (untranslatedTexts.length > 0) {
277
+ outputLines.push('');
278
+ outputLines.push(`共发现 ${untranslatedTexts.length} 条未翻译的中文文案:`);
279
+ untranslatedTexts.forEach((item) => {
280
+ outputLines.push(` ${item.file}:${item.line}:${item.column} "${item.text}"`);
281
+ });
282
+ } else {
283
+ outputLines.push('');
284
+ outputLines.push('没有发现未翻译的中文文案');
285
+ }
286
+
287
+ outputLines.push('');
288
+ outputLines.push('=== 检查完成 ===');
289
+
290
+ return outputLines;
291
+ };
292
+
293
+ const handleOutputAndJsonGeneration = (
294
+ outputLines,
295
+ missingKeys,
296
+ unusedKeys,
297
+ untranslatedTexts,
298
+ autoDeleteUnused,
299
+ ) => {
300
+ if (outputLines.length > 100 && !autoDeleteUnused) {
301
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
302
+ const jsonFileName = `check-result-${timestamp}.json`;
303
+
304
+ const jsonResult = {
305
+ timestamp: new Date().toISOString(),
306
+ summary: {
307
+ totalMissingKeys: Object.values(missingKeys).reduce((sum, keys) => sum + keys.length, 0),
308
+ totalUnusedKeys: Object.values(unusedKeys).reduce((sum, keys) => sum + keys.length, 0),
309
+ totalUntranslatedTexts: untranslatedTexts.length,
310
+ totalOutputLines: outputLines.length,
311
+ },
312
+ missingKeys,
313
+ unusedKeys,
314
+ untranslatedTexts,
315
+ };
316
+
317
+ try {
318
+ fs.writeJsonSync(jsonFileName, jsonResult, { spaces: 2 });
319
+
320
+ console.log('\n=== 检查结果 ===');
321
+ console.log(`\n⚠️ 输出内容较多(${outputLines.length}行),详细结果已保存到: ${jsonFileName}`);
322
+ console.log('\n📊 概要信息:');
323
+ console.log(` • 缺失的翻译key总数: ${jsonResult.summary.totalMissingKeys}`);
324
+ console.log(` • 未使用的翻译key总数: ${jsonResult.summary.totalUnusedKeys}`);
325
+ console.log(` • 未翻译的中文文案总数: ${jsonResult.summary.totalUntranslatedTexts}`);
326
+
327
+ console.log('\n📋 各语言统计:');
328
+ console.log(' 缺失的key:');
329
+ for (const [language, missing] of Object.entries(missingKeys)) {
330
+ const displayLang = language === 'zhCN' ? 'zh-cn' : language;
331
+ console.log(` ${displayLang}.json: ${missing.length}个`);
332
+ }
333
+
334
+ console.log(' 未使用的key:');
335
+ for (const [language, unused] of Object.entries(unusedKeys)) {
336
+ const displayLang = language === 'zhCN' ? 'zh-cn' : language;
337
+ console.log(` ${displayLang}.json: ${unused.length}个`);
338
+ }
339
+
340
+ if (untranslatedTexts.length > 0) {
341
+ console.log(` 未翻译的中文文案: ${untranslatedTexts.length}条`);
342
+ }
343
+
344
+ console.log('\n=== 检查完成 ===\n');
345
+ } catch (error) {
346
+ console.error(`\n❌ 生成JSON文件失败: ${error.message}`);
347
+ if (!autoDeleteUnused) {
348
+ outputLines.forEach((line) => console.log(line));
349
+ }
350
+ }
351
+ } else {
352
+ if (!autoDeleteUnused) {
353
+ console.log('');
354
+ outputLines.forEach((line) => console.log(line));
355
+ console.log('');
356
+ }
357
+ }
358
+ };
359
+
360
+ const handleUnusedKeysDeletion = async (
361
+ unusedKeys,
362
+ zhCN,
363
+ languageFiles,
364
+ localesFilePath,
365
+ autoDeleteUnused,
366
+ ) => {
367
+ const hasUnusedKeys = Object.values(unusedKeys).some((keys) => keys.length > 0);
368
+ if (!hasUnusedKeys) return;
369
+
370
+ let shouldDelete = false;
371
+
372
+ if (autoDeleteUnused) {
373
+ shouldDelete = true;
374
+ console.log(' # 自动删除未使用的key...');
375
+ } else {
376
+ const answer = await askQuestion('是否删除未使用的key?(y/n): ');
377
+ shouldDelete = answer === 'y' || answer === 'yes';
378
+ }
379
+
380
+ if (shouldDelete) {
381
+ if (unusedKeys.zhCN.length > 0) {
382
+ unusedKeys.zhCN.forEach((key) => {
383
+ delete zhCN[key];
384
+ });
385
+ fs.writeJsonSync(`${localesFilePath}/zh-cn.json`, zhCN, { spaces: 2 });
386
+ console.log(` # 已从zh-cn.json中删除${unusedKeys.zhCN.length}个未使用的key`);
387
+ }
388
+
389
+ for (const [language, unused] of Object.entries(unusedKeys)) {
390
+ if (language === 'zhCN') continue;
391
+ if (unused.length > 0) {
392
+ unused.forEach((key) => {
393
+ delete languageFiles[language][key];
394
+ });
395
+ fs.writeJsonSync(`${localesFilePath}/${language}.json`, languageFiles[language], {
396
+ spaces: 2,
397
+ });
398
+ console.log(` # 已从${language}.json中删除${unused.length}个未使用的key`);
399
+ }
400
+ }
401
+ } else {
402
+ console.log(' # 已取消删除操作');
403
+ }
404
+ };
405
+
406
+ const handleExcelGeneration = async (missingKeys, usedKeysData, zhCN, params, autoDeleteUnused) => {
407
+ const hasAnyMissingKeys = Object.values(missingKeys).some((keys) => keys.length > 0);
408
+
409
+ if (!hasAnyMissingKeys || !params.medusa || autoDeleteUnused) {
410
+ if (hasAnyMissingKeys && !params.medusa && !autoDeleteUnused) {
411
+ console.log('\n=== 提示 ===');
412
+ console.log(' [提示] 检测到丢失的翻译key,但未配置medusa,无法生成Excel文件');
413
+ console.log(' [提示] 请在配置文件中添加medusa配置以启用Excel生成功能');
414
+ }
415
+ return;
416
+ }
417
+
418
+ console.log('\n=== Excel生成选项 ===');
419
+ const answer = await askQuestion('检测到丢失的翻译key,是否生成Excel文件用于翻译?(y/n): ');
420
+
421
+ if (answer !== 'y' && answer !== 'yes') {
422
+ console.log(' [取消] 已取消Excel文件生成');
423
+ return;
424
+ }
425
+
426
+ try {
427
+ console.log('\n各语言缺失的翻译key数量:');
428
+ const languageOptions = [];
429
+
430
+ for (const [language, keys] of Object.entries(missingKeys)) {
431
+ if (keys.length > 0) {
432
+ const languageName = language === 'zhCN' ? 'zh-cn' : language;
433
+ console.log(` ${languageName}: ${keys.length}个缺失key`);
434
+ languageOptions.push({ code: languageName, keys: keys, originalCode: language });
435
+ }
436
+ }
437
+
438
+ if (languageOptions.length === 0) {
439
+ console.log(' 没有缺失的key');
440
+ return;
441
+ }
442
+
443
+ let selectedLanguage;
444
+
445
+ try {
446
+ const inquirer = await import('inquirer');
447
+
448
+ const choices = languageOptions.map((option) => ({
449
+ name: `${option.code} (${option.keys.length}个缺失key)`,
450
+ value: option,
451
+ }));
452
+
453
+ const result = await inquirer.default.prompt([
454
+ {
455
+ type: 'list',
456
+ name: 'selectedLanguage',
457
+ message: '请选择要生成Excel的语言(使用上下键选择,回车确认):',
458
+ choices: choices,
459
+ pageSize: 10,
460
+ },
461
+ ]);
462
+
463
+ selectedLanguage = result.selectedLanguage;
464
+ } catch (error) {
465
+ if (error.isTtyError) {
466
+ console.log('\n当前环境不支持交互式选择,使用数字输入模式:');
467
+ languageOptions.forEach((option, index) => {
468
+ console.log(` ${index + 1}. ${option.code} (${option.keys.length}个缺失key)`);
469
+ });
470
+
471
+ const languageAnswer = await askQuestion(
472
+ `请输入数字选择语言 (1-${languageOptions.length}): `,
473
+ );
474
+ const selectedIndex = parseInt(languageAnswer) - 1;
475
+
476
+ if (selectedIndex < 0 || selectedIndex >= languageOptions.length) {
477
+ console.log(' [取消] 选择无效,已取消Excel文件生成');
478
+ return;
479
+ }
480
+
481
+ selectedLanguage = languageOptions[selectedIndex];
482
+ } else {
483
+ throw error;
484
+ }
485
+ }
486
+
487
+ const missingKeysArray = selectedLanguage.keys;
488
+
489
+ console.log(
490
+ `\n已选择语言: ${selectedLanguage.code},将为${missingKeysArray.length}个缺失key生成Excel`,
491
+ );
492
+
493
+ if (missingKeysArray.length > 0) {
494
+ const customData = {};
495
+ console.log(` [信息] 准备为${missingKeysArray.length}个缺失的key生成Excel`);
496
+
497
+ for (const key of missingKeysArray) {
498
+ customData[key] = {};
499
+
500
+ for (const [languageFile] of Object.entries(params.medusa.keyMap)) {
501
+ if (languageFile === 'zh-cn') {
502
+ if (selectedLanguage.originalCode === 'zhCN') {
503
+ const dmValue = usedKeysData[key] || '';
504
+ customData[key][languageFile] = dmValue;
505
+ } else {
506
+ const existingZhValue = zhCN[key] || usedKeysData[key] || '';
507
+ customData[key][languageFile] = existingZhValue;
508
+ }
509
+ } else {
510
+ customData[key][languageFile] = '';
511
+ }
512
+ }
513
+ }
514
+
515
+ const excelPath = await generateMedusaExcel({
516
+ localesFilePath: params.localesFilePath,
517
+ medusa: params.medusa,
518
+ newKeys: missingKeysArray,
519
+ customData,
520
+ });
521
+
522
+ console.log(` [完成] Excel文件已生成: ${excelPath}`);
523
+ console.log(' [提示] 请在Excel中完成翻译后,使用相应的导入功能更新翻译文件');
524
+ } else {
525
+ console.log(' [信息] 没有需要生成Excel的丢失key');
526
+ }
527
+ } catch (error) {
528
+ console.error(` [错误] 生成Excel文件失败: ${error.message}`);
529
+ if (error.stack) {
530
+ console.error(error.stack);
531
+ }
532
+ }
533
+ };
534
+
535
+ module.exports = {
536
+ scanFilesAndCollectKeys,
537
+ scanUntranslatedTexts,
538
+ readTranslationFiles,
539
+ analyzeMissingAndUnusedKeys,
540
+ generateCheckOutput,
541
+ handleOutputAndJsonGeneration,
542
+ handleUnusedKeysDeletion,
543
+ handleExcelGeneration,
544
+ };