must-cli 1.9.0
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/LICENSE +22 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +192 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +5 -0
- package/dist/cli.js.map +1 -0
- package/dist/config/index.d.ts +16 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +142 -0
- package/dist/config/index.js.map +1 -0
- package/dist/extractors/base.d.ts +59 -0
- package/dist/extractors/base.d.ts.map +1 -0
- package/dist/extractors/base.js +204 -0
- package/dist/extractors/base.js.map +1 -0
- package/dist/extractors/html.d.ts +8 -0
- package/dist/extractors/html.d.ts.map +1 -0
- package/dist/extractors/html.js +70 -0
- package/dist/extractors/html.js.map +1 -0
- package/dist/extractors/index.d.ts +33 -0
- package/dist/extractors/index.d.ts.map +1 -0
- package/dist/extractors/index.js +86 -0
- package/dist/extractors/index.js.map +1 -0
- package/dist/extractors/javascript.d.ts +17 -0
- package/dist/extractors/javascript.d.ts.map +1 -0
- package/dist/extractors/javascript.js +249 -0
- package/dist/extractors/javascript.js.map +1 -0
- package/dist/extractors/vue.d.ts +9 -0
- package/dist/extractors/vue.d.ts.map +1 -0
- package/dist/extractors/vue.js +99 -0
- package/dist/extractors/vue.js.map +1 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +414 -0
- package/dist/index.js.map +1 -0
- package/dist/transformer/index.d.ts +102 -0
- package/dist/transformer/index.d.ts.map +1 -0
- package/dist/transformer/index.js +844 -0
- package/dist/transformer/index.js.map +1 -0
- package/dist/translators/azure.d.ts +7 -0
- package/dist/translators/azure.d.ts.map +1 -0
- package/dist/translators/azure.js +43 -0
- package/dist/translators/azure.js.map +1 -0
- package/dist/translators/baidu.d.ts +8 -0
- package/dist/translators/baidu.d.ts.map +1 -0
- package/dist/translators/baidu.js +67 -0
- package/dist/translators/baidu.js.map +1 -0
- package/dist/translators/base.d.ts +15 -0
- package/dist/translators/base.d.ts.map +1 -0
- package/dist/translators/base.js +50 -0
- package/dist/translators/base.js.map +1 -0
- package/dist/translators/custom.d.ts +19 -0
- package/dist/translators/custom.d.ts.map +1 -0
- package/dist/translators/custom.js +82 -0
- package/dist/translators/custom.js.map +1 -0
- package/dist/translators/google.d.ts +6 -0
- package/dist/translators/google.d.ts.map +1 -0
- package/dist/translators/google.js +25 -0
- package/dist/translators/google.js.map +1 -0
- package/dist/translators/index.d.ts +16 -0
- package/dist/translators/index.d.ts.map +1 -0
- package/dist/translators/index.js +57 -0
- package/dist/translators/index.js.map +1 -0
- package/dist/types/index.d.ts +385 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/file.d.ts +5 -0
- package/dist/utils/file.d.ts.map +1 -0
- package/dist/utils/file.js +41 -0
- package/dist/utils/file.js.map +1 -0
- package/dist/utils/interpolation.d.ts +68 -0
- package/dist/utils/interpolation.d.ts.map +1 -0
- package/dist/utils/interpolation.js +174 -0
- package/dist/utils/interpolation.js.map +1 -0
- package/dist/utils/text.d.ts +42 -0
- package/dist/utils/text.d.ts.map +1 -0
- package/dist/utils/text.js +234 -0
- package/dist/utils/text.js.map +1 -0
- package/package.json +86 -0
|
@@ -0,0 +1,844 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.CodeTransformer = void 0;
|
|
37
|
+
const parser = __importStar(require("@babel/parser"));
|
|
38
|
+
const t = __importStar(require("@babel/types"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
const interpolation_1 = require("../utils/interpolation");
|
|
41
|
+
const traverse = require('@babel/traverse').default;
|
|
42
|
+
const generate = require('@babel/generator').default;
|
|
43
|
+
class CodeTransformer {
|
|
44
|
+
constructor(config, keyMap) {
|
|
45
|
+
this.config = config;
|
|
46
|
+
this.keyMap = keyMap;
|
|
47
|
+
this.interpolation = (0, interpolation_1.createInterpolationHandler)(config.interpolation);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* 从表达式节点提取变量名
|
|
51
|
+
*/
|
|
52
|
+
extractExpressionName(expression) {
|
|
53
|
+
// 简单标识符: username
|
|
54
|
+
if (t.isIdentifier(expression)) {
|
|
55
|
+
return expression.name;
|
|
56
|
+
}
|
|
57
|
+
// 成员表达式: user.name -> 'user_name'
|
|
58
|
+
if (t.isMemberExpression(expression)) {
|
|
59
|
+
const object = this.extractExpressionName(expression.object);
|
|
60
|
+
const property = t.isIdentifier(expression.property)
|
|
61
|
+
? expression.property.name
|
|
62
|
+
: undefined;
|
|
63
|
+
if (object && property) {
|
|
64
|
+
return `${object}_${property}`;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* 解析导入配置,支持字符串和对象两种格式
|
|
71
|
+
*/
|
|
72
|
+
parseImportConfig() {
|
|
73
|
+
const importStatement = this.config.transform?.importStatement;
|
|
74
|
+
if (!importStatement) {
|
|
75
|
+
// 默认配置
|
|
76
|
+
return {
|
|
77
|
+
unified: false,
|
|
78
|
+
global: "import { useTranslation } from 'react-i18next';",
|
|
79
|
+
contextInjection: "const { t } = useTranslation();",
|
|
80
|
+
staticFileImport: "import i18n from '@/i18n';",
|
|
81
|
+
staticFileWrapper: "i18n.t"
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
if (typeof importStatement === 'string') {
|
|
85
|
+
// 向后兼容:字符串格式只设置全局导入
|
|
86
|
+
return {
|
|
87
|
+
unified: false,
|
|
88
|
+
global: importStatement,
|
|
89
|
+
contextInjection: "const { t } = useTranslation();",
|
|
90
|
+
staticFileImport: "import i18n from '@/i18n';",
|
|
91
|
+
staticFileWrapper: "i18n.t"
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
// 对象格式
|
|
95
|
+
// 统一模式:所有文件使用相同的导入和包裹方式
|
|
96
|
+
if (importStatement.unified) {
|
|
97
|
+
return {
|
|
98
|
+
unified: true,
|
|
99
|
+
global: importStatement.global || "import { t } from 'i18n';",
|
|
100
|
+
wrapper: importStatement.wrapper || "t",
|
|
101
|
+
// 统一模式下,这些配置指向统一配置
|
|
102
|
+
contextInjection: undefined,
|
|
103
|
+
staticFileImport: importStatement.global || "import { t } from 'i18n';",
|
|
104
|
+
staticFileWrapper: importStatement.wrapper || "t",
|
|
105
|
+
componentWrapper: importStatement.wrapper || "t"
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
// 非统一模式
|
|
109
|
+
return {
|
|
110
|
+
unified: false,
|
|
111
|
+
global: importStatement.global || "import { useTranslation } from 'react-i18next';",
|
|
112
|
+
contextInjection: importStatement.contextInjection || "const { t } = useTranslation();",
|
|
113
|
+
staticFileImport: importStatement.staticFileImport || "import i18n from '@/i18n';",
|
|
114
|
+
staticFileWrapper: importStatement.staticFileWrapper || "i18n.t",
|
|
115
|
+
componentWrapper: importStatement.componentWrapper
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* 检查包裹器是否为模板字符串格式(包含 {{key}} 或 {{text}})
|
|
120
|
+
*/
|
|
121
|
+
isTemplateWrapper(wrapper) {
|
|
122
|
+
if (typeof wrapper === 'string') {
|
|
123
|
+
return wrapper.includes('{{key}}') || wrapper.includes('{{text}}');
|
|
124
|
+
}
|
|
125
|
+
return typeof wrapper === 'function';
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* 使用模板或函数生成包裹代码
|
|
129
|
+
*/
|
|
130
|
+
generateWrapperCode(wrapper, key, originalText, interpolations) {
|
|
131
|
+
if (typeof wrapper === 'function') {
|
|
132
|
+
return wrapper(key, originalText, interpolations);
|
|
133
|
+
}
|
|
134
|
+
// 模板字符串格式
|
|
135
|
+
let result = wrapper
|
|
136
|
+
.replace(/\{\{key\}\}/g, key)
|
|
137
|
+
.replace(/\{\{text\}\}/g, originalText);
|
|
138
|
+
// 替换插值占位符
|
|
139
|
+
if (interpolations) {
|
|
140
|
+
interpolations.forEach((expr, index) => {
|
|
141
|
+
result = result.replace(new RegExp(`\\{\\{${index}\\}\\}`, 'g'), expr);
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
return result;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* 解析模板生成的代码为 AST 表达式
|
|
148
|
+
*/
|
|
149
|
+
parseWrapperToAST(wrapperCode) {
|
|
150
|
+
try {
|
|
151
|
+
const ast = parser.parse(wrapperCode, {
|
|
152
|
+
sourceType: 'module',
|
|
153
|
+
plugins: ['typescript', 'jsx']
|
|
154
|
+
});
|
|
155
|
+
// 获取表达式
|
|
156
|
+
const stmt = ast.program.body[0];
|
|
157
|
+
if (t.isExpressionStatement(stmt)) {
|
|
158
|
+
// 深度克隆表达式,避免引用问题
|
|
159
|
+
return t.cloneNode(stmt.expression, true);
|
|
160
|
+
}
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
console.warn('Failed to parse wrapper code:', wrapperCode, error);
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* 检测文件是否为静态文件(非 React 组件)
|
|
170
|
+
* 静态文件特征:
|
|
171
|
+
* 1. 不包含 JSX
|
|
172
|
+
* 2. 不包含 React 组件定义
|
|
173
|
+
* 3. 不导入 React
|
|
174
|
+
*/
|
|
175
|
+
isStaticFile(ast) {
|
|
176
|
+
let hasJSX = false;
|
|
177
|
+
let hasReactImport = false;
|
|
178
|
+
let hasReactComponent = false;
|
|
179
|
+
traverse(ast, {
|
|
180
|
+
JSXElement: () => {
|
|
181
|
+
hasJSX = true;
|
|
182
|
+
},
|
|
183
|
+
JSXFragment: () => {
|
|
184
|
+
hasJSX = true;
|
|
185
|
+
},
|
|
186
|
+
ImportDeclaration: (nodePath) => {
|
|
187
|
+
const source = nodePath.node.source.value;
|
|
188
|
+
if (source === 'react' || source === 'React') {
|
|
189
|
+
hasReactImport = true;
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
// 检查是否有函数组件(返回 JSX 的函数)
|
|
193
|
+
FunctionDeclaration: (nodePath) => {
|
|
194
|
+
const name = nodePath.node.id?.name;
|
|
195
|
+
// React 组件通常以大写字母开头
|
|
196
|
+
if (name && /^[A-Z]/.test(name)) {
|
|
197
|
+
hasReactComponent = true;
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
VariableDeclarator: (nodePath) => {
|
|
201
|
+
const id = nodePath.node.id;
|
|
202
|
+
if (t.isIdentifier(id) && /^[A-Z]/.test(id.name)) {
|
|
203
|
+
const init = nodePath.node.init;
|
|
204
|
+
if (t.isArrowFunctionExpression(init) || t.isFunctionExpression(init)) {
|
|
205
|
+
hasReactComponent = true;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
// 如果没有 JSX 且没有 React 导入,认为是静态文件
|
|
211
|
+
return !hasJSX && !hasReactImport && !hasReactComponent;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* 转换文件中的硬编码文本
|
|
215
|
+
*/
|
|
216
|
+
async transform(code, filePath) {
|
|
217
|
+
if (!this.config.transform?.enabled) {
|
|
218
|
+
return { code, modified: false, translations: new Map() };
|
|
219
|
+
}
|
|
220
|
+
const ext = filePath.split('.').pop();
|
|
221
|
+
const isTSX = ext === 'tsx' || ext === 'jsx';
|
|
222
|
+
try {
|
|
223
|
+
// 解析代码
|
|
224
|
+
const ast = parser.parse(code, {
|
|
225
|
+
sourceType: 'module',
|
|
226
|
+
plugins: [
|
|
227
|
+
'typescript',
|
|
228
|
+
'jsx',
|
|
229
|
+
]
|
|
230
|
+
});
|
|
231
|
+
// 检测是否为静态文件
|
|
232
|
+
const isStatic = this.isStaticFile(ast);
|
|
233
|
+
let modified = false;
|
|
234
|
+
const translations = new Map();
|
|
235
|
+
const importConfig = this.parseImportConfig();
|
|
236
|
+
// 根据文件类型选择不同的包裹配置
|
|
237
|
+
// 统一模式:所有文件使用相同的包裹配置
|
|
238
|
+
// 非统一模式:静态文件使用 staticFileWrapper,React 组件使用 componentWrapper 或 wrapperFunction
|
|
239
|
+
let wrapperConfig;
|
|
240
|
+
if (importConfig.unified) {
|
|
241
|
+
wrapperConfig = importConfig.wrapper || 't';
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
wrapperConfig = isStatic
|
|
245
|
+
? (importConfig.staticFileWrapper || 'i18n.t')
|
|
246
|
+
: (importConfig.componentWrapper || this.config.transform?.wrapperFunction || 't');
|
|
247
|
+
}
|
|
248
|
+
// 简单函数名(用于检测解构等)
|
|
249
|
+
const simpleWrapperFunction = typeof wrapperConfig === 'string' && !this.isTemplateWrapper(wrapperConfig)
|
|
250
|
+
? wrapperConfig.split('.').pop() || 't'
|
|
251
|
+
: 't';
|
|
252
|
+
let hasI18nImport = false;
|
|
253
|
+
// 记录需要注入上下文的函数/组件
|
|
254
|
+
const functionsNeedingInjection = new Set();
|
|
255
|
+
// 记录已经有上下文注入的函数(完整的,包含 t)
|
|
256
|
+
const functionsWithInjection = new Set();
|
|
257
|
+
// 记录需要更新解构的 useTranslation 调用(有 useTranslation 但缺少 t)
|
|
258
|
+
const useTranslationDeclarationsToUpdate = [];
|
|
259
|
+
// 第一遍:检查是否已有 import 和 useTranslation 调用
|
|
260
|
+
traverse(ast, {
|
|
261
|
+
ImportDeclaration: (nodePath) => {
|
|
262
|
+
const importSource = nodePath.node.source.value;
|
|
263
|
+
// 检查是否已导入 i18n 相关模块
|
|
264
|
+
if (importSource === 'react-i18next' ||
|
|
265
|
+
importSource === 'i18next' ||
|
|
266
|
+
importSource.includes('i18n') ||
|
|
267
|
+
importSource.includes('/i18n')) {
|
|
268
|
+
hasI18nImport = true;
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
// 检查是否已经有 useTranslation 调用
|
|
272
|
+
VariableDeclaration: (nodePath) => {
|
|
273
|
+
const declarations = nodePath.node.declarations;
|
|
274
|
+
for (const decl of declarations) {
|
|
275
|
+
if (t.isCallExpression(decl.init)) {
|
|
276
|
+
const callee = decl.init.callee;
|
|
277
|
+
if (t.isIdentifier(callee) && callee.name === 'useTranslation') {
|
|
278
|
+
// 找到包含此声明的函数
|
|
279
|
+
const funcParent = nodePath.findParent((p) => t.isFunctionDeclaration(p.node) ||
|
|
280
|
+
t.isFunctionExpression(p.node) ||
|
|
281
|
+
t.isArrowFunctionExpression(p.node));
|
|
282
|
+
if (funcParent) {
|
|
283
|
+
// 检查解构中是否已经有 t
|
|
284
|
+
const hasT = this.checkHasDestructuredProperty(decl.id, simpleWrapperFunction);
|
|
285
|
+
if (hasT) {
|
|
286
|
+
// 已经有完整的注入
|
|
287
|
+
functionsWithInjection.add(funcParent.node);
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
// 有 useTranslation 但缺少 t,需要更新
|
|
291
|
+
useTranslationDeclarationsToUpdate.push({
|
|
292
|
+
nodePath,
|
|
293
|
+
funcNode: funcParent.node
|
|
294
|
+
});
|
|
295
|
+
// 标记为已有注入,避免重复添加新的 useTranslation 调用
|
|
296
|
+
functionsWithInjection.add(funcParent.node);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
// 记录已处理的 JSX 元素,避免重复处理
|
|
305
|
+
const processedJSXElements = new Set();
|
|
306
|
+
// 第二遍:遍历并替换字符串,同时记录需要注入的函数
|
|
307
|
+
traverse(ast, {
|
|
308
|
+
// 处理 JSX 元素中的混合内容(文本 + 表达式)
|
|
309
|
+
JSXElement: (nodePath) => {
|
|
310
|
+
if (processedJSXElements.has(nodePath.node))
|
|
311
|
+
return;
|
|
312
|
+
const result = this.tryMergeJSXChildren(nodePath, simpleWrapperFunction);
|
|
313
|
+
if (result) {
|
|
314
|
+
const { mergedText, expressions } = result;
|
|
315
|
+
const key = this.keyMap.get(mergedText);
|
|
316
|
+
if (key) {
|
|
317
|
+
processedJSXElements.add(nodePath.node);
|
|
318
|
+
translations.set(mergedText, key);
|
|
319
|
+
this.markFunctionForInjection(nodePath, functionsNeedingInjection);
|
|
320
|
+
// 创建带插值的 t() 调用
|
|
321
|
+
const interpolationObj = expressions.length > 0
|
|
322
|
+
? t.objectExpression(expressions.map((expr, index) => t.objectProperty(t.identifier(String(index)), t.cloneNode(expr, true))))
|
|
323
|
+
: undefined;
|
|
324
|
+
const callExpression = this.createTCallWithComment(wrapperConfig, key, mergedText, interpolationObj, expressions);
|
|
325
|
+
// 替换所有子元素为单个 {t(...)}
|
|
326
|
+
nodePath.node.children = [t.jsxExpressionContainer(callExpression)];
|
|
327
|
+
modified = true;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
},
|
|
331
|
+
// 处理普通字符串
|
|
332
|
+
StringLiteral: (nodePath) => {
|
|
333
|
+
const text = nodePath.node.value;
|
|
334
|
+
const key = this.keyMap.get(text);
|
|
335
|
+
if (key && this.shouldTransform(nodePath)) {
|
|
336
|
+
translations.set(text, key);
|
|
337
|
+
// 记录包含此字符串的函数组件
|
|
338
|
+
this.markFunctionForInjection(nodePath, functionsNeedingInjection);
|
|
339
|
+
// 使用 t(key /* 原文 */) 替换
|
|
340
|
+
const callExpression = this.createTCallWithComment(wrapperConfig, key, text);
|
|
341
|
+
// 如果是 JSX 属性值,需要包裹在 JSXExpressionContainer 中
|
|
342
|
+
if (nodePath.parent && t.isJSXAttribute(nodePath.parent)) {
|
|
343
|
+
nodePath.replaceWith(t.jsxExpressionContainer(callExpression));
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
nodePath.replaceWith(callExpression);
|
|
347
|
+
}
|
|
348
|
+
// 跳过替换后的节点,避免重复处理
|
|
349
|
+
nodePath.skip();
|
|
350
|
+
modified = true;
|
|
351
|
+
}
|
|
352
|
+
},
|
|
353
|
+
// 处理模板字符串
|
|
354
|
+
TemplateLiteral: (nodePath) => {
|
|
355
|
+
if (!this.shouldTransformTemplate(nodePath))
|
|
356
|
+
return;
|
|
357
|
+
const { quasis, expressions } = nodePath.node;
|
|
358
|
+
// 只处理包含中文的模板字符串
|
|
359
|
+
const hasChineseText = quasis.some((quasi) => /[\u4e00-\u9fa5]/.test(quasi.value.raw));
|
|
360
|
+
if (!hasChineseText)
|
|
361
|
+
return;
|
|
362
|
+
// 构建完整的模板字符串文本(用于查找 key,使用可配置的占位符格式)
|
|
363
|
+
let fullText = '';
|
|
364
|
+
quasis.forEach((quasi, index) => {
|
|
365
|
+
fullText += quasi.value.raw;
|
|
366
|
+
if (index < expressions.length) {
|
|
367
|
+
const exprName = this.extractExpressionName(expressions[index]);
|
|
368
|
+
fullText += this.interpolation.formatPlaceholder(index, exprName);
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
const key = this.keyMap.get(fullText.trim());
|
|
372
|
+
if (!key)
|
|
373
|
+
return;
|
|
374
|
+
translations.set(fullText.trim(), key);
|
|
375
|
+
// 记录包含此模板字符串的函数组件
|
|
376
|
+
this.markFunctionForInjection(nodePath, functionsNeedingInjection);
|
|
377
|
+
// 构建 t(key /* 原文 */, { 0: expr0, 1: expr1, ... })
|
|
378
|
+
const interpolationObj = expressions.length > 0
|
|
379
|
+
? t.objectExpression(expressions.map((expr, index) => t.objectProperty(t.identifier(String(index)), expr)))
|
|
380
|
+
: undefined;
|
|
381
|
+
const callExpression = this.createTCallWithComment(wrapperConfig, key, fullText.trim(), interpolationObj, expressions);
|
|
382
|
+
nodePath.replaceWith(callExpression);
|
|
383
|
+
nodePath.skip();
|
|
384
|
+
modified = true;
|
|
385
|
+
},
|
|
386
|
+
// 处理 JSX 文本
|
|
387
|
+
JSXText: (nodePath) => {
|
|
388
|
+
const text = nodePath.node.value.trim();
|
|
389
|
+
if (!text)
|
|
390
|
+
return;
|
|
391
|
+
const key = this.keyMap.get(text);
|
|
392
|
+
if (key) {
|
|
393
|
+
translations.set(text, key);
|
|
394
|
+
// 记录包含此 JSX 文本的函数组件
|
|
395
|
+
this.markFunctionForInjection(nodePath, functionsNeedingInjection);
|
|
396
|
+
// JSX 中需要用 {t(key /* 原文 */)} 包裹
|
|
397
|
+
const callExpression = this.createTCallWithComment(wrapperConfig, key, text);
|
|
398
|
+
const jsxExpression = t.jsxExpressionContainer(callExpression);
|
|
399
|
+
nodePath.replaceWith(jsxExpression);
|
|
400
|
+
nodePath.skip();
|
|
401
|
+
modified = true;
|
|
402
|
+
}
|
|
403
|
+
},
|
|
404
|
+
// 处理 JSX 属性中的模板字符串
|
|
405
|
+
JSXExpressionContainer: (nodePath) => {
|
|
406
|
+
const expr = nodePath.node.expression;
|
|
407
|
+
if (!t.isTemplateLiteral(expr))
|
|
408
|
+
return;
|
|
409
|
+
const { quasis, expressions } = expr;
|
|
410
|
+
// 只处理包含中文的模板字符串
|
|
411
|
+
const hasChineseText = quasis.some((quasi) => /[\u4e00-\u9fa5]/.test(quasi.value.raw));
|
|
412
|
+
if (!hasChineseText)
|
|
413
|
+
return;
|
|
414
|
+
// 构建完整的模板字符串文本(使用可配置的占位符格式)
|
|
415
|
+
let fullText = '';
|
|
416
|
+
quasis.forEach((quasi, index) => {
|
|
417
|
+
fullText += quasi.value.raw;
|
|
418
|
+
if (index < expressions.length) {
|
|
419
|
+
const exprName = this.extractExpressionName(expressions[index]);
|
|
420
|
+
fullText += this.interpolation.formatPlaceholder(index, exprName);
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
const key = this.keyMap.get(fullText.trim());
|
|
424
|
+
if (!key)
|
|
425
|
+
return;
|
|
426
|
+
translations.set(fullText.trim(), key);
|
|
427
|
+
// 记录包含此模板字符串的函数组件
|
|
428
|
+
this.markFunctionForInjection(nodePath, functionsNeedingInjection);
|
|
429
|
+
// 构建 t(key /* 原文 */, { 0: expr0, 1: expr1, ... })
|
|
430
|
+
const interpolationObj = expressions.length > 0
|
|
431
|
+
? t.objectExpression(expressions.map((expr, index) => t.objectProperty(t.identifier(String(index)), expr)))
|
|
432
|
+
: undefined;
|
|
433
|
+
const callExpression = this.createTCallWithComment(wrapperConfig, key, fullText.trim(), interpolationObj, expressions);
|
|
434
|
+
nodePath.node.expression = callExpression;
|
|
435
|
+
modified = true;
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
// 如果修改了代码,添加必要的导入和上下文注入
|
|
439
|
+
if (modified) {
|
|
440
|
+
if (isStatic) {
|
|
441
|
+
// 静态文件:添加 i18n 导入
|
|
442
|
+
if (!hasI18nImport && importConfig.staticFileImport) {
|
|
443
|
+
this.addGlobalImport(ast, importConfig.staticFileImport);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
else {
|
|
447
|
+
// React 组件文件:添加 useTranslation 导入和上下文注入
|
|
448
|
+
if (!hasI18nImport && importConfig.global) {
|
|
449
|
+
this.addGlobalImport(ast, importConfig.global);
|
|
450
|
+
}
|
|
451
|
+
// 更新已有的 useTranslation 调用,添加缺少的 t
|
|
452
|
+
for (const { nodePath, funcNode } of useTranslationDeclarationsToUpdate) {
|
|
453
|
+
// 只有当这个函数需要注入时才更新
|
|
454
|
+
if (functionsNeedingInjection.has(funcNode)) {
|
|
455
|
+
this.updateUseTranslationDestructure(nodePath, simpleWrapperFunction);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
// 添加上下文注入(到需要的函数中)
|
|
459
|
+
if (importConfig.contextInjection) {
|
|
460
|
+
this.addContextInjections(ast, functionsNeedingInjection, functionsWithInjection, importConfig.contextInjection);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
// 生成新代码
|
|
465
|
+
const output = generate(ast, {
|
|
466
|
+
retainLines: false,
|
|
467
|
+
comments: true,
|
|
468
|
+
jsescapeOption: {
|
|
469
|
+
minimal: true
|
|
470
|
+
}
|
|
471
|
+
});
|
|
472
|
+
let resultCode = output.code;
|
|
473
|
+
// 格式化代码
|
|
474
|
+
if (this.config.transform.formatCode !== false) {
|
|
475
|
+
resultCode = await this.formatCode(resultCode, filePath);
|
|
476
|
+
}
|
|
477
|
+
return {
|
|
478
|
+
code: resultCode,
|
|
479
|
+
modified,
|
|
480
|
+
translations
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
catch (error) {
|
|
484
|
+
console.warn(`Failed to transform ${filePath}:`, error);
|
|
485
|
+
return { code, modified: false, translations: new Map() };
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* 标记函数需要注入上下文
|
|
490
|
+
*/
|
|
491
|
+
markFunctionForInjection(nodePath, functionsNeedingInjection) {
|
|
492
|
+
const funcParent = nodePath.findParent((p) => t.isFunctionDeclaration(p.node) ||
|
|
493
|
+
t.isFunctionExpression(p.node) ||
|
|
494
|
+
t.isArrowFunctionExpression(p.node));
|
|
495
|
+
if (funcParent) {
|
|
496
|
+
functionsNeedingInjection.add(funcParent.node);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* 创建翻译函数调用
|
|
501
|
+
* 支持多种格式:
|
|
502
|
+
* 1. 简单函数: t("key") 或 i18n.t("key")
|
|
503
|
+
* 2. 模板格式: getLocal('{{key}}', '{{text}}') -> getLocal('key', '原文')
|
|
504
|
+
* 3. 函数生成器: 自定义函数
|
|
505
|
+
*/
|
|
506
|
+
createTCallWithComment(wrapperConfig, key, originalText, interpolationArgs, interpolationExpressions) {
|
|
507
|
+
// 检查是否为模板或函数格式
|
|
508
|
+
if (this.isTemplateWrapper(wrapperConfig)) {
|
|
509
|
+
// 生成插值表达式的代码字符串
|
|
510
|
+
const interpolations = interpolationExpressions?.map(expr => {
|
|
511
|
+
const { code } = generate(expr, { compact: true });
|
|
512
|
+
return code;
|
|
513
|
+
});
|
|
514
|
+
// 使用模板生成代码
|
|
515
|
+
const wrapperCode = this.generateWrapperCode(wrapperConfig, key, originalText, interpolations);
|
|
516
|
+
// 解析为 AST
|
|
517
|
+
const astExpr = this.parseWrapperToAST(wrapperCode);
|
|
518
|
+
if (astExpr) {
|
|
519
|
+
return astExpr;
|
|
520
|
+
}
|
|
521
|
+
// 如果解析失败,回退到简单模式
|
|
522
|
+
console.warn('Failed to parse wrapper, falling back to simple mode');
|
|
523
|
+
}
|
|
524
|
+
// 简单函数格式: t 或 i18n.t
|
|
525
|
+
const wrapperFunction = typeof wrapperConfig === 'string' ? wrapperConfig : 't';
|
|
526
|
+
const keyLiteral = t.stringLiteral(key);
|
|
527
|
+
// 添加尾部注释(原文)
|
|
528
|
+
const maxCommentLength = 30;
|
|
529
|
+
let commentText = originalText.replace(/\n/g, ' ').trim();
|
|
530
|
+
if (commentText.length > maxCommentLength) {
|
|
531
|
+
commentText = commentText.substring(0, maxCommentLength) + '...';
|
|
532
|
+
}
|
|
533
|
+
t.addComment(keyLiteral, 'trailing', ` ${commentText} `, false);
|
|
534
|
+
// 构建调用表达式的 callee
|
|
535
|
+
let callee;
|
|
536
|
+
if (wrapperFunction.includes('.')) {
|
|
537
|
+
const parts = wrapperFunction.split('.');
|
|
538
|
+
callee = t.memberExpression(t.identifier(parts[0]), t.identifier(parts[1]));
|
|
539
|
+
}
|
|
540
|
+
else {
|
|
541
|
+
callee = t.identifier(wrapperFunction);
|
|
542
|
+
}
|
|
543
|
+
if (interpolationArgs) {
|
|
544
|
+
return t.callExpression(callee, [keyLiteral, interpolationArgs]);
|
|
545
|
+
}
|
|
546
|
+
else {
|
|
547
|
+
return t.callExpression(callee, [keyLiteral]);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* 检查解构模式中是否已经有指定的属性
|
|
552
|
+
*/
|
|
553
|
+
checkHasDestructuredProperty(pattern, propertyName) {
|
|
554
|
+
if (t.isObjectPattern(pattern)) {
|
|
555
|
+
return pattern.properties.some((prop) => {
|
|
556
|
+
if (t.isObjectProperty(prop)) {
|
|
557
|
+
const key = prop.key;
|
|
558
|
+
if (t.isIdentifier(key) && key.name === propertyName) {
|
|
559
|
+
return true;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
return false;
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
return false;
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* 更新 useTranslation 的解构,添加缺少的属性
|
|
569
|
+
*/
|
|
570
|
+
updateUseTranslationDestructure(nodePath, propertyName) {
|
|
571
|
+
const declarations = nodePath.node.declarations;
|
|
572
|
+
for (const decl of declarations) {
|
|
573
|
+
if (t.isCallExpression(decl.init)) {
|
|
574
|
+
const callee = decl.init.callee;
|
|
575
|
+
if (t.isIdentifier(callee) && callee.name === 'useTranslation') {
|
|
576
|
+
// 如果是对象解构模式
|
|
577
|
+
if (t.isObjectPattern(decl.id)) {
|
|
578
|
+
// 检查是否已经有这个属性
|
|
579
|
+
const hasProperty = this.checkHasDestructuredProperty(decl.id, propertyName);
|
|
580
|
+
if (!hasProperty) {
|
|
581
|
+
// 添加新的属性到解构中
|
|
582
|
+
// 创建 shorthand 属性: { t } 而不是 { t: t }
|
|
583
|
+
const newProperty = t.objectProperty(t.identifier(propertyName), t.identifier(propertyName), false, // computed
|
|
584
|
+
true // shorthand
|
|
585
|
+
);
|
|
586
|
+
decl.id.properties.push(newProperty);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* 添加全局导入语句
|
|
595
|
+
*/
|
|
596
|
+
addGlobalImport(ast, importStatement) {
|
|
597
|
+
try {
|
|
598
|
+
// 解析 import 语句
|
|
599
|
+
const importAST = parser.parse(importStatement, {
|
|
600
|
+
sourceType: 'module'
|
|
601
|
+
});
|
|
602
|
+
const importDeclaration = importAST.program.body[0];
|
|
603
|
+
if (t.isImportDeclaration(importDeclaration)) {
|
|
604
|
+
// 找到最后一个 import 语句的位置
|
|
605
|
+
let lastImportIndex = -1;
|
|
606
|
+
ast.program.body.forEach((node, index) => {
|
|
607
|
+
if (t.isImportDeclaration(node)) {
|
|
608
|
+
lastImportIndex = index;
|
|
609
|
+
}
|
|
610
|
+
});
|
|
611
|
+
// 在最后一个 import 后插入
|
|
612
|
+
if (lastImportIndex >= 0) {
|
|
613
|
+
ast.program.body.splice(lastImportIndex + 1, 0, importDeclaration);
|
|
614
|
+
}
|
|
615
|
+
else {
|
|
616
|
+
ast.program.body.unshift(importDeclaration);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
catch (error) {
|
|
621
|
+
console.warn('Failed to add global import:', error);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* 添加上下文注入到函数中
|
|
626
|
+
*/
|
|
627
|
+
addContextInjections(ast, functionsNeedingInjection, functionsWithInjection, contextInjection) {
|
|
628
|
+
try {
|
|
629
|
+
// 解析上下文注入语句
|
|
630
|
+
const injectionAST = parser.parse(contextInjection, {
|
|
631
|
+
sourceType: 'module'
|
|
632
|
+
});
|
|
633
|
+
const injectionStatement = injectionAST.program.body[0];
|
|
634
|
+
if (!injectionStatement)
|
|
635
|
+
return;
|
|
636
|
+
// 遍历 AST,找到需要注入的函数并添加语句
|
|
637
|
+
traverse(ast, {
|
|
638
|
+
FunctionDeclaration: (nodePath) => {
|
|
639
|
+
if (functionsNeedingInjection.has(nodePath.node) && !functionsWithInjection.has(nodePath.node)) {
|
|
640
|
+
this.injectToFunctionBody(nodePath, injectionStatement);
|
|
641
|
+
functionsWithInjection.add(nodePath.node);
|
|
642
|
+
}
|
|
643
|
+
},
|
|
644
|
+
FunctionExpression: (nodePath) => {
|
|
645
|
+
if (functionsNeedingInjection.has(nodePath.node) && !functionsWithInjection.has(nodePath.node)) {
|
|
646
|
+
this.injectToFunctionBody(nodePath, injectionStatement);
|
|
647
|
+
functionsWithInjection.add(nodePath.node);
|
|
648
|
+
}
|
|
649
|
+
},
|
|
650
|
+
ArrowFunctionExpression: (nodePath) => {
|
|
651
|
+
if (functionsNeedingInjection.has(nodePath.node) && !functionsWithInjection.has(nodePath.node)) {
|
|
652
|
+
this.injectToArrowFunction(nodePath, injectionStatement);
|
|
653
|
+
functionsWithInjection.add(nodePath.node);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
catch (error) {
|
|
659
|
+
console.warn('Failed to add context injection:', error);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* 注入语句到普通函数体
|
|
664
|
+
*/
|
|
665
|
+
injectToFunctionBody(nodePath, injectionStatement) {
|
|
666
|
+
const body = nodePath.node.body;
|
|
667
|
+
if (t.isBlockStatement(body)) {
|
|
668
|
+
// 克隆注入语句以避免重复使用同一节点
|
|
669
|
+
const clonedStatement = t.cloneNode(injectionStatement, true);
|
|
670
|
+
body.body.unshift(clonedStatement);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* 注入语句到箭头函数
|
|
675
|
+
*/
|
|
676
|
+
injectToArrowFunction(nodePath, injectionStatement) {
|
|
677
|
+
const body = nodePath.node.body;
|
|
678
|
+
if (t.isBlockStatement(body)) {
|
|
679
|
+
// 已经是块语句,直接在开头插入
|
|
680
|
+
const clonedStatement = t.cloneNode(injectionStatement, true);
|
|
681
|
+
body.body.unshift(clonedStatement);
|
|
682
|
+
}
|
|
683
|
+
else {
|
|
684
|
+
// 是表达式,需要转换为块语句
|
|
685
|
+
const clonedStatement = t.cloneNode(injectionStatement, true);
|
|
686
|
+
const returnStatement = t.returnStatement(body);
|
|
687
|
+
nodePath.node.body = t.blockStatement([clonedStatement, returnStatement]);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* 格式化代码
|
|
692
|
+
*/
|
|
693
|
+
async formatCode(code, filePath) {
|
|
694
|
+
try {
|
|
695
|
+
// 尝试使用项目的 prettier
|
|
696
|
+
const prettierPath = this.findPrettier();
|
|
697
|
+
if (prettierPath) {
|
|
698
|
+
const prettier = require(prettierPath);
|
|
699
|
+
// 尝试加载项目的 prettier 配置
|
|
700
|
+
const prettierConfig = await prettier.resolveConfig(filePath) || {};
|
|
701
|
+
// 根据文件扩展名选择正确的 parser
|
|
702
|
+
const ext = filePath.split('.').pop()?.toLowerCase();
|
|
703
|
+
let parser = 'babel';
|
|
704
|
+
if (ext === 'ts' || ext === 'tsx') {
|
|
705
|
+
parser = 'typescript';
|
|
706
|
+
}
|
|
707
|
+
else if (ext === 'jsx') {
|
|
708
|
+
parser = 'babel';
|
|
709
|
+
}
|
|
710
|
+
return prettier.format(code, {
|
|
711
|
+
...prettierConfig,
|
|
712
|
+
filepath: filePath,
|
|
713
|
+
parser
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
catch (error) {
|
|
718
|
+
// prettier 格式化失败,返回原代码
|
|
719
|
+
console.warn('Prettier formatting failed:', error);
|
|
720
|
+
}
|
|
721
|
+
return code;
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* 查找项目中的 prettier
|
|
725
|
+
*/
|
|
726
|
+
findPrettier() {
|
|
727
|
+
const possiblePaths = [
|
|
728
|
+
path.join(process.cwd(), 'node_modules', 'prettier'),
|
|
729
|
+
path.join(process.cwd(), '..', 'node_modules', 'prettier'),
|
|
730
|
+
'prettier'
|
|
731
|
+
];
|
|
732
|
+
for (const p of possiblePaths) {
|
|
733
|
+
try {
|
|
734
|
+
require.resolve(p);
|
|
735
|
+
return p;
|
|
736
|
+
}
|
|
737
|
+
catch {
|
|
738
|
+
continue;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
return null;
|
|
742
|
+
}
|
|
743
|
+
/**
|
|
744
|
+
* 判断是否应该转换这个字符串
|
|
745
|
+
*/
|
|
746
|
+
shouldTransform(nodePath) {
|
|
747
|
+
// 不转换 import 语句中的字符串
|
|
748
|
+
if (nodePath.findParent((p) => t.isImportDeclaration(p.node))) {
|
|
749
|
+
return false;
|
|
750
|
+
}
|
|
751
|
+
// 不转换技术性 JSX 属性(但 placeholder, title, alt 等需要翻译)
|
|
752
|
+
if (nodePath.parent && t.isJSXAttribute(nodePath.parent)) {
|
|
753
|
+
const attrName = nodePath.parent.name;
|
|
754
|
+
if (t.isJSXIdentifier(attrName)) {
|
|
755
|
+
// 只跳过纯技术性属性,用户可见的属性(placeholder, title, alt, aria-label)需要翻译
|
|
756
|
+
const skipAttrs = ['className', 'class', 'id', 'key', 'ref', 'style', 'src', 'href', 'type', 'name', 'htmlFor', 'data-testid'];
|
|
757
|
+
if (skipAttrs.includes(attrName.name) || attrName.name.startsWith('data-')) {
|
|
758
|
+
return false;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
// 不转换对象的 key
|
|
763
|
+
if (nodePath.parent && t.isObjectProperty(nodePath.parent) && nodePath.parent.key === nodePath.node) {
|
|
764
|
+
return false;
|
|
765
|
+
}
|
|
766
|
+
// 不转换 console.log 等调试语句
|
|
767
|
+
if (nodePath.findParent((p) => {
|
|
768
|
+
if (t.isCallExpression(p.node)) {
|
|
769
|
+
const callee = p.node.callee;
|
|
770
|
+
if (t.isMemberExpression(callee) && t.isIdentifier(callee.object) && callee.object.name === 'console') {
|
|
771
|
+
return true;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
return false;
|
|
775
|
+
})) {
|
|
776
|
+
return false;
|
|
777
|
+
}
|
|
778
|
+
return true;
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
781
|
+
* 判断是否应该转换模板字符串
|
|
782
|
+
*/
|
|
783
|
+
shouldTransformTemplate(nodePath) {
|
|
784
|
+
// 不转换 import 语句
|
|
785
|
+
if (nodePath.findParent((p) => t.isImportDeclaration(p.node))) {
|
|
786
|
+
return false;
|
|
787
|
+
}
|
|
788
|
+
// 不转换 tagged template literals (如 styled-components)
|
|
789
|
+
if (nodePath.parent && t.isTaggedTemplateExpression(nodePath.parent)) {
|
|
790
|
+
return false;
|
|
791
|
+
}
|
|
792
|
+
return true;
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* 尝试合并 JSX 元素的子节点为一个带插值的字符串
|
|
796
|
+
* 例如: <p>当前等级:{level} 级</p> => "当前等级:{level} 级"
|
|
797
|
+
*/
|
|
798
|
+
tryMergeJSXChildren(nodePath, wrapperFunction) {
|
|
799
|
+
const children = nodePath.node.children;
|
|
800
|
+
if (!children || children.length <= 1)
|
|
801
|
+
return null;
|
|
802
|
+
// 检查是否有混合内容(文本 + 表达式)
|
|
803
|
+
const hasText = children.some((child) => t.isJSXText(child) && /[\u4e00-\u9fa5]/.test(child.value));
|
|
804
|
+
const hasExpression = children.some((child) => t.isJSXExpressionContainer(child) && !t.isJSXEmptyExpression(child.expression));
|
|
805
|
+
if (!hasText || !hasExpression)
|
|
806
|
+
return null;
|
|
807
|
+
// 构建合并的文本和表达式列表
|
|
808
|
+
let mergedText = '';
|
|
809
|
+
const expressions = [];
|
|
810
|
+
let expressionIndex = 0;
|
|
811
|
+
for (const child of children) {
|
|
812
|
+
if (t.isJSXText(child)) {
|
|
813
|
+
// 处理文本节点
|
|
814
|
+
const text = child.value.replace(/^\s*\n\s*/g, '').replace(/\s*\n\s*$/g, '');
|
|
815
|
+
if (text) {
|
|
816
|
+
mergedText += text;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
else if (t.isJSXExpressionContainer(child) && !t.isJSXEmptyExpression(child.expression)) {
|
|
820
|
+
const expr = child.expression;
|
|
821
|
+
// 如果表达式已经是 t() 调用,跳过合并
|
|
822
|
+
if (t.isCallExpression(expr)) {
|
|
823
|
+
const callee = expr.callee;
|
|
824
|
+
if (t.isIdentifier(callee) && callee.name === wrapperFunction) {
|
|
825
|
+
return null; // 已经被翻译过
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
// 使用可配置的占位符格式(支持命名参数)
|
|
829
|
+
const exprName = this.extractExpressionName(expr);
|
|
830
|
+
mergedText += this.interpolation.formatPlaceholder(expressionIndex, exprName);
|
|
831
|
+
expressions.push(expr);
|
|
832
|
+
expressionIndex++;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
const trimmedText = mergedText.trim();
|
|
836
|
+
// 只有当包含中文且在 keyMap 中有对应的 key 时才返回
|
|
837
|
+
if (trimmedText && /[\u4e00-\u9fa5]/.test(trimmedText) && this.keyMap.has(trimmedText)) {
|
|
838
|
+
return { mergedText: trimmedText, expressions };
|
|
839
|
+
}
|
|
840
|
+
return null;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
exports.CodeTransformer = CodeTransformer;
|
|
844
|
+
//# sourceMappingURL=index.js.map
|