vue-i18n-extract-plugin 1.0.47
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/.prettierrc +8 -0
- package/LICENSE +20 -0
- package/README.md +292 -0
- package/lib/babel-plugin-i18n.js +13 -0
- package/lib/babel-plugin-import-i18n.js +154 -0
- package/lib/cli.js +54 -0
- package/lib/extract.js +564 -0
- package/lib/import-i18n-transform.js +146 -0
- package/lib/index.js +74 -0
- package/lib/options.js +39 -0
- package/lib/translate.js +308 -0
- package/lib/translators/baidu.js +91 -0
- package/lib/translators/google.js +68 -0
- package/lib/translators/index.js +15 -0
- package/lib/translators/scan.js +24 -0
- package/lib/translators/translator/IntervalQueue.js +60 -0
- package/lib/translators/translator/Translator.js +62 -0
- package/lib/translators/translator/index.js +5 -0
- package/lib/translators/volcengine.js +131 -0
- package/lib/translators/youdao.js +89 -0
- package/lib/utils.js +234 -0
- package/lib/visitors.js +246 -0
- package/lib/vite-plugin-i18n.js +73 -0
- package/lib/vite-plugin-import-i18n.js +46 -0
- package/lib/vue-i18n-loader.js +54 -0
- package/lib/webpack-import-i18n-loader.js +69 -0
- package/lib/webpack-plugin-i18n.js +176 -0
- package/package.json +47 -0
package/lib/extract.js
ADDED
|
@@ -0,0 +1,564 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
const fs = require("fs-extra");
|
|
3
|
+
const glob = require("fast-glob");
|
|
4
|
+
const parser = require("@babel/parser");
|
|
5
|
+
const traverse = require("@babel/traverse").default;
|
|
6
|
+
const generate = require("@babel/generator").default;
|
|
7
|
+
const t = require("@babel/types");
|
|
8
|
+
const { parse: parseSFC } = require("@vue/compiler-sfc");
|
|
9
|
+
const {
|
|
10
|
+
parse: parseTemplate,
|
|
11
|
+
transform,
|
|
12
|
+
// generate: genTemplate,
|
|
13
|
+
getBaseTransformPreset,
|
|
14
|
+
NodeTypes
|
|
15
|
+
} = require("@vue/compiler-dom");
|
|
16
|
+
const MagicString = require("magic-string");
|
|
17
|
+
const prettier = require("prettier");
|
|
18
|
+
const { createI18nVisitor } = require("./visitors");
|
|
19
|
+
const {
|
|
20
|
+
relativeCWDPath,
|
|
21
|
+
shouldExtract,
|
|
22
|
+
trimEmptyLine,
|
|
23
|
+
padEmptyLine,
|
|
24
|
+
isEmptyObject,
|
|
25
|
+
getLangJsonPath,
|
|
26
|
+
readJsonWithDefault
|
|
27
|
+
} = require("./utils");
|
|
28
|
+
const { defaultOptions } = require("./options");
|
|
29
|
+
const { autoTranslate, cleanTranslate, cleanI18nMap } = require("./translate");
|
|
30
|
+
const { i18nImportAstTransform } = require("./import-i18n-transform");
|
|
31
|
+
|
|
32
|
+
let globalI18nMap = {};
|
|
33
|
+
|
|
34
|
+
function encodeToString(str) {
|
|
35
|
+
return str.indexOf("'") === -1 ? `'${str}'` : `"${str}"`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function rebuildPattern(p, extensions) {
|
|
39
|
+
if (path.extname(p) !== "") {
|
|
40
|
+
return p;
|
|
41
|
+
}
|
|
42
|
+
if (p.endsWith("/")) {
|
|
43
|
+
p = p.slice(0, -1);
|
|
44
|
+
}
|
|
45
|
+
if (p.endsWith("**")) {
|
|
46
|
+
return `${p}/*.{${extensions}}`;
|
|
47
|
+
}
|
|
48
|
+
if (p.endsWith("*")) {
|
|
49
|
+
return `${p}.{${extensions}}`;
|
|
50
|
+
}
|
|
51
|
+
return `${p}/**/*.{${extensions}}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function wrapAsStatement(expression) {
|
|
55
|
+
return `const _expr = ${expression};`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function unwrapTransformedCode(ast) {
|
|
59
|
+
let extractedNode = null;
|
|
60
|
+
|
|
61
|
+
traverse(ast, {
|
|
62
|
+
VariableDeclarator(path) {
|
|
63
|
+
if (path.node.id.name === "_expr") {
|
|
64
|
+
extractedNode = path.node.init;
|
|
65
|
+
path.stop();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
if (extractedNode) {
|
|
71
|
+
const { code: exprCode } = generate(extractedNode);
|
|
72
|
+
return exprCode;
|
|
73
|
+
} else {
|
|
74
|
+
console.warn("未能提取到 _expr 表达式");
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function transformScriptExpression(expression, options, i18nMap) {
|
|
80
|
+
const wrapped = wrapAsStatement(expression);
|
|
81
|
+
const transformedAst = transformScript(
|
|
82
|
+
wrapped,
|
|
83
|
+
{ ...options, autoImportI18n: false },
|
|
84
|
+
true,
|
|
85
|
+
i18nMap
|
|
86
|
+
);
|
|
87
|
+
const transformedCode = unwrapTransformedCode(transformedAst);
|
|
88
|
+
|
|
89
|
+
// if (useAst) {
|
|
90
|
+
// // 使用 transformScript 解析之后确保返回 CallExpression
|
|
91
|
+
// const ast = parser.parseExpression(transformedCode);
|
|
92
|
+
|
|
93
|
+
// // 判断是否是 CallExpression 类型,并返回
|
|
94
|
+
// if (t.isCallExpression(ast)) {
|
|
95
|
+
// return ast;
|
|
96
|
+
// } else {
|
|
97
|
+
// throw new Error(`Expected CallExpression but got: ${ast.type}`);
|
|
98
|
+
// }
|
|
99
|
+
// }
|
|
100
|
+
return transformedCode;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function extractTCallsFromInterpolation(code, options, i18nMap) {
|
|
104
|
+
let ast;
|
|
105
|
+
try {
|
|
106
|
+
// 这是一个 JS 表达式,不是完整语句,所以使用 parseExpression
|
|
107
|
+
ast = parser.parseExpression(code, {
|
|
108
|
+
plugins: ["typescript"]
|
|
109
|
+
});
|
|
110
|
+
} catch (e) {
|
|
111
|
+
console.error("插值解析失败:", e.message);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 包装成完整的 AST
|
|
116
|
+
const program = t.program([t.expressionStatement(ast)]);
|
|
117
|
+
|
|
118
|
+
traverse(program, createI18nVisitor(options, i18nMap));
|
|
119
|
+
|
|
120
|
+
if (options.rewrite) {
|
|
121
|
+
return generate(ast, { compact: true }).code;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// return ast;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function createInterpolationNode(content) {
|
|
128
|
+
const interpolationNode = {
|
|
129
|
+
type: NodeTypes.INTERPOLATION,
|
|
130
|
+
content: {
|
|
131
|
+
type: NodeTypes.SIMPLE_EXPRESSION,
|
|
132
|
+
content
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
return interpolationNode;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function createDirectiveFromProp(prop, content) {
|
|
139
|
+
function getDirectiveExpression() {
|
|
140
|
+
if (content) return content;
|
|
141
|
+
if (prop.value) {
|
|
142
|
+
return `'${prop.value.content}'`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return "true";
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
type: NodeTypes.DIRECTIVE,
|
|
150
|
+
name: "bind",
|
|
151
|
+
rawName: `:${prop.name}`,
|
|
152
|
+
exp: {
|
|
153
|
+
type: NodeTypes.SIMPLE_EXPRESSION,
|
|
154
|
+
content: getDirectiveExpression(),
|
|
155
|
+
isStatic: false
|
|
156
|
+
},
|
|
157
|
+
arg: {
|
|
158
|
+
type: NodeTypes.SIMPLE_EXPRESSION,
|
|
159
|
+
content: prop.name,
|
|
160
|
+
isStatic: true
|
|
161
|
+
},
|
|
162
|
+
modifiers: [],
|
|
163
|
+
loc: prop.loc
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function addI18nImportIfNeeded(ast, options, generateCode) {
|
|
168
|
+
i18nImportAstTransform(ast, options.translateKey, options.i18nPkgImportPath);
|
|
169
|
+
|
|
170
|
+
if (generateCode) {
|
|
171
|
+
return generate(ast);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return ast;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function writeI18nMapToFile(i18nMap, options, checkDiffs) {
|
|
178
|
+
const outputJSONPath = getLangJsonPath(options.fromLang, options);
|
|
179
|
+
let originalJson;
|
|
180
|
+
|
|
181
|
+
if (checkDiffs) {
|
|
182
|
+
// 检查是否有差异
|
|
183
|
+
originalJson = readJsonWithDefault(outputJSONPath, null);
|
|
184
|
+
if (originalJson) {
|
|
185
|
+
const i18nMapKeys = Object.keys(i18nMap);
|
|
186
|
+
let hasDiff = i18nMapKeys.length !== Object.keys(originalJson).length;
|
|
187
|
+
if (!hasDiff) {
|
|
188
|
+
hasDiff = i18nMapKeys.some(key => i18nMap[key] !== originalJson[key]);
|
|
189
|
+
}
|
|
190
|
+
if (!hasDiff) {
|
|
191
|
+
console.warn("新的 i18nMap 与源文件没有差异,跳过写入文件...");
|
|
192
|
+
return Promise.resolve({ hasDiff: false, data: i18nMap });
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
originalJson = originalJson ?? readJsonWithDefault(outputJSONPath, null);
|
|
198
|
+
if (originalJson) {
|
|
199
|
+
if (options.rewrite) {
|
|
200
|
+
i18nMap = Object.assign(originalJson, i18nMap);
|
|
201
|
+
} else {
|
|
202
|
+
i18nMap = Object.assign(cleanI18nMap(i18nMap, originalJson), i18nMap);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
await fs.outputJson(outputJSONPath, i18nMap, {
|
|
207
|
+
spaces: 2
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
return Promise.resolve({ hasDiff: true, data: i18nMap });
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function handleFinalI18nMap(i18nMap, options, checkDiffs) {
|
|
214
|
+
const { hasDiff } = await writeI18nMapToFile(i18nMap, options, checkDiffs);
|
|
215
|
+
|
|
216
|
+
if (!hasDiff) return;
|
|
217
|
+
|
|
218
|
+
if (options.autoTranslate) {
|
|
219
|
+
await autoTranslate(i18nMap, options);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (options.cleanTranslate) {
|
|
223
|
+
await cleanTranslate(options);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function transformScript(code, options, useAst, i18nMap) {
|
|
228
|
+
const innerI18nMap = i18nMap || {};
|
|
229
|
+
const ast = parser.parse(code, {
|
|
230
|
+
sourceType: "module",
|
|
231
|
+
plugins: ["typescript", "jsx"]
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
traverse(ast, createI18nVisitor(options, innerI18nMap));
|
|
235
|
+
|
|
236
|
+
const isEmpty = isEmptyObject(innerI18nMap);
|
|
237
|
+
|
|
238
|
+
if (!isEmpty) {
|
|
239
|
+
if (options.autoImportI18n && options.rewrite) {
|
|
240
|
+
addI18nImportIfNeeded(ast, options);
|
|
241
|
+
}
|
|
242
|
+
Object.assign(globalI18nMap, innerI18nMap);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (useAst) {
|
|
246
|
+
return ast;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (options.rewrite) {
|
|
250
|
+
if (isEmpty) {
|
|
251
|
+
return { changed: false, code };
|
|
252
|
+
}
|
|
253
|
+
return {
|
|
254
|
+
changed: true,
|
|
255
|
+
code: generate(ast, { retainLines: true }).code
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return {
|
|
260
|
+
changed: false,
|
|
261
|
+
code
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function transformTemplate(templateContent, options) {
|
|
266
|
+
const innerI18nMap = {};
|
|
267
|
+
let magicString;
|
|
268
|
+
|
|
269
|
+
if (options.rewrite) {
|
|
270
|
+
magicString = new MagicString(templateContent);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const ast = parseTemplate(templateContent, {
|
|
274
|
+
comments: true, // 保留注释
|
|
275
|
+
whitespace: "preserve" // 保留空白
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
const [nodeTransforms, directiveTransforms] = getBaseTransformPreset(true);
|
|
279
|
+
|
|
280
|
+
transform(ast, {
|
|
281
|
+
nodeTransforms: [
|
|
282
|
+
...nodeTransforms,
|
|
283
|
+
node => {
|
|
284
|
+
if (node.type === NodeTypes.INTERPOLATION) {
|
|
285
|
+
const { content } = node;
|
|
286
|
+
if (!content) return;
|
|
287
|
+
const text = content.content.replace(/\n+/g, " ").trim();
|
|
288
|
+
if (!text) return;
|
|
289
|
+
try {
|
|
290
|
+
let newContent = "";
|
|
291
|
+
// {{ '中文' }} 不包含 $t
|
|
292
|
+
if (text.indexOf(options.translateKey) === -1) {
|
|
293
|
+
newContent = transformScriptExpression(
|
|
294
|
+
text,
|
|
295
|
+
options,
|
|
296
|
+
innerI18nMap
|
|
297
|
+
);
|
|
298
|
+
} else {
|
|
299
|
+
newContent = extractTCallsFromInterpolation(
|
|
300
|
+
text,
|
|
301
|
+
options,
|
|
302
|
+
innerI18nMap
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
if (newContent) {
|
|
306
|
+
content.content = newContent;
|
|
307
|
+
magicString?.overwrite(
|
|
308
|
+
node.loc.start.offset,
|
|
309
|
+
node.loc.end.offset,
|
|
310
|
+
`{{ ${newContent} }}`
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
} catch (e) {
|
|
314
|
+
console.error(`模板表达式解析失败: text: ${text}, err: ${e}`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
if (node.type === NodeTypes.TEXT) {
|
|
318
|
+
let { content: text } = node;
|
|
319
|
+
try {
|
|
320
|
+
text = text.replace(/\n+/g, " ").trim();
|
|
321
|
+
if (!text) return;
|
|
322
|
+
const newContent = transformScriptExpression(
|
|
323
|
+
encodeToString(text),
|
|
324
|
+
options,
|
|
325
|
+
innerI18nMap
|
|
326
|
+
);
|
|
327
|
+
if (newContent) {
|
|
328
|
+
// node.content = newContent;
|
|
329
|
+
|
|
330
|
+
// 创建一个新的 INTERPOLATION 节点
|
|
331
|
+
const interpolationNode = createInterpolationNode(newContent);
|
|
332
|
+
|
|
333
|
+
// 替换原来的 TEXT 节点为 INTERPOLATION 节点
|
|
334
|
+
Object.assign(node, interpolationNode);
|
|
335
|
+
|
|
336
|
+
magicString?.overwrite(
|
|
337
|
+
node.loc.start.offset,
|
|
338
|
+
node.loc.end.offset,
|
|
339
|
+
`{{ ${newContent} }}`
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
} catch (e) {
|
|
343
|
+
console.error(`文本解析失败: text: ${text}, err: ${e}`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
if (node.type === NodeTypes.ELEMENT) {
|
|
347
|
+
node.props.forEach((prop, index) => {
|
|
348
|
+
let text = "";
|
|
349
|
+
if (prop.type === NodeTypes.ATTRIBUTE) {
|
|
350
|
+
text = prop.value?.content;
|
|
351
|
+
if (
|
|
352
|
+
text &&
|
|
353
|
+
typeof text === "string" &&
|
|
354
|
+
shouldExtract(text, options.fromLang)
|
|
355
|
+
) {
|
|
356
|
+
try {
|
|
357
|
+
const newValue = transformScriptExpression(
|
|
358
|
+
encodeToString(text.trim()),
|
|
359
|
+
options,
|
|
360
|
+
innerI18nMap
|
|
361
|
+
);
|
|
362
|
+
if (newValue) {
|
|
363
|
+
// prop.value.content = newValue;
|
|
364
|
+
const directive = createDirectiveFromProp(prop, newValue);
|
|
365
|
+
// 将原属性替换为指令
|
|
366
|
+
node.props[index] = directive;
|
|
367
|
+
|
|
368
|
+
magicString?.overwrite(
|
|
369
|
+
prop.loc.start.offset,
|
|
370
|
+
prop.loc.end.offset,
|
|
371
|
+
`:${prop.name}="${newValue.replace(/"/g, "'")}"`
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
} catch (e) {
|
|
375
|
+
console.error(`静态属性解析失败: text: ${text}, err: ${e}`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
if (prop.type === NodeTypes.DIRECTIVE && prop.name === "bind") {
|
|
380
|
+
const arg = prop.arg;
|
|
381
|
+
if (
|
|
382
|
+
arg?.type === NodeTypes.SIMPLE_EXPRESSION &&
|
|
383
|
+
arg.content === "key"
|
|
384
|
+
) {
|
|
385
|
+
return; // 忽略 key
|
|
386
|
+
}
|
|
387
|
+
text = prop.exp?.content;
|
|
388
|
+
if (
|
|
389
|
+
text &&
|
|
390
|
+
typeof text === "string" &&
|
|
391
|
+
shouldExtract(text, options.fromLang)
|
|
392
|
+
) {
|
|
393
|
+
try {
|
|
394
|
+
const newValue = extractTCallsFromInterpolation(
|
|
395
|
+
text.trim(),
|
|
396
|
+
options,
|
|
397
|
+
innerI18nMap
|
|
398
|
+
);
|
|
399
|
+
if (newValue) {
|
|
400
|
+
prop.exp.content = newValue;
|
|
401
|
+
|
|
402
|
+
magicString?.overwrite(
|
|
403
|
+
prop.loc.start.offset,
|
|
404
|
+
prop.loc.end.offset,
|
|
405
|
+
`${prop.rawName}="${newValue.replace(/"/g, "'")}"`
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
} catch (e) {
|
|
409
|
+
console.error(`动态指令解析失败: text: ${text}, err: ${e}`);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
],
|
|
417
|
+
directiveTransforms
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
const isEmpty = isEmptyObject(innerI18nMap);
|
|
421
|
+
|
|
422
|
+
if (!isEmpty) {
|
|
423
|
+
Object.assign(globalI18nMap, innerI18nMap);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (options.rewrite) {
|
|
427
|
+
if (isEmpty) {
|
|
428
|
+
return {
|
|
429
|
+
changed: false,
|
|
430
|
+
code: templateContent
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// if (options.sourcemap) {
|
|
435
|
+
// const map = magicString.generateMap({
|
|
436
|
+
// source: filePath,
|
|
437
|
+
// hires: true
|
|
438
|
+
// });
|
|
439
|
+
|
|
440
|
+
// return {
|
|
441
|
+
// changed: true,
|
|
442
|
+
// code: trimEmptyLine(magicString.toString()),
|
|
443
|
+
// map
|
|
444
|
+
// };
|
|
445
|
+
// }
|
|
446
|
+
|
|
447
|
+
return {
|
|
448
|
+
changed: true,
|
|
449
|
+
code: trimEmptyLine(magicString.toString())
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
// return {
|
|
453
|
+
// changed: true,
|
|
454
|
+
// code: genTemplate(ast).code
|
|
455
|
+
// };
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return {
|
|
459
|
+
changed: false,
|
|
460
|
+
code: templateContent
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function processVueFile(code, options) {
|
|
465
|
+
const sfc = parseSFC(code);
|
|
466
|
+
|
|
467
|
+
let transformedScript = "";
|
|
468
|
+
let transformedTemplate = "";
|
|
469
|
+
let scriptChanged = false;
|
|
470
|
+
let templateChanged = false;
|
|
471
|
+
|
|
472
|
+
const scriptContent =
|
|
473
|
+
sfc.descriptor.script?.content || sfc.descriptor.scriptSetup?.content || "";
|
|
474
|
+
const templateContent = sfc.descriptor.template?.content || "";
|
|
475
|
+
|
|
476
|
+
if (scriptContent) {
|
|
477
|
+
const scriptResult = transformScript(scriptContent, options);
|
|
478
|
+
scriptChanged = scriptResult.changed;
|
|
479
|
+
transformedScript = scriptResult.code;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (templateContent) {
|
|
483
|
+
const templateResult = transformTemplate(templateContent, options);
|
|
484
|
+
templateChanged = templateResult.changed;
|
|
485
|
+
transformedTemplate = templateResult.code;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (options.rewrite && (scriptChanged || templateChanged)) {
|
|
489
|
+
return {
|
|
490
|
+
changed: true,
|
|
491
|
+
code: code
|
|
492
|
+
.replace(scriptContent, padEmptyLine(transformedScript))
|
|
493
|
+
.replace(templateContent, padEmptyLine(transformedTemplate))
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
return { changed: false, code };
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
async function formatFile(code, filePath) {
|
|
501
|
+
const options = await prettier.resolveConfig(filePath);
|
|
502
|
+
return await prettier.format(code, {
|
|
503
|
+
...options,
|
|
504
|
+
filepath: filePath
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
async function extractI18n(options) {
|
|
509
|
+
options = { ...defaultOptions, ...options };
|
|
510
|
+
|
|
511
|
+
let includePath = Array.isArray(options.includePath)
|
|
512
|
+
? options.includePath
|
|
513
|
+
: [options.includePath];
|
|
514
|
+
|
|
515
|
+
includePath = includePath.map(p => {
|
|
516
|
+
if (p instanceof RegExp) {
|
|
517
|
+
p = p.source.replace(/\\/g, "").replace(/\/\//, "/");
|
|
518
|
+
}
|
|
519
|
+
return relativeCWDPath(p);
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
const extensions = options.allowedExtensions
|
|
523
|
+
.map(s => s.replace(/^\./, ""))
|
|
524
|
+
.join(",");
|
|
525
|
+
const globPattern = includePath.map(p => rebuildPattern(p, extensions));
|
|
526
|
+
const files = await glob(globPattern, { ignore: options.excludedPath });
|
|
527
|
+
|
|
528
|
+
for (const file of files) {
|
|
529
|
+
const content = await fs.readFile(file, "utf8");
|
|
530
|
+
|
|
531
|
+
let changed = false;
|
|
532
|
+
let code = "";
|
|
533
|
+
|
|
534
|
+
if (!content.trim()) continue;
|
|
535
|
+
|
|
536
|
+
try {
|
|
537
|
+
if (file.endsWith(".vue")) {
|
|
538
|
+
const vueContent = processVueFile(content, options);
|
|
539
|
+
changed = vueContent.changed;
|
|
540
|
+
code = vueContent.code;
|
|
541
|
+
} else {
|
|
542
|
+
const scriptContent = transformScript(content, options);
|
|
543
|
+
changed = scriptContent.changed;
|
|
544
|
+
code = scriptContent.code;
|
|
545
|
+
}
|
|
546
|
+
} catch (err) {
|
|
547
|
+
console.error(`processing error with file ${file}`, err);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
if (options.rewrite && changed) {
|
|
551
|
+
code = await formatFile(code, file);
|
|
552
|
+
await fs.writeFile(file, code, "utf8");
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
await handleFinalI18nMap(globalI18nMap, options);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
module.exports = {
|
|
560
|
+
extractI18n,
|
|
561
|
+
writeI18nMapToFile,
|
|
562
|
+
handleFinalI18nMap,
|
|
563
|
+
globalI18nMap
|
|
564
|
+
};
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
const parser = 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
|
+
function i18nImportAstTransform(ast, importName, importPath) {
|
|
7
|
+
let hasI18nImport = false;
|
|
8
|
+
let lastImportNode = null;
|
|
9
|
+
let needTransform = false;
|
|
10
|
+
let conflictTDefined = false;
|
|
11
|
+
|
|
12
|
+
traverse(ast, {
|
|
13
|
+
ImportDeclaration(path) {
|
|
14
|
+
lastImportNode = path.node;
|
|
15
|
+
|
|
16
|
+
const sourcePath = path.node.source.value;
|
|
17
|
+
|
|
18
|
+
// 判断是否是其它路径导入了 $t
|
|
19
|
+
const importedTElsewhere = path.node.specifiers.some(spec => {
|
|
20
|
+
return (
|
|
21
|
+
(t.isImportSpecifier(spec) || t.isImportDefaultSpecifier(spec)) &&
|
|
22
|
+
spec.local.name === importName &&
|
|
23
|
+
sourcePath !== importPath
|
|
24
|
+
);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
if (importedTElsewhere) {
|
|
28
|
+
conflictTDefined = true;
|
|
29
|
+
path.stop();
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 检查是否已经导入目标路径
|
|
34
|
+
if (sourcePath === importPath) {
|
|
35
|
+
hasI18nImport = true;
|
|
36
|
+
|
|
37
|
+
// 情况1:已有 import { $t } from '@/i18n'
|
|
38
|
+
const existImport = path.node.specifiers.some(
|
|
39
|
+
spec => t.isImportSpecifier(spec) && spec.imported.name === importName
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
if (existImport) return;
|
|
43
|
+
|
|
44
|
+
// 添加 $t 到已有的 import
|
|
45
|
+
path.node.specifiers.push(
|
|
46
|
+
t.importSpecifier(t.identifier(importName), t.identifier(importName))
|
|
47
|
+
);
|
|
48
|
+
needTransform = true;
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
VariableDeclarator(path) {
|
|
52
|
+
if (t.isIdentifier(path.node.id) && path.node.id.name === importName) {
|
|
53
|
+
conflictTDefined = true;
|
|
54
|
+
path.stop();
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
FunctionDeclaration(path) {
|
|
59
|
+
if (t.isIdentifier(path.node.id) && path.node.id.name === importName) {
|
|
60
|
+
conflictTDefined = true;
|
|
61
|
+
path.stop();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// 跳过导入语句的生成
|
|
67
|
+
if (conflictTDefined) {
|
|
68
|
+
return {
|
|
69
|
+
ast,
|
|
70
|
+
needTransform: false
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 情况4:完全没有导入 @/i18n
|
|
75
|
+
if (!hasI18nImport) {
|
|
76
|
+
const importNode = t.importDeclaration(
|
|
77
|
+
[
|
|
78
|
+
// t.importDefaultSpecifier(t.identifier("i18n")),
|
|
79
|
+
t.importSpecifier(t.identifier(importName), t.identifier(importName))
|
|
80
|
+
],
|
|
81
|
+
t.stringLiteral(importPath)
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
if (lastImportNode) {
|
|
85
|
+
ast.program.body.splice(
|
|
86
|
+
ast.program.body.indexOf(lastImportNode) + 1,
|
|
87
|
+
0,
|
|
88
|
+
importNode
|
|
89
|
+
);
|
|
90
|
+
} else {
|
|
91
|
+
ast.program.body.unshift(importNode);
|
|
92
|
+
}
|
|
93
|
+
needTransform = true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
ast,
|
|
98
|
+
needTransform
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function i18nImportTransform(code, path, importName, importPath) {
|
|
103
|
+
const scriptContent = extractScriptContent(code, path);
|
|
104
|
+
if (!scriptContent) return code;
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
const ast = parser.parse(scriptContent, {
|
|
108
|
+
sourceType: "module",
|
|
109
|
+
plugins: [
|
|
110
|
+
"typescript",
|
|
111
|
+
"jsx",
|
|
112
|
+
path.endsWith(".vue") ? "topLevelAwait" : null
|
|
113
|
+
].filter(Boolean)
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const { needTransform } = i18nImportAstTransform(
|
|
117
|
+
ast,
|
|
118
|
+
importName,
|
|
119
|
+
importPath
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
// 只有当需要修改时才重新生成代码
|
|
123
|
+
if (needTransform) {
|
|
124
|
+
const { code: newScript } = generate(ast);
|
|
125
|
+
return path.endsWith(".vue")
|
|
126
|
+
? code.replace(scriptContent, newScript)
|
|
127
|
+
: newScript;
|
|
128
|
+
}
|
|
129
|
+
return code;
|
|
130
|
+
} catch (err) {
|
|
131
|
+
console.warn(`[auto-import-i18n] Failed to parse ${path}:`, err);
|
|
132
|
+
return code;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function extractScriptContent(code, path) {
|
|
137
|
+
if (!path.endsWith(".vue")) return code;
|
|
138
|
+
const scriptMatch = code.match(/<script\b[^>]*>([\s\S]*?)<\/script>/);
|
|
139
|
+
return scriptMatch?.[1] || "";
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
module.exports = {
|
|
143
|
+
i18nImportAstTransform,
|
|
144
|
+
i18nImportTransform,
|
|
145
|
+
extractScriptContent
|
|
146
|
+
};
|