@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
package/lib/index.js
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
const {
|
|
2
|
-
|
|
3
|
-
initMT,
|
|
4
|
-
patch,
|
|
5
|
-
patchMT,
|
|
2
|
+
runTranslation,
|
|
6
3
|
check,
|
|
7
4
|
reverse,
|
|
8
5
|
translate,
|
|
@@ -11,10 +8,7 @@ const {
|
|
|
11
8
|
const { run: medusaRun } = require('./utils/medusa');
|
|
12
9
|
|
|
13
10
|
module.exports = {
|
|
14
|
-
|
|
15
|
-
initMT,
|
|
16
|
-
patch,
|
|
17
|
-
patchMT,
|
|
11
|
+
runTranslation,
|
|
18
12
|
check,
|
|
19
13
|
reverse,
|
|
20
14
|
translate,
|
package/lib/parse-jsx.js
CHANGED
|
@@ -10,19 +10,26 @@ const {
|
|
|
10
10
|
onlyContainsEmoji,
|
|
11
11
|
isI18nCall,
|
|
12
12
|
isInConsoleCall,
|
|
13
|
+
isDirectCallMode,
|
|
13
14
|
} = require('./utils/ast-utils');
|
|
14
15
|
|
|
15
|
-
// 生成i18n
|
|
16
|
-
const createI18nCallNode = (i18nKey, defaultMessage, variables = []) => {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
16
|
+
// 生成i18n调用节点,支持成员调用和直接函数调用两种形式
|
|
17
|
+
const createI18nCallNode = (i18nKey, defaultMessage, variables = [], callExpr) => {
|
|
18
|
+
let callee;
|
|
19
|
+
if (isDirectCallMode(callExpr)) {
|
|
20
|
+
callee = t.identifier(callExpr.functionName);
|
|
21
|
+
} else {
|
|
22
|
+
const objectName = (callExpr && callExpr.objectName) || '$i18n';
|
|
23
|
+
const methodName = (callExpr && callExpr.methodName) || 'get';
|
|
24
|
+
callee = t.memberExpression(t.identifier(objectName), t.identifier(methodName));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const baseNode = t.callExpression(callee, [
|
|
28
|
+
t.objectExpression([
|
|
29
|
+
t.objectProperty(t.identifier('id'), t.stringLiteral(i18nKey)),
|
|
30
|
+
t.objectProperty(t.identifier('dm'), t.stringLiteral(defaultMessage)),
|
|
31
|
+
]),
|
|
32
|
+
]);
|
|
26
33
|
|
|
27
34
|
if (variables.length > 0) {
|
|
28
35
|
baseNode.arguments.push(t.objectExpression(variables));
|
|
@@ -46,7 +53,7 @@ const processExpression = (exp) => {
|
|
|
46
53
|
};
|
|
47
54
|
|
|
48
55
|
// 处理模板字符串,提取变量和生成i18n调用
|
|
49
|
-
const processTemplateLiteral = (path, getOrCreateKey) => {
|
|
56
|
+
const processTemplateLiteral = (path, getOrCreateKey, callExpr) => {
|
|
50
57
|
const val = [];
|
|
51
58
|
const expressions = [];
|
|
52
59
|
|
|
@@ -102,7 +109,7 @@ const processTemplateLiteral = (path, getOrCreateKey) => {
|
|
|
102
109
|
return t.objectProperty(t.identifier(name), exprNode);
|
|
103
110
|
});
|
|
104
111
|
|
|
105
|
-
return createI18nCallNode(i18nKey, defaultMessage, varProperties);
|
|
112
|
+
return createI18nCallNode(i18nKey, defaultMessage, varProperties, callExpr);
|
|
106
113
|
};
|
|
107
114
|
|
|
108
115
|
// 解析JSX文件,提取中文文案并生成i18n调用
|
|
@@ -110,7 +117,8 @@ const parseJSX = (filePath, params = {}) => {
|
|
|
110
117
|
const i18n = {};
|
|
111
118
|
const contentKeyMap = new Map();
|
|
112
119
|
let uniqueCounter = 0;
|
|
113
|
-
let ignoredLineCount = 0;
|
|
120
|
+
let ignoredLineCount = 0;
|
|
121
|
+
const callExpr = params.callExpression;
|
|
114
122
|
|
|
115
123
|
if (!params.keyPrefix) {
|
|
116
124
|
throw new Error('keyPrefix is required in params');
|
|
@@ -190,8 +198,8 @@ const parseJSX = (filePath, params = {}) => {
|
|
|
190
198
|
const { value } = path.node;
|
|
191
199
|
if (
|
|
192
200
|
chnRegExp.test(value) &&
|
|
193
|
-
!isI18nCall(path.parent) &&
|
|
194
|
-
!path.findParent((p) => isI18nCall(p.node)) &&
|
|
201
|
+
!isI18nCall(path.parent, callExpr) &&
|
|
202
|
+
!path.findParent((p) => isI18nCall(p.node, callExpr)) &&
|
|
195
203
|
!isInConsoleCall(path) &&
|
|
196
204
|
!shouldIgnoreNode(path)
|
|
197
205
|
) {
|
|
@@ -206,7 +214,7 @@ const parseJSX = (filePath, params = {}) => {
|
|
|
206
214
|
const trimmedValue = rawValue.replace(/^\s+|\s+$/g, '');
|
|
207
215
|
|
|
208
216
|
const i18nKey = getOrCreateKey(trimmedValue);
|
|
209
|
-
const i18nCallExpression = createI18nCallNode(i18nKey, trimmedValue);
|
|
217
|
+
const i18nCallExpression = createI18nCallNode(i18nKey, trimmedValue, [], callExpr);
|
|
210
218
|
|
|
211
219
|
const parentType = path.parent.type;
|
|
212
220
|
if (parentType === 'JSXAttribute') {
|
|
@@ -226,12 +234,12 @@ const parseJSX = (filePath, params = {}) => {
|
|
|
226
234
|
},
|
|
227
235
|
TemplateLiteral(path) {
|
|
228
236
|
if (
|
|
229
|
-
!isI18nCall(path.parent) &&
|
|
230
|
-
!path.findParent((p) => isI18nCall(p.node)) &&
|
|
237
|
+
!isI18nCall(path.parent, callExpr) &&
|
|
238
|
+
!path.findParent((p) => isI18nCall(p.node, callExpr)) &&
|
|
231
239
|
!isInConsoleCall(path) &&
|
|
232
240
|
!shouldIgnoreNode(path)
|
|
233
241
|
) {
|
|
234
|
-
const newNode = processTemplateLiteral(path, getOrCreateKey);
|
|
242
|
+
const newNode = processTemplateLiteral(path, getOrCreateKey, callExpr);
|
|
235
243
|
if (newNode) {
|
|
236
244
|
path.replaceWith(newNode);
|
|
237
245
|
hasChanges = true;
|
|
@@ -243,8 +251,8 @@ const parseJSX = (filePath, params = {}) => {
|
|
|
243
251
|
const { value } = path.node;
|
|
244
252
|
if (
|
|
245
253
|
chnRegExp.test(value) &&
|
|
246
|
-
!isI18nCall(path.parent) &&
|
|
247
|
-
!path.findParent((p) => isI18nCall(p.node)) &&
|
|
254
|
+
!isI18nCall(path.parent, callExpr) &&
|
|
255
|
+
!path.findParent((p) => isI18nCall(p.node, callExpr)) &&
|
|
248
256
|
!isInConsoleCall(path) &&
|
|
249
257
|
!shouldIgnoreNode(path)
|
|
250
258
|
) {
|
|
@@ -258,7 +266,9 @@ const parseJSX = (filePath, params = {}) => {
|
|
|
258
266
|
const trimmedValue = rawValue.replace(/^\s+|\s+$/g, '');
|
|
259
267
|
|
|
260
268
|
const i18nKey = getOrCreateKey(trimmedValue);
|
|
261
|
-
const newNode = t.jsxExpressionContainer(
|
|
269
|
+
const newNode = t.jsxExpressionContainer(
|
|
270
|
+
createI18nCallNode(i18nKey, trimmedValue, [], callExpr),
|
|
271
|
+
);
|
|
262
272
|
|
|
263
273
|
path.replaceWith(newNode);
|
|
264
274
|
hasChanges = true;
|
package/lib/utils/ast-utils.js
CHANGED
|
@@ -42,19 +42,50 @@ function extractStringValue(node) {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
/**
|
|
45
|
-
*
|
|
45
|
+
* 判断调用表达式是否使用直接函数调用模式 (functionName(...))
|
|
46
|
+
* 而非成员调用模式 (objectName.methodName(...))
|
|
47
|
+
*/
|
|
48
|
+
function isDirectCallMode(callExpr) {
|
|
49
|
+
return callExpr && callExpr.functionName && !callExpr.objectName && !callExpr.methodName;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 检查 CallExpression 的 callee 是否匹配配置的调用形式
|
|
54
|
+
* 支持两种形式:
|
|
55
|
+
* - 成员调用:objectName.methodName(...) (默认 $i18n.get)
|
|
56
|
+
* - 直接调用:functionName(...) (如 translate、t)
|
|
57
|
+
*/
|
|
58
|
+
function isMatchingCallee(node, callExpr) {
|
|
59
|
+
if (!node || node.type !== 'CallExpression') return false;
|
|
60
|
+
|
|
61
|
+
if (isDirectCallMode(callExpr)) {
|
|
62
|
+
return (
|
|
63
|
+
node.callee &&
|
|
64
|
+
node.callee.type === 'Identifier' &&
|
|
65
|
+
node.callee.name === callExpr.functionName
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const objectName = (callExpr && callExpr.objectName) || '$i18n';
|
|
70
|
+
const methodName = (callExpr && callExpr.methodName) || 'get';
|
|
71
|
+
return (
|
|
72
|
+
node.callee &&
|
|
73
|
+
node.callee.type === 'MemberExpression' &&
|
|
74
|
+
node.callee.object &&
|
|
75
|
+
node.callee.object.name === objectName &&
|
|
76
|
+
node.callee.property &&
|
|
77
|
+
node.callee.property.name === methodName
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 检查 i18n 调用并提取 key
|
|
46
83
|
* @param {Object} node - CallExpression AST 节点
|
|
84
|
+
* @param {Object} [callExpr] - 自定义调用表达式配置
|
|
47
85
|
* @returns {Object|null} 返回 {id, dm} 或 null
|
|
48
86
|
*/
|
|
49
|
-
function extractI18nKey(node) {
|
|
50
|
-
if (
|
|
51
|
-
node.type !== 'CallExpression' ||
|
|
52
|
-
node.callee.type !== 'MemberExpression' ||
|
|
53
|
-
!node.callee.object ||
|
|
54
|
-
node.callee.object.name !== '$i18n' ||
|
|
55
|
-
!node.callee.property ||
|
|
56
|
-
node.callee.property.name !== 'get'
|
|
57
|
-
) {
|
|
87
|
+
function extractI18nKey(node, callExpr) {
|
|
88
|
+
if (!isMatchingCallee(node, callExpr)) {
|
|
58
89
|
return null;
|
|
59
90
|
}
|
|
60
91
|
|
|
@@ -63,7 +94,6 @@ function extractI18nKey(node) {
|
|
|
63
94
|
return null;
|
|
64
95
|
}
|
|
65
96
|
|
|
66
|
-
// 处理对象形式:i18n.get({id: key, dm: defaultMessage})
|
|
67
97
|
if (args[0].type === 'ObjectExpression') {
|
|
68
98
|
const idProperty = args[0].properties.find((prop) => prop.key && prop.key.name === 'id');
|
|
69
99
|
|
|
@@ -79,7 +109,7 @@ function extractI18nKey(node) {
|
|
|
79
109
|
return id ? { id, dm } : null;
|
|
80
110
|
}
|
|
81
111
|
|
|
82
|
-
//
|
|
112
|
+
// 直接传递 key 的形式:translate(key) 或 $i18n.get(key)
|
|
83
113
|
else {
|
|
84
114
|
const id = extractStringValue(args[0]);
|
|
85
115
|
return id ? { id, dm: null } : null;
|
|
@@ -89,8 +119,8 @@ function extractI18nKey(node) {
|
|
|
89
119
|
/**
|
|
90
120
|
* 增强的 checkI18nCall 函数,支持更多 key 格式
|
|
91
121
|
*/
|
|
92
|
-
function checkI18nCall(node) {
|
|
93
|
-
return extractI18nKey(node);
|
|
122
|
+
function checkI18nCall(node, callExpr) {
|
|
123
|
+
return extractI18nKey(node, callExpr);
|
|
94
124
|
}
|
|
95
125
|
|
|
96
126
|
const chnRegExp = /[^\x00-\xff]/;
|
|
@@ -110,17 +140,8 @@ const onlyContainsEmoji = (text) => {
|
|
|
110
140
|
return trimmedText.replace(emojiRegex, '') === '';
|
|
111
141
|
};
|
|
112
142
|
|
|
113
|
-
function isI18nCall(node) {
|
|
114
|
-
return (
|
|
115
|
-
node &&
|
|
116
|
-
node.type === 'CallExpression' &&
|
|
117
|
-
node.callee &&
|
|
118
|
-
node.callee.type === 'MemberExpression' &&
|
|
119
|
-
node.callee.object &&
|
|
120
|
-
node.callee.object.name === '$i18n' &&
|
|
121
|
-
node.callee.property &&
|
|
122
|
-
node.callee.property.name === 'get'
|
|
123
|
-
);
|
|
143
|
+
function isI18nCall(node, callExpr) {
|
|
144
|
+
return isMatchingCallee(node, callExpr);
|
|
124
145
|
}
|
|
125
146
|
|
|
126
147
|
function isInConsoleCall(path) {
|
|
@@ -163,4 +184,6 @@ module.exports = {
|
|
|
163
184
|
onlyContainsEmoji,
|
|
164
185
|
isI18nCall,
|
|
165
186
|
isInConsoleCall,
|
|
187
|
+
isDirectCallMode,
|
|
188
|
+
isMatchingCallee,
|
|
166
189
|
};
|
package/lib/utils/file-utils.js
CHANGED
|
@@ -16,22 +16,61 @@ const shouldIgnoreFile = (file, doNotTranslateFiles) => {
|
|
|
16
16
|
};
|
|
17
17
|
|
|
18
18
|
// 添加导入语句到文件
|
|
19
|
-
const injectImportStatement = (filePath, importPath) => {
|
|
19
|
+
const injectImportStatement = (filePath, importPath, callExpr) => {
|
|
20
20
|
try {
|
|
21
21
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
22
|
-
const
|
|
22
|
+
const importName = (callExpr && callExpr.importName) || '$i18n';
|
|
23
|
+
const namedImport = callExpr && callExpr.namedImport;
|
|
24
|
+
|
|
25
|
+
// 对于 namedImport 模式,先检查是否已从同一路径导入
|
|
26
|
+
if (namedImport) {
|
|
27
|
+
const escapedPath = importPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
28
|
+
// 匹配单条 import { ... } from 'path' 语句(含多行),
|
|
29
|
+
// 用 (?:(?!import\s)[\s\S]) 防止跨越多条 import 语句边界
|
|
30
|
+
const existingImportRegex = new RegExp(
|
|
31
|
+
`(import\\s+\\{(?:(?!\\bimport\\s)[\\s\\S])*?\\}\\s*from\\s*['"]${escapedPath}['"];?)`,
|
|
32
|
+
'gm',
|
|
33
|
+
);
|
|
34
|
+
const match = existingImportRegex.exec(content);
|
|
35
|
+
|
|
36
|
+
if (match) {
|
|
37
|
+
const existingImport = match[1];
|
|
38
|
+
// 提取花括号内的 specifiers 并逐个比对
|
|
39
|
+
const braceContent = existingImport.match(/\{([\s\S]*?)\}/);
|
|
40
|
+
if (braceContent) {
|
|
41
|
+
const specifiers = braceContent[1].split(',').map((s) => s.trim()).filter(Boolean);
|
|
42
|
+
if (specifiers.includes(importName)) {
|
|
43
|
+
return; // 已存在,无需添加
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// 追加到已有的 named import 中:在最后一个 `}` 前插入
|
|
47
|
+
const newImport = existingImport.replace(/\s*}\s*from/, `, ${importName} } from`);
|
|
48
|
+
const newContent = content.replace(existingImport, newImport);
|
|
49
|
+
fs.writeFileSync(filePath, newContent, 'utf8');
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const importStatement = namedImport
|
|
55
|
+
? `import { ${importName} } from '${importPath}';\n`
|
|
56
|
+
: `import ${importName} from '${importPath}';\n`;
|
|
23
57
|
|
|
24
58
|
// 检查文件是否已经有这个导入语句
|
|
25
59
|
if (!content.includes(importStatement.trim())) {
|
|
26
|
-
//
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
60
|
+
// 匹配所有 import 语句(含多行),找到最后一条的结束位置
|
|
61
|
+
const multiLineImportRegex = /^import\s[\s\S]*?from\s*['"][^'"]*['"];?/gm;
|
|
62
|
+
let lastImportEnd = -1;
|
|
63
|
+
let m;
|
|
64
|
+
while ((m = multiLineImportRegex.exec(content)) !== null) {
|
|
65
|
+
const end = m.index + m[0].length;
|
|
66
|
+
if (end > lastImportEnd) lastImportEnd = end;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (lastImportEnd > -1) {
|
|
70
|
+
// 在最后一条 import 语句之后插入新行
|
|
71
|
+
const before = content.slice(0, lastImportEnd);
|
|
72
|
+
const after = content.slice(lastImportEnd);
|
|
73
|
+
fs.writeFileSync(filePath, before + '\n' + importStatement + after.replace(/^\n/, ''), 'utf8');
|
|
35
74
|
} else {
|
|
36
75
|
// 如果没有导入语句,添加到文件开头
|
|
37
76
|
fs.writeFileSync(filePath, importStatement + content, 'utf8');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentscope-ai/i18n",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "A tool for translating Chinese content in frontend code repositories",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"i18n",
|
|
@@ -14,18 +14,24 @@
|
|
|
14
14
|
"files": [
|
|
15
15
|
"lib/*",
|
|
16
16
|
"skills/*",
|
|
17
|
+
"i18n.config.example.js",
|
|
17
18
|
"README.md"
|
|
18
19
|
],
|
|
19
20
|
"scripts": {
|
|
20
21
|
"lint": "eslint .",
|
|
21
|
-
"format": "prettier --write \"lib/**/*.{ts,tsx}\""
|
|
22
|
+
"format": "prettier --write \"lib/**/*.{ts,tsx}\"",
|
|
23
|
+
"test": "jest"
|
|
22
24
|
},
|
|
23
25
|
"lint-staged": {
|
|
24
26
|
"**/*.{js,jsx,html,css,json}": [
|
|
25
27
|
"npx prettier --write"
|
|
26
28
|
]
|
|
27
29
|
},
|
|
28
|
-
"
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "http://gitlab.alibaba-inc.com/liveme-console/spark-i18n.git"
|
|
33
|
+
},
|
|
34
|
+
"author": "wangqifeng.wqf@alibaba-inc.com",
|
|
29
35
|
"license": "ISC",
|
|
30
36
|
"dependencies": {
|
|
31
37
|
"@babel/generator": "^7.23.6",
|
|
@@ -61,7 +67,6 @@
|
|
|
61
67
|
"node": ">=14.0.0"
|
|
62
68
|
},
|
|
63
69
|
"publishConfig": {
|
|
64
|
-
"registry": "https://registry.npmjs.org
|
|
65
|
-
"access": "public"
|
|
70
|
+
"registry": "https://registry.npmjs.org"
|
|
66
71
|
}
|
|
67
72
|
}
|