@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
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
const axios = require('axios');
|
|
2
|
+
const fs = require('fs-extra');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
// 语言映射
|
|
6
|
+
const langMap = {
|
|
7
|
+
'en-us': 'English',
|
|
8
|
+
'ja-jp': 'Japanese',
|
|
9
|
+
'zh-cn': 'Chinese',
|
|
10
|
+
'zh-tw': 'Chinese',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// 日志工具函数
|
|
14
|
+
const logMTError = (error, context) => {
|
|
15
|
+
const logDir = path.join(process.cwd(), 'logs');
|
|
16
|
+
const logFile = path.join(logDir, 'mt-translation-errors.log');
|
|
17
|
+
|
|
18
|
+
// 确保日志目录存在
|
|
19
|
+
fs.ensureDirSync(logDir);
|
|
20
|
+
|
|
21
|
+
const timestamp = new Date().toISOString();
|
|
22
|
+
const logEntry = {
|
|
23
|
+
timestamp,
|
|
24
|
+
error: {
|
|
25
|
+
message: error.message,
|
|
26
|
+
stack: error.stack,
|
|
27
|
+
...(error.response && {
|
|
28
|
+
response: {
|
|
29
|
+
status: error.response.status,
|
|
30
|
+
statusText: error.response.statusText,
|
|
31
|
+
data: error.response.data,
|
|
32
|
+
headers: error.response.headers,
|
|
33
|
+
},
|
|
34
|
+
}),
|
|
35
|
+
},
|
|
36
|
+
context,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// 追加到日志文件
|
|
40
|
+
const logLine = JSON.stringify(logEntry, null, 2) + '\n' + '-'.repeat(80) + '\n';
|
|
41
|
+
fs.appendFileSync(logFile, logLine);
|
|
42
|
+
|
|
43
|
+
console.error(` [日志] MT翻译错误已记录到: ${logFile}`);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// 调用DashScope机器翻译API进行批量翻译
|
|
47
|
+
const callDashScopeMTBatch = async (prompts, targetLang, apiKey, retryCount = 3, type) => {
|
|
48
|
+
// 多条文案组合成一个请求
|
|
49
|
+
const combinedPrompt = prompts.map((text, index) => `${index + 1}. ${text}`).join('\n');
|
|
50
|
+
const instructionPrompt = `
|
|
51
|
+
请将以下${prompts.length}条中文文案翻译${type === 'key' ? '并总结为' : ''}为${type === 'key' ? '7个单词以内的' : ''}${langMap[targetLang] || targetLang},
|
|
52
|
+
保持原有的序号格式:\n\n${combinedPrompt}
|
|
53
|
+
`;
|
|
54
|
+
const url = 'https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation';
|
|
55
|
+
const data = {
|
|
56
|
+
model: 'qwen-mt-plus',
|
|
57
|
+
input: {
|
|
58
|
+
messages: [
|
|
59
|
+
{
|
|
60
|
+
role: 'user',
|
|
61
|
+
content: instructionPrompt,
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
},
|
|
65
|
+
parameters: {
|
|
66
|
+
translation_options: {
|
|
67
|
+
source_lang: 'auto',
|
|
68
|
+
target_lang: langMap[targetLang] || targetLang,
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
for (let i = 0; i < retryCount; i++) {
|
|
74
|
+
try {
|
|
75
|
+
const response = await axios.post(url, data, {
|
|
76
|
+
headers: {
|
|
77
|
+
Authorization: `Bearer ${apiKey}`,
|
|
78
|
+
'Content-Type': 'application/json',
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
if (response.status === 200) {
|
|
83
|
+
const result = response.data?.output?.choices?.[0]?.message?.content;
|
|
84
|
+
if (result) {
|
|
85
|
+
// 解析批量翻译结果
|
|
86
|
+
const translations = parseBatchTranslationResult(result.trim(), prompts.length);
|
|
87
|
+
return translations;
|
|
88
|
+
} else {
|
|
89
|
+
throw new Error('批量翻译结果为空');
|
|
90
|
+
}
|
|
91
|
+
} else {
|
|
92
|
+
// 记录非200状态码错误
|
|
93
|
+
const error = new Error(`MT批量API返回非200状态码: ${response.status}`);
|
|
94
|
+
const context = {
|
|
95
|
+
attempt: i + 1,
|
|
96
|
+
totalAttempts: retryCount,
|
|
97
|
+
url,
|
|
98
|
+
targetLang,
|
|
99
|
+
promptsCount: prompts.length,
|
|
100
|
+
requestData: data,
|
|
101
|
+
response: {
|
|
102
|
+
status: response.status,
|
|
103
|
+
headers: response.headers,
|
|
104
|
+
data: response.data,
|
|
105
|
+
},
|
|
106
|
+
timestamp: new Date().toISOString(),
|
|
107
|
+
};
|
|
108
|
+
logMTError(error, context);
|
|
109
|
+
|
|
110
|
+
console.log(` [请求] request_id=${response.headers['request_id']}`);
|
|
111
|
+
console.log(` [请求] code=${response.status}`);
|
|
112
|
+
console.log(` [请求] message=${response.data.message}`);
|
|
113
|
+
|
|
114
|
+
if (i < retryCount - 1) {
|
|
115
|
+
console.log(` [重试] 第${i + 1}次MT批量翻译失败,3秒后重试...`);
|
|
116
|
+
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
} catch (error) {
|
|
120
|
+
// 记录网络错误或其他异常
|
|
121
|
+
const context = {
|
|
122
|
+
attempt: i + 1,
|
|
123
|
+
totalAttempts: retryCount,
|
|
124
|
+
url,
|
|
125
|
+
targetLang,
|
|
126
|
+
promptsCount: prompts.length,
|
|
127
|
+
requestData: data,
|
|
128
|
+
timestamp: new Date().toISOString(),
|
|
129
|
+
};
|
|
130
|
+
logMTError(error, context);
|
|
131
|
+
|
|
132
|
+
console.error(` [错误] 调用DashScope MT批量翻译失败: ${error.message}`);
|
|
133
|
+
if (error.response) {
|
|
134
|
+
console.error(` [错误] 响应状态: ${error.response.status}`);
|
|
135
|
+
console.error(` [错误] 响应数据: ${JSON.stringify(error.response.data, null, 2)}`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (i < retryCount - 1) {
|
|
139
|
+
console.log(` [重试] 第${i + 1}次MT批量翻译失败,3秒后重试...`);
|
|
140
|
+
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
throw new Error(`[错误] MT批量翻译${retryCount}次重试后仍然失败`);
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// 解析批量翻译结果
|
|
149
|
+
const parseBatchTranslationResult = (result, expectedCount) => {
|
|
150
|
+
const lines = result.split('\n').filter((line) => line.trim());
|
|
151
|
+
const translations = [];
|
|
152
|
+
|
|
153
|
+
for (let i = 0; i < expectedCount; i++) {
|
|
154
|
+
const expectedPrefix = `${i + 1}.`;
|
|
155
|
+
let found = false;
|
|
156
|
+
|
|
157
|
+
for (const line of lines) {
|
|
158
|
+
if (line.trim().startsWith(expectedPrefix)) {
|
|
159
|
+
const translation = line.replace(expectedPrefix, '').trim();
|
|
160
|
+
translations.push(translation);
|
|
161
|
+
found = true;
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (!found) {
|
|
167
|
+
// 如果找不到对应序号的翻译,尝试按顺序匹配
|
|
168
|
+
if (translations.length < lines.length) {
|
|
169
|
+
const fallbackTranslation = lines[translations.length]?.replace(/^\d+\.\s*/, '').trim();
|
|
170
|
+
translations.push(fallbackTranslation || '');
|
|
171
|
+
} else {
|
|
172
|
+
translations.push(''); // 翻译失败,返回空字符串
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return translations;
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
module.exports = {
|
|
181
|
+
callDashScopeMTBatch,
|
|
182
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
const t = require('@babel/types');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 尝试从 AST 节点中提取字符串值
|
|
5
|
+
* 支持多种节点类型:StringLiteral, BinaryExpression, TemplateLiteral 等
|
|
6
|
+
*/
|
|
7
|
+
function extractStringValue(node) {
|
|
8
|
+
if (!node) return null;
|
|
9
|
+
|
|
10
|
+
switch (node.type) {
|
|
11
|
+
case 'StringLiteral':
|
|
12
|
+
return node.value;
|
|
13
|
+
|
|
14
|
+
case 'BinaryExpression':
|
|
15
|
+
// 处理字符串连接,如 'a' + 'b'
|
|
16
|
+
if (node.operator === '+') {
|
|
17
|
+
const left = extractStringValue(node.left);
|
|
18
|
+
const right = extractStringValue(node.right);
|
|
19
|
+
if (left !== null && right !== null) {
|
|
20
|
+
return left + right;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
|
|
25
|
+
case 'TemplateLiteral':
|
|
26
|
+
// 处理模板字符串,如 `hello ${name}`
|
|
27
|
+
if (node.expressions.length === 0) {
|
|
28
|
+
// 纯模板字符串,没有插值
|
|
29
|
+
return node.quasis.map((quasi) => quasi.value.cooked).join('');
|
|
30
|
+
}
|
|
31
|
+
// 包含插值的模板字符串无法静态分析
|
|
32
|
+
return null;
|
|
33
|
+
|
|
34
|
+
case 'Identifier':
|
|
35
|
+
// 变量引用无法静态分析,但返回变量名用于识别
|
|
36
|
+
// 注意:这种情况下无法确定实际的key值
|
|
37
|
+
return `[variable:${node.name}]`;
|
|
38
|
+
|
|
39
|
+
default:
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 检查 i18n.get 调用并提取 key
|
|
46
|
+
* @param {Object} node - CallExpression AST 节点
|
|
47
|
+
* @returns {Object|null} 返回 {id, dm} 或 null
|
|
48
|
+
*/
|
|
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
|
+
) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const args = node.arguments;
|
|
62
|
+
if (args.length === 0) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 处理对象形式:i18n.get({id: key, dm: defaultMessage})
|
|
67
|
+
if (args[0].type === 'ObjectExpression') {
|
|
68
|
+
const idProperty = args[0].properties.find((prop) => prop.key && prop.key.name === 'id');
|
|
69
|
+
|
|
70
|
+
const dmProperty = args[0].properties.find((prop) => prop.key && prop.key.name === 'dm');
|
|
71
|
+
|
|
72
|
+
if (!idProperty || !idProperty.value) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const id = extractStringValue(idProperty.value);
|
|
77
|
+
const dm = dmProperty ? extractStringValue(dmProperty.value) : null;
|
|
78
|
+
|
|
79
|
+
return id ? { id, dm } : null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 处理直接传递key的形式:i18n.get(key)
|
|
83
|
+
else {
|
|
84
|
+
const id = extractStringValue(args[0]);
|
|
85
|
+
return id ? { id, dm: null } : null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 增强的 checkI18nCall 函数,支持更多 key 格式
|
|
91
|
+
*/
|
|
92
|
+
function checkI18nCall(node) {
|
|
93
|
+
return extractI18nKey(node);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
module.exports = {
|
|
97
|
+
extractStringValue,
|
|
98
|
+
extractI18nKey,
|
|
99
|
+
checkI18nCall,
|
|
100
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const readline = require('readline');
|
|
2
|
+
|
|
3
|
+
const askQuestion = (query) => {
|
|
4
|
+
const rl = readline.createInterface({
|
|
5
|
+
input: process.stdin,
|
|
6
|
+
output: process.stdout,
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
return new Promise((resolve) =>
|
|
10
|
+
rl.question(query, (ans) => {
|
|
11
|
+
rl.close();
|
|
12
|
+
resolve(ans.toLowerCase());
|
|
13
|
+
}),
|
|
14
|
+
);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
module.exports = {
|
|
18
|
+
askQuestion,
|
|
19
|
+
};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
const XLSX = require('xlsx');
|
|
2
|
+
const fs = require('fs-extra');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 生成medusa excel文件
|
|
7
|
+
* @param {Object} params 参数对象
|
|
8
|
+
* @param {string} params.localesFilePath 翻译文件目录
|
|
9
|
+
* @param {Object} params.medusa medusa配置
|
|
10
|
+
* @param {string} params.medusa.appName 应用名称
|
|
11
|
+
* @param {string} params.medusa.group 分组名称
|
|
12
|
+
* @param {Object} params.medusa.keyMap 语言映射配置
|
|
13
|
+
* @param {Array} params.newKeys 新增的翻译key列表
|
|
14
|
+
* @returns {string} 生成的excel文件路径
|
|
15
|
+
*/
|
|
16
|
+
const generateMedusaExcel = async (params) => {
|
|
17
|
+
const { localesFilePath, medusa, newKeys, customData } = params;
|
|
18
|
+
|
|
19
|
+
if (!medusa || !medusa.appName || !medusa.group || !medusa.keyMap) {
|
|
20
|
+
throw new Error('medusa配置不完整,需要appName、group和keyMap');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// 读取所有翻译文件
|
|
24
|
+
const translationFiles = {};
|
|
25
|
+
|
|
26
|
+
// 读取中文翻译文件
|
|
27
|
+
try {
|
|
28
|
+
translationFiles['zh-cn'] = fs.readJsonSync(path.join(localesFilePath, 'zh-cn.json'));
|
|
29
|
+
} catch (error) {
|
|
30
|
+
console.log(' [警告] 未找到zh-cn.json文件');
|
|
31
|
+
translationFiles['zh-cn'] = {};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 根据keyMap读取其他语言文件
|
|
35
|
+
for (const [languageFile, columnName] of Object.entries(medusa.keyMap)) {
|
|
36
|
+
if (languageFile === 'zh-cn') continue; // 跳过中文,已经处理过了
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
translationFiles[languageFile] = fs.readJsonSync(
|
|
40
|
+
path.join(localesFilePath, `${languageFile}.json`),
|
|
41
|
+
);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
console.log(` [警告] 未找到${languageFile}.json文件`);
|
|
44
|
+
translationFiles[languageFile] = {};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 准备Excel数据
|
|
49
|
+
const excelData = [];
|
|
50
|
+
|
|
51
|
+
// 如果没有指定newKeys,则使用所有key
|
|
52
|
+
const keysToProcess =
|
|
53
|
+
newKeys && newKeys.length > 0
|
|
54
|
+
? newKeys
|
|
55
|
+
: customData
|
|
56
|
+
? Object.keys(customData)
|
|
57
|
+
: Object.keys(translationFiles['zh-cn'] || {});
|
|
58
|
+
|
|
59
|
+
for (const key of keysToProcess) {
|
|
60
|
+
const row = {
|
|
61
|
+
AppName: medusa.appName,
|
|
62
|
+
Group: medusa.group,
|
|
63
|
+
Key: key,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// 添加各语言的翻译
|
|
67
|
+
for (const [languageFile, columnName] of Object.entries(medusa.keyMap)) {
|
|
68
|
+
let translation = '';
|
|
69
|
+
|
|
70
|
+
if (customData) {
|
|
71
|
+
// 如果使用自定义数据,从customData中获取
|
|
72
|
+
translation = customData[key]?.[languageFile] || '';
|
|
73
|
+
} else {
|
|
74
|
+
// 否则从翻译文件中获取
|
|
75
|
+
translation = translationFiles[languageFile]?.[key] || '';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
row[columnName] = translation;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
excelData.push(row);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 创建工作簿
|
|
85
|
+
const workbook = XLSX.utils.book_new();
|
|
86
|
+
const worksheet = XLSX.utils.json_to_sheet(excelData);
|
|
87
|
+
|
|
88
|
+
// 设置列宽
|
|
89
|
+
const columnWidths = [
|
|
90
|
+
{ wch: 20 }, // AppName
|
|
91
|
+
{ wch: 15 }, // Group
|
|
92
|
+
{ wch: 30 }, // Key
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
// 为每种语言添加列宽
|
|
96
|
+
Object.keys(medusa.keyMap).forEach(() => {
|
|
97
|
+
columnWidths.push({ wch: 25 });
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
worksheet['!cols'] = columnWidths;
|
|
101
|
+
|
|
102
|
+
// 将工作表添加到工作簿
|
|
103
|
+
XLSX.utils.book_append_sheet(workbook, worksheet, 'Translations');
|
|
104
|
+
|
|
105
|
+
// 生成文件名(包含完整时间戳)
|
|
106
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').replace('T', '_');
|
|
107
|
+
const excelFileName = `medusa-translations-${timestamp}.xlsx`;
|
|
108
|
+
const excelFilePath = path.join(localesFilePath, excelFileName);
|
|
109
|
+
|
|
110
|
+
// 确保目录存在
|
|
111
|
+
fs.ensureDirSync(localesFilePath);
|
|
112
|
+
|
|
113
|
+
// 写入文件
|
|
114
|
+
XLSX.writeFile(workbook, excelFilePath);
|
|
115
|
+
|
|
116
|
+
console.log(` [完成] 已生成medusa excel文件: ${excelFilePath}`);
|
|
117
|
+
console.log(` [信息] 共包含${excelData.length}条翻译记录`);
|
|
118
|
+
|
|
119
|
+
return excelFilePath;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
module.exports = {
|
|
123
|
+
generateMedusaExcel,
|
|
124
|
+
};
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const find = require('find');
|
|
3
|
+
|
|
4
|
+
// 检查文件是否在忽略列表中
|
|
5
|
+
const shouldIgnoreFile = (file, doNotTranslateFiles) => {
|
|
6
|
+
if (!doNotTranslateFiles || !doNotTranslateFiles.length) return false;
|
|
7
|
+
|
|
8
|
+
return doNotTranslateFiles.some((pattern) => {
|
|
9
|
+
if (typeof pattern === 'string') {
|
|
10
|
+
return file.includes(pattern);
|
|
11
|
+
} else if (pattern instanceof RegExp) {
|
|
12
|
+
return pattern.test(file);
|
|
13
|
+
}
|
|
14
|
+
return false;
|
|
15
|
+
});
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// 添加导入语句到文件
|
|
19
|
+
const injectImportStatement = (filePath, importPath) => {
|
|
20
|
+
try {
|
|
21
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
22
|
+
const importStatement = `import $i18n from '${importPath}';\n`;
|
|
23
|
+
|
|
24
|
+
// 检查文件是否已经有这个导入语句
|
|
25
|
+
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');
|
|
35
|
+
} else {
|
|
36
|
+
// 如果没有导入语句,添加到文件开头
|
|
37
|
+
fs.writeFileSync(filePath, importStatement + content, 'utf8');
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
} catch (error) {
|
|
41
|
+
console.error(` [错误] 添加导入语句失败: ${filePath}`, error);
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 检查文件是否应该被忽略(基于第一行注释)
|
|
48
|
+
*/
|
|
49
|
+
const shouldIgnoreByComment = (filePath) => {
|
|
50
|
+
try {
|
|
51
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
52
|
+
const lines = content.split('\n');
|
|
53
|
+
|
|
54
|
+
// 检查第一行(去除空白字符)
|
|
55
|
+
const firstLine = lines[0].trim();
|
|
56
|
+
|
|
57
|
+
// 检查是否是注释且包含 @i18n-ignore
|
|
58
|
+
if (firstLine.startsWith('//') && firstLine.includes('@i18n-ignore')) {
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 检查是否是块注释
|
|
63
|
+
if (firstLine.startsWith('/*') && firstLine.includes('@i18n-ignore')) {
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 检查是否是 JSX 注释
|
|
68
|
+
if (firstLine.startsWith('{/*') && firstLine.includes('@i18n-ignore')) {
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return false;
|
|
73
|
+
} catch (error) {
|
|
74
|
+
// 如果读取文件失败,默认不忽略
|
|
75
|
+
console.warn(` [警告] 无法读取文件 ${filePath} 进行注释检查:`, error.message);
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 检查特定行是否应该被忽略(基于行注释)
|
|
82
|
+
* @param {string} filePath - 文件路径
|
|
83
|
+
* @param {number} lineNumber - 行号(从1开始)
|
|
84
|
+
* @returns {boolean} 是否应该忽略该行
|
|
85
|
+
*/
|
|
86
|
+
const shouldIgnoreLineByComment = (filePath, lineNumber) => {
|
|
87
|
+
try {
|
|
88
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
89
|
+
const lines = content.split('\n');
|
|
90
|
+
|
|
91
|
+
// 检查行号是否有效
|
|
92
|
+
if (lineNumber < 1 || lineNumber > lines.length) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const line = lines[lineNumber - 1]; // 数组索引从0开始
|
|
97
|
+
const trimmedLine = line.trim();
|
|
98
|
+
|
|
99
|
+
// 检查是否是单行注释且包含 @i18n-ignore-line
|
|
100
|
+
if (trimmedLine.startsWith('//') && trimmedLine.includes('@i18n-ignore-line')) {
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 检查是否是块注释且包含 @i18n-ignore-line
|
|
105
|
+
if (trimmedLine.startsWith('/*') && trimmedLine.includes('@i18n-ignore-line')) {
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 检查是否是 JSX 注释且包含 @i18n-ignore-line
|
|
110
|
+
if (trimmedLine.startsWith('{/*') && trimmedLine.includes('@i18n-ignore-line')) {
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// 检查同一行是否包含注释中的 @i18n-ignore-line(可能在代码后面)
|
|
115
|
+
if (line.includes('@i18n-ignore-line')) {
|
|
116
|
+
// 检查是否在注释中
|
|
117
|
+
const commentIndex = line.indexOf('@i18n-ignore-line');
|
|
118
|
+
const beforeComment = line.substring(0, commentIndex).trim();
|
|
119
|
+
if (
|
|
120
|
+
beforeComment.includes('//') ||
|
|
121
|
+
beforeComment.includes('/*') ||
|
|
122
|
+
beforeComment.includes('{/*')
|
|
123
|
+
) {
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return false;
|
|
129
|
+
} catch (error) {
|
|
130
|
+
// 如果读取文件失败,默认不忽略
|
|
131
|
+
console.warn(` [警告] 无法读取文件 ${filePath} 进行行注释检查:`, error.message);
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* 检查特定行是否在忽略块中(基于块注释)
|
|
138
|
+
* @param {string} filePath - 文件路径
|
|
139
|
+
* @param {number} lineNumber - 行号(从1开始)
|
|
140
|
+
* @returns {boolean} 是否应该忽略该行
|
|
141
|
+
*/
|
|
142
|
+
const shouldIgnoreBlockByComment = (filePath, lineNumber) => {
|
|
143
|
+
try {
|
|
144
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
145
|
+
const lines = content.split('\n');
|
|
146
|
+
|
|
147
|
+
// 检查行号是否有效
|
|
148
|
+
if (lineNumber < 1 || lineNumber > lines.length) {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// 查找所有的 @i18n-ignore-block-start 和 @i18n-ignore-block-end 标记
|
|
153
|
+
const blockStarts = [];
|
|
154
|
+
const blockEnds = [];
|
|
155
|
+
|
|
156
|
+
lines.forEach((line, index) => {
|
|
157
|
+
const lineNum = index + 1;
|
|
158
|
+
const trimmedLine = line.trim();
|
|
159
|
+
|
|
160
|
+
// 检查是否是注释中的 @i18n-ignore-block-start
|
|
161
|
+
if (
|
|
162
|
+
(trimmedLine.startsWith('//') ||
|
|
163
|
+
trimmedLine.startsWith('/*') ||
|
|
164
|
+
trimmedLine.startsWith('{/*')) &&
|
|
165
|
+
line.includes('@i18n-ignore-block-start')
|
|
166
|
+
) {
|
|
167
|
+
blockStarts.push(lineNum);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// 检查是否是注释中的 @i18n-ignore-block-end
|
|
171
|
+
if (
|
|
172
|
+
(trimmedLine.startsWith('//') ||
|
|
173
|
+
trimmedLine.startsWith('/*') ||
|
|
174
|
+
trimmedLine.startsWith('{/*')) &&
|
|
175
|
+
line.includes('@i18n-ignore-block-end')
|
|
176
|
+
) {
|
|
177
|
+
blockEnds.push(lineNum);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// 检查同一行是否包含注释中的标记(可能在代码后面)
|
|
181
|
+
if (line.includes('@i18n-ignore-block-start')) {
|
|
182
|
+
// 检查是否在注释中
|
|
183
|
+
const commentIndex = line.indexOf('@i18n-ignore-block-start');
|
|
184
|
+
const beforeComment = line.substring(0, commentIndex).trim();
|
|
185
|
+
if (
|
|
186
|
+
beforeComment.includes('//') ||
|
|
187
|
+
beforeComment.includes('/*') ||
|
|
188
|
+
beforeComment.includes('{/*')
|
|
189
|
+
) {
|
|
190
|
+
blockStarts.push(lineNum);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (line.includes('@i18n-ignore-block-end')) {
|
|
195
|
+
// 检查是否在注释中
|
|
196
|
+
const commentIndex = line.indexOf('@i18n-ignore-block-end');
|
|
197
|
+
const beforeComment = line.substring(0, commentIndex).trim();
|
|
198
|
+
if (
|
|
199
|
+
beforeComment.includes('//') ||
|
|
200
|
+
beforeComment.includes('/*') ||
|
|
201
|
+
beforeComment.includes('{/*')
|
|
202
|
+
) {
|
|
203
|
+
blockEnds.push(lineNum);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// 检查当前行是否在任何忽略块中
|
|
209
|
+
for (let i = 0; i < blockStarts.length; i++) {
|
|
210
|
+
const startLine = blockStarts[i];
|
|
211
|
+
// 找到对应的end标记(最近的end)
|
|
212
|
+
let endLine = null;
|
|
213
|
+
for (let j = 0; j < blockEnds.length; j++) {
|
|
214
|
+
if (blockEnds[j] > startLine) {
|
|
215
|
+
endLine = blockEnds[j];
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// 如果找到了对应的end标记,检查当前行是否在这个范围内
|
|
221
|
+
if (endLine && lineNumber >= startLine && lineNumber <= endLine) {
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return false;
|
|
227
|
+
} catch (error) {
|
|
228
|
+
// 如果读取文件失败,默认不忽略
|
|
229
|
+
console.warn(` [警告] 无法读取文件 ${filePath} 进行块注释检查:`, error.message);
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* 文件扫描,过滤不需要翻译白名单的文件,选取符合fileType类型的文件
|
|
236
|
+
*/
|
|
237
|
+
const scanFiles = (path, fileType, doNotTranslateFiles = []) => {
|
|
238
|
+
try {
|
|
239
|
+
const fileTypes = fileType.split(',').map((s) => s.trim());
|
|
240
|
+
const re = new RegExp(`(${fileTypes.map((ft) => ft.replace('.', '\\.')).join('|')})$`);
|
|
241
|
+
const files = find.fileSync(re, path);
|
|
242
|
+
|
|
243
|
+
// 过滤掉在忽略列表中的文件
|
|
244
|
+
let filteredFiles = files.filter((file) => !shouldIgnoreFile(file, doNotTranslateFiles));
|
|
245
|
+
|
|
246
|
+
// 过滤掉有 @i18n-ignore 注释的文件
|
|
247
|
+
const ignoredByComment = [];
|
|
248
|
+
filteredFiles = filteredFiles.filter((file) => {
|
|
249
|
+
if (shouldIgnoreByComment(file)) {
|
|
250
|
+
ignoredByComment.push(file);
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
return true;
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
if (ignoredByComment.length > 0) {
|
|
257
|
+
console.log(` # 发现 ${ignoredByComment.length} 个文件包含 @i18n-ignore 注释,已跳过:`);
|
|
258
|
+
ignoredByComment.forEach((file) => {
|
|
259
|
+
console.log(` # - ${file}`);
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
console.log(
|
|
264
|
+
` # 过滤不需翻译白名单后,找到符合${fileType}类型的文件:${filteredFiles.length}个`,
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
return filteredFiles;
|
|
268
|
+
} catch (error) {
|
|
269
|
+
console.error(' [错误] 文件扫描失败:', error);
|
|
270
|
+
throw error;
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
module.exports = {
|
|
275
|
+
shouldIgnoreFile,
|
|
276
|
+
injectImportStatement,
|
|
277
|
+
scanFiles,
|
|
278
|
+
shouldIgnoreLineByComment,
|
|
279
|
+
shouldIgnoreBlockByComment,
|
|
280
|
+
};
|