@agentscope-ai/i18n 0.1.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 +545 -0
- package/lib/cli.js +224 -0
- package/lib/core/ast-processor.js +75 -0
- package/lib/core/file-processor.js +397 -0
- package/lib/core/translator.js +924 -0
- package/lib/i18n.ts +134 -0
- package/lib/index.js +12 -0
- package/lib/parse-jsx.js +380 -0
- package/lib/services/dashscope.js +118 -0
- package/lib/services/dashscopeMT.js +182 -0
- package/lib/utils/ast-utils.js +100 -0
- package/lib/utils/cli-utils.js +19 -0
- package/lib/utils/excel-utils.js +124 -0
- package/lib/utils/file-utils.js +280 -0
- package/lib/utils/medusa.js +228 -0
- package/lib/utils/translation-utils.js +572 -0
- package/package.json +66 -0
package/lib/cli.js
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { program } = require('commander');
|
|
4
|
+
const { init, initMT, patch, patchMT, check, reverse, medusaRun } = require('./index');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const fs = require('fs-extra');
|
|
7
|
+
|
|
8
|
+
// 读取配置文件
|
|
9
|
+
const getConfig = (options) => {
|
|
10
|
+
const configPath = options.config || path.resolve(process.cwd(), 'i18n.config.js');
|
|
11
|
+
if (!fs.existsSync(configPath)) {
|
|
12
|
+
throw new Error(`配置文件 ${configPath} 不存在`);
|
|
13
|
+
}
|
|
14
|
+
const config = require(configPath);
|
|
15
|
+
return config;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
program
|
|
19
|
+
.name('agentscope-ai-i18n')
|
|
20
|
+
.description('A tool for translating Chinese content in frontend code repositories')
|
|
21
|
+
.version('0.1.0');
|
|
22
|
+
|
|
23
|
+
program
|
|
24
|
+
.command('init-agent')
|
|
25
|
+
.description('Initialize translation for the project using Bailian Agent')
|
|
26
|
+
.option('-c, --config <path>', 'Path to config file')
|
|
27
|
+
.action(async (options) => {
|
|
28
|
+
try {
|
|
29
|
+
const config = getConfig(options);
|
|
30
|
+
await init(config.targetPath, config);
|
|
31
|
+
} catch (error) {
|
|
32
|
+
console.error('初始化失败:', error.message);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
program
|
|
38
|
+
.command('init-mt')
|
|
39
|
+
.description('Initialize translation using MT (Machine Translation) method')
|
|
40
|
+
.option('-c, --config <path>', 'Path to config file')
|
|
41
|
+
.action(async (options) => {
|
|
42
|
+
try {
|
|
43
|
+
const config = getConfig(options);
|
|
44
|
+
await initMT(config.targetPath, config);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.error('MT初始化失败:', error.message);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
program
|
|
52
|
+
.command('init')
|
|
53
|
+
.description('Initialize translation using MT (Machine Translation) method')
|
|
54
|
+
.option('-c, --config <path>', 'Path to config file')
|
|
55
|
+
.action(async (options) => {
|
|
56
|
+
try {
|
|
57
|
+
const config = getConfig(options);
|
|
58
|
+
await initMT(config.targetPath, config);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.error('MT初始化失败:', error.message);
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
program
|
|
66
|
+
.command('patch-agent')
|
|
67
|
+
.description('Update translations for new content using Bailian Agent')
|
|
68
|
+
.option('-c, --config <path>', 'Path to config file')
|
|
69
|
+
.action(async (options) => {
|
|
70
|
+
try {
|
|
71
|
+
const config = getConfig(options);
|
|
72
|
+
await patch(config.targetPath, config);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
console.error('补丁更新失败:', error.message);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
program
|
|
80
|
+
.command('patch-mt')
|
|
81
|
+
.description('Update translations for new content using MT (Machine Translation) method')
|
|
82
|
+
.option('-c, --config <path>', 'Path to config file')
|
|
83
|
+
.action(async (options) => {
|
|
84
|
+
try {
|
|
85
|
+
const config = getConfig(options);
|
|
86
|
+
await patchMT(config.targetPath, config);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
console.error('MT补丁更新失败:', error.message);
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
program
|
|
94
|
+
.command('patch')
|
|
95
|
+
.description('Update translations for new content using MT (Machine Translation) method')
|
|
96
|
+
.option('-c, --config <path>', 'Path to config file')
|
|
97
|
+
.action(async (options) => {
|
|
98
|
+
try {
|
|
99
|
+
const config = getConfig(options);
|
|
100
|
+
await patchMT(config.targetPath, config);
|
|
101
|
+
} catch (error) {
|
|
102
|
+
console.error('MT补丁更新失败:', error.message);
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
program
|
|
108
|
+
.command('check')
|
|
109
|
+
.description('Check translation status')
|
|
110
|
+
.option('-c, --config <path>', 'Path to config file')
|
|
111
|
+
.option('--auto-delete-unused', 'Automatically delete unused translation keys')
|
|
112
|
+
.action(async (options) => {
|
|
113
|
+
try {
|
|
114
|
+
const config = getConfig(options);
|
|
115
|
+
// 传递auto-delete-unused参数到check方法
|
|
116
|
+
config.autoDeleteUnused = options.autoDeleteUnused;
|
|
117
|
+
await check(config.targetPath, config);
|
|
118
|
+
} catch (error) {
|
|
119
|
+
console.error('检查失败:', error.message);
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
program
|
|
125
|
+
.command('reverse')
|
|
126
|
+
.description('Reverse internationalized code back to Chinese')
|
|
127
|
+
.option('-c, --config <path>', 'Path to config file')
|
|
128
|
+
.action(async (options) => {
|
|
129
|
+
try {
|
|
130
|
+
const config = getConfig(options);
|
|
131
|
+
await reverse(config.targetPath, config);
|
|
132
|
+
} catch (error) {
|
|
133
|
+
console.error('还原失败:', error.message);
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
program
|
|
139
|
+
.command('medusa')
|
|
140
|
+
.description('Pull translation files from Medusa platform')
|
|
141
|
+
.option('-c, --config <path>', 'Path to config file')
|
|
142
|
+
.option('-o, --output <dir>', 'Output directory (overrides config)')
|
|
143
|
+
.option('-a, --app <name>', 'Medusa app name (overrides config)')
|
|
144
|
+
.option('-t, --tag <name>', 'Medusa tag name (overrides config)')
|
|
145
|
+
.option('--type <type>', 'Package type (json/android/ios) (overrides config)')
|
|
146
|
+
.option('--no-merge', 'Do not merge with existing files, overwrite directly')
|
|
147
|
+
.action(async (options) => {
|
|
148
|
+
try {
|
|
149
|
+
console.log('🚀 Medusa 国际化文件拉取工具');
|
|
150
|
+
console.log('='.repeat(50));
|
|
151
|
+
|
|
152
|
+
// 尝试读取配置文件
|
|
153
|
+
let configFileSettings = {};
|
|
154
|
+
try {
|
|
155
|
+
const configFromFile = getConfig(options);
|
|
156
|
+
configFileSettings = configFromFile.medusa || {};
|
|
157
|
+
} catch (error) {
|
|
158
|
+
console.log('⚠️ 未找到配置文件,使用默认配置');
|
|
159
|
+
console.log(` ${error.message}`);
|
|
160
|
+
console.log('');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// 合并配置文件和命令行参数(命令行参数优先)
|
|
164
|
+
const config = {
|
|
165
|
+
medusa: {
|
|
166
|
+
appName: options.app || configFileSettings.appName || 'broadscope-bailian-frontend',
|
|
167
|
+
tag: options.tag || configFileSettings.tag || 'bailian-fe',
|
|
168
|
+
},
|
|
169
|
+
type: options.type || configFileSettings.type || 'json',
|
|
170
|
+
cwd: process.cwd(),
|
|
171
|
+
path: options.output || configFileSettings.outputPath || './locales',
|
|
172
|
+
mergeExisting: options.merge !== false && configFileSettings.mergeExisting !== false,
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
// 显示配置信息
|
|
176
|
+
console.log('📝 配置信息:');
|
|
177
|
+
console.log(
|
|
178
|
+
` 应用名称: ${config.medusa.appName} ${options.app ? '(命令行)' : configFileSettings.appName ? '(配置文件)' : '(默认)'}`,
|
|
179
|
+
);
|
|
180
|
+
console.log(
|
|
181
|
+
` 标签名称: ${config.medusa.tag} ${options.tag ? '(命令行)' : configFileSettings.tag ? '(配置文件)' : '(默认)'}`,
|
|
182
|
+
);
|
|
183
|
+
console.log(
|
|
184
|
+
` 包类型: ${config.type} ${options.type ? '(命令行)' : configFileSettings.type ? '(配置文件)' : '(默认)'}`,
|
|
185
|
+
);
|
|
186
|
+
console.log(
|
|
187
|
+
` 输出目录: ${path.resolve(config.path)} ${options.output ? '(命令行)' : configFileSettings.outputPath ? '(配置文件)' : '(默认)'}`,
|
|
188
|
+
);
|
|
189
|
+
console.log(
|
|
190
|
+
` 合并现有文件: ${config.mergeExisting ? '是' : '否'} ${options.merge === false ? '(命令行)' : configFileSettings.mergeExisting !== undefined ? '(配置文件)' : '(默认)'}`,
|
|
191
|
+
);
|
|
192
|
+
console.log('');
|
|
193
|
+
|
|
194
|
+
console.log('🔄 开始从 Medusa 平台拉取文件...');
|
|
195
|
+
await medusaRun(config);
|
|
196
|
+
console.log('');
|
|
197
|
+
console.log('✅ 拉取成功!');
|
|
198
|
+
console.log(`📁 文件已保存到: ${path.resolve(config.path)}`);
|
|
199
|
+
} catch (error) {
|
|
200
|
+
console.error('❌ Medusa 拉取失败:', error.message);
|
|
201
|
+
|
|
202
|
+
// 提供更详细的错误信息
|
|
203
|
+
if (error.response) {
|
|
204
|
+
console.error(' HTTP 状态码:', error.response.status);
|
|
205
|
+
if (error.response.status === 404) {
|
|
206
|
+
console.error(' 可能的原因:');
|
|
207
|
+
console.error(' - 应用名称不存在');
|
|
208
|
+
console.error(' - 标签名称不存在');
|
|
209
|
+
console.error(' - API 端点已更改');
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (error.code === 'ENOTFOUND' || error.code === 'ECONNREFUSED') {
|
|
214
|
+
console.error(' 网络连接错误,请检查:');
|
|
215
|
+
console.error(' - 网络连接是否正常');
|
|
216
|
+
console.error(' - 是否需要连接内网或VPN');
|
|
217
|
+
console.error(' - 防火墙设置');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
process.exit(1);
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
program.parse();
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
const babylon = require('@babel/parser');
|
|
2
|
+
const traverse = require('@babel/traverse').default;
|
|
3
|
+
const generate = require('@babel/generator').default;
|
|
4
|
+
const t = require('@babel/types');
|
|
5
|
+
|
|
6
|
+
// 检查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');
|
|
20
|
+
|
|
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
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const parseAST = (code) => {
|
|
35
|
+
return babylon.parse(code, {
|
|
36
|
+
sourceType: 'module',
|
|
37
|
+
allowImportExportEverywhere: true,
|
|
38
|
+
plugins: [
|
|
39
|
+
'jsx',
|
|
40
|
+
'objectRestSpread',
|
|
41
|
+
'classProperties',
|
|
42
|
+
'functionBind',
|
|
43
|
+
'typescript',
|
|
44
|
+
'decorators-legacy',
|
|
45
|
+
'dynamicImport',
|
|
46
|
+
'classPrivateProperties',
|
|
47
|
+
'classPrivateMethods',
|
|
48
|
+
'optionalChaining',
|
|
49
|
+
'nullishCoalescingOperator',
|
|
50
|
+
].filter(Boolean),
|
|
51
|
+
tokens: true,
|
|
52
|
+
errorRecovery: true,
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const generateCode = (ast, originalCode) => {
|
|
57
|
+
return generate(
|
|
58
|
+
ast,
|
|
59
|
+
{
|
|
60
|
+
jsescOption: {
|
|
61
|
+
minimal: true,
|
|
62
|
+
quotes: 'single',
|
|
63
|
+
},
|
|
64
|
+
retainLines: true,
|
|
65
|
+
compact: false,
|
|
66
|
+
},
|
|
67
|
+
originalCode,
|
|
68
|
+
).code;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
module.exports = {
|
|
72
|
+
checkI18nCall,
|
|
73
|
+
parseAST,
|
|
74
|
+
generateCode,
|
|
75
|
+
};
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
const find = require('find');
|
|
2
|
+
const fs = require('fs-extra');
|
|
3
|
+
const traverse = require('@babel/traverse').default;
|
|
4
|
+
const t = require('@babel/types');
|
|
5
|
+
const { parseAST, generateCode } = require('./ast-processor');
|
|
6
|
+
const nodePath = require('path');
|
|
7
|
+
const { injectImportStatement } = require('../utils/file-utils');
|
|
8
|
+
const {
|
|
9
|
+
batchKeyTranslate,
|
|
10
|
+
batchTranslate,
|
|
11
|
+
singleKeyTranslate,
|
|
12
|
+
singleTranslate,
|
|
13
|
+
} = require('../utils/translation-utils');
|
|
14
|
+
const { extractStringValue } = require('../utils/ast-utils');
|
|
15
|
+
const { askQuestion } = require('../utils/cli-utils');
|
|
16
|
+
const { generateMedusaExcel } = require('../utils/excel-utils');
|
|
17
|
+
const parseJSX = require('../parse-jsx');
|
|
18
|
+
|
|
19
|
+
// 提取共用的文案处理函数
|
|
20
|
+
const processFiles = async (files, params) => {
|
|
21
|
+
console.log(` [开始] 提取和替换文件中的中文内容...`);
|
|
22
|
+
|
|
23
|
+
const { keyPrefix, functionImportPath } = params;
|
|
24
|
+
const fileCodeMap = new Map(); // 存储文件路径和替换后的代码的映射
|
|
25
|
+
const needImportFiles = new Set(); // 存储需要添加导入语句的文件
|
|
26
|
+
|
|
27
|
+
const promises = files.map((file) => {
|
|
28
|
+
if (/(\.js|\.ts|\.tsx|\.jsx)$/i.test(file)) {
|
|
29
|
+
return parseJSX(file, { ...params, keyPrefix }).then(({ i18n, newCode }) => {
|
|
30
|
+
if (Object.keys(i18n).length > 0 && functionImportPath) {
|
|
31
|
+
needImportFiles.add(file);
|
|
32
|
+
}
|
|
33
|
+
if (newCode) {
|
|
34
|
+
fileCodeMap.set(file, newCode);
|
|
35
|
+
}
|
|
36
|
+
return i18n;
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return Promise.resolve({});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const results = await Promise.all(promises);
|
|
43
|
+
const i18n = {};
|
|
44
|
+
results.forEach((langObj) => {
|
|
45
|
+
Object.keys(langObj).forEach((lang) => {
|
|
46
|
+
i18n[lang] = langObj[lang];
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
return { i18n, fileCodeMap, needImportFiles };
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// 提取共用的翻译和文件更新函数
|
|
54
|
+
const translateAndUpdateFiles = async (
|
|
55
|
+
i18n,
|
|
56
|
+
path,
|
|
57
|
+
localesFilePath,
|
|
58
|
+
fileType,
|
|
59
|
+
fileCodeMap,
|
|
60
|
+
needImportFiles,
|
|
61
|
+
functionImportPath,
|
|
62
|
+
isUpdate = false,
|
|
63
|
+
dashScope,
|
|
64
|
+
medusa,
|
|
65
|
+
) => {
|
|
66
|
+
if (Object.keys(i18n).length === 0) {
|
|
67
|
+
console.log(' [提示] 没有找到需要提取的文案');
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 生成临时的key.json文件
|
|
72
|
+
const keyJsonPath = `${localesFilePath}/key.json`;
|
|
73
|
+
fs.ensureDirSync(localesFilePath);
|
|
74
|
+
fs.writeJsonSync(keyJsonPath, i18n, { spaces: 2 });
|
|
75
|
+
console.log(` [进度] 生成临时key.json文件,共${Object.keys(i18n).length}条文案`);
|
|
76
|
+
|
|
77
|
+
// 调用key翻译
|
|
78
|
+
console.log(` [进度] AI正在翻译key...请稍后`);
|
|
79
|
+
const { keyMap, zhCN } = await batchKeyTranslate(i18n, dashScope);
|
|
80
|
+
|
|
81
|
+
// 更新zh-cn.json
|
|
82
|
+
const zhCNPath = `${localesFilePath}/zh-cn.json`;
|
|
83
|
+
let existingZhCN = {};
|
|
84
|
+
if (isUpdate) {
|
|
85
|
+
try {
|
|
86
|
+
existingZhCN = fs.readJsonSync(zhCNPath);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
console.log(' [提示] 未找到现有的zh-cn.json文件,将创建新文件');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const finalZhCN = isUpdate ? { ...existingZhCN, ...zhCN } : zhCN;
|
|
92
|
+
fs.writeJsonSync(zhCNPath, finalZhCN, { spaces: 2 });
|
|
93
|
+
console.log(` [完成] ${isUpdate ? '更新' : '生成'}zh-cn.json文件成功`);
|
|
94
|
+
|
|
95
|
+
// 调用value翻译生成多语言文件
|
|
96
|
+
console.log(` [进度] AI正在翻译value...请稍后`);
|
|
97
|
+
const translatedResults = await batchTranslate(zhCN, dashScope);
|
|
98
|
+
|
|
99
|
+
// 为每种语言生成对应的翻译文件
|
|
100
|
+
const generatedFiles = [];
|
|
101
|
+
for (const [language, translatedI18n] of Object.entries(translatedResults)) {
|
|
102
|
+
const languagePath = `${localesFilePath}/${language}.json`;
|
|
103
|
+
let existingLanguage = {};
|
|
104
|
+
|
|
105
|
+
if (isUpdate) {
|
|
106
|
+
try {
|
|
107
|
+
existingLanguage = fs.readJsonSync(languagePath);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
console.log(` [提示] 未找到现有的${language}.json文件,将创建新文件`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const finalLanguage = isUpdate ? { ...existingLanguage, ...translatedI18n } : translatedI18n;
|
|
114
|
+
fs.writeJsonSync(languagePath, finalLanguage, { spaces: 2 });
|
|
115
|
+
console.log(` [完成] ${isUpdate ? '更新' : '生成'}${language}.json文件成功`);
|
|
116
|
+
generatedFiles.push(languagePath);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// 提示用户检查翻译文件
|
|
120
|
+
console.log('\n=== 翻译文件已生成 ===');
|
|
121
|
+
console.log('请检查以下文件:');
|
|
122
|
+
console.log(`1. ${zhCNPath}`);
|
|
123
|
+
generatedFiles.forEach((file, index) => {
|
|
124
|
+
console.log(`${index + 2}. ${file}`);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const answer = await askQuestion('翻译文件检查无误后,是否继续替换代码中的文案?(y/n): ');
|
|
128
|
+
if (answer === 'y' || answer === 'yes') {
|
|
129
|
+
// 替换文件中的key
|
|
130
|
+
console.log(` [进度] 开始替换文件中的key...`);
|
|
131
|
+
await replaceKeysInFiles(path, keyMap, fileType, fileCodeMap);
|
|
132
|
+
|
|
133
|
+
// 添加导入语句
|
|
134
|
+
if (needImportFiles.size > 0) {
|
|
135
|
+
console.log(` [进度] 开始添加导入语句...`);
|
|
136
|
+
needImportFiles.forEach((file) => {
|
|
137
|
+
injectImportStatement(file, functionImportPath);
|
|
138
|
+
});
|
|
139
|
+
console.log(` [完成] 已为${needImportFiles.size}个文件添加导入语句`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
console.log(' [完成] 代码替换完成!');
|
|
143
|
+
} else {
|
|
144
|
+
console.log(' [取消] 已取消代码替换操作');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// 询问是否生成medusa excel文件
|
|
148
|
+
if (medusa && medusa.appName && medusa.group && medusa.keyMap) {
|
|
149
|
+
const excelAnswer = await askQuestion('是否需要生成medusa excel文件?(y/n): ');
|
|
150
|
+
if (excelAnswer === 'y' || excelAnswer === 'yes') {
|
|
151
|
+
try {
|
|
152
|
+
const newKeys = Object.keys(zhCN); // 新增的key
|
|
153
|
+
await generateMedusaExcel({
|
|
154
|
+
localesFilePath,
|
|
155
|
+
medusa,
|
|
156
|
+
newKeys,
|
|
157
|
+
});
|
|
158
|
+
} catch (error) {
|
|
159
|
+
console.error(' [错误] 生成medusa excel文件失败:', error.message);
|
|
160
|
+
}
|
|
161
|
+
} else {
|
|
162
|
+
console.log(' [取消] 已取消生成medusa excel文件');
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
fs.removeSync(keyJsonPath);
|
|
167
|
+
console.log(` [提示] key.json文件已保留用于调试`);
|
|
168
|
+
|
|
169
|
+
return Object.keys(zhCN).length;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
// 单条翻译和文件更新函数(使用MT方式)
|
|
173
|
+
const singleTranslateAndUpdateFiles = async (
|
|
174
|
+
i18n,
|
|
175
|
+
path,
|
|
176
|
+
localesFilePath,
|
|
177
|
+
fileType,
|
|
178
|
+
fileCodeMap,
|
|
179
|
+
needImportFiles,
|
|
180
|
+
functionImportPath,
|
|
181
|
+
isUpdate = false,
|
|
182
|
+
dashScope,
|
|
183
|
+
medusa,
|
|
184
|
+
) => {
|
|
185
|
+
if (Object.keys(i18n).length === 0) {
|
|
186
|
+
console.log(' [提示] 没有找到需要提取的文案');
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// 生成临时的key.json文件
|
|
191
|
+
const keyJsonPath = `${localesFilePath}/key.json`;
|
|
192
|
+
fs.ensureDirSync(localesFilePath);
|
|
193
|
+
fs.writeJsonSync(keyJsonPath, i18n, { spaces: 2 });
|
|
194
|
+
console.log(` [进度] 生成临时key.json文件,共${Object.keys(i18n).length}条文案`);
|
|
195
|
+
|
|
196
|
+
// 调用单条key翻译
|
|
197
|
+
console.log(` [进度] 使用MT方式逐条翻译key...请稍后`);
|
|
198
|
+
const { keyMap, zhCN } = await singleKeyTranslate(i18n, dashScope);
|
|
199
|
+
|
|
200
|
+
// 更新zh-cn.json
|
|
201
|
+
const zhCNPath = `${localesFilePath}/zh-cn.json`;
|
|
202
|
+
let existingZhCN = {};
|
|
203
|
+
if (isUpdate) {
|
|
204
|
+
try {
|
|
205
|
+
existingZhCN = fs.readJsonSync(zhCNPath);
|
|
206
|
+
} catch (error) {
|
|
207
|
+
console.log(' [提示] 未找到现有的zh-cn.json文件,将创建新文件');
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
const finalZhCN = isUpdate ? { ...existingZhCN, ...zhCN } : zhCN;
|
|
211
|
+
fs.writeJsonSync(zhCNPath, finalZhCN, { spaces: 2 });
|
|
212
|
+
console.log(` [完成] ${isUpdate ? '更新' : '生成'}zh-cn.json文件成功`);
|
|
213
|
+
|
|
214
|
+
// 调用单条value翻译生成多语言文件
|
|
215
|
+
console.log(` [进度] 使用MT方式逐条翻译value...请稍后`);
|
|
216
|
+
const translatedResults = await singleTranslate(zhCN, dashScope);
|
|
217
|
+
|
|
218
|
+
// 为每种语言生成对应的翻译文件
|
|
219
|
+
const generatedFiles = [];
|
|
220
|
+
for (const [language, translatedI18n] of Object.entries(translatedResults)) {
|
|
221
|
+
const languagePath = `${localesFilePath}/${language}.json`;
|
|
222
|
+
let existingLanguage = {};
|
|
223
|
+
|
|
224
|
+
if (isUpdate) {
|
|
225
|
+
try {
|
|
226
|
+
existingLanguage = fs.readJsonSync(languagePath);
|
|
227
|
+
} catch (error) {
|
|
228
|
+
console.log(` [提示] 未找到现有的${language}.json文件,将创建新文件`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const finalLanguage = isUpdate ? { ...existingLanguage, ...translatedI18n } : translatedI18n;
|
|
233
|
+
fs.writeJsonSync(languagePath, finalLanguage, { spaces: 2 });
|
|
234
|
+
console.log(` [完成] ${isUpdate ? '更新' : '生成'}${language}.json文件成功`);
|
|
235
|
+
generatedFiles.push(languagePath);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// 提示用户检查翻译文件
|
|
239
|
+
console.log('\n=== 翻译文件已生成(MT方式) ===');
|
|
240
|
+
console.log('请检查以下文件:');
|
|
241
|
+
console.log(`1. ${zhCNPath}`);
|
|
242
|
+
generatedFiles.forEach((file, index) => {
|
|
243
|
+
console.log(`${index + 2}. ${file}`);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
const answer = await askQuestion('翻译文件检查无误后,是否继续替换代码中的文案?(y/n): ');
|
|
247
|
+
if (answer === 'y' || answer === 'yes') {
|
|
248
|
+
// 替换文件中的key
|
|
249
|
+
console.log(` [进度] 开始替换文件中的key...`);
|
|
250
|
+
await replaceKeysInFiles(path, keyMap, fileType, fileCodeMap);
|
|
251
|
+
|
|
252
|
+
// 添加导入语句
|
|
253
|
+
if (needImportFiles.size > 0) {
|
|
254
|
+
console.log(` [进度] 开始添加导入语句...`);
|
|
255
|
+
needImportFiles.forEach((file) => {
|
|
256
|
+
injectImportStatement(file, functionImportPath);
|
|
257
|
+
});
|
|
258
|
+
console.log(` [完成] 已为${needImportFiles.size}个文件添加导入语句`);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
console.log(' [完成] 代码替换完成!');
|
|
262
|
+
} else {
|
|
263
|
+
console.log(' [取消] 已取消代码替换操作');
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// 询问是否生成medusa excel文件
|
|
267
|
+
if (medusa && medusa.appName && medusa.group && medusa.keyMap) {
|
|
268
|
+
const excelAnswer = await askQuestion('是否需要生成medusa excel文件?(y/n): ');
|
|
269
|
+
if (excelAnswer === 'y' || excelAnswer === 'yes') {
|
|
270
|
+
try {
|
|
271
|
+
const newKeys = Object.keys(zhCN); // 新增的key
|
|
272
|
+
await generateMedusaExcel({
|
|
273
|
+
localesFilePath,
|
|
274
|
+
medusa,
|
|
275
|
+
newKeys,
|
|
276
|
+
});
|
|
277
|
+
} catch (error) {
|
|
278
|
+
console.error(' [错误] 生成medusa excel文件失败:', error.message);
|
|
279
|
+
}
|
|
280
|
+
} else {
|
|
281
|
+
console.log(' [取消] 已取消生成medusa excel文件');
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
fs.removeSync(keyJsonPath);
|
|
286
|
+
console.log(` [提示] key.json文件已删除`);
|
|
287
|
+
|
|
288
|
+
return Object.keys(zhCN).length;
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
// 修改replaceKeysInFiles函数以支持fileCodeMap
|
|
292
|
+
const replaceKeysInFiles = async (path, keyMap, fileType, fileCodeMap) => {
|
|
293
|
+
const fileTypes = fileType.split(',').map((s) => s.trim());
|
|
294
|
+
const re = new RegExp(`(${fileTypes.map((ft) => ft.replace('.', '\\.')).join('|')})$`);
|
|
295
|
+
const files = find.fileSync(re, path);
|
|
296
|
+
|
|
297
|
+
console.log(` [进度] 开始替换${files.length}个文件中的key...`);
|
|
298
|
+
|
|
299
|
+
// 仅当目标项目根目录存在 .prettierrc 时才尝试格式化
|
|
300
|
+
const hasPrettierRc = fs.existsSync(nodePath.join(path, '.prettierrc'));
|
|
301
|
+
|
|
302
|
+
// 动态导入 Prettier 并应用项目配置(如果可用)
|
|
303
|
+
const formatWithPrettier = async (text, filePath) => {
|
|
304
|
+
if (!hasPrettierRc) return text;
|
|
305
|
+
try {
|
|
306
|
+
const prettierModule = await import('prettier');
|
|
307
|
+
const prettier = prettierModule.default || prettierModule;
|
|
308
|
+
const resolved = (await prettier.resolveConfig(filePath)) || {};
|
|
309
|
+
return prettier.format(text, {
|
|
310
|
+
...resolved,
|
|
311
|
+
// 传入 filepath 以便 Prettier 自动推断 parser(ts/tsx/js/jsx)
|
|
312
|
+
filepath: filePath,
|
|
313
|
+
});
|
|
314
|
+
} catch (_error) {
|
|
315
|
+
// Prettier 不可用或解析失败则跳过格式化
|
|
316
|
+
return text;
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
for (const file of files) {
|
|
321
|
+
try {
|
|
322
|
+
// 使用fileCodeMap中的代码,如果没有则读取文件
|
|
323
|
+
const code = fileCodeMap.get(file) || fs.readFileSync(file, 'utf8');
|
|
324
|
+
|
|
325
|
+
// 解析文件
|
|
326
|
+
const ast = parseAST(code);
|
|
327
|
+
|
|
328
|
+
let hasChanges = false;
|
|
329
|
+
|
|
330
|
+
// 遍历并替换key
|
|
331
|
+
traverse(ast, {
|
|
332
|
+
CallExpression(path) {
|
|
333
|
+
// 检查是否是 $i18n.get 调用
|
|
334
|
+
if (
|
|
335
|
+
path.node.callee.type === 'MemberExpression' &&
|
|
336
|
+
path.node.callee.object &&
|
|
337
|
+
path.node.callee.object.name === '$i18n' &&
|
|
338
|
+
path.node.callee.property &&
|
|
339
|
+
path.node.callee.property.name === 'get'
|
|
340
|
+
) {
|
|
341
|
+
const args = path.node.arguments;
|
|
342
|
+
// 检查第一个参数是否是对象表达式
|
|
343
|
+
if (args.length > 0 && args[0].type === 'ObjectExpression') {
|
|
344
|
+
// 查找 id 属性
|
|
345
|
+
const idProperty = args[0].properties.find(
|
|
346
|
+
(prop) => prop.key && prop.key.name === 'id',
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
if (idProperty && idProperty.value) {
|
|
350
|
+
// 尝试提取 key 值(支持多种格式)
|
|
351
|
+
const oldKey = extractStringValue(idProperty.value);
|
|
352
|
+
|
|
353
|
+
if (oldKey) {
|
|
354
|
+
const newKey = keyMap[oldKey];
|
|
355
|
+
|
|
356
|
+
// 如果在 keyMap 中找到对应的新 key,进行替换
|
|
357
|
+
if (newKey && newKey !== oldKey) {
|
|
358
|
+
// 只替换字符串字面量,其他类型保持不变
|
|
359
|
+
if (idProperty.value.type === 'StringLiteral') {
|
|
360
|
+
idProperty.value = t.stringLiteral(newKey);
|
|
361
|
+
hasChanges = true;
|
|
362
|
+
} else {
|
|
363
|
+
console.log(
|
|
364
|
+
` [警告] 无法替换非字符串字面量的 key: ${oldKey} (${idProperty.value.type})`,
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
},
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
// 如果有改动,写回文件
|
|
376
|
+
if (hasChanges) {
|
|
377
|
+
const newCode = generateCode(ast, code);
|
|
378
|
+
const formatted = await formatWithPrettier(newCode, file);
|
|
379
|
+
fs.writeFileSync(file, formatted);
|
|
380
|
+
}
|
|
381
|
+
} catch (error) {
|
|
382
|
+
console.error(` [错误] 更新文件失败: ${file}`, error);
|
|
383
|
+
if (error.loc) {
|
|
384
|
+
console.error(` [错误] 错误位置: 第${error.loc.line}行, 第${error.loc.column}列`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
console.log(' [完成] 完成所有key替换');
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
module.exports = {
|
|
393
|
+
processFiles,
|
|
394
|
+
translateAndUpdateFiles,
|
|
395
|
+
singleTranslateAndUpdateFiles,
|
|
396
|
+
replaceKeysInFiles,
|
|
397
|
+
};
|