@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.
- package/README.md +57 -135
- package/i18n.config.example.js +106 -0
- package/lib/cli.js +112 -221
- package/lib/core/ast-processor.js +17 -21
- package/lib/core/checker.js +544 -0
- package/lib/core/file-processor.js +44 -204
- package/lib/core/translator.js +95 -936
- package/lib/index.js +2 -8
- package/lib/parse-jsx.js +33 -23
- package/lib/utils/ast-utils.js +48 -25
- package/lib/utils/file-utils.js +50 -11
- package/package.json +10 -5
- package/skills/i18n-helper/SKILL.md +186 -82
|
@@ -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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
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
|
-
|
|
213
|
-
|
|
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
|
-
|
|
231
|
-
|
|
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(
|
|
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);
|
|
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
|
-
|
|
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
|
-
|
|
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
|
};
|