@agentscope-ai/i18n 0.1.9-rc.1 → 0.1.9-rc.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.
@@ -10,8 +10,21 @@ const {
10
10
  translateAndUpdateFiles,
11
11
  singleTranslateAndUpdateFiles,
12
12
  } = require('./file-processor');
13
- const { extractI18nKey } = require('../utils/ast-utils');
13
+ const {
14
+ extractI18nKey,
15
+ chnRegExp,
16
+ onlyContainsChinesePunctuation,
17
+ onlyContainsEmoji,
18
+ isI18nCall,
19
+ isInConsoleCall,
20
+ } = require('../utils/ast-utils');
14
21
  const { askQuestion } = require('../utils/cli-utils');
22
+ const {
23
+ shouldIgnoreLineByComment,
24
+ shouldIgnoreBlockByComment,
25
+ shouldIgnoreByComment,
26
+ shouldIgnoreFile,
27
+ } = require('../utils/file-utils');
15
28
  const { generateMedusaExcel } = require('../utils/excel-utils');
16
29
  // inquirer需要动态导入,因为它是ES模块
17
30
  const t = require('@babel/types');
@@ -310,6 +323,113 @@ const scanFilesAndCollectKeys = (files) => {
310
323
  return { usedKeys, usedKeysData };
311
324
  };
312
325
 
326
+ // 扫描未翻译的中文文案(未被 $i18n.get 包裹的中文字符串)
327
+ const scanUntranslatedTexts = (files, doNotTranslateFiles) => {
328
+ const untranslatedTexts = [];
329
+
330
+ const filteredFiles = files.filter((file) => {
331
+ if (shouldIgnoreFile(file, doNotTranslateFiles || [])) return false;
332
+ if (shouldIgnoreByComment(file)) return false;
333
+ return true;
334
+ });
335
+
336
+ filteredFiles.forEach((file) => {
337
+ try {
338
+ const code = fs.readFileSync(file, 'utf8');
339
+ const ast = parseAST(code);
340
+
341
+ const shouldIgnoreNode = (nodePath) => {
342
+ if (!nodePath.node.loc) return false;
343
+ const lineNumber = nodePath.node.loc.start.line;
344
+ return (
345
+ shouldIgnoreLineByComment(file, lineNumber) ||
346
+ shouldIgnoreBlockByComment(file, lineNumber)
347
+ );
348
+ };
349
+
350
+ traverse(ast, {
351
+ StringLiteral(astPath) {
352
+ const { value } = astPath.node;
353
+ if (
354
+ chnRegExp.test(value) &&
355
+ !isI18nCall(astPath.parent) &&
356
+ !astPath.findParent((p) => isI18nCall(p.node)) &&
357
+ !isInConsoleCall(astPath) &&
358
+ !shouldIgnoreNode(astPath) &&
359
+ !onlyContainsChinesePunctuation(value) &&
360
+ !onlyContainsEmoji(value)
361
+ ) {
362
+ untranslatedTexts.push({
363
+ file,
364
+ line: astPath.node.loc?.start.line,
365
+ column: astPath.node.loc?.start.column,
366
+ type: 'StringLiteral',
367
+ text: value,
368
+ });
369
+ }
370
+ },
371
+ TemplateLiteral(astPath) {
372
+ if (
373
+ !isI18nCall(astPath.parent) &&
374
+ !astPath.findParent((p) => isI18nCall(p.node)) &&
375
+ !isInConsoleCall(astPath) &&
376
+ !shouldIgnoreNode(astPath)
377
+ ) {
378
+ const staticParts = astPath.node.quasis.map((q) => q.value.raw).join('');
379
+ if (
380
+ chnRegExp.test(staticParts) &&
381
+ !onlyContainsChinesePunctuation(staticParts) &&
382
+ !onlyContainsEmoji(staticParts)
383
+ ) {
384
+ const fullText = astPath.node.quasis
385
+ .map((q, i) => {
386
+ const expr = astPath.node.expressions[i];
387
+ const exprStr = expr ? `\${${generate(expr, { compact: true }).code}}` : '';
388
+ return q.value.raw + exprStr;
389
+ })
390
+ .join('');
391
+ untranslatedTexts.push({
392
+ file,
393
+ line: astPath.node.loc?.start.line,
394
+ column: astPath.node.loc?.start.column,
395
+ type: 'TemplateLiteral',
396
+ text: fullText,
397
+ });
398
+ }
399
+ }
400
+ },
401
+ JSXText(astPath) {
402
+ const { value } = astPath.node;
403
+ if (
404
+ chnRegExp.test(value) &&
405
+ !isI18nCall(astPath.parent) &&
406
+ !astPath.findParent((p) => isI18nCall(p.node)) &&
407
+ !isInConsoleCall(astPath) &&
408
+ !shouldIgnoreNode(astPath) &&
409
+ !onlyContainsChinesePunctuation(value) &&
410
+ !onlyContainsEmoji(value)
411
+ ) {
412
+ const trimmedValue = value.replace(/^\s+|\s+$/g, '');
413
+ if (trimmedValue) {
414
+ untranslatedTexts.push({
415
+ file,
416
+ line: astPath.node.loc?.start.line,
417
+ column: astPath.node.loc?.start.column,
418
+ type: 'JSXText',
419
+ text: trimmedValue,
420
+ });
421
+ }
422
+ }
423
+ },
424
+ });
425
+ } catch (error) {
426
+ console.error(` # 扫描未翻译文案失败: ${file}`, error.message);
427
+ }
428
+ });
429
+
430
+ return untranslatedTexts;
431
+ };
432
+
313
433
  // 读取翻译文件
