@agentscope-ai/i18n 0.1.9 → 1.0.0

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
@@ -1,7 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  const { program } = require('commander');
4
- const { init, initMT, patch, patchMT, check, reverse, medusaRun } = require('./index');
4
+ const {
5
+ init,
6
+ initMT,
7
+ patch,
8
+ patchMT,
9
+ check,
10
+ reverse,
11
+ translate,
12
+ replaceOnly,
13
+ medusaRun,
14
+ } = require('./index');
5
15
  const path = require('path');
6
16
  const fs = require('fs-extra');
7
17
 
@@ -191,6 +201,54 @@ withCommonOptions(
191
201
  }
192
202
  });
193
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
+ });
251
+
194
252
  program
195
253
  .command('init-config')
196
254
  .description('Create i18n.config.js configuration file from template')
@@ -14,6 +14,7 @@ const {
14
14
  const { extractStringValue } = require('../utils/ast-utils');
15
15
  const { askQuestion } = require('../utils/cli-utils');
16
16
  const { generateMedusaExcel } = require('../utils/excel-utils');
17
+ const { generateMdsPayload } = require('../utils/mds-payload');
17
18
  const parseJSX = require('../parse-jsx');
18
19
 
19
20
  // 提取共用的文案处理函数
@@ -285,6 +286,16 @@ const singleTranslateAndUpdateFiles = async (
285
286
  console.log(' [取消] 已取消代码替换操作');
286
287
  }
287
288
 
289
+ // 生成 mds-payload.json(供 sync-to-mds 脚本使用)
290
+ if (medusa && medusa.appName) {
291
+ const newKeys = Object.keys(zhCN);
292
+ generateMdsPayload({
293
+ localesFilePath,
294
+ appName: medusa.appName,
295
+ filterKeys: newKeys,
296
+ });
297
+ }
298
+
288
299
  // 询问是否生成medusa excel文件
289
300
  if (medusa && medusa.appName && medusa.group && medusa.keyMap) {
290
301
  let shouldGenExcel = skipConfirm;
@@ -4,12 +4,15 @@ 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 { scanFiles } = require('../utils/file-utils');
7
+ const { scanFiles, resolveFiles, injectImportStatement } = require('../utils/file-utils');
8
8
  const {
9
9
  processFiles,
10
10
  translateAndUpdateFiles,
11
11
  singleTranslateAndUpdateFiles,
12
+ replaceKeysInFiles,
12
13
  } = require('./file-processor');
14
+ const { generateMdsPayload } = require('../utils/mds-payload');
15
+ const { singleKeyTranslate, singleTranslate } = require('../utils/translation-utils');
13
16
  const {
14
17
  extractI18nKey,
15
18
  chnRegExp,
@@ -1129,6 +1132,181 @@ const reverse = async function (sourcePath, params) {
1129
1132
  };
1130
1133
  };
1131
1134
 
1135
+ /**
1136
+ * translate: 提取中文 → AI翻译 → 写locale文件 → 生成mds-payload.json
1137
+ * 不会修改源代码,是三步工作流的第一步。
1138
+ */
1139
+ const translate = async function (sourcePath, params) {
1140
+ try {
1141
+ const {
1142
+ fileType = '.tsx,.js,.ts,.jsx',
1143
+ localesFilePath,
1144
+ keyPrefix,
1145
+ doNotTranslateFiles,
1146
+ dashScope,
1147
+ medusa,
1148
+ file: singleFile,
1149
+ } = params;
1150
+
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
+ }
1157
+
1158
+ console.log('');
1159
+ console.log('=== [Step 1/3] translate ===');
1160
+ console.log(' 提取中文 → AI翻译 → 生成locale文件和mds-payload');
1161
+ console.log('');
1162
+
1163
+ const files = resolveFiles(sourcePath, fileType, doNotTranslateFiles, singleFile);
1164
+ console.log(` # 扫描到 ${files.length} 个文件`);
1165
+
1166
+ const { i18n } = await processFiles(files, params);
1167
+
1168
+ if (Object.keys(i18n).length === 0) {
1169
+ console.log(' [完成] 没有找到需要提取的文案');
1170
+ return { newKeysCount: 0, newKeys: [], keyMap: {} };
1171
+ }
1172
+
1173
+ console.log(` [进度] AI正在翻译key...共${Object.keys(i18n).length}条`);
1174
+ const { keyMap, zhCN } = await singleKeyTranslate(i18n, dashScope);
1175
+
1176
+ const zhCNPath = `${localesFilePath}/zh-cn.json`;
1177
+ let existingZhCN = {};
1178
+ try {
1179
+ existingZhCN = fs.readJsonSync(zhCNPath);
1180
+ } catch {
1181
+ // first run
1182
+ }
1183
+ const finalZhCN = { ...existingZhCN, ...zhCN };
1184
+ fs.ensureDirSync(localesFilePath);
1185
+ fs.writeJsonSync(zhCNPath, finalZhCN, { spaces: 2 });
1186
+ console.log(` [完成] 更新 zh-cn.json (新增${Object.keys(zhCN).length}条)`);
1187
+
1188
+ console.log(` [进度] AI正在翻译多语言...`);
1189
+ const translatedResults = await singleTranslate(zhCN, dashScope);
1190
+
1191
+ for (const [language, translatedI18n] of Object.entries(translatedResults)) {
1192
+ const languagePath = `${localesFilePath}/${language}.json`;
1193
+ let existing = {};
1194
+ try {
1195
+ existing = fs.readJsonSync(languagePath);
1196
+ } catch {
1197
+ // first run
1198
+ }
1199
+ fs.writeJsonSync(languagePath, { ...existing, ...translatedI18n }, { spaces: 2 });
1200
+ console.log(` [完成] 更新 ${language}.json`);
1201
+ }
1202
+
1203
+ const newKeys = Object.keys(zhCN);
1204
+
1205
+ generateMdsPayload({
1206
+ localesFilePath,
1207
+ appName: medusa?.appName,
1208
+ filterKeys: newKeys,
1209
+ });
1210
+
1211
+ 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
+ );
1217
+ console.log(` [完成] 翻译状态已保存到 ${statePath}`);
1218
+
1219
+ if (medusa && medusa.appName && medusa.group && medusa.keyMap) {
1220
+ try {
1221
+ await generateMedusaExcel({ localesFilePath, medusa, newKeys });
1222
+ } catch (error) {
1223
+ console.error(' [警告] 生成medusa excel文件失败:', error.message);
1224
+ }
1225
+ }
1226
+
1227
+ console.log('');
1228
+ console.log(` ✅ translate 完成!新增 ${newKeys.length} 条翻译文案`);
1229
+ console.log(' 下一步: 运行 sync-to-mds 同步到美杜莎,然后运行 replace 替换代码');
1230
+ console.log('');
1231
+
1232
+ return { newKeysCount: newKeys.length, newKeys, keyMap };
1233
+ } catch (error) {
1234
+ console.error(' [错误] translate 失败:', error);
1235
+ throw error;
1236
+ }
1237
+ };
1238
+
1239
+ /**
1240
+ * replaceOnly: 将代码中的中文替换为 $i18n.get 调用。
1241
+ * 依赖 translate 步骤保存的 .i18n-state.json,是三步工作流的第三步。
1242
+ */
1243
+ const replaceOnly = async function (sourcePath, params) {
1244
+ try {
1245
+ const {
1246
+ fileType = '.tsx,.js,.ts,.jsx',
1247
+ localesFilePath,
1248
+ keyPrefix,
1249
+ doNotTranslateFiles,
1250
+ functionImportPath,
1251
+ i18nFilePath,
1252
+ file: singleFile,
1253
+ } = params;
1254
+
1255
+ if (!keyPrefix) {
1256
+ throw new Error('keyPrefix is required in params');
1257
+ }
1258
+
1259
+ console.log('');
1260
+ console.log('=== [Step 3/3] replace ===');
1261
+ console.log(' 将代码中的中文替换为 $i18n.get 调用');
1262
+ console.log('');
1263
+
1264
+ const statePath = path.resolve(localesFilePath, '.i18n-state.json');
1265
+ if (!fs.existsSync(statePath)) {
1266
+ throw new Error(`.i18n-state.json 不存在于 ${localesFilePath},请先运行 translate 命令`);
1267
+ }
1268
+ const state = fs.readJsonSync(statePath);
1269
+ const { keyMap } = state;
1270
+ console.log(` [进度] 已加载翻译状态 (${Object.keys(keyMap).length} 条keyMap)`);
1271
+
1272
+ const files = resolveFiles(sourcePath, fileType, doNotTranslateFiles, singleFile);
1273
+ console.log(` # 扫描到 ${files.length} 个文件`);
1274
+
1275
+ const { fileCodeMap, needImportFiles } = await processFiles(files, params);
1276
+
1277
+ console.log(` [进度] 开始替换文件中的key...`);
1278
+ await replaceKeysInFiles(sourcePath, keyMap, fileType, fileCodeMap);
1279
+
1280
+ if (needImportFiles.size > 0 && functionImportPath) {
1281
+ console.log(` [进度] 开始添加导入语句...`);
1282
+ needImportFiles.forEach((file) => {
1283
+ injectImportStatement(file, functionImportPath);
1284
+ });
1285
+ console.log(` [完成] 已为${needImportFiles.size}个文件添加导入语句`);
1286
+ }
1287
+
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
+ }
1297
+
1298
+ fs.removeSync(statePath);
1299
+ console.log(` [完成] 已清理临时状态文件`);
1300
+
1301
+ console.log('');
1302
+ console.log(' ✅ replace 完成!代码中的中文已替换为 $i18n.get 调用');
1303
+ console.log('');
1304
+ } catch (error) {
1305
+ console.error(' [错误] replace 失败:', error);
1306
+ throw error;
1307
+ }
1308
+ };
1309
+
1132
1310
  module.exports = {
1133
1311
  init,
1134
1312
  initMT,
@@ -1136,4 +1314,6 @@ module.exports = {
1136
1314
  patchMT,
1137
1315
  check,
1138
1316
  reverse,
1317
+ translate,
1318
+ replaceOnly,
1139
1319
  };
package/lib/index.js CHANGED
@@ -1,4 +1,13 @@
1
- const { init, initMT, patch, patchMT, check, reverse } = require('./core/translator');
1
+ const {
2
+ init,
3
+ initMT,
4
+ patch,
5
+ patchMT,
6
+ check,
7
+ reverse,
8
+ translate,
9
+ replaceOnly,
10
+ } = require('./core/translator');
2
11
  const { run: medusaRun } = require('./utils/medusa');
3
12
 
4
13
  module.exports = {
@@ -8,5 +17,7 @@ module.exports = {
8
17
  patchMT,
9
18
  check,
10
19
  reverse,
20
+ translate,
21
+ replaceOnly,
11
22
  medusaRun,
12
23
  };
@@ -271,11 +271,32 @@ const scanFiles = (path, fileType, doNotTranslateFiles = []) => {
271
271
  }
272
272
  };
273
273
 
274
+ const resolveFiles = (sourcePath, fileType, doNotTranslateFiles, singleFile) => {
275
+ if (singleFile) {
276
+ const nodePath = require('path');
277
+ const absFile = nodePath.isAbsolute(singleFile)
278
+ ? singleFile
279
+ : nodePath.resolve(process.cwd(), singleFile);
280
+
281
+ if (!fs.existsSync(absFile)) {
282
+ throw new Error(`文件不存在: ${absFile}`);
283
+ }
284
+ if (shouldIgnoreByComment(absFile)) {
285
+ console.log(` # 文件 ${absFile} 包含 @i18n-ignore,已跳过`);
286
+ return [];
287
+ }
288
+ console.log(` # 单文件模式: ${absFile}`);
289
+ return [absFile];
290
+ }
291
+ return scanFiles(sourcePath, fileType, doNotTranslateFiles);
292
+ };
293
+
274
294
  module.exports = {
275
295
  shouldIgnoreFile,
276
296
  shouldIgnoreByComment,
277
297
  injectImportStatement,
278
298
  scanFiles,
299
+ resolveFiles,
279
300
  shouldIgnoreLineByComment,
280
301
  shouldIgnoreBlockByComment,
281
302
  };
@@ -0,0 +1,86 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+
4
+ const LANG_CODE_TO_MCMS = {
5
+ 'en-us': 'en_US',
6
+ 'zh-cn': 'zh_CN',
7
+ 'ja-jp': 'ja_JP',
8
+ 'ko-kr': 'ko_KR',
9
+ 'zh-tw': 'zh_TW',
10
+ 'fr-fr': 'fr_FR',
11
+ 'de-de': 'de_DE',
12
+ 'es-es': 'es_ES',
13
+ 'pt-br': 'pt_BR',
14
+ 'ru-ru': 'ru_RU',
15
+ 'ar-sa': 'ar_SA',
16
+ 'th-th': 'th_TH',
17
+ 'vi-vn': 'vi_VN',
18
+ 'id-id': 'id_ID',
19
+ 'ms-my': 'ms_MY',
20
+ };
21
+
22
+ /**
23
+ * 从 locale JSON 文件生成 MDS payload(McmsWriteDTO 数组)。
24
+ * 只包含 filterKeys 中的 key(即本次新增/变更的 key)。
25
+ *
26
+ * @param {object} options
27
+ * @param {string} options.localesFilePath - locale 文件目录
28
+ * @param {string} options.appName - 美杜莎应用名
29
+ * @param {string[]} options.filterKeys - 要包含的 key 列表
30
+ * @param {object} [options.langCodeMap] - 自定义语言 code 映射 { 'en-us': 'en_US' }
31
+ * @returns {object[]} McmsWriteDTO 数组
32
+ */
33
+ const generateMdsPayload = ({ localesFilePath, appName, filterKeys, langCodeMap }) => {
34
+ if (!appName) {
35
+ console.log(' [跳过] 未配置 medusa.appName,跳过 mds-payload 生成');
36
+ return [];
37
+ }
38
+ if (!filterKeys || filterKeys.length === 0) {
39
+ return [];
40
+ }
41
+
42
+ const codeMap = { ...LANG_CODE_TO_MCMS, ...langCodeMap };
43
+
44
+ const localeDir = path.resolve(localesFilePath);
45
+ if (!fs.existsSync(localeDir)) {
46
+ console.log(` [跳过] locale 目录不存在: ${localeDir}`);
47
+ return [];
48
+ }
49
+
50
+ const localeFiles = fs
51
+ .readdirSync(localeDir)
52
+ .filter((f) => f.endsWith('.json') && f !== 'key.json');
53
+ const locales = {};
54
+ for (const file of localeFiles) {
55
+ const langCode = file.replace('.json', '');
56
+ const mcmsLang = codeMap[langCode] || langCode;
57
+ try {
58
+ locales[mcmsLang] = fs.readJsonSync(path.join(localeDir, file));
59
+ } catch {
60
+ // skip unreadable files
61
+ }
62
+ }
63
+
64
+ const keysSet = new Set(filterKeys);
65
+ const payload = [];
66
+
67
+ for (const key of keysSet) {
68
+ const i18n = {};
69
+ for (const [mcmsLang, data] of Object.entries(locales)) {
70
+ if (data[key] !== undefined) {
71
+ i18n[mcmsLang] = data[key];
72
+ }
73
+ }
74
+ if (Object.keys(i18n).length > 0) {
75
+ payload.push({ key, appName, i18n });
76
+ }
77
+ }
78
+
79
+ const outputPath = path.resolve(process.cwd(), 'mds-payload.json');
80
+ fs.writeJsonSync(outputPath, payload, { spaces: 2 });
81
+ console.log(` [完成] 已生成 mds-payload.json (${payload.length} 条), 路径: ${outputPath}`);
82
+
83
+ return payload;
84
+ };
85
+
86
+ module.exports = { generateMdsPayload };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentscope-ai/i18n",
3
- "version": "0.1.9",
3
+ "version": "1.0.0",
4
4
  "description": "A tool for translating Chinese content in frontend code repositories",
5
5
  "keywords": [
6
6
  "i18n",