@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.
@@ -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
- // inquirer需要动态导入,因为它是ES模块
33
- const t = require('@babel/types');
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
- const init = async function (sourcePath, params) {
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
- // 如果配置了i18nFilePath,复制i18n.ts文件
95
- if (i18nFilePath) {
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
- fs.ensureDirSync(i18nFilePath);
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
- // 使用MT方式的初始化翻译
115
- const initMT = async function (sourcePath, params) {
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
- medusa,
126
- skipConfirm,
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
- console.log(' [信息] 使用MT方式进行单条翻译...');
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
- const { i18n, fileCodeMap, needImportFiles } = await processFiles(files, params);
141
- await singleTranslateAndUpdateFiles(
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
- // 扫描文件并收集使用的key和dm值
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
- files.forEach((file) => {
301
- try {
302
- const code = fs.readFileSync(file, 'utf8');
303
- const ast = parseAST(code);
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
- return outputLines;
573
- };
102
+ copyI18nFile(i18nFilePath, isUpdate);
574
103
 
575
- // 处理输出和JSON文件生成
576
- const handleOutputAndJsonGeneration = (
577
- outputLines,
578
- missingKeys,
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
- try {
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(` [错误] 生成Excel文件失败: ${error.message}`);
847
- if (error.stack) {
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
- // 收集使用的key和dm值
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
- handleOutputAndJsonGeneration(
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
- const result = {
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 } = params;
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
- let chineseText = null;
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 === '$i18n') ||
1067
- (spec.type === 'ImportDefaultSpecifier' && spec.local.name === '$i18n'),
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 === '$i18n') ||
1094
- (spec.type === 'ImportDefaultSpecifier' && spec.local.name === '$i18n')
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}个文件中删除$i18n导入语句`);
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: 提取中文 AI翻译 locale文件 → 生成mds-payload.json
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
- throw new Error('keyPrefix is required in params');
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,29 +370,17 @@ 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
386
  if (medusa && medusa.appName && medusa.keyMap) {
@@ -1237,8 +404,7 @@ const translate = async function (sourcePath, params) {
1237
404
  };
1238
405
 
1239
406
  /**
1240
- * replaceOnly: 将代码中的中文替换为 $i18n.get 调用。
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
- if (!keyPrefix) {
1256
- throw new Error('keyPrefix is required in params');
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(' 将代码中的中文替换为 $i18n.get 调用');
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
- if (i18nFilePath) {
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(' ✅ replace 完成!代码中的中文已替换为 $i18n.get 调用');
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
- init,
1312
- initMT,
1313
- patch,
1314
- patchMT,
473
+ runTranslation,
1315
474
  check,
1316
475
  reverse,
1317
476
  translate,