@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/i18n.ts
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// Interface for translation variables
|
|
2
|
+
interface I18nVariables {
|
|
3
|
+
[key: string]: any;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
// Interface for multi-language mapping
|
|
7
|
+
interface IMultiLangMap {
|
|
8
|
+
[key: string]: { [key: string]: string };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Supported language types
|
|
12
|
+
type TLanguage = 'en' | 'zh' | string;
|
|
13
|
+
|
|
14
|
+
type I18nOptions = {
|
|
15
|
+
id: string;
|
|
16
|
+
dm: string;
|
|
17
|
+
} | string;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Internationalization (i18n) utility class
|
|
21
|
+
* Handles language management and string translations
|
|
22
|
+
*/
|
|
23
|
+
class I18N {
|
|
24
|
+
multiLangMap: IMultiLangMap;
|
|
25
|
+
language: TLanguage;
|
|
26
|
+
private translations: Record<string, string> = {};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Initialize I18N instance with multi-language map
|
|
30
|
+
* @param config Configuration object containing multiLangMap
|
|
31
|
+
*/
|
|
32
|
+
constructor({ multiLangMap }: { multiLangMap: IMultiLangMap }) {
|
|
33
|
+
this.multiLangMap = multiLangMap;
|
|
34
|
+
this.language = localStorage.getItem('data-prefers-language') || 'en';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Convert value to string with fallback
|
|
39
|
+
* @param value Input value to convert
|
|
40
|
+
* @param defaultValue Fallback value if conversion fails
|
|
41
|
+
* @returns String representation of the value
|
|
42
|
+
*/
|
|
43
|
+
toString = (value: any, defaultValue: string) => {
|
|
44
|
+
if (value === undefined || value === null) {
|
|
45
|
+
return defaultValue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (typeof value === 'string') {
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (value?.toString && typeof value.toString === 'function') {
|
|
53
|
+
return value.toString();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return value || defaultValue;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Set current application language
|
|
61
|
+
* @param language Language code to set ('en' or 'zh')
|
|
62
|
+
*/
|
|
63
|
+
setCurrentLanguage(language: TLanguage) {
|
|
64
|
+
localStorage.setItem('data-prefers-language', language);
|
|
65
|
+
this.language = language;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Get current application language
|
|
70
|
+
* @returns Current language code
|
|
71
|
+
*/
|
|
72
|
+
getCurrentLanguage() {
|
|
73
|
+
return this.language;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Format translation string with variables
|
|
78
|
+
* @param idObj Translation ID or object containing ID and default message
|
|
79
|
+
* @param variables Variables to interpolate into the translation
|
|
80
|
+
* @returns Formatted translation string
|
|
81
|
+
*/
|
|
82
|
+
format = (
|
|
83
|
+
idObj: string | { id: string; dm: string },
|
|
84
|
+
variables: I18nVariables,
|
|
85
|
+
) => {
|
|
86
|
+
const { multiLangMap, language } = this;
|
|
87
|
+
const langMap = multiLangMap[language] || {};
|
|
88
|
+
let template = '';
|
|
89
|
+
|
|
90
|
+
if (typeof idObj === 'string') {
|
|
91
|
+
template = langMap[idObj as keyof typeof langMap] || idObj;
|
|
92
|
+
} else {
|
|
93
|
+
const { id, dm } = idObj;
|
|
94
|
+
template = langMap[id as keyof typeof langMap] || dm || id;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return template.replace(/\{(\w+)\}/g, (_match: string, key: string) =>
|
|
98
|
+
this.toString(variables[key], _match),
|
|
99
|
+
);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Get translation for given ID
|
|
104
|
+
* @param id Translation ID or object containing ID and default message
|
|
105
|
+
* @param variables Optional variables to interpolate
|
|
106
|
+
* @returns Translated string
|
|
107
|
+
*/
|
|
108
|
+
get(options: I18nOptions, variables?: I18nVariables): string {
|
|
109
|
+
const { id, dm } = options;
|
|
110
|
+
let message = this.translations[id] || dm;
|
|
111
|
+
|
|
112
|
+
if (variables) {
|
|
113
|
+
Object.entries(variables).forEach(([key, value]) => {
|
|
114
|
+
message = message.replace(new RegExp(`{${key}}`, 'g'), String(value));
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return message;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
setTranslations(translations: Record<string, string>): void {
|
|
122
|
+
this.translations = translations;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
import enLangMap from '../locales/en-us.json';
|
|
127
|
+
import cnLangMap from '../locales/zh-cn.json';
|
|
128
|
+
|
|
129
|
+
const multiLangMap = {
|
|
130
|
+
zh: cnLangMap,
|
|
131
|
+
en: enLangMap,
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export default new I18N({ multiLangMap });
|
package/lib/index.js
ADDED
package/lib/parse-jsx.js
ADDED
|
@@ -0,0 +1,380 @@
|
|
|
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
|
+
const fs = require('fs-extra');
|
|
6
|
+
const { shouldIgnoreLineByComment, shouldIgnoreBlockByComment } = require('./utils/file-utils');
|
|
7
|
+
|
|
8
|
+
// 中文识别正则表达式
|
|
9
|
+
const chnRegExp = new RegExp(/[^\x00-\xff]/);
|
|
10
|
+
|
|
11
|
+
// 检查是否只包含中文标点符号
|
|
12
|
+
const onlyContainsChinesePunctuation = (text) => {
|
|
13
|
+
// 中文标点符号正则,包括全角标点符号和常见符号
|
|
14
|
+
const chinesePunctuationRegex =
|
|
15
|
+
/[,。!?;:""''()【】《》|、…—·~~@#$%^&*()_+\-={}\[\]|\\:;"'<>,.?\/]/g;
|
|
16
|
+
// 移除所有中文标点符号
|
|
17
|
+
const textWithoutPunctuation = text.replace(chinesePunctuationRegex, '');
|
|
18
|
+
// 如果移除标点后为空,说明只包含标点符号
|
|
19
|
+
return textWithoutPunctuation.trim() === '';
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const onlyContainsEmoji = (text) => {
|
|
23
|
+
// Emoji 正则表达式,包含常见的emoji Unicode范围
|
|
24
|
+
const emojiRegex =
|
|
25
|
+
/[\u{1F600}-\u{1F64F}]|[\u{1F300}-\u{1F5FF}]|[\u{1F680}-\u{1F6FF}]|[\u{1F1E0}-\u{1F1FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]|[\u{1F900}-\u{1F9FF}]|[\u{1F018}-\u{1F270}]|[\u{238C}-\u{2454}]|[\u{20D0}-\u{20FF}]|[\u{FE00}-\u{FE0F}]|[\u{1F200}-\u{1F2FF}]|[\u{1F700}-\u{1F77F}]|[\u{1F780}-\u{1F7FF}]|[\u{1F800}-\u{1F8FF}]|[\u{2300}-\u{23FF}]|[\u{2B00}-\u{2BFF}]|[\u{3030}]|[\u{303D}]|[\u{3297}]|[\u{3299}]/gu;
|
|
26
|
+
|
|
27
|
+
// 移除所有空白字符
|
|
28
|
+
const trimmedText = text.trim();
|
|
29
|
+
|
|
30
|
+
// 如果文本为空,返回false
|
|
31
|
+
if (!trimmedText) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 移除所有emoji字符
|
|
36
|
+
const textWithoutEmoji = trimmedText.replace(emojiRegex, '');
|
|
37
|
+
|
|
38
|
+
// 如果移除emoji后为空,说明只包含emoji
|
|
39
|
+
return textWithoutEmoji === '';
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// 检查节点是否是i18n调用
|
|
43
|
+
function isI18nCall(node) {
|
|
44
|
+
return (
|
|
45
|
+
node &&
|
|
46
|
+
node.type === 'CallExpression' &&
|
|
47
|
+
node.callee &&
|
|
48
|
+
node.callee.type === 'MemberExpression' &&
|
|
49
|
+
node.callee.object &&
|
|
50
|
+
node.callee.object.name === '$i18n' &&
|
|
51
|
+
node.callee.property &&
|
|
52
|
+
node.callee.property.name === 'get'
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 检测是否在 console 方法中
|
|
57
|
+
function isInConsoleCall(path) {
|
|
58
|
+
const consoleMethods = [
|
|
59
|
+
'log',
|
|
60
|
+
'warn',
|
|
61
|
+
'error',
|
|
62
|
+
'info',
|
|
63
|
+
'debug',
|
|
64
|
+
'trace',
|
|
65
|
+
'group',
|
|
66
|
+
'groupEnd',
|
|
67
|
+
'groupCollapsed',
|
|
68
|
+
'assert',
|
|
69
|
+
'table',
|
|
70
|
+
'time',
|
|
71
|
+
'timeEnd',
|
|
72
|
+
'count',
|
|
73
|
+
'countReset',
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
return path.findParent((p) => {
|
|
77
|
+
return (
|
|
78
|
+
p.isCallExpression() &&
|
|
79
|
+
p.node.callee &&
|
|
80
|
+
p.node.callee.type === 'MemberExpression' &&
|
|
81
|
+
p.node.callee.object &&
|
|
82
|
+
p.node.callee.object.name === 'console' &&
|
|
83
|
+
p.node.callee.property &&
|
|
84
|
+
consoleMethods.includes(p.node.callee.property.name)
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// 生成i18n调用节点
|
|
90
|
+
const createI18nCallNode = (i18nKey, defaultMessage, variables = []) => {
|
|
91
|
+
const baseNode = t.callExpression(
|
|
92
|
+
t.memberExpression(t.identifier('$i18n'), t.identifier('get')),
|
|
93
|
+
[
|
|
94
|
+
t.objectExpression([
|
|
95
|
+
t.objectProperty(t.identifier('id'), t.stringLiteral(i18nKey)),
|
|
96
|
+
t.objectProperty(t.identifier('dm'), t.stringLiteral(defaultMessage)),
|
|
97
|
+
]),
|
|
98
|
+
],
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
if (variables.length > 0) {
|
|
102
|
+
baseNode.arguments.push(t.objectExpression(variables));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return baseNode;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// 处理复杂表达式,生成代码字符串
|
|
109
|
+
const processExpression = (exp) => {
|
|
110
|
+
try {
|
|
111
|
+
const generatedCode = generate(exp, {
|
|
112
|
+
compact: true,
|
|
113
|
+
jsescOption: { minimal: true },
|
|
114
|
+
}).code;
|
|
115
|
+
return generatedCode.replace(/;$/, '');
|
|
116
|
+
} catch (error) {
|
|
117
|
+
console.error(' [错误] 处理表达式时出错:', error);
|
|
118
|
+
return '';
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// 处理模板字符串,提取变量和生成i18n调用
|
|
123
|
+
const processTemplateLiteral = (path, getOrCreateKey) => {
|
|
124
|
+
const val = [];
|
|
125
|
+
const expressions = [];
|
|
126
|
+
|
|
127
|
+
// 收集模板字符串的静态部分
|
|
128
|
+
path.node.quasis.forEach((i) => {
|
|
129
|
+
if (i.value.raw) {
|
|
130
|
+
val.push(i.value.raw);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// 处理表达式部分
|
|
135
|
+
path.node.expressions.forEach((exp) => {
|
|
136
|
+
expressions.push(processExpression(exp));
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const joinedVal = val.join('');
|
|
140
|
+
if (
|
|
141
|
+
!chnRegExp.test(joinedVal) ||
|
|
142
|
+
onlyContainsChinesePunctuation(joinedVal) ||
|
|
143
|
+
onlyContainsEmoji(joinedVal)
|
|
144
|
+
) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
let key = '';
|
|
149
|
+
let defaultMessage = '';
|
|
150
|
+
let variables = new Map();
|
|
151
|
+
|
|
152
|
+
// 构建key和defaultMessage,同时收集变量
|
|
153
|
+
path.node.quasis.forEach((quasi, index) => {
|
|
154
|
+
const rawValue = quasi.value.raw.replace(/^['"]|['"]$/g, '');
|
|
155
|
+
const trimmedValue = rawValue.replace(/^\s+|\s+$/g, '');
|
|
156
|
+
key += trimmedValue;
|
|
157
|
+
defaultMessage += trimmedValue;
|
|
158
|
+
|
|
159
|
+
if (index < expressions.length) {
|
|
160
|
+
const expr = expressions[index];
|
|
161
|
+
if (expr) {
|
|
162
|
+
const varName = `var${index + 1}`;
|
|
163
|
+
key += `{${varName}}`;
|
|
164
|
+
defaultMessage += `{${varName}}`;
|
|
165
|
+
variables.set(varName, expr);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const i18nKey = getOrCreateKey(key);
|
|
171
|
+
|
|
172
|
+
// 将变量转换为AST节点
|
|
173
|
+
const varProperties = Array.from(variables.entries()).map(([name, expr]) => {
|
|
174
|
+
const ast = babylon.parse(`(${expr})`);
|
|
175
|
+
const exprNode = ast.program.body[0].expression;
|
|
176
|
+
return t.objectProperty(t.identifier(name), exprNode);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
return createI18nCallNode(i18nKey, defaultMessage, varProperties);
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
// 解析JSX文件,提取中文文案并生成i18n调用
|
|
183
|
+
const parseJSX = (filePath, params = {}) => {
|
|
184
|
+
const i18n = {};
|
|
185
|
+
const contentKeyMap = new Map();
|
|
186
|
+
let uniqueCounter = 0;
|
|
187
|
+
let ignoredLineCount = 0; // 记录被忽略的行数
|
|
188
|
+
|
|
189
|
+
if (!params.keyPrefix) {
|
|
190
|
+
throw new Error('keyPrefix is required in params');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
let code = fs.readFileSync(filePath, {
|
|
194
|
+
encoding: 'utf8',
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// 生成文件路径key
|
|
198
|
+
let filePathKey = filePath.split('src/')[1] || filePath;
|
|
199
|
+
filePathKey = filePathKey.replace(/\.(tsx|jsx|js|ts)$/, '');
|
|
200
|
+
filePathKey = filePathKey.replace(/\//g, '.');
|
|
201
|
+
|
|
202
|
+
console.log(` [解析] 开始解析文件: ${filePath}`);
|
|
203
|
+
|
|
204
|
+
// 获取或创建i18n key
|
|
205
|
+
const getOrCreateKey = (content) => {
|
|
206
|
+
if (contentKeyMap.has(content)) {
|
|
207
|
+
return contentKeyMap.get(content);
|
|
208
|
+
}
|
|
209
|
+
uniqueCounter++;
|
|
210
|
+
const i18nKey = `${params.keyPrefix}.${filePathKey}.${uniqueCounter}`;
|
|
211
|
+
contentKeyMap.set(content, i18nKey);
|
|
212
|
+
i18n[i18nKey] = content;
|
|
213
|
+
return i18nKey;
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
// 检查节点是否应该被忽略
|
|
217
|
+
const shouldIgnoreNode = (path) => {
|
|
218
|
+
if (!path.node.loc) {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const lineNumber = path.node.loc.start.line;
|
|
223
|
+
|
|
224
|
+
// 检查行级别忽略
|
|
225
|
+
const lineIgnored = shouldIgnoreLineByComment(filePath, lineNumber);
|
|
226
|
+
if (lineIgnored) {
|
|
227
|
+
ignoredLineCount++;
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// 检查块级别忽略
|
|
232
|
+
const blockIgnored = shouldIgnoreBlockByComment(filePath, lineNumber);
|
|
233
|
+
if (blockIgnored) {
|
|
234
|
+
ignoredLineCount++;
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return false;
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
// 解析文件AST
|
|
242
|
+
const ast = babylon.parse(code, {
|
|
243
|
+
sourceType: 'module',
|
|
244
|
+
allowImportExportEverywhere: true,
|
|
245
|
+
plugins: [
|
|
246
|
+
'tsx',
|
|
247
|
+
'ts',
|
|
248
|
+
'js',
|
|
249
|
+
'jsx',
|
|
250
|
+
'typescript',
|
|
251
|
+
'functionBind',
|
|
252
|
+
'objectRestSpread',
|
|
253
|
+
'classProperties',
|
|
254
|
+
'transform-decorators',
|
|
255
|
+
'decorators-legacy',
|
|
256
|
+
],
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
let hasChanges = false;
|
|
260
|
+
|
|
261
|
+
// 遍历AST,处理不同类型的节点
|
|
262
|
+
traverse(ast, {
|
|
263
|
+
StringLiteral(path) {
|
|
264
|
+
const { value } = path.node;
|
|
265
|
+
if (
|
|
266
|
+
chnRegExp.test(value) &&
|
|
267
|
+
!isI18nCall(path.parent) &&
|
|
268
|
+
!path.findParent((p) => isI18nCall(p.node)) &&
|
|
269
|
+
!isInConsoleCall(path) &&
|
|
270
|
+
!shouldIgnoreNode(path)
|
|
271
|
+
) {
|
|
272
|
+
if (onlyContainsChinesePunctuation(value) || onlyContainsEmoji(value)) {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
try {
|
|
276
|
+
const rawValue = (path.node.extra?.raw || JSON.stringify(value)).replace(
|
|
277
|
+
/^['"]|['"]$/g,
|
|
278
|
+
'',
|
|
279
|
+
);
|
|
280
|
+
const trimmedValue = rawValue.replace(/^\s+|\s+$/g, '');
|
|
281
|
+
|
|
282
|
+
const i18nKey = getOrCreateKey(trimmedValue);
|
|
283
|
+
const i18nCallExpression = createI18nCallNode(i18nKey, trimmedValue);
|
|
284
|
+
|
|
285
|
+
const parentType = path.parent.type;
|
|
286
|
+
if (parentType === 'JSXAttribute') {
|
|
287
|
+
path.replaceWith(t.jsxExpressionContainer(i18nCallExpression));
|
|
288
|
+
} else {
|
|
289
|
+
path.replaceWith(i18nCallExpression);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
hasChanges = true;
|
|
293
|
+
path.skip();
|
|
294
|
+
} catch (e) {
|
|
295
|
+
console.error(' [错误] 处理StringLiteral时出错:', e);
|
|
296
|
+
console.error(' [错误] 节点:', path.node);
|
|
297
|
+
console.error(' [错误] 父节点:', path.parent);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
},
|
|
301
|
+
TemplateLiteral(path) {
|
|
302
|
+
if (
|
|
303
|
+
!isI18nCall(path.parent) &&
|
|
304
|
+
!path.findParent((p) => isI18nCall(p.node)) &&
|
|
305
|
+
!isInConsoleCall(path) &&
|
|
306
|
+
!shouldIgnoreNode(path)
|
|
307
|
+
) {
|
|
308
|
+
const newNode = processTemplateLiteral(path, getOrCreateKey);
|
|
309
|
+
if (newNode) {
|
|
310
|
+
path.replaceWith(newNode);
|
|
311
|
+
hasChanges = true;
|
|
312
|
+
path.skip();
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
},
|
|
316
|
+
JSXText(path) {
|
|
317
|
+
const { value } = path.node;
|
|
318
|
+
if (
|
|
319
|
+
chnRegExp.test(value) &&
|
|
320
|
+
!isI18nCall(path.parent) &&
|
|
321
|
+
!path.findParent((p) => isI18nCall(p.node)) &&
|
|
322
|
+
!isInConsoleCall(path) &&
|
|
323
|
+
!shouldIgnoreNode(path)
|
|
324
|
+
) {
|
|
325
|
+
if (onlyContainsChinesePunctuation(value) || onlyContainsEmoji(value)) {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
const rawValue = (path.node.extra?.raw || JSON.stringify(value)).replace(
|
|
329
|
+
/^['"]|['"]$/g,
|
|
330
|
+
'',
|
|
331
|
+
);
|
|
332
|
+
const trimmedValue = rawValue.replace(/^\s+|\s+$/g, '');
|
|
333
|
+
|
|
334
|
+
const i18nKey = getOrCreateKey(trimmedValue);
|
|
335
|
+
const newNode = t.jsxExpressionContainer(createI18nCallNode(i18nKey, trimmedValue));
|
|
336
|
+
|
|
337
|
+
path.replaceWith(newNode);
|
|
338
|
+
hasChanges = true;
|
|
339
|
+
path.skip();
|
|
340
|
+
}
|
|
341
|
+
},
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
return new Promise((resolve) => {
|
|
345
|
+
if (Object.keys(i18n).length === 0) {
|
|
346
|
+
console.log(` [提示] 文件 ${filePath} 中没有找到需要提取的文案`);
|
|
347
|
+
if (ignoredLineCount > 0) {
|
|
348
|
+
console.log(
|
|
349
|
+
` [提示] 文件 ${filePath} 中有 ${ignoredLineCount} 行包含 @i18n-ignore-line 注释,已跳过`,
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
return resolve({ i18n, newCode: null });
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
let message = ` [完成] 从文件 ${filePath} 中提取到${Object.keys(i18n).length}条文案`;
|
|
356
|
+
if (ignoredLineCount > 0) {
|
|
357
|
+
message += `,跳过${ignoredLineCount}行包含 @i18n-ignore-line 注释的内容`;
|
|
358
|
+
}
|
|
359
|
+
console.log(message);
|
|
360
|
+
|
|
361
|
+
const newCode = hasChanges
|
|
362
|
+
? generate(
|
|
363
|
+
ast,
|
|
364
|
+
{
|
|
365
|
+
jsescOption: {
|
|
366
|
+
minimal: true,
|
|
367
|
+
quotes: 'single',
|
|
368
|
+
},
|
|
369
|
+
retainLines: true,
|
|
370
|
+
compact: false,
|
|
371
|
+
},
|
|
372
|
+
code,
|
|
373
|
+
).code
|
|
374
|
+
: null;
|
|
375
|
+
|
|
376
|
+
resolve({ i18n, newCode });
|
|
377
|
+
});
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
module.exports = parseJSX;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
const axios = require('axios');
|
|
2
|
+
const fs = require('fs-extra');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
// 日志工具函数
|
|
6
|
+
const logApiError = (error, context) => {
|
|
7
|
+
const logDir = path.join(process.cwd(), 'logs');
|
|
8
|
+
const logFile = path.join(logDir, 'api-errors.log');
|
|
9
|
+
|
|
10
|
+
// 确保日志目录存在
|
|
11
|
+
fs.ensureDirSync(logDir);
|
|
12
|
+
|
|
13
|
+
const timestamp = new Date().toISOString();
|
|
14
|
+
const logEntry = {
|
|
15
|
+
timestamp,
|
|
16
|
+
error: {
|
|
17
|
+
message: error.message,
|
|
18
|
+
stack: error.stack,
|
|
19
|
+
...(error.response && {
|
|
20
|
+
response: {
|
|
21
|
+
status: error.response.status,
|
|
22
|
+
statusText: error.response.statusText,
|
|
23
|
+
data: error.response.data,
|
|
24
|
+
headers: error.response.headers,
|
|
25
|
+
},
|
|
26
|
+
}),
|
|
27
|
+
},
|
|
28
|
+
context,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// 追加到日志文件
|
|
32
|
+
const logLine = JSON.stringify(logEntry, null, 2) + '\n' + '-'.repeat(80) + '\n';
|
|
33
|
+
fs.appendFileSync(logFile, logLine);
|
|
34
|
+
|
|
35
|
+
console.error(` [日志] API错误已记录到: ${logFile}`);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// 调用DashScope API进行翻译
|
|
39
|
+
const callDashScope = async (fileContent, tokenName, appId, apiKey, retryCount = 3) => {
|
|
40
|
+
const url = `https://dashscope.aliyuncs.com/api/v1/apps/${appId}/completion`;
|
|
41
|
+
const data = {
|
|
42
|
+
input: {
|
|
43
|
+
prompt: fileContent,
|
|
44
|
+
tokenName,
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
for (let i = 0; i < retryCount; i++) {
|
|
49
|
+
try {
|
|
50
|
+
const response = await axios.post(url, data, {
|
|
51
|
+
headers: {
|
|
52
|
+
Authorization: `Bearer ${apiKey}`,
|
|
53
|
+
'Content-Type': 'application/json',
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
if (response.status === 200) {
|
|
58
|
+
return response.data.output.text;
|
|
59
|
+
} else {
|
|
60
|
+
// 记录非200状态码错误
|
|
61
|
+
const error = new Error(`API返回非200状态码: ${response.status}`);
|
|
62
|
+
const context = {
|
|
63
|
+
attempt: i + 1,
|
|
64
|
+
totalAttempts: retryCount,
|
|
65
|
+
url,
|
|
66
|
+
appId,
|
|
67
|
+
tokenName,
|
|
68
|
+
requestData: data,
|
|
69
|
+
response: {
|
|
70
|
+
status: response.status,
|
|
71
|
+
headers: response.headers,
|
|
72
|
+
data: response.data,
|
|
73
|
+
},
|
|
74
|
+
timestamp: new Date().toISOString(),
|
|
75
|
+
};
|
|
76
|
+
logApiError(error, context);
|
|
77
|
+
|
|
78
|
+
console.log(` [请求] request_id=${response.headers['request_id']}`);
|
|
79
|
+
console.log(` [请求] code=${response.status}`);
|
|
80
|
+
console.log(` [请求] message=${response.data.message}`);
|
|
81
|
+
|
|
82
|
+
if (i < retryCount - 1) {
|
|
83
|
+
console.log(` [重试] 第${i + 1}次请求失败,10秒后重试...`);
|
|
84
|
+
await new Promise((resolve) => setTimeout(resolve, 10000));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
} catch (error) {
|
|
88
|
+
// 记录网络错误或其他异常
|
|
89
|
+
const context = {
|
|
90
|
+
attempt: i + 1,
|
|
91
|
+
totalAttempts: retryCount,
|
|
92
|
+
url,
|
|
93
|
+
appId,
|
|
94
|
+
tokenName,
|
|
95
|
+
requestData: data,
|
|
96
|
+
timestamp: new Date().toISOString(),
|
|
97
|
+
};
|
|
98
|
+
logApiError(error, context);
|
|
99
|
+
|
|
100
|
+
console.error(` [错误] 调用DashScope失败: ${error.message}`);
|
|
101
|
+
if (error.response) {
|
|
102
|
+
console.error(` [错误] 响应状态: ${error.response.status}`);
|
|
103
|
+
console.error(` [错误] 响应数据: ${JSON.stringify(error.response.data, null, 2)}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (i < retryCount - 1) {
|
|
107
|
+
console.log(` [重试] 第${i + 1}次请求失败,10秒后重试...`);
|
|
108
|
+
await new Promise((resolve) => setTimeout(resolve, 10000));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
throw new Error(`[错误] ${retryCount}次重试后仍然失败`);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
module.exports = {
|
|
117
|
+
callDashScope,
|
|
118
|
+
};
|