314
434
  const readTranslationFiles = (localesFilePath, dashScope, checkLanguages) => {
315
435
  // 读取中文翻译文件
@@ -376,7 +496,7 @@ const analyzeMissingAndUnusedKeys = (usedKeys, zhCN, languageFiles) => {
376
496
  };
377
497
 
378
498
  // 生成检查结果输出
379
- const generateCheckOutput = (missingKeys, unusedKeys) => {
499
+ const generateCheckOutput = (missingKeys, unusedKeys, untranslatedTexts) => {
380
500
  const outputLines = [];
381
501
  outputLines.push('=== 检查结果 ===');
382
502
  outputLines.push('');
@@ -392,7 +512,7 @@ const generateCheckOutput = (missingKeys, unusedKeys) => {
392
512
  }
393
513
 
394
514
  for (const [language, missing] of Object.entries(missingKeys)) {
395
- if (language === 'zhCN') continue; // 跳过中文,已经处理过了
515
+ if (language === 'zhCN') continue;
396
516
 
397
517
  if (missing.length > 0) {
398
518
  outputLines.push('');
@@ -417,7 +537,7 @@ const generateCheckOutput = (missingKeys, unusedKeys) => {
417
537
  }
418
538
 
419
539
  for (const [language, unused] of Object.entries(unusedKeys)) {
420
- if (language === 'zhCN') continue; // 跳过中文,已经处理过了
540
+ if (language === 'zhCN') continue;
421
541
 
422
542
  if (unused.length > 0) {
423
543
  outputLines.push('');
@@ -429,6 +549,20 @@ const generateCheckOutput = (missingKeys, unusedKeys) => {
429
549
  }
430
550
  }
431
551
 
552
+ outputLines.push('');
553
+ outputLines.push('3. 未翻译的中文文案:');
554
+
555
+ if (untranslatedTexts.length > 0) {
556
+ outputLines.push('');
557
+ outputLines.push(`共发现 ${untranslatedTexts.length} 条未翻译的中文文案:`);
558
+ untranslatedTexts.forEach((item) => {
559
+ outputLines.push(` ${item.file}:${item.line}:${item.column} "${item.text}"`);
560
+ });
561
+ } else {
562
+ outputLines.push('');
563
+ outputLines.push('没有发现未翻译的中文文案');
564
+ }
565
+
432
566
  outputLines.push('');
433
567
  outputLines.push('=== 检查完成 ===');
434
568
 
@@ -436,7 +570,13 @@ const generateCheckOutput = (missingKeys, unusedKeys) => {
436
570
  };
437
571
 
438
572
  // 处理输出和JSON文件生成
439
- const handleOutputAndJsonGeneration = (outputLines, missingKeys, unusedKeys, autoDeleteUnused) => {
573
+ const handleOutputAndJsonGeneration = (
574
+ outputLines,
575
+ missingKeys,
576
+ unusedKeys,
577
+ untranslatedTexts,
578
+ autoDeleteUnused,
579
+ ) => {
440
580
  // 判断是否需要生成JSON文件(自动删除模式下不生成日志文件)
441
581
  if (outputLines.length > 100 && !autoDeleteUnused) {
442
582
  // 生成JSON文件
@@ -448,10 +588,12 @@ const handleOutputAndJsonGeneration = (outputLines, missingKeys, unusedKeys, aut
448
588
  summary: {
449
589
  totalMissingKeys: Object.values(missingKeys).reduce((sum, keys) => sum + keys.length, 0),
450
590
  totalUnusedKeys: Object.values(unusedKeys).reduce((sum, keys) => sum + keys.length, 0),
591
+ totalUntranslatedTexts: untranslatedTexts.length,
451
592
  totalOutputLines: outputLines.length,
452
593
  },
453
594
  missingKeys,
454
595
  unusedKeys,
596
+ untranslatedTexts,
455
597
  };
456
598
 
457
599
  try {
@@ -463,6 +605,7 @@ const handleOutputAndJsonGeneration = (outputLines, missingKeys, unusedKeys, aut
463
605
  console.log('\n📊 概要信息:');
464
606
  console.log(` • 缺失的翻译key总数: ${jsonResult.summary.totalMissingKeys}`);
465
607
  console.log(` • 未使用的翻译key总数: ${jsonResult.summary.totalUnusedKeys}`);
608
+ console.log(` • 未翻译的中文文案总数: ${jsonResult.summary.totalUntranslatedTexts}`);
466
609
 
467
610
  // 显示各语言的统计
468
611
  console.log('\n📋 各语言统计:');
@@ -478,10 +621,13 @@ const handleOutputAndJsonGeneration = (outputLines, missingKeys, unusedKeys, aut
478
621
  console.log(` ${displayLang}.json: ${unused.length}个`);
479
622
  }
480
623
 
624
+ if (untranslatedTexts.length > 0) {
625
+ console.log(` 未翻译的中文文案: ${untranslatedTexts.length}条`);
626
+ }
627
+
481
628
  console.log('\n=== 检查完成 ===\n');
482
629
  } catch (error) {
483
630
  console.error(`\n❌ 生成JSON文件失败: ${error.message}`);
484
- // 如果JSON文件生成失败,仍然输出完整结果(自动删除模式下不输出)
485
631
  if (!autoDeleteUnused) {
486
632
  outputLines.forEach((line) => console.log(line));
487
633
  }
@@ -710,6 +856,7 @@ const check = async function (sourcePath, params) {
710
856
  medusa,
711
857
  autoDeleteUnused,
712
858
  summaryOnly,
859
+ doNotTranslateFiles,
713
860
  check: checkConfig,
714
861
  } = params;
715
862
 
@@ -730,6 +877,15 @@ const check = async function (sourcePath, params) {
730
877
  // 分析缺失和未使用的key
731
878
  const { missingKeys, unusedKeys } = analyzeMissingAndUnusedKeys(usedKeys, zhCN, languageFiles);
732
879
 
880
+ // 扫描未翻译的中文文案
881
+ console.log(` # 扫描未翻译的中文文案...`);
882
+ const untranslatedTexts = scanUntranslatedTexts(files, doNotTranslateFiles);
883
+ if (untranslatedTexts.length > 0) {
884
+ console.log(` # 发现 ${untranslatedTexts.length} 条未翻译的中文文案`);
885
+ } else {
886
+ console.log(` # 未发现未翻译的中文文案`);
887
+ }
888
+
733
889
  // summaryOnly 模式:只输出各语言缺失的key数量,不做删除/生成等操作
734
890
  if (summaryOnly) {
735
891
  console.log('');
@@ -755,19 +911,34 @@ const check = async function (sourcePath, params) {
755
911
  }
756
912
 
757
913
  console.log('');
758
- if (allGood) {
759
- console.log('✅ 所有语言翻译完整');
914
+ if (untranslatedTexts.length > 0) {
915
+ console.log(` 未翻译的中文文案: ${untranslatedTexts.length} 条`);
916
+ } else {
917
+ console.log(` 未翻译的中文文案: ✓ 无`);
918
+ }
919
+
920
+ console.log('');
921
+ if (allGood && untranslatedTexts.length === 0) {
922
+ console.log('✅ 所有语言翻译完整,无未翻译文案');
923
+ } else if (allGood) {
924
+ console.log('❌ 存在未翻译的中文文案,请运行 npx i18n check 查看详情');
760
925
  } else {
761
926
  console.log('❌ 存在翻译缺失,请运行 npx i18n check 查看详情');
762
927
  }
763
928
 
764
- return { missingKeys, unusedKeys, usedKeysData };
929
+ return { missingKeys, unusedKeys, usedKeysData, untranslatedTexts };
765
930
  }
766
931
 
767
932
  // 完整模式:输出详情、删除未使用key、生成Excel等
768
- const outputLines = generateCheckOutput(missingKeys, unusedKeys);
933
+ const outputLines = generateCheckOutput(missingKeys, unusedKeys, untranslatedTexts);
769
934
 
770
- handleOutputAndJsonGeneration(outputLines, missingKeys, unusedKeys, autoDeleteUnused);
935
+ handleOutputAndJsonGeneration(
936
+ outputLines,
937
+ missingKeys,
938
+ unusedKeys,
939
+ untranslatedTexts,
940
+ autoDeleteUnused,
941
+ );
771
942
 
772
943
  await handleUnusedKeysDeletion(
773
944
  unusedKeys,
@@ -783,6 +954,7 @@ const check = async function (sourcePath, params) {
783
954
  missingKeys,
784
955
  unusedKeys,
785
956
  usedKeysData,
957
+ untranslatedTexts,
786
958
  };
787
959
 
788
960
  return result;
package/lib/parse-jsx.js CHANGED
@@ -4,87 +4,13 @@ const generate = require('@babel/generator').default;
4
4
  const t = require('@babel/types');
5
5
  const fs = require('fs-extra');
6
6
  const { shouldIgnoreLineByComment, shouldIgnoreBlockByComment } = require('./utils/file-utils');
7
-
8
- // 中文识别正则表达式
9
- const chnRegExp = new RegExp(/[^\x00-\xff]/);
10
-
11
- // 检查是否只包含中文标点符号
12
- const onlyContainsChinesePunctuation = (text) => {
13
- // 中文标点符号正则,包括全角标点符号和常见符号
14
- const chinesePunctuationRegex =
15
- /[,。!?;:""''()【】《》|、…—·~~@#$%^&*()_+\-={}\[\]|\\:;"'<>,.?\/]/g;
16
- // 移除所有中文标点符号
17
- const textWithoutPunctuation = text.replace(chinesePunctuationRegex, '');
18
- // 如果移除标点后为空,说明只包含标点符号
19
- return textWithoutPunctuation.trim() === '';
20
- };
21
-
22
- const onlyContainsEmoji = (text) => {
23
- // Emoji 正则表达式,包含常见的emoji Unicode范围
24
- const emojiRegex =
25
- /[\u{1F600}-\u{1F64F}]|[\u{1F300}-\u{1F5FF}]|[\u{1F680}-\u{1F6FF}]|[\u{1F1E0}-\u{1F1FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]|[\u{1F900}-\u{1F9FF}]|[\u{1F018}-\u{1F270}]|[\u{238C}-\u{2454}]|[\u{20D0}-\u{20FF}]|[\u{FE00}-\u{FE0F}]|[\u{1F200}-\u{1F2FF}]|[\u{1F700}-\u{1F77F}]|[\u{1F780}-\u{1F7FF}]|[\u{1F800}-\u{1F8FF}]|[\u{2300}-\u{23FF}]|[\u{2B00}-\u{2BFF}]|[\u{3030}]|[\u{303D}]|[\u{3297}]|[\u{3299}]/gu;
26
-
27
- // 移除所有空白字符
28
- const trimmedText = text.trim();
29
-
30
- // 如果文本为空,返回false
31
- if (!trimmedText) {
32
- return false;
33
- }
34
-
35
- // 移除所有emoji字符
36
- const textWithoutEmoji = trimmedText.replace(emojiRegex, '');
37
-
38
- // 如果移除emoji后为空,说明只包含emoji
39
- return textWithoutEmoji === '';
40
- };
41
-
42
- // 检查节点是否是i18n调用
43
- function isI18nCall(node) {
44
- return (
45
- node &&
46
- node.type === 'CallExpression' &&
47
- node.callee &&
48
- node.callee.type === 'MemberExpression' &&
49
- node.callee.object &&
50
- node.callee.object.name === '$i18n' &&
51
- node.callee.property &&
52
- node.callee.property.name === 'get'
53
- );
54
- }
55
-
56
- // 检测是否在 console 方法中
57
- function isInConsoleCall(path) {
58
- const consoleMethods = [
59
- 'log',
60
- 'warn',
61
- 'error',
62
- 'info',
63
- 'debug',
64
- 'trace',
65
- 'group',
66
- 'groupEnd',
67
- 'groupCollapsed',
68
- 'assert',
69
- 'table',
70
- 'time',
71
- 'timeEnd',
72
- 'count',
73
- 'countReset',
74
- ];
75
-
76
- return path.findParent((p) => {
77
- return (
78
- p.isCallExpression() &&
79
- p.node.callee &&
80
- p.node.callee.type === 'MemberExpression' &&
81
- p.node.callee.object &&
82
- p.node.callee.object.name === 'console' &&
83
- p.node.callee.property &&
84
- consoleMethods.includes(p.node.callee.property.name)
85
- );
86
- });
87
- }
7
+ const {
8
+ chnRegExp,
9
+ onlyContainsChinesePunctuation,
10
+ onlyContainsEmoji,
11
+ isI18nCall,
12
+ isInConsoleCall,
13
+ } = require('./utils/ast-utils');
88
14
 
89
15
  // 生成i18n调用节点
90
16
  const createI18nCallNode = (i18nKey, defaultMessage, variables = []) => {
@@ -93,8 +93,74 @@ function checkI18nCall(node) {
93
93
  return extractI18nKey(node);
94
94
  }
95
95
 
96
+ const chnRegExp = /[^\x00-\xff]/;
97
+
98
+ const onlyContainsChinesePunctuation = (text) => {
99
+ const chinesePunctuationRegex =
100
+ /[,。!?;:""''()【】《》|、…—·~~@#$%^&*()_+\-={}\[\]|\\:;"'<>,.?\/]/g;
101
+ const textWithoutPunctuation = text.replace(chinesePunctuationRegex, '');
102
+ return textWithoutPunctuation.trim() === '';
103
+ };
104
+
105
+ const onlyContainsEmoji = (text) => {
106
+ const emojiRegex =
107
+ /[\u{1F600}-\u{1F64F}]|[\u{1F300}-\u{1F5FF}]|[\u{1F680}-\u{1F6FF}]|[\u{1F1E0}-\u{1F1FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]|[\u{1F900}-\u{1F9FF}]|[\u{1F018}-\u{1F270}]|[\u{238C}-\u{2454}]|[\u{20D0}-\u{20FF}]|[\u{FE00}-\u{FE0F}]|[\u{1F200}-\u{1F2FF}]|[\u{1F700}-\u{1F77F}]|[\u{1F780}-\u{1F7FF}]|[\u{1F800}-\u{1F8FF}]|[\u{2300}-\u{23FF}]|[\u{2B00}-\u{2BFF}]|[\u{3030}]|[\u{303D}]|[\u{3297}]|[\u{3299}]/gu;
108
+ const trimmedText = text.trim();
109
+ if (!trimmedText) return false;
110
+ return trimmedText.replace(emojiRegex, '') === '';
111
+ };
112
+
113
+ function isI18nCall(node) {
114
+ return (
115
+ node &&
116
+ node.type === 'CallExpression' &&
117
+ node.callee &&
118
+ node.callee.type === 'MemberExpression' &&
119
+ node.callee.object &&
120
+ node.callee.object.name === '$i18n' &&
121
+ node.callee.property &&
122
+ node.callee.property.name === 'get'
123
+ );
124
+ }
125
+
126
+ function isInConsoleCall(path) {
127
+ const consoleMethods = [
128
+ 'log',
129
+ 'warn',
130
+ 'error',
131
+ 'info',
132
+ 'debug',
133
+ 'trace',
134
+ 'group',
135
+ 'groupEnd',
136
+ 'groupCollapsed',
137
+ 'assert',
138
+ 'table',
139
+ 'time',
140
+ 'timeEnd',
141
+ 'count',
142
+ 'countReset',
143
+ ];
144
+ return path.findParent((p) => {
145
+ return (
146
+ p.isCallExpression() &&
147
+ p.node.callee &&
148
+ p.node.callee.type === 'MemberExpression' &&
149
+ p.node.callee.object &&
150
+ p.node.callee.object.name === 'console' &&
151
+ p.node.callee.property &&
152
+ consoleMethods.includes(p.node.callee.property.name)
153
+ );
154
+ });
155
+ }
156
+
96
157
  module.exports = {
97
158
  extractStringValue,
98
159
  extractI18nKey,
99
160
  checkI18nCall,
161
+ chnRegExp,
162
+ onlyContainsChinesePunctuation,
163
+ onlyContainsEmoji,
164
+ isI18nCall,
165
+ isInConsoleCall,
100
166
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentscope-ai/i18n",
3
- "version": "0.1.9-rc.1",
3
+ "version": "0.1.9-rc.2",
4
4
  "description": "A tool for translating Chinese content in frontend code repositories",
5
5
  "keywords": [
6
6
  "i18n",