@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.
@@ -11,19 +11,23 @@ const {
11
11
  singleKeyTranslate,
12
12
  singleTranslate,
13
13
  } = require('../utils/translation-utils');
14
- const { extractStringValue } = require('../utils/ast-utils');
14
+ const { extractStringValue, isMatchingCallee } = require('../utils/ast-utils');
15
15
  const { askQuestion } = require('../utils/cli-utils');
16
16
  const { generateMedusaExcel } = require('../utils/excel-utils');
17
17
  const { generateMdsPayload } = require('../utils/mds-payload');
18
18
  const parseJSX = require('../parse-jsx');
19
19
 
20
- // 提取共用的文案处理函数
20
+ const ENGINES = {
21
+ mt: { keyTranslate: singleKeyTranslate, valueTranslate: singleTranslate },
22
+ agent: { keyTranslate: batchKeyTranslate, valueTranslate: batchTranslate },
23
+ };
24
+
21
25
  const processFiles = async (files, params) => {
22
26
  console.log(` [开始] 提取和替换文件中的中文内容...`);
23
27
 
24
28
  const { keyPrefix, functionImportPath } = params;
25
- const fileCodeMap = new Map(); // 存储文件路径和替换后的代码的映射
26
- const needImportFiles = new Set(); // 存储需要添加导入语句的文件
29
+ const fileCodeMap = new Map();
30
+ const needImportFiles = new Set();
27
31
 
28
32
  const promises = files.map((file) => {
29
33
  if (/(\.js|\.ts|\.tsx|\.jsx)$/i.test(file)) {
@@ -51,169 +55,42 @@ const processFiles = async (files, params) => {
51
55
  return { i18n, fileCodeMap, needImportFiles };
52
56
  };
53
57
 
54
- // 提取共用的翻译和文件更新函数
55
- const translateAndUpdateFiles = async (
56
- i18n,
57
- path,
58
- localesFilePath,
59
- fileType,
60
- fileCodeMap,
61
- needImportFiles,
62
- functionImportPath,
58
+ /**
59
+ * 统一的翻译和文件更新函数。通过 engine 参数('mt' | 'agent')选择翻译策略。
60
+ */
61
+ const translateAndUpdateFiles = async (i18n, config, {
63
62
  isUpdate = false,
64
- dashScope,
65
- medusa,
66
- skipConfirm = false,
67
- ) => {
68
- if (Object.keys(i18n).length === 0) {
69
- console.log(' [提示] 没有找到需要提取的文案');
70
- return;
71
- }
72
-
73
- // 生成临时的key.json文件
74
- const keyJsonPath = `${localesFilePath}/key.json`;
75
- fs.ensureDirSync(localesFilePath);
76
- fs.writeJsonSync(keyJsonPath, i18n, { spaces: 2 });
77
- console.log(` [进度] 生成临时key.json文件,共${Object.keys(i18n).length}条文案`);
78
-
79
- // 调用key翻译
80
- console.log(` [进度] AI正在翻译key...请稍后`);
81
- const { keyMap, zhCN } = await batchKeyTranslate(i18n, dashScope);
82
-
83
- // 更新zh-cn.json
84
- const zhCNPath = `${localesFilePath}/zh-cn.json`;
85
- let existingZhCN = {};
86
- if (isUpdate) {
87
- try {
88
- existingZhCN = fs.readJsonSync(zhCNPath);
89
- } catch (error) {
90
- console.log(' [提示] 未找到现有的zh-cn.json文件,将创建新文件');
91
- }
92
- }
93
- const finalZhCN = isUpdate ? { ...existingZhCN, ...zhCN } : zhCN;
94
- fs.writeJsonSync(zhCNPath, finalZhCN, { spaces: 2 });
95
- console.log(` [完成] ${isUpdate ? '更新' : '生成'}zh-cn.json文件成功`);
96
-
97
- // 调用value翻译生成多语言文件
98
- console.log(` [进度] AI正在翻译value...请稍后`);
99
- const translatedResults = await batchTranslate(zhCN, dashScope);
100
-
101
- // 为每种语言生成对应的翻译文件
102
- const generatedFiles = [];
103
- for (const [language, translatedI18n] of Object.entries(translatedResults)) {
104
- const languagePath = `${localesFilePath}/${language}.json`;
105
- let existingLanguage = {};
106
-
107
- if (isUpdate) {
108
- try {
109
- existingLanguage = fs.readJsonSync(languagePath);
110
- } catch (error) {
111
- console.log(` [提示] 未找到现有的${language}.json文件,将创建新文件`);
112
- }
113
- }
114
-
115
- const finalLanguage = isUpdate ? { ...existingLanguage, ...translatedI18n } : translatedI18n;
116
- fs.writeJsonSync(languagePath, finalLanguage, { spaces: 2 });
117
- console.log(` [完成] ${isUpdate ? '更新' : '生成'}${language}.json文件成功`);
118
- generatedFiles.push(languagePath);
119
- }
120
-
121
- // 提示用户检查翻译文件
122
- console.log('\n=== 翻译文件已生成 ===');
123
- console.log('请检查以下文件:');
124
- console.log(`1. ${zhCNPath}`);
125
- generatedFiles.forEach((file, index) => {
126
- console.log(`${index + 2}. ${file}`);
127
- });
128
-
129
- let shouldReplace = skipConfirm;
130
- if (!skipConfirm) {
131
- const answer = await askQuestion('翻译文件检查无误后,是否继续替换代码中的文案?(y/n): ');
132
- shouldReplace = answer === 'y' || answer === 'yes';
133
- } else {
134
- console.log(' [跳过确认] 自动继续替换代码中的文案');
135
- }
136
-
137
- if (shouldReplace) {
138
- // 替换文件中的key
139
- console.log(` [进度] 开始替换文件中的key...`);
140
- await replaceKeysInFiles(path, keyMap, fileType, fileCodeMap);
141
-
142
- // 添加导入语句
143
- if (needImportFiles.size > 0) {
144
- console.log(` [进度] 开始添加导入语句...`);
145
- needImportFiles.forEach((file) => {
146
- injectImportStatement(file, functionImportPath);
147
- });
148
- console.log(` [完成] 已为${needImportFiles.size}个文件添加导入语句`);
149
- }
150
-
151
- console.log(' [完成] 代码替换完成!');
152
- } else {
153
- console.log(' [取消] 已取消代码替换操作');
154
- }
155
-
156
- // 询问是否生成medusa excel文件
157
- if (medusa && medusa.appName && medusa.keyMap) {
158
- let shouldGenExcel = skipConfirm;
159
- if (!skipConfirm) {
160
- const excelAnswer = await askQuestion('是否需要生成medusa excel文件?(y/n): ');
161
- shouldGenExcel = excelAnswer === 'y' || excelAnswer === 'yes';
162
- } else {
163
- console.log(' [跳过确认] 自动生成medusa excel文件');
164
- }
165
- if (shouldGenExcel) {
166
- try {
167
- const newKeys = Object.keys(zhCN); // 新增的key
168
- await generateMedusaExcel({
169
- localesFilePath,
170
- medusa,
171
- newKeys,
172
- });
173
- } catch (error) {
174
- console.error(' [错误] 生成medusa excel文件失败:', error.message);
175
- }
176
- } else {
177
- console.log(' [取消] 已取消生成medusa excel文件');
178
- }
179
- }
180
-
181
- fs.removeSync(keyJsonPath);
182
- console.log(` [提示] key.json文件已保留用于调试`);
183
-
184
- return Object.keys(zhCN).length;
185
- };
186
-
187
- // 单条翻译和文件更新函数(使用MT方式)
188
- const singleTranslateAndUpdateFiles = async (
189
- i18n,
190
- path,
191
- localesFilePath,
192
- fileType,
193
63
  fileCodeMap,
194
64
  needImportFiles,
195
- functionImportPath,
196
- isUpdate = false,
197
- dashScope,
198
- medusa,
199
- skipConfirm = false,
200
- ) => {
65
+ engine = 'mt',
66
+ } = {}) => {
67
+ const {
68
+ targetPath: sourcePath,
69
+ localesFilePath,
70
+ fileType = '.tsx,.js,.ts,.jsx',
71
+ functionImportPath,
72
+ dashScope,
73
+ medusa,
74
+ skipConfirm = false,
75
+ callExpression,
76
+ } = config;
77
+
201
78
  if (Object.keys(i18n).length === 0) {
202
79
  console.log(' [提示] 没有找到需要提取的文案');
203
- return;
80
+ return 0;
204
81
  }
205
82
 
206
- // 生成临时的key.json文件
83
+ const { keyTranslate, valueTranslate } = ENGINES[engine] || ENGINES.mt;
84
+ const engineLabel = engine === 'agent' ? 'Agent' : 'MT';
85
+
207
86
  const keyJsonPath = `${localesFilePath}/key.json`;
208
87
  fs.ensureDirSync(localesFilePath);
209
88
  fs.writeJsonSync(keyJsonPath, i18n, { spaces: 2 });
210
89
  console.log(` [进度] 生成临时key.json文件,共${Object.keys(i18n).length}条文案`);
211
90
 
212
- // 调用单条key翻译
213
- console.log(` [进度] 使用MT方式逐条翻译key...请稍后`);
214
- const { keyMap, zhCN } = await singleKeyTranslate(i18n, dashScope);
91
+ console.log(` [进度] 使用${engineLabel}方式翻译key...请稍后`);
92
+ const { keyMap, zhCN } = await keyTranslate(i18n, dashScope);
215
93
 
216
- // 更新zh-cn.json
217
94
  const zhCNPath = `${localesFilePath}/zh-cn.json`;
218
95
  let existingZhCN = {};
219
96
  if (isUpdate) {
@@ -227,11 +104,9 @@ const singleTranslateAndUpdateFiles = async (
227
104
  fs.writeJsonSync(zhCNPath, finalZhCN, { spaces: 2 });
228
105
  console.log(` [完成] ${isUpdate ? '更新' : '生成'}zh-cn.json文件成功`);
229
106
 
230
- // 调用单条value翻译生成多语言文件
231
- console.log(` [进度] 使用MT方式逐条翻译value...请稍后`);
232
- const translatedResults = await singleTranslate(zhCN, dashScope);
107
+ console.log(` [进度] 使用${engineLabel}方式翻译value...请稍后`);
108
+ const translatedResults = await valueTranslate(zhCN, dashScope);
233
109
 
234
- // 为每种语言生成对应的翻译文件
235
110
  const generatedFiles = [];
236
111
  for (const [language, translatedI18n] of Object.entries(translatedResults)) {
237
112
  const languagePath = `${localesFilePath}/${language}.json`;
@@ -251,8 +126,7 @@ const singleTranslateAndUpdateFiles = async (
251
126
  generatedFiles.push(languagePath);
252
127
  }
253
128
 
254
- // 提示用户检查翻译文件
255
- console.log('\n=== 翻译文件已生成(MT方式) ===');
129
+ console.log(`\n=== 翻译文件已生成(${engineLabel}方式) ===`);
256
130
  console.log('请检查以下文件:');
257
131
  console.log(`1. ${zhCNPath}`);
258
132
  generatedFiles.forEach((file, index) => {
@@ -268,15 +142,13 @@ const singleTranslateAndUpdateFiles = async (
268
142
  }
269
143
 
270
144
  if (shouldReplace) {
271
- // 替换文件中的key
272
145
  console.log(` [进度] 开始替换文件中的key...`);
273
- await replaceKeysInFiles(path, keyMap, fileType, fileCodeMap);
146
+ await replaceKeysInFiles(sourcePath, keyMap, fileType, fileCodeMap, callExpression);
274
147
 
275
- // 添加导入语句
276
- if (needImportFiles.size > 0) {
148
+ if (needImportFiles && needImportFiles.size > 0) {
277
149
  console.log(` [进度] 开始添加导入语句...`);
278
150
  needImportFiles.forEach((file) => {
279
- injectImportStatement(file, functionImportPath);
151
+ injectImportStatement(file, functionImportPath, callExpression);
280
152
  });
281
153
  console.log(` [完成] 已为${needImportFiles.size}个文件添加导入语句`);
282
154
  }
@@ -286,7 +158,6 @@ const singleTranslateAndUpdateFiles = async (
286
158
  console.log(' [取消] 已取消代码替换操作');
287
159
  }
288
160
 
289
- // 生成 mds-payload.json(供 sync-to-mds 脚本使用)
290
161
  if (medusa && medusa.appName) {
291
162
  const newKeys = Object.keys(zhCN);
292
163
  generateMdsPayload({
@@ -296,7 +167,6 @@ const singleTranslateAndUpdateFiles = async (
296
167
  });
297
168
  }
298
169
 
299
- // 询问是否生成medusa excel文件
300
170
  if (medusa && medusa.appName && medusa.keyMap) {
301
171
  let shouldGenExcel = skipConfirm;
302
172
  if (!skipConfirm) {
@@ -307,12 +177,8 @@ const singleTranslateAndUpdateFiles = async (
307
177
  }
308
178
  if (shouldGenExcel) {
309
179
  try {
310
- const newKeys = Object.keys(zhCN); // 新增的key
311
- await generateMedusaExcel({
312
- localesFilePath,
313
- medusa,
314
- newKeys,
315
- });
180
+ const newKeys = Object.keys(zhCN);
181
+ await generateMedusaExcel({ localesFilePath, medusa, newKeys });
316
182
  } catch (error) {
317
183
  console.error(' [错误] 生成medusa excel文件失败:', error.message);
318
184
  }
@@ -322,79 +188,54 @@ const singleTranslateAndUpdateFiles = async (
322
188
  }
323
189
 
324
190
  fs.removeSync(keyJsonPath);
325
- console.log(` [提示] key.json文件已删除`);
191
+ console.log(` [提示] key.json临时文件已删除`);
326
192
 
327
193
  return Object.keys(zhCN).length;
328
194
  };
329
195
 
330
- // 修改replaceKeysInFiles函数以支持fileCodeMap
331
- const replaceKeysInFiles = async (path, keyMap, fileType, fileCodeMap) => {
196
+ const replaceKeysInFiles = async (path, keyMap, fileType, fileCodeMap, callExpr) => {
332
197
  const fileTypes = fileType.split(',').map((s) => s.trim());
333
198
  const re = new RegExp(`(${fileTypes.map((ft) => ft.replace('.', '\\.')).join('|')})$`);
334
199
  const files = find.fileSync(re, path);
335
200
 
336
201
  console.log(` [进度] 开始替换${files.length}个文件中的key...`);
337
202
 
338
- // 仅当目标项目根目录存在 .prettierrc 时才尝试格式化
339
203
  const hasPrettierRc = fs.existsSync(nodePath.join(path, '.prettierrc'));
340
204
 
341
- // 动态导入 Prettier 并应用项目配置(如果可用)
342
205
  const formatWithPrettier = async (text, filePath) => {
343
206
  if (!hasPrettierRc) return text;
344
207
  try {
345
208
  const prettierModule = await import('prettier');
346
209
  const prettier = prettierModule.default || prettierModule;
347
210
  const resolved = (await prettier.resolveConfig(filePath)) || {};
348
- return prettier.format(text, {
349
- ...resolved,
350
- // 传入 filepath 以便 Prettier 自动推断 parser(ts/tsx/js/jsx)
351
- filepath: filePath,
352
- });
211
+ return prettier.format(text, { ...resolved, filepath: filePath });
353
212
  } catch (_error) {
354
- // Prettier 不可用或解析失败则跳过格式化
355
213
  return text;
356
214
  }
357
215
  };
358
216
 
359
217
  for (const file of files) {
360
218
  try {
361
- // 使用fileCodeMap中的代码,如果没有则读取文件
362
219
  const code = fileCodeMap.get(file) || fs.readFileSync(file, 'utf8');
363
-
364
- // 解析文件
365
220
  const ast = parseAST(code);
366
-
367
221
  let hasChanges = false;
368
222
 
369
- // 遍历并替换key
370
223
  traverse(ast, {
371
224
  CallExpression(path) {
372
- // 检查是否是 $i18n.get 调用
373
- if (
374
- path.node.callee.type === 'MemberExpression' &&
375
- path.node.callee.object &&
376
- path.node.callee.object.name === '$i18n' &&
377
- path.node.callee.property &&
378
- path.node.callee.property.name === 'get'
379
- ) {
225
+ if (isMatchingCallee(path.node, callExpr)) {
380
226
  const args = path.node.arguments;
381
- // 检查第一个参数是否是对象表达式
382
227
  if (args.length > 0 && args[0].type === 'ObjectExpression') {
383
- // 查找 id 属性
384
228
  const idProperty = args[0].properties.find(
385
229
  (prop) => prop.key && prop.key.name === 'id',
386
230
  );
387
231
 
388
232
  if (idProperty && idProperty.value) {
389
- // 尝试提取 key 值(支持多种格式)
390
233
  const oldKey = extractStringValue(idProperty.value);
391
234
 
392
235
  if (oldKey) {
393
236
  const newKey = keyMap[oldKey];
394
237
 
395
- // 如果在 keyMap 中找到对应的新 key,进行替换
396
238
  if (newKey && newKey !== oldKey) {
397
- // 只替换字符串字面量,其他类型保持不变
398
239
  if (idProperty.value.type === 'StringLiteral') {
399
240
  idProperty.value = t.stringLiteral(newKey);
400
241
  hasChanges = true;
@@ -411,7 +252,6 @@ const replaceKeysInFiles = async (path, keyMap, fileType, fileCodeMap) => {
411
252
  },
412
253
  });
413
254
 
414
- // 如果有改动,写回文件
415
255
  if (hasChanges) {
416
256
  const newCode = generateCode(ast, code);
417
257
  const formatted = await formatWithPrettier(newCode, file);
@@ -429,8 +269,8 @@ const replaceKeysInFiles = async (path, keyMap, fileType, fileCodeMap) => {
429
269
  };
430
270
 
431
271
  module.exports = {
272
+ ENGINES,
432
273
  processFiles,
433
274
  translateAndUpdateFiles,
434
- singleTranslateAndUpdateFiles,
435
275
  replaceKeysInFiles,
436
276
  };