@agentscope-ai/i18n 0.1.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,924 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const find = require('find');
4
+ const { parse } = require('@babel/parser');
5
+ const traverse = require('@babel/traverse').default;
6
+ const generate = require('@babel/generator').default;
7
+ const { scanFiles } = require('../utils/file-utils');
8
+ const {
9
+ processFiles,
10
+ translateAndUpdateFiles,
11
+ singleTranslateAndUpdateFiles,
12
+ } = require('./file-processor');
13
+ const { extractI18nKey } = require('../utils/ast-utils');
14
+ const { askQuestion } = require('../utils/cli-utils');
15
+ const { generateMedusaExcel } = require('../utils/excel-utils');
16
+ // inquirer需要动态导入,因为它是ES模块
17
+ const t = require('@babel/types');
18
+
19
+ // 解析AST
20
+ const parseAST = (code) => {
21
+ return parse(code, {
22
+ sourceType: 'module',
23
+ plugins: ['jsx', 'typescript'],
24
+ });
25
+ };
26
+
27
+ // 生成代码
28
+ const generateCode = (ast, originalCode) => {
29
+ return generate(
30
+ ast,
31
+ {
32
+ jsescOption: {
33
+ minimal: true,
34
+ quotes: 'single',
35
+ },
36
+ retainLines: false,
37
+ compact: 'auto',
38
+ },
39
+ originalCode,
40
+ ).code;
41
+ };
42
+
43
+ // 初始化翻译
44
+ const init = async function (sourcePath, params) {
45
+ try {
46
+ const {
47
+ fileType = '.tsx,.js,.ts,.jsx',
48
+ localesFilePath,
49
+ keyPrefix,
50
+ doNotTranslateFiles,
51
+ functionImportPath,
52
+ i18nFilePath,
53
+ dashScope,
54
+ medusa,
55
+ } = params;
56
+
57
+ if (!keyPrefix) {
58
+ throw new Error('keyPrefix is required in params');
59
+ }
60
+
61
+ const files = scanFiles(sourcePath, fileType, doNotTranslateFiles);
62
+ const { i18n, fileCodeMap, needImportFiles } = await processFiles(files, params);
63
+ await translateAndUpdateFiles(
64
+ i18n,
65
+ sourcePath,
66
+ localesFilePath,
67
+ fileType,
68
+ fileCodeMap,
69
+ needImportFiles,
70
+ functionImportPath,
71
+ false,
72
+ dashScope,
73
+ medusa,
74
+ );
75
+
76
+ // 如果配置了i18nFilePath,复制i18n.ts文件
77
+ if (i18nFilePath) {
78
+ const sourceI18nPath = path.join(__dirname, '../i18n.ts');
79
+ const targetI18nPath = path.join(i18nFilePath, 'index.ts');
80
+
81
+ // 确保目标目录存在
82
+ fs.ensureDirSync(i18nFilePath);
83
+
84
+ // 复制文件
85
+ fs.copyFileSync(sourceI18nPath, targetI18nPath);
86
+ console.log(` i18n.ts文件已复制到 ${targetI18nPath}`);
87
+ }
88
+
89
+ console.log(` init: ${sourcePath} 目录下文件中文文案提取、替换和翻译成功!`);
90
+ } catch (error) {
91
+ console.error(' [错误] 初始化失败:', error);
92
+ throw error;
93
+ }
94
+ };
95
+
96
+ // 使用MT方式的初始化翻译
97
+ const initMT = async function (sourcePath, params) {
98
+ try {
99
+ const {
100
+ fileType = '.tsx,.js,.ts,.jsx',
101
+ localesFilePath,
102
+ keyPrefix,
103
+ doNotTranslateFiles,
104
+ functionImportPath,
105
+ i18nFilePath,
106
+ dashScope,
107
+ medusa,
108
+ } = params;
109
+
110
+ if (!keyPrefix) {
111
+ throw new Error('keyPrefix is required in params');
112
+ }
113
+
114
+ if (!dashScope || !dashScope.apiKey) {
115
+ throw new Error('dashScope.apiKey is required for MT translation');
116
+ }
117
+
118
+ console.log(' [信息] 使用MT方式进行单条翻译...');
119
+
120
+ const files = scanFiles(sourcePath, fileType, doNotTranslateFiles);
121
+ const { i18n, fileCodeMap, needImportFiles } = await processFiles(files, params);
122
+ await singleTranslateAndUpdateFiles(
123
+ i18n,
124
+ sourcePath,
125
+ localesFilePath,
126
+ fileType,
127
+ fileCodeMap,
128
+ needImportFiles,
129
+ functionImportPath,
130
+ false,
131
+ dashScope,
132
+ medusa,
133
+ );
134
+
135
+ // 如果配置了i18nFilePath,复制i18n.ts文件
136
+ if (i18nFilePath) {
137
+ const sourceI18nPath = path.join(__dirname, '../i18n.ts');
138
+ const targetI18nPath = path.join(i18nFilePath, 'index.ts');
139
+
140
+ // 确保目标目录存在
141
+ fs.ensureDirSync(i18nFilePath);
142
+
143
+ // 复制文件
144
+ fs.copyFileSync(sourceI18nPath, targetI18nPath);
145
+ console.log(` i18n.ts文件已复制到 ${targetI18nPath}`);
146
+ }
147
+
148
+ console.log(` initMT: ${sourcePath} 目录下文件中文文案提取、替换和MT翻译成功!`);
149
+ } catch (error) {
150
+ console.error(' [错误] MT初始化失败:', error);
151
+ throw error;
152
+ }
153
+ };
154
+
155
+ // 补丁更新
156
+ const patch = async function (sourcePath, params) {
157
+ const {
158
+ fileType = '.tsx,.js,.ts,.jsx',
159
+ localesFilePath,
160
+ keyPrefix,
161
+ doNotTranslateFiles,
162
+ functionImportPath,
163
+ i18nFilePath,
164
+ dashScope,
165
+ medusa,
166
+ } = params;
167
+
168
+ if (!keyPrefix) {
169
+ throw new Error('keyPrefix is required in params');
170
+ }
171
+
172
+ const files = scanFiles(sourcePath, fileType, doNotTranslateFiles);
173
+ console.log(` # 检查${files.length}个${fileType}文件`);
174
+
175
+ const { i18n, fileCodeMap, needImportFiles } = await processFiles(files, params);
176
+ const newKeysCount = await translateAndUpdateFiles(
177
+ i18n,
178
+ sourcePath,
179
+ localesFilePath,
180
+ fileType,
181
+ fileCodeMap,
182
+ needImportFiles,
183
+ functionImportPath,
184
+ true,
185
+ dashScope,
186
+ medusa,
187
+ );
188
+
189
+ // 如果配置了i18nFilePath,检查并复制i18n.ts文件
190
+ if (i18nFilePath) {
191
+ const sourceI18nPath = path.join(__dirname, '../i18n.ts');
192
+ const targetI18nPath = path.join(i18nFilePath, 'index.ts');
193
+
194
+ // 检查目标文件是否已存在
195
+ if (!fs.existsSync(targetI18nPath)) {
196
+ // 确保目标目录存在
197
+ fs.ensureDirSync(i18nFilePath);
198
+
199
+ // 复制文件
200
+ fs.copyFileSync(sourceI18nPath, targetI18nPath);
201
+ console.log(` [完成] i18n.ts文件已复制到 ${targetI18nPath}`);
202
+ } else {
203
+ console.log(` [提示] i18n.ts文件已存在于 ${targetI18nPath},跳过复制`);
204
+ }
205
+ }
206
+
207
+ console.log(` 新增文案补充翻译完成!新增${newKeysCount}条翻译文案`);
208
+ };
209
+
210
+ // 使用MT方式的补丁更新
211
+ const patchMT = async function (sourcePath, params) {
212
+ const {
213
+ fileType = '.tsx,.js,.ts,.jsx',
214
+ localesFilePath,
215
+ keyPrefix,
216
+ doNotTranslateFiles,
217
+ functionImportPath,
218
+ i18nFilePath,
219
+ dashScope,
220
+ medusa,
221
+ } = params;
222
+
223
+ if (!keyPrefix) {
224
+ throw new Error('keyPrefix is required in params');
225
+ }
226
+
227
+ if (!dashScope || !dashScope.apiKey) {
228
+ throw new Error('dashScope.apiKey is required for MT translation');
229
+ }
230
+
231
+ console.log(' [信息] 使用MT方式进行单条翻译补丁更新...');
232
+
233
+ const files = scanFiles(sourcePath, fileType, doNotTranslateFiles);
234
+ console.log(` # 检查${files.length}个${fileType}文件`);
235
+
236
+ const { i18n, fileCodeMap, needImportFiles } = await processFiles(files, params);
237
+ const newKeysCount = await singleTranslateAndUpdateFiles(
238
+ i18n,
239
+ sourcePath,
240
+ localesFilePath,
241
+ fileType,
242
+ fileCodeMap,
243
+ needImportFiles,
244
+ functionImportPath,
245
+ true,
246
+ dashScope,
247
+ medusa,
248
+ );
249
+
250
+ // 如果配置了i18nFilePath,检查并复制i18n.ts文件
251
+ if (i18nFilePath) {
252
+ const sourceI18nPath = path.join(__dirname, '../i18n.ts');
253
+ const targetI18nPath = path.join(i18nFilePath, 'index.ts');
254
+
255
+ // 检查目标文件是否已存在
256
+ if (!fs.existsSync(targetI18nPath)) {
257
+ // 确保目标目录存在
258
+ fs.ensureDirSync(i18nFilePath);
259
+
260
+ // 复制文件
261
+ fs.copyFileSync(sourceI18nPath, targetI18nPath);
262
+ console.log(` [完成] i18n.ts文件已复制到 ${targetI18nPath}`);
263
+ } else {
264
+ console.log(` [提示] i18n.ts文件已存在于 ${targetI18nPath},跳过复制`);
265
+ }
266
+ }
267
+
268
+ console.log(` 新增文案MT翻译补充完成!新增${newKeysCount}条翻译文案`);
269
+ };
270
+
271
+ // 扫描文件并收集使用的key和dm值
272
+ const scanFilesAndCollectKeys = (files) => {
273
+ const usedKeys = new Set();
274
+ const usedKeysData = {}; // 存储key对应的dm值(中文默认值)
275
+
276
+ files.forEach((file) => {
277
+ try {
278
+ const code = fs.readFileSync(file, 'utf8');
279
+ const ast = parseAST(code);
280
+
281
+ traverse(ast, {
282
+ CallExpression(path) {
283
+ const result = extractI18nKey(path.node);
284
+ if (result && result.id) {
285
+ usedKeys.add(result.id);
286
+
287
+ // 如果有dm值,记录下来(dm是中文默认值)
288
+ if (result.dm && typeof result.dm === 'string') {
289
+ usedKeysData[result.id] = result.dm;
290
+ }
291
+ }
292
+ },
293
+ });
294
+ } catch (error) {
295
+ console.error(` # 处理文件失败: ${file}`, error);
296
+ if (error.loc) {
297
+ console.error(` # 错误位置: 第${error.loc.line}行, 第${error.loc.column}列`);
298
+ }
299
+ }
300
+ });
301
+
302
+ return { usedKeys, usedKeysData };
303
+ };
304
+
305
+ // 读取翻译文件
306
+ const readTranslationFiles = (localesFilePath, dashScope) => {
307
+ // 读取中文翻译文件
308
+ let zhCN = {};
309
+ try {
310
+ zhCN = fs.readJsonSync(`${localesFilePath}/zh-cn.json`);
311
+ } catch (error) {
312
+ console.log(' 未找到zh-cn.json文件');
313
+ }
314
+
315
+ // 根据配置确定需要检查的语言文件
316
+ const languageFiles = {};
317
+ if (dashScope && dashScope.valueTranslateAppId) {
318
+ if (typeof dashScope.valueTranslateAppId === 'string') {
319
+ // 兼容旧版本配置,只检查英文
320
+ try {
321
+ languageFiles['en-us'] = fs.readJsonSync(`${localesFilePath}/en-us.json`);
322
+ } catch (error) {
323
+ console.log(' 未找到en-us.json文件');
324
+ }
325
+ } else if (typeof dashScope.valueTranslateAppId === 'object') {
326
+ // 新版本配置,检查所有配置的语言
327
+ for (const language of Object.keys(dashScope.valueTranslateAppId)) {
328
+ try {
329
+ languageFiles[language] = fs.readJsonSync(`${localesFilePath}/${language}.json`);
330
+ } catch (error) {
331
+ console.log(` 未找到${language}.json文件`);
332
+ }
333
+ }
334
+ }
335
+ }
336
+
337
+ return { zhCN, languageFiles };
338
+ };
339
+
340
+ // 分析缺失和未使用的key
341
+ const analyzeMissingAndUnusedKeys = (usedKeys, zhCN, languageFiles) => {
342
+ const zhCNKeys = new Set(Object.keys(zhCN));
343
+ const languageKeys = {};
344
+ for (const [language, content] of Object.entries(languageFiles)) {
345
+ languageKeys[language] = new Set(Object.keys(content));
346
+ }
347
+
348
+ // 找出缺失的key
349
+ const missingKeys = {
350
+ zhCN: Array.from(usedKeys).filter((key) => !zhCNKeys.has(key)),
351
+ };
352
+
353
+ for (const [language, keys] of Object.entries(languageKeys)) {
354
+ missingKeys[language] = Array.from(usedKeys).filter((key) => !keys.has(key));
355
+ }
356
+
357
+ // 找出多余的key
358
+ const unusedKeys = {
359
+ zhCN: Array.from(zhCNKeys).filter((key) => !usedKeys.has(key)),
360
+ };
361
+
362
+ for (const [language, keys] of Object.entries(languageKeys)) {
363
+ unusedKeys[language] = Array.from(keys).filter((key) => !usedKeys.has(key));
364
+ }
365
+
366
+ return { missingKeys, unusedKeys };
367
+ };
368
+
369
+ // 生成检查结果输出
370
+ const generateCheckOutput = (missingKeys, unusedKeys) => {
371
+ const outputLines = [];
372
+ outputLines.push('=== 检查结果 ===');
373
+ outputLines.push('');
374
+ outputLines.push('1. 缺失的翻译key:');
375
+
376
+ if (missingKeys.zhCN.length > 0) {
377
+ outputLines.push('');
378
+ outputLines.push('zh-cn.json 缺失的key:');
379
+ missingKeys.zhCN.forEach((key) => outputLines.push(` ${key}`));
380
+ } else {
381
+ outputLines.push('');
382
+ outputLines.push('zh-cn.json 没有缺失的key');
383
+ }
384
+
385
+ for (const [language, missing] of Object.entries(missingKeys)) {
386
+ if (language === 'zhCN') continue; // 跳过中文,已经处理过了
387
+
388
+ if (missing.length > 0) {
389
+ outputLines.push('');
390
+ outputLines.push(`${language}.json 缺失的key:`);
391
+ missing.forEach((key) => outputLines.push(` ${key}`));
392
+ } else {
393
+ outputLines.push('');
394
+ outputLines.push(`${language}.json 没有缺失的key`);
395
+ }
396
+ }
397
+
398
+ outputLines.push('');
399
+ outputLines.push('2. 未使用的翻译key:');
400
+
401
+ if (unusedKeys.zhCN.length > 0) {
402
+ outputLines.push('');
403
+ outputLines.push('zh-cn.json 中未使用的key:');
404
+ unusedKeys.zhCN.forEach((key) => outputLines.push(` ${key}`));
405
+ } else {
406
+ outputLines.push('');
407
+ outputLines.push('zh-cn.json 没有未使用的key');
408
+ }
409
+
410
+ for (const [language, unused] of Object.entries(unusedKeys)) {
411
+ if (language === 'zhCN') continue; // 跳过中文,已经处理过了
412
+
413
+ if (unused.length > 0) {
414
+ outputLines.push('');
415
+ outputLines.push(`${language}.json 中未使用的key:`);
416
+ unused.forEach((key) => outputLines.push(` ${key}`));
417
+ } else {
418
+ outputLines.push('');
419
+ outputLines.push(`${language}.json 没有未使用的key`);
420
+ }
421
+ }
422
+
423
+ outputLines.push('');
424
+ outputLines.push('=== 检查完成 ===');
425
+
426
+ return outputLines;
427
+ };
428
+
429
+ // 处理输出和JSON文件生成
430
+ const handleOutputAndJsonGeneration = (outputLines, missingKeys, unusedKeys, autoDeleteUnused) => {
431
+ // 判断是否需要生成JSON文件(自动删除模式下不生成日志文件)
432
+ if (outputLines.length > 100 && !autoDeleteUnused) {
433
+ // 生成JSON文件
434
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
435
+ const jsonFileName = `check-result-${timestamp}.json`;
436
+
437
+ const jsonResult = {
438
+ timestamp: new Date().toISOString(),
439
+ summary: {
440
+ totalMissingKeys: Object.values(missingKeys).reduce((sum, keys) => sum + keys.length, 0),
441
+ totalUnusedKeys: Object.values(unusedKeys).reduce((sum, keys) => sum + keys.length, 0),
442
+ totalOutputLines: outputLines.length,
443
+ },
444
+ missingKeys,
445
+ unusedKeys,
446
+ };
447
+
448
+ try {
449
+ fs.writeJsonSync(jsonFileName, jsonResult, { spaces: 2 });
450
+
451
+ // 输出简化版本
452
+ console.log('\n=== 检查结果 ===');
453
+ console.log(`\n⚠️ 输出内容较多(${outputLines.length}行),详细结果已保存到: ${jsonFileName}`);
454
+ console.log('\n📊 概要信息:');
455
+ console.log(` • 缺失的翻译key总数: ${jsonResult.summary.totalMissingKeys}`);
456
+ console.log(` • 未使用的翻译key总数: ${jsonResult.summary.totalUnusedKeys}`);
457
+
458
+ // 显示各语言的统计
459
+ console.log('\n📋 各语言统计:');
460
+ console.log(' 缺失的key:');
461
+ for (const [language, missing] of Object.entries(missingKeys)) {
462
+ const displayLang = language === 'zhCN' ? 'zh-cn' : language;
463
+ console.log(` ${displayLang}.json: ${missing.length}个`);
464
+ }
465
+
466
+ console.log(' 未使用的key:');
467
+ for (const [language, unused] of Object.entries(unusedKeys)) {
468
+ const displayLang = language === 'zhCN' ? 'zh-cn' : language;
469
+ console.log(` ${displayLang}.json: ${unused.length}个`);
470
+ }
471
+
472
+ console.log('\n=== 检查完成 ===\n');
473
+ } catch (error) {
474
+ console.error(`\n❌ 生成JSON文件失败: ${error.message}`);
475
+ // 如果JSON文件生成失败,仍然输出完整结果(自动删除模式下不输出)
476
+ if (!autoDeleteUnused) {
477
+ outputLines.forEach((line) => console.log(line));
478
+ }
479
+ }
480
+ } else {
481
+ // 正常输出(自动删除模式下不输出日志)
482
+ if (!autoDeleteUnused) {
483
+ console.log('');
484
+ outputLines.forEach((line) => console.log(line));
485
+ console.log('');
486
+ }
487
+ }
488
+ };
489
+
490
+ // 处理未使用key的删除
491
+ const handleUnusedKeysDeletion = async (
492
+ unusedKeys,
493
+ zhCN,
494
+ languageFiles,
495
+ localesFilePath,
496
+ autoDeleteUnused,
497
+ ) => {
498
+ const hasUnusedKeys = Object.values(unusedKeys).some((keys) => keys.length > 0);
499
+ if (!hasUnusedKeys) return;
500
+
501
+ let shouldDelete = false;
502
+
503
+ if (autoDeleteUnused) {
504
+ // 自动删除模式
505
+ shouldDelete = true;
506
+ console.log(' # 自动删除未使用的key...');
507
+ } else {
508
+ // 询问用户是否删除
509
+ const answer = await askQuestion('是否删除未使用的key?(y/n): ');
510
+ shouldDelete = answer === 'y' || answer === 'yes';
511
+ }
512
+
513
+ if (shouldDelete) {
514
+ // 删除zh-cn.json中未使用的key
515
+ if (unusedKeys.zhCN.length > 0) {
516
+ unusedKeys.zhCN.forEach((key) => {
517
+ delete zhCN[key];
518
+ });
519
+ fs.writeJsonSync(`${localesFilePath}/zh-cn.json`, zhCN, { spaces: 2 });
520
+ console.log(` # 已从zh-cn.json中删除${unusedKeys.zhCN.length}个未使用的key`);
521
+ }
522
+
523
+ // 删除其他语言文件中未使用的key
524
+ for (const [language, unused] of Object.entries(unusedKeys)) {
525
+ if (language === 'zhCN') continue; // 跳过中文,已经处理过了
526
+
527
+ if (unused.length > 0) {
528
+ unused.forEach((key) => {
529
+ delete languageFiles[language][key];
530
+ });
531
+ fs.writeJsonSync(`${localesFilePath}/${language}.json`, languageFiles[language], {
532
+ spaces: 2,
533
+ });
534
+ console.log(` # 已从${language}.json中删除${unused.length}个未使用的key`);
535
+ }
536
+ }
537
+ } else {
538
+ console.log(' # 已取消删除操作');
539
+ }
540
+ };
541
+
542
+ // 处理Excel生成的交互逻辑
543
+ const handleExcelGeneration = async (missingKeys, usedKeysData, zhCN, params, autoDeleteUnused) => {
544
+ const hasAnyMissingKeys = Object.values(missingKeys).some((keys) => keys.length > 0);
545
+
546
+ if (!hasAnyMissingKeys || !params.medusa || autoDeleteUnused) {
547
+ if (hasAnyMissingKeys && !params.medusa && !autoDeleteUnused) {
548
+ console.log('\n=== 提示 ===');
549
+ console.log(' [提示] 检测到丢失的翻译key,但未配置medusa,无法生成Excel文件');
550
+ console.log(' [提示] 请在配置文件中添加medusa配置以启用Excel生成功能');
551
+ }
552
+ return;
553
+ }
554
+
555
+ console.log('\n=== Excel生成选项 ===');
556
+ const answer = await askQuestion('检测到丢失的翻译key,是否生成Excel文件用于翻译?(y/n): ');
557
+
558
+ if (answer !== 'y' && answer !== 'yes') {
559
+ console.log(' [取消] 已取消Excel文件生成');
560
+ return;
561
+ }
562
+
563
+ try {
564
+ // 让用户选择要生成Excel的语言
565
+ console.log('\n各语言缺失的翻译key数量:');
566
+ const languageOptions = [];
567
+
568
+ for (const [language, keys] of Object.entries(missingKeys)) {
569
+ if (keys.length > 0) {
570
+ const languageName = language === 'zhCN' ? 'zh-cn' : language;
571
+ console.log(` ${languageName}: ${keys.length}个缺失key`);
572
+ languageOptions.push({ code: languageName, keys: keys, originalCode: language });
573
+ }
574
+ }
575
+
576
+ if (languageOptions.length === 0) {
577
+ console.log(' 没有缺失的key');
578
+ return;
579
+ }
580
+
581
+ // 尝试使用inquirer进行交互式选择,如果不支持则回退到数字输入
582
+ let selectedLanguage;
583
+
584
+ try {
585
+ // 动态导入inquirer
586
+ const inquirer = await import('inquirer');
587
+
588
+ const choices = languageOptions.map((option) => ({
589
+ name: `${option.code} (${option.keys.length}个缺失key)`,
590
+ value: option,
591
+ }));
592
+
593
+ const result = await inquirer.default.prompt([
594
+ {
595
+ type: 'list',
596
+ name: 'selectedLanguage',
597
+ message: '请选择要生成Excel的语言(使用上下键选择,回车确认):',
598
+ choices: choices,
599
+ pageSize: 10,
600
+ },
601
+ ]);
602
+
603
+ selectedLanguage = result.selectedLanguage;
604
+ } catch (error) {
605
+ if (error.isTtyError) {
606
+ // 回退到数字输入模式
607
+ console.log('\n当前环境不支持交互式选择,使用数字输入模式:');
608
+ languageOptions.forEach((option, index) => {
609
+ console.log(` ${index + 1}. ${option.code} (${option.keys.length}个缺失key)`);
610
+ });
611
+
612
+ const languageAnswer = await askQuestion(
613
+ `请输入数字选择语言 (1-${languageOptions.length}): `,
614
+ );
615
+ const selectedIndex = parseInt(languageAnswer) - 1;
616
+
617
+ if (selectedIndex < 0 || selectedIndex >= languageOptions.length) {
618
+ console.log(' [取消] 选择无效,已取消Excel文件生成');
619
+ return;
620
+ }
621
+
622
+ selectedLanguage = languageOptions[selectedIndex];
623
+ } else {
624
+ throw error;
625
+ }
626
+ }
627
+
628
+ const missingKeysArray = selectedLanguage.keys;
629
+
630
+ console.log(
631
+ `\n已选择语言: ${selectedLanguage.code},将为${missingKeysArray.length}个缺失key生成Excel`,
632
+ );
633
+
634
+ if (missingKeysArray.length > 0) {
635
+ // 构建自定义数据源,只包含缺失的key和对应的dm值
636
+ const customData = {};
637
+
638
+ console.log(` [信息] 准备为${missingKeysArray.length}个缺失的key生成Excel`);
639
+
640
+ for (const key of missingKeysArray) {
641
+ customData[key] = {};
642
+
643
+ // 为每种语言设置数据
644
+ for (const [languageFile, columnName] of Object.entries(params.medusa.keyMap)) {
645
+ if (languageFile === 'zh-cn') {
646
+ // Simple Chinese列的逻辑
647
+ if (selectedLanguage.originalCode === 'zhCN') {
648
+ // 如果选择的是中文语言,使用dm值
649
+ const dmValue = usedKeysData[key] || '';
650
+ customData[key][languageFile] = dmValue;
651
+
652
+ if (dmValue) {
653
+ console.log(` [数据] ${key} -> "${dmValue}"`);
654
+ } else {
655
+ console.log(` [数据] ${key} -> (无dm值)`);
656
+ }
657
+ } else {
658
+ // 如果选择的是其他语言,从现有的中文翻译文件中获取值
659
+ const existingZhValue = zhCN[key] || usedKeysData[key] || '';
660
+ customData[key][languageFile] = existingZhValue;
661
+
662
+ if (existingZhValue) {
663
+ console.log(` [数据] ${key} -> "${existingZhValue}" (来自zh-cn.json或dm)`);
664
+ } else {
665
+ console.log(` [数据] ${key} -> (无中文值)`);
666
+ }
667
+ }
668
+ } else {
669
+ // 其他语言列为空,等待翻译
670
+ customData[key][languageFile] = '';
671
+ }
672
+ }
673
+ }
674
+
675
+ const excelPath = await generateMedusaExcel({
676
+ localesFilePath: params.localesFilePath,
677
+ medusa: params.medusa,
678
+ newKeys: missingKeysArray,
679
+ customData,
680
+ });
681
+
682
+ console.log(` [完成] Excel文件已生成: ${excelPath}`);
683
+ console.log(' [提示] 请在Excel中完成翻译后,使用相应的导入功能更新翻译文件');
684
+ } else {
685
+ console.log(' [信息] 没有需要生成Excel的丢失key');
686
+ }
687
+ } catch (error) {
688
+ console.error(` [错误] 生成Excel文件失败: ${error.message}`);
689
+ if (error.stack) {
690
+ console.error(error.stack);
691
+ }
692
+ }
693
+ };
694
+
695
+ // 检查翻译
696
+ const check = async function (sourcePath, params) {
697
+ const {
698
+ fileType = '.tsx,.js,.ts,.jsx',
699
+ localesFilePath,
700
+ dashScope,
701
+ medusa,
702
+ autoDeleteUnused,
703
+ } = params;
704
+
705
+ // 扫描文件
706
+ const fileTypes = fileType.split(',').map((s) => s.trim());
707
+ const re = new RegExp(`(${fileTypes.map((ft) => ft.replace('.', '\\.')).join('|')})$`);
708
+ const files = find.fileSync(re, sourcePath);
709
+ console.log(` # 检查${files.length}个${fileType}文件`);
710
+
711
+ // 收集使用的key和dm值
712
+ const { usedKeys, usedKeysData } = scanFilesAndCollectKeys(files);
713
+
714
+ // 读取翻译文件
715
+ const { zhCN, languageFiles } = readTranslationFiles(localesFilePath, dashScope);
716
+
717
+ // 分析缺失和未使用的key
718
+ const { missingKeys, unusedKeys } = analyzeMissingAndUnusedKeys(usedKeys, zhCN, languageFiles);
719
+
720
+ // 生成输出内容
721
+ const outputLines = generateCheckOutput(missingKeys, unusedKeys);
722
+
723
+ // 处理输出和JSON文件生成
724
+ handleOutputAndJsonGeneration(outputLines, missingKeys, unusedKeys, autoDeleteUnused);
725
+
726
+ // 处理未使用key的删除
727
+ await handleUnusedKeysDeletion(
728
+ unusedKeys,
729
+ zhCN,
730
+ languageFiles,
731
+ localesFilePath,
732
+ autoDeleteUnused,
733
+ );
734
+
735
+ // 处理Excel生成
736
+ await handleExcelGeneration(missingKeys, usedKeysData, zhCN, params, autoDeleteUnused);
737
+
738
+ // 返回结果,以便其他程序可能需要使用
739
+ const result = {
740
+ missingKeys,
741
+ unusedKeys,
742
+ usedKeysData, // 添加使用的key数据
743
+ };
744
+
745
+ return result;
746
+ };
747
+
748
+ // 还原国际化代码为中文
749
+ const reverse = async function (sourcePath, params) {
750
+ const { fileType = '.tsx,.js,.ts,.jsx', localesFilePath, functionImportPath } = params;
751
+
752
+ console.log(` [开始] 开始还原国际化代码为中文...`);
753
+
754
+ // 读取中文翻译文件
755
+ let zhCN = {};
756
+ try {
757
+ zhCN = fs.readJsonSync(`${localesFilePath}/zh-cn.json`);
758
+ } catch (error) {
759
+ console.log(' [警告] 未找到zh-cn.json文件,无法还原字符串key');
760
+ }
761
+
762
+ const fileTypes = fileType.split(',').map((s) => s.trim());
763
+ const re = new RegExp(`(${fileTypes.map((ft) => ft.replace('.', '\\.')).join('|')})$`);
764
+
765
+ // 检索文件
766
+ const files = find.fileSync(re, sourcePath);
767
+ console.log(` [进度] 找到${files.length}个${fileType}文件需要处理`);
768
+
769
+ let totalReversed = 0;
770
+ let totalFilesModified = 0;
771
+
772
+ // 遍历所有文件
773
+ for (const file of files) {
774
+ try {
775
+ const code = fs.readFileSync(file, 'utf8');
776
+
777
+ // 解析文件
778
+ const ast = parseAST(code);
779
+ let hasChanges = false;
780
+ let reversedCount = 0;
781
+
782
+ // 遍历AST查找$i18n.get调用并替换
783
+ traverse(ast, {
784
+ CallExpression(path) {
785
+ const result = extractI18nKey(path.node);
786
+ if (result && result.id) {
787
+ let chineseText = null;
788
+
789
+ // 优先使用dm字段(defaultMessage)
790
+ if (result.dm) {
791
+ chineseText = result.dm;
792
+ }
793
+ // 如果没有dm字段,从zh-cn.json中查找
794
+ else if (zhCN[result.id]) {
795
+ chineseText = zhCN[result.id];
796
+ }
797
+
798
+ if (chineseText) {
799
+ // 替换为中文文本
800
+ path.replaceWith(t.stringLiteral(chineseText));
801
+ hasChanges = true;
802
+ reversedCount++;
803
+ }
804
+ }
805
+ },
806
+ });
807
+
808
+ // 如果有改动,写回文件
809
+ if (hasChanges) {
810
+ const newCode = generateCode(ast, code);
811
+ fs.writeFileSync(file, newCode);
812
+ totalFilesModified++;
813
+ totalReversed += reversedCount;
814
+ console.log(` [完成] 文件 ${file} 还原了${reversedCount}处国际化代码`);
815
+ }
816
+ } catch (error) {
817
+ console.error(` [错误] 处理文件失败: ${file}`, error);
818
+ if (error.loc) {
819
+ console.error(` [错误] 错误位置: 第${error.loc.line}行, 第${error.loc.column}列`);
820
+ }
821
+ }
822
+ }
823
+
824
+ // 删除导入语句
825
+ console.log(` [进度] 开始删除导入语句...`);
826
+ if (functionImportPath) {
827
+ console.log(` [信息] 匹配的导入路径: ${functionImportPath}`);
828
+ } else {
829
+ console.log(` [信息] 未配置functionImportPath,将删除所有$i18n导入语句`);
830
+ }
831
+ let importRemovedCount = 0;
832
+
833
+ for (const file of files) {
834
+ try {
835
+ const code = fs.readFileSync(file, 'utf8');
836
+ const ast = parseAST(code);
837
+ let hasImportChanges = false;
838
+
839
+ // 查找并删除 $i18n 的导入语句
840
+ traverse(ast, {
841
+ ImportDeclaration(path) {
842
+ const source = path.node.source.value;
843
+ const specifiers = path.node.specifiers;
844
+
845
+ // 检查是否是来自指定路径的 $i18n 导入(支持默认导入和命名导入)
846
+ const hasI18nImport = specifiers.some(
847
+ (spec) =>
848
+ (spec.type === 'ImportSpecifier' && spec.imported.name === '$i18n') ||
849
+ (spec.type === 'ImportDefaultSpecifier' && spec.local.name === '$i18n'),
850
+ );
851
+
852
+ // 添加调试信息
853
+ if (hasI18nImport) {
854
+ console.log(` [调试] 找到$i18n导入语句:`);
855
+ console.log(` - 导入路径: "${source}"`);
856
+ console.log(` - 配置的functionImportPath: "${functionImportPath}"`);
857
+ }
858
+
859
+ // 检查导入路径是否匹配(如果未配置functionImportPath,则匹配所有)
860
+ const isMatchingPath =
861
+ !functionImportPath ||
862
+ source === functionImportPath ||
863
+ source.endsWith(functionImportPath.replace(/^@\//, '')) ||
864
+ functionImportPath.endsWith(source.replace(/^@\//, ''));
865
+
866
+ if (hasI18nImport) {
867
+ console.log(` - 是否匹配路径: ${isMatchingPath}`);
868
+ }
869
+
870
+ if (hasI18nImport && isMatchingPath) {
871
+ // 移除 $i18n 的导入(支持默认导入和命名导入)
872
+ const remainingSpecifiers = specifiers.filter(
873
+ (spec) =>
874
+ !(
875
+ (spec.type === 'ImportSpecifier' && spec.imported.name === '$i18n') ||
876
+ (spec.type === 'ImportDefaultSpecifier' && spec.local.name === '$i18n')
877
+ ),
878
+ );
879
+
880
+ if (remainingSpecifiers.length === 0) {
881
+ // 如果没有其他导入,删除整个导入语句
882
+ path.remove();
883
+ } else {
884
+ // 更新导入语句,移除 $i18n
885
+ path.node.specifiers = remainingSpecifiers;
886
+ }
887
+ hasImportChanges = true;
888
+ console.log(` [删除] 从 ${file} 中删除 $i18n 导入: ${source}`);
889
+ }
890
+ },
891
+ });
892
+
893
+ if (hasImportChanges) {
894
+ const newCode = generateCode(ast, code);
895
+ fs.writeFileSync(file, newCode);
896
+ importRemovedCount++;
897
+ }
898
+ } catch (error) {
899
+ console.error(` [错误] 删除导入语句失败: ${file}`, error);
900
+ }
901
+ }
902
+
903
+ console.log(` [完成] 已从${importRemovedCount}个文件中删除$i18n导入语句`);
904
+
905
+ console.log(`\n=== 还原完成 ===`);
906
+ console.log(`共处理了${files.length}个文件`);
907
+ console.log(`修改了${totalFilesModified}个文件`);
908
+ console.log(`还原了${totalReversed}处国际化代码`);
909
+
910
+ return {
911
+ totalFiles: files.length,
912
+ modifiedFiles: totalFilesModified,
913
+ reversedCount: totalReversed,
914
+ };
915
+ };
916
+
917
+ module.exports = {
918
+ init,
919
+ initMT,
920
+ patch,
921
+ patchMT,
922
+ check,
923
+ reverse,
924
+ };