@agentscope-ai/i18n 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/cli.js CHANGED
@@ -2,10 +2,7 @@
2
2
 
3
3
  const { program } = require('commander');
4
4
  const {
5
- init,
6
- initMT,
7
- patch,
8
- patchMT,
5
+ runTranslation,
9
6
  check,
10
7
  reverse,
11
8
  translate,
@@ -15,11 +12,9 @@ const {
15
12
  const path = require('path');
16
13
  const fs = require('fs-extra');
17
14
 
18
- // 读取配置文件
19
15
  const getConfig = (options) => {
20
16
  let configPath = options.config;
21
17
 
22
- // 如果没有指定配置文件路径,按优先级查找
23
18
  if (!configPath) {
24
19
  const configFiles = ['i18n.config.js', 'i18n.config.cjs', 'i18n.config.mjs'];
25
20
 
@@ -35,31 +30,25 @@ const getConfig = (options) => {
35
30
  throw new Error(`配置文件不存在,请在项目根目录创建 i18n.config.js 或 i18n.config.cjs`);
36
31
  }
37
32
  } else {
38
- // 如果指定了配置文件路径,检查是否存在
39
33
  if (!fs.existsSync(configPath)) {
40
34
  throw new Error(`配置文件 ${configPath} 不存在`);
41
35
  }
42
36
  }
43
37
 
44
- const config = require(configPath);
45
- return config;
38
+ return require(configPath);
46
39
  };
47
40
 
48
- // 解析/覆盖 targetPath:命令行优先,其次配置文件
49
41
  const resolveTargetPath = (options, config) => {
50
42
  const fromCli = options.targetPath || options['target-path'];
51
- const fromConfig = config && config.targetPath;
52
- const raw = fromCli || fromConfig;
43
+ const raw = fromCli || (config && config.targetPath);
53
44
 
54
45
  if (!raw) {
55
46
  throw new Error('targetPath 未配置:请在配置文件中提供 targetPath,或通过 --target-path 传入');
56
47
  }
57
48
 
58
- // 统一将相对路径转换为绝对路径(相对于当前工作目录)
59
49
  return path.isAbsolute(raw) ? raw : path.resolve(process.cwd(), raw);
60
50
  };
61
51
 
62
- // 将命令行通用选项统一解析并合并到 config 中
63
52
  const resolveCommonOptions = (options, config) => {
64
53
  const targetPath = resolveTargetPath(options, config);
65
54
  config.targetPath = targetPath;
@@ -67,187 +56,119 @@ const resolveCommonOptions = (options, config) => {
67
56
  return targetPath;
68
57
  };
69
58
 
70
- // 为每个命令添加通用入参:config + target-path + skip-confirm
71
59
  const withCommonOptions = (cmd) =>
72
60
  cmd
73
61
  .option('-c, --config <path>', 'Path to config file')
74
62
  .option('-p, --target-path <path>', 'Override targetPath in config file')
75
63
  .option('--skip-confirm', 'Skip all confirmation prompts, auto confirm (y/n) questions');
76
64
 
77
- program
78
- .name('agentscope-ai-i18n')
79
- .description('A tool for translating Chinese content in frontend code repositories')
80
- .version('0.1.0');
81
-
82
- withCommonOptions(
83
- program
84
- .command('init-agent')
85
- .description('Initialize translation for the project using Bailian Agent'),
86
- ).action(async (options) => {
87
- try {
88
- const config = getConfig(options);
89
- const targetPath = resolveCommonOptions(options, config);
90
- await init(targetPath, config);
91
- } catch (error) {
92
- console.error('初始化失败:', error.message);
93
- process.exit(1);
94
- }
95
- });
96
-
97
- withCommonOptions(
98
- program
99
- .command('init-mt')
100
- .description('Initialize translation using MT (Machine Translation) method'),
101
- ).action(async (options) => {
102
- try {
103
- const config = getConfig(options);
104
- const targetPath = resolveCommonOptions(options, config);
105
- await initMT(targetPath, config);
106
- } catch (error) {
107
- console.error('MT初始化失败:', error.message);
108
- process.exit(1);
109
- }
110
- });
111
-
112
- withCommonOptions(
113
- program
114
- .command('init')
115
- .description('Initialize translation using MT (Machine Translation) method'),
116
- ).action(async (options) => {
117
- try {
118
- const config = getConfig(options);
119
- const targetPath = resolveCommonOptions(options, config);
120
- await initMT(targetPath, config);
121
- } catch (error) {
122
- console.error('MT初始化失败:', error.message);
123
- process.exit(1);
124
- }
125
- });
126
-
127
- withCommonOptions(
128
- program
129
- .command('patch-agent')
130
- .description('Update translations for new content using Bailian Agent'),
131
- ).action(async (options) => {
132
- try {
133
- const config = getConfig(options);
134
- const targetPath = resolveCommonOptions(options, config);
135
- await patch(targetPath, config);
136
- } catch (error) {
137
- console.error('补丁更新失败:', error.message);
138
- process.exit(1);
139
- }
140
- });
141
-
142
- withCommonOptions(
143
- program
144
- .command('patch-mt')
145
- .description('Update translations for new content using MT (Machine Translation) method'),
146
- ).action(async (options) => {
147
- try {
148
- const config = getConfig(options);
149
- const targetPath = resolveCommonOptions(options, config);
150
- await patchMT(targetPath, config);
151
- } catch (error) {
152
- console.error('MT补丁更新失败:', error.message);
153
- process.exit(1);
154
- }
155
- });
156
-
157
- withCommonOptions(
158
- program
159
- .command('patch')
160
- .description('Update translations for new content using MT (Machine Translation) method'),
161
- ).action(async (options) => {
162
- try {
163
- const config = getConfig(options);
164
- const targetPath = resolveCommonOptions(options, config);
165
- await patchMT(targetPath, config);
166
- } catch (error) {
167
- console.error('MT补丁更新失败:', error.message);
168
- process.exit(1);
169
- }
170
- });
171
-
172
- withCommonOptions(program.command('check').description('Check translation status'))
173
- .option('--auto-delete-unused', 'Automatically delete unused translation keys')
174
- .option(
175
- '--summary-only',
176
- 'Only output missing key counts per language, skip deletion and file generation',
177
- )
178
- .action(async (options) => {
65
+ /**
66
+ * 注册一个带通用选项的命令,自动处理 config 加载、targetPath 解析和错误处理。
67
+ */
68
+ const registerCommand = (name, description, handler, { extraOptions = [], useCommon = true } = {}) => {
69
+ const cmd = program.command(name).description(description);
70
+ if (useCommon) withCommonOptions(cmd);
71
+ extraOptions.forEach((opt) => cmd.option(...opt));
72
+ cmd.action(async (options) => {
179
73
  try {
180
- const config = getConfig(options);
181
- config.autoDeleteUnused = options.autoDeleteUnused;
182
- config.summaryOnly = options.summaryOnly;
183
- const targetPath = resolveCommonOptions(options, config);
184
- await check(targetPath, config);
74
+ if (useCommon) {
75
+ const config = getConfig(options);
76
+ resolveCommonOptions(options, config);
77
+ await handler(config, options);
78
+ } else {
79
+ await handler(options);
80
+ }
185
81
  } catch (error) {
186
- console.error('检查失败:', error.message);
82
+ console.error(`❌ ${description}失败:`, error.message);
187
83
  process.exit(1);
188
84
  }
189
85
  });
86
+ return cmd;
87
+ };
190
88
 
191
- withCommonOptions(
192
- program.command('reverse').description('Reverse internationalized code back to Chinese'),
193
- ).action(async (options) => {
194
- try {
195
- const config = getConfig(options);
196
- const targetPath = resolveCommonOptions(options, config);
197
- await reverse(targetPath, config);
198
- } catch (error) {
199
- console.error('还原失败:', error.message);
200
- process.exit(1);
201
- }
202
- });
203
-
204
- withCommonOptions(
205
- program
206
- .command('translate')
207
- .description(
208
- 'Extract Chinese text, translate, and generate locale files + mds-payload.json (Step 1/3, no code modification)',
209
- )
210
- .option(
211
- '-f, --file <path>',
212
- 'Process a single file instead of the entire targetPath directory',
213
- ),
214
- ).action(async (options) => {
215
- try {
216
- const config = getConfig(options);
217
- const targetPath = resolveCommonOptions(options, config);
218
- if (options.file) {
219
- config.file = options.file;
220
- }
221
- await translate(targetPath, config);
222
- } catch (error) {
223
- console.error('translate 失败:', error.message);
224
- process.exit(1);
225
- }
226
- });
227
-
228
- withCommonOptions(
229
- program
230
- .command('replace')
231
- .description(
232
- 'Replace Chinese text in code with $i18n.get calls using saved translation state (Step 3/3)',
233
- )
234
- .option(
235
- '-f, --file <path>',
236
- 'Process a single file instead of the entire targetPath directory',
237
- ),
238
- ).action(async (options) => {
239
- try {
240
- const config = getConfig(options);
241
- const targetPath = resolveCommonOptions(options, config);
242
- if (options.file) {
243
- config.file = options.file;
244
- }
245
- await replaceOnly(targetPath, config);
246
- } catch (error) {
247
- console.error('replace 失败:', error.message);
248
- process.exit(1);
249
- }
250
- });
89
+ program
90
+ .name('i18n')
91
+ .description('A tool for translating Chinese content in frontend code repositories')
92
+ .version('1.0.1');
93
+
94
+ // --- 核心翻译命令 ---
95
+
96
+ registerCommand(
97
+ 'init',
98
+ '初始化翻译',
99
+ (config, options) => runTranslation(config.targetPath, config, {
100
+ isUpdate: false,
101
+ engine: options.engine || 'mt',
102
+ }),
103
+ { extraOptions: [['--engine <engine>', 'Translation engine: mt (default) or agent', 'mt']] },
104
+ );
105
+
106
+ registerCommand(
107
+ 'patch',
108
+ '增量翻译',
109
+ (config, options) => runTranslation(config.targetPath, config, {
110
+ isUpdate: true,
111
+ engine: options.engine || 'mt',
112
+ }),
113
+ { extraOptions: [['--engine <engine>', 'Translation engine: mt (default) or agent', 'mt']] },
114
+ );
115
+
116
+ registerCommand(
117
+ 'check',
118
+ '检查翻译状态',
119
+ (config, options) => {
120
+ config.autoDeleteUnused = options.autoDeleteUnused;
121
+ config.summaryOnly = options.summaryOnly;
122
+ return check(config.targetPath, config);
123
+ },
124
+ {
125
+ extraOptions: [
126
+ ['--auto-delete-unused', 'Automatically delete unused translation keys'],
127
+ ['--summary-only', 'Only output missing key counts per language, skip deletion and file generation'],
128
+ ],
129
+ },
130
+ );
131
+
132
+ registerCommand('reverse', '还原国际化代码为中文', (config) =>
133
+ reverse(config.targetPath, config),
134
+ );
135
+
136
+ registerCommand(
137
+ 'translate',
138
+ '提取中文并翻译生成locale文件 (Step 1/3, 不修改源码)',
139
+ (config, options) => {
140
+ if (options.file) config.file = options.file;
141
+ return translate(config.targetPath, config);
142
+ },
143
+ { extraOptions: [['-f, --file <path>', 'Process a single file instead of the entire targetPath directory']] },
144
+ );
145
+
146
+ registerCommand(
147
+ 'replace',
148
+ '用翻译状态替换代码中的中文 (Step 3/3)',
149
+ (config, options) => {
150
+ if (options.file) config.file = options.file;
151
+ return replaceOnly(config.targetPath, config);
152
+ },
153
+ { extraOptions: [['-f, --file <path>', 'Process a single file instead of the entire targetPath directory']] },
154
+ );
155
+
156
+ // --- 向后兼容别名(隐藏,不在 help 中显示) ---
157
+
158
+ const registerHiddenAlias = (name, target) => {
159
+ program.command(name, { hidden: true }).action(() => {
160
+ console.log(`⚠️ "${name}" 已弃用,请使用 "${target}" 代替`);
161
+ process.argv[2] = target;
162
+ program.parse(process.argv);
163
+ });
164
+ };
165
+
166
+ registerHiddenAlias('init-mt', 'init');
167
+ registerHiddenAlias('init-agent', 'init --engine agent');
168
+ registerHiddenAlias('patch-mt', 'patch');
169
+ registerHiddenAlias('patch-agent', 'patch --engine agent');
170
+
171
+ // --- 工具命令 ---
251
172
 
252
173
  program
253
174
  .command('init-config')
@@ -269,14 +190,12 @@ program
269
190
 
270
191
  fs.copySync(templatePath, configPath);
271
192
 
272
- // 检查 .env 文件是否存在
273
193
  const envPath = path.resolve(cwd, '.env');
274
194
  if (!fs.existsSync(envPath)) {
275
195
  fs.writeFileSync(envPath, 'DASHSCOPE_APIKEY=\n', 'utf8');
276
196
  console.log('📝 已创建 .env 文件,请填写 DASHSCOPE_APIKEY');
277
197
  }
278
198
 
279
- // 检查 .gitignore 中是否包含 .env
280
199
  const gitignorePath = path.resolve(cwd, '.gitignore');
281
200
  if (fs.existsSync(gitignorePath)) {
282
201
  const gitignoreContent = fs.readFileSync(gitignorePath, 'utf8');
@@ -313,7 +232,6 @@ program
313
232
  try {
314
233
  const cwd = process.cwd();
315
234
 
316
- // 读取配置
317
235
  let config = {};
318
236
  try {
319
237
  config = getConfig(options);
@@ -339,13 +257,10 @@ program
339
257
  process.exit(1);
340
258
  }
341
259
  if (!appId) {
342
- console.error(
343
- '❌ 缺少 appId,请通过 --app-id 参数传入,或在 i18n.config.js 的 medusa.appId 中配置',
344
- );
260
+ console.error('❌ 缺少 appId,请通过 --app-id 参数传入,或在 i18n.config.js 的 medusa.appId 中配置');
345
261
  process.exit(1);
346
262
  }
347
263
 
348
- // 各 IDE/Agent 的 skills 目录映射
349
264
  const agentDirMap = {
350
265
  cursor: '.cursor/skills',
351
266
  qoder: '.qoder/skills',
@@ -354,10 +269,8 @@ program
354
269
  opencode: '.opencode/skill',
355
270
  };
356
271
 
357
- // 确定目标 agents
358
272
  let targetAgents = options.agent || [];
359
273
  if (targetAgents.length === 0) {
360
- // 自动检测:目录已存在的 IDE 默认安装,否则至少安装 cursor
361
274
  targetAgents = Object.entries(agentDirMap)
362
275
  .filter(([, dir]) => fs.existsSync(path.resolve(cwd, dir.split('/')[0])))
363
276
  .map(([name]) => name);
@@ -366,7 +279,6 @@ program
366
279
  }
367
280
  }
368
281
 
369
- // 读取模板
370
282
  const skillSource = path.join(__dirname, '../skills/i18n-helper/SKILL.md');
371
283
  if (!fs.existsSync(skillSource)) {
372
284
  throw new Error(`SKILL.md 模板未找到: ${skillSource}`);
@@ -379,7 +291,6 @@ program
379
291
  content = content.replace(/\{\{LOCALES_FILE_PATH\}\}/g, localesFilePath);
380
292
  content = content.replace(/\{\{LANGUAGES\}\}/g, languages.join('、'));
381
293
 
382
- // 写入各 IDE 目录
383
294
  const installed = [];
384
295
  for (const agent of targetAgents) {
385
296
  const baseDir = agentDirMap[agent];
@@ -428,7 +339,6 @@ program
428
339
  console.log('🚀 Medusa 国际化文件拉取工具');
429
340
  console.log('='.repeat(50));
430
341
 
431
- // 尝试读取配置文件
432
342
  let configFileSettings = {};
433
343
  try {
434
344
  const configFromFile = getConfig(options);
@@ -441,50 +351,32 @@ program
441
351
 
442
352
  const appName = options.app || configFileSettings.appName;
443
353
  if (!appName) {
444
- console.error(
445
- '❌ 缺少 Medusa 应用名称 appName:请通过 --app 传入,或在 i18n.config.js 的 medusa.appName 中配置',
446
- );
354
+ console.error('❌ 缺少 Medusa 应用名称 appName:请通过 --app 传入,或在 i18n.config.js 的 medusa.appName 中配置');
447
355
  process.exit(1);
448
356
  }
449
357
 
450
- // tag 选填:命令行优先,其次配置文件,不设默认值
451
358
  const tag = options.tag ?? configFileSettings.tag;
452
359
 
453
- // 合并配置文件和命令行参数(命令行参数优先)
454
360
  const config = {
455
- medusa: {
456
- appName,
457
- tag,
458
- },
361
+ medusa: { appName, tag },
459
362
  type: options.type || configFileSettings.type || 'json',
460
363
  cwd: process.cwd(),
461
364
  path: options.output || configFileSettings.outputPath || './locales',
462
365
  mergeExisting: options.merge !== false && configFileSettings.mergeExisting !== false,
463
366
  };
464
367
 
465
- // 显示配置信息
466
368
  console.log('📝 配置信息:');
467
- console.log(
468
- ` 应用名称: ${config.medusa.appName} ${options.app ? '(命令行)' : '(配置文件)'}`,
469
- );
369
+ console.log(` 应用名称: ${config.medusa.appName} ${options.app ? '(命令行)' : '(配置文件)'}`);
470
370
  const tagSource =
471
371
  options.tag != null && options.tag !== ''
472
372
  ? '(命令行)'
473
373
  : configFileSettings.tag != null && configFileSettings.tag !== ''
474
374
  ? '(配置文件)'
475
375
  : '';
476
- console.log(
477
- ` 标签名称: ${config.medusa.tag ?? '(未设置)'}${tagSource ? ` ${tagSource}` : ''}`,
478
- );
479
- console.log(
480
- ` 包类型: ${config.type} ${options.type ? '(命令行)' : configFileSettings.type ? '(配置文件)' : '(默认)'}`,
481
- );
482
- console.log(
483
- ` 输出目录: ${path.resolve(config.path)} ${options.output ? '(命令行)' : configFileSettings.outputPath ? '(配置文件)' : '(默认)'}`,
484
- );
485
- console.log(
486
- ` 合并现有文件: ${config.mergeExisting ? '是' : '否'} ${options.merge === false ? '(命令行)' : configFileSettings.mergeExisting !== undefined ? '(配置文件)' : '(默认)'}`,
487
- );
376
+ console.log(` 标签名称: ${config.medusa.tag ?? '(未设置)'}${tagSource ? ` ${tagSource}` : ''}`);
377
+ console.log(` 包类型: ${config.type} ${options.type ? '(命令行)' : configFileSettings.type ? '(配置文件)' : '(默认)'}`);
378
+ console.log(` 输出目录: ${path.resolve(config.path)} ${options.output ? '(命令行)' : configFileSettings.outputPath ? '(配置文件)' : '(默认)'}`);
379
+ console.log(` 合并现有文件: ${config.mergeExisting ? '是' : '否'} ${options.merge === false ? '(命令行)' : configFileSettings.mergeExisting !== undefined ? '(配置文件)' : '(默认)'}`);
488
380
  console.log('');
489
381
 
490
382
  console.log('🔄 开始从 Medusa 平台拉取文件...');
@@ -495,7 +387,6 @@ program
495
387
  } catch (error) {
496
388
  console.error('❌ Medusa 拉取失败:', error.message);
497
389
 
498
- // 提供更详细的错误信息
499
390
  if (error.response) {
500
391
  console.error(' HTTP 状态码:', error.response.status);
501
392
  if (error.response.status === 404) {
@@ -2,30 +2,26 @@ const babylon = require('@babel/parser');
2
2
  const traverse = require('@babel/traverse').default;
3
3
  const generate = require('@babel/generator').default;
4
4
  const t = require('@babel/types');
5
+ const { isMatchingCallee } = require('../utils/ast-utils');
5
6
 
6
7
  // 检查i18n调用中id和dm是否匹配
7
- const checkI18nCall = (node) => {
8
- if (
9
- node.type === 'CallExpression' &&
10
- node.callee.type === 'MemberExpression' &&
11
- node.callee.object &&
12
- node.callee.object.name === '$i18n' &&
13
- node.callee.property &&
14
- node.callee.property.name === 'get'
15
- ) {
16
- const args = node.arguments;
17
- if (args.length > 0 && args[0].type === 'ObjectExpression') {
18
- const idProperty = args[0].properties.find((prop) => prop.key && prop.key.name === 'id');
19
- const dmProperty = args[0].properties.find((prop) => prop.key && prop.key.name === 'dm');
8
+ const checkI18nCall = (node, callExpr) => {
9
+ if (!isMatchingCallee(node, callExpr)) {
10
+ return null;
11
+ }
12
+
13
+ const args = node.arguments;
14
+ if (args.length > 0 && args[0].type === 'ObjectExpression') {
15
+ const idProperty = args[0].properties.find((prop) => prop.key && prop.key.name === 'id');
16
+ const dmProperty = args[0].properties.find((prop) => prop.key && prop.key.name === 'dm');
20
17
 
21
- if (
22
- idProperty?.value?.type === 'StringLiteral' &&
23
- dmProperty?.value?.type === 'StringLiteral'
24
- ) {
25
- const id = idProperty.value.value;
26
- const dm = dmProperty.value.value;
27
- return { id, dm };
28
- }
18
+ if (
19
+ idProperty?.value?.type === 'StringLiteral' &&
20
+ dmProperty?.value?.type === 'StringLiteral'
21
+ ) {
22
+ const id = idProperty.value.value;
23
+ const dm = dmProperty.value.value;
24
+ return { id, dm };
29
25
  }
30
26
  }
31
27
  return null;