@agentscope-ai/i18n 1.0.0 → 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/lib/index.js CHANGED
@@ -1,8 +1,5 @@
1
1
  const {
2
- init,
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
- init,
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
- const baseNode = t.callExpression(
18
- t.memberExpression(t.identifier('$i18n'), t.identifier('get')),
19
- [
20
- t.objectExpression([
21
- t.objectProperty(t.identifier('id'), t.stringLiteral(i18nKey)),
22
- t.objectProperty(t.identifier('dm'), t.stringLiteral(defaultMessage)),
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(createI18nCallNode(i18nKey, trimmedValue));
269
+ const newNode = t.jsxExpressionContainer(
270
+ createI18nCallNode(i18nKey, trimmedValue, [], callExpr),
271
+ );
262
272
 
263
273
  path.replaceWith(newNode);
264
274
  hasChanges = true;
@@ -42,19 +42,50 @@ function extractStringValue(node) {
42
42
  }
43
43
 
44
44
  /**
45
- * 检查 i18n.get 调用并提取 key
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
- // 处理直接传递key的形式:i18n.get(key)
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
  };
@@ -8,7 +8,7 @@ const path = require('path');
8
8
  * @param {string} params.localesFilePath 翻译文件目录
9
9
  * @param {Object} params.medusa medusa配置
10
10
  * @param {string} params.medusa.appName 应用名称
11
- * @param {string} params.medusa.group 分组名称
11
+ * @param {string} [params.medusa.group] 分组名称(可为空字符串)
12
12
  * @param {Object} params.medusa.keyMap 语言映射配置
13
13
  * @param {Array} params.newKeys 新增的翻译key列表
14
14
  * @returns {string} 生成的excel文件路径
@@ -16,10 +16,13 @@ const path = require('path');
16
16
  const generateMedusaExcel = async (params) => {
17
17
  const { localesFilePath, medusa, newKeys, customData } = params;
18
18
 
19
- if (!medusa || !medusa.appName || !medusa.group || !medusa.keyMap) {
20
- throw new Error('medusa配置不完整,需要appName、group和keyMap');
19
+ if (!medusa || !medusa.appName || !medusa.keyMap) {
20
+ throw new Error('medusa配置不完整,需要 appName keyMap(group 可选,可为空字符串)');
21
21
  }
22
22
 
23
+ // group 允许 '',不能用 !medusa.group 判断
24
+ const group = medusa.group == null ? '' : String(medusa.group);
25
+
23
26
  // 读取所有翻译文件
24
27
  const translationFiles = {};
25
28
 
@@ -59,7 +62,7 @@ const generateMedusaExcel = async (params) => {
59
62
  for (const key of keysToProcess) {
60
63
  const row = {
61
64
  AppName: medusa.appName,
62
- Group: medusa.group,
65
+ Group: group,
63
66
  Key: key,
64
67
  };
65
68
 
@@ -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 importStatement = `import $i18n from '${importPath}';\n`;
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 importRegex = /^import\s+.*?from\s+['"].*?['"];?\n/gm;
28
- const imports = content.match(importRegex);
29
-
30
- if (imports) {
31
- // 在最后一个导入语句后添加
32
- const lastImport = imports[imports.length - 1];
33
- const newContent = content.replace(lastImport, `${lastImport}${importStatement}`);
34
- fs.writeFileSync(filePath, newContent, 'utf8');
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.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
- "author": [],
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
  }