ai-localize-codemods 1.0.1 → 2.0.1
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/CHANGELOG.md +19 -0
- package/dist/index.d.mts +78 -15
- package/dist/index.d.ts +78 -15
- package/dist/index.js +162 -75
- package/dist/index.mjs +160 -74
- package/package.json +2 -2
- package/src/angular-codemod.ts +39 -22
- package/src/codemod-runner.ts +43 -28
- package/src/react-codemod.ts +250 -104
- package/src/vue-codemod.ts +35 -26
package/dist/index.mjs
CHANGED
|
@@ -1,19 +1,34 @@
|
|
|
1
1
|
// src/react-codemod.ts
|
|
2
2
|
import * as fs from "fs";
|
|
3
|
+
import * as nodePath from "path";
|
|
3
4
|
import * as parser from "@babel/parser";
|
|
4
5
|
import traverse from "@babel/traverse";
|
|
5
6
|
import generate from "@babel/generator";
|
|
6
7
|
import * as t from "@babel/types";
|
|
7
|
-
var
|
|
8
|
-
var
|
|
8
|
+
var DEFAULT_IMPORT_PACKAGE = "react-i18next";
|
|
9
|
+
var DEFAULT_HOOK_NAME = "useTranslation";
|
|
10
|
+
var DEFAULT_TRANSLATION_FN = "t";
|
|
11
|
+
var DEFAULT_ACCESSOR_STYLE = "function";
|
|
9
12
|
var ReactCodemod = class {
|
|
10
13
|
filePath;
|
|
11
14
|
content;
|
|
12
15
|
texts;
|
|
13
|
-
|
|
16
|
+
/** Resolved import specifier string (may differ per file for local paths) */
|
|
17
|
+
importSpecifier;
|
|
18
|
+
hookName;
|
|
19
|
+
translationFn;
|
|
20
|
+
namespace;
|
|
21
|
+
accessorStyle;
|
|
22
|
+
constructor(filePath, content, texts, codemodConfig, cwd = process.cwd()) {
|
|
14
23
|
this.filePath = filePath;
|
|
15
24
|
this.content = content;
|
|
16
25
|
this.texts = texts;
|
|
26
|
+
this.hookName = codemodConfig?.hookName ?? DEFAULT_HOOK_NAME;
|
|
27
|
+
this.translationFn = codemodConfig?.translationFunction ?? DEFAULT_TRANSLATION_FN;
|
|
28
|
+
this.namespace = codemodConfig?.namespace;
|
|
29
|
+
this.accessorStyle = codemodConfig?.accessorStyle ?? DEFAULT_ACCESSOR_STYLE;
|
|
30
|
+
const pkg = codemodConfig?.importPackage ?? DEFAULT_IMPORT_PACKAGE;
|
|
31
|
+
this.importSpecifier = resolveImportSpecifier(pkg, filePath, cwd);
|
|
17
32
|
}
|
|
18
33
|
transform() {
|
|
19
34
|
if (this.texts.length === 0) {
|
|
@@ -57,58 +72,66 @@ var ReactCodemod = class {
|
|
|
57
72
|
textKeyMap.set(dt.text, dt.suggestedKey);
|
|
58
73
|
}
|
|
59
74
|
let replacedTexts = 0;
|
|
60
|
-
let
|
|
61
|
-
let
|
|
75
|
+
let hasImport = false;
|
|
76
|
+
let hasHook = false;
|
|
62
77
|
let injectedImport = false;
|
|
63
78
|
let injectedHook = false;
|
|
79
|
+
const translationFn = this.translationFn;
|
|
80
|
+
const hookName = this.hookName;
|
|
81
|
+
const importSpecifier2 = this.importSpecifier;
|
|
82
|
+
const accessorStyle = this.accessorStyle;
|
|
83
|
+
const buildTranslationExpr = (key) => {
|
|
84
|
+
if (accessorStyle === "bracket") {
|
|
85
|
+
return t.memberExpression(
|
|
86
|
+
t.identifier(translationFn),
|
|
87
|
+
t.stringLiteral(key),
|
|
88
|
+
true
|
|
89
|
+
/* computed */
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
return t.callExpression(t.identifier(translationFn), [t.stringLiteral(key)]);
|
|
93
|
+
};
|
|
64
94
|
traverse(ast, {
|
|
65
|
-
ImportDeclaration(
|
|
66
|
-
if (
|
|
67
|
-
|
|
95
|
+
ImportDeclaration(nodePath2) {
|
|
96
|
+
if (nodePath2.node.source.value === importSpecifier2) {
|
|
97
|
+
hasImport = true;
|
|
68
98
|
}
|
|
69
99
|
},
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const id = nodePath.node.id;
|
|
100
|
+
VariableDeclarator(nodePath2) {
|
|
101
|
+
const id = nodePath2.node.id;
|
|
73
102
|
if (t.isObjectPattern(id) && id.properties.some(
|
|
74
|
-
(p) => t.isObjectProperty(p) && t.isIdentifier(p.key) && p.key.name ===
|
|
103
|
+
(p) => t.isObjectProperty(p) && t.isIdentifier(p.key) && p.key.name === translationFn
|
|
75
104
|
)) {
|
|
76
|
-
|
|
105
|
+
hasHook = true;
|
|
77
106
|
}
|
|
78
107
|
}
|
|
79
108
|
});
|
|
80
109
|
traverse(ast, {
|
|
81
|
-
JSXText(
|
|
82
|
-
const text =
|
|
110
|
+
JSXText(nodePath2) {
|
|
111
|
+
const text = nodePath2.node.value.trim();
|
|
83
112
|
if (!text) return;
|
|
84
113
|
const key = textKeyMap.get(text);
|
|
85
114
|
if (!key) return;
|
|
86
|
-
|
|
87
|
-
t.callExpression(t.identifier("t"), [t.stringLiteral(key)])
|
|
88
|
-
);
|
|
89
|
-
nodePath.replaceWith(tCall);
|
|
115
|
+
nodePath2.replaceWith(t.jsxExpressionContainer(buildTranslationExpr(key)));
|
|
90
116
|
replacedTexts++;
|
|
91
117
|
},
|
|
92
|
-
StringLiteral(
|
|
93
|
-
const text =
|
|
118
|
+
StringLiteral(nodePath2) {
|
|
119
|
+
const text = nodePath2.node.value;
|
|
94
120
|
const key = textKeyMap.get(text);
|
|
95
121
|
if (!key) return;
|
|
96
|
-
if (t.isImportDeclaration(
|
|
97
|
-
if (t.isObjectProperty(
|
|
98
|
-
if (t.isJSXAttribute(
|
|
99
|
-
|
|
100
|
-
nodePath.replaceWith(tCall);
|
|
122
|
+
if (t.isImportDeclaration(nodePath2.parent)) return;
|
|
123
|
+
if (t.isObjectProperty(nodePath2.parent) && nodePath2.parent.key === nodePath2.node) return;
|
|
124
|
+
if (t.isJSXAttribute(nodePath2.parent)) return;
|
|
125
|
+
nodePath2.replaceWith(buildTranslationExpr(key));
|
|
101
126
|
replacedTexts++;
|
|
102
127
|
},
|
|
103
|
-
JSXAttribute(
|
|
104
|
-
const valueNode =
|
|
128
|
+
JSXAttribute(nodePath2) {
|
|
129
|
+
const valueNode = nodePath2.node.value;
|
|
105
130
|
if (!t.isStringLiteral(valueNode)) return;
|
|
106
131
|
const text = valueNode.value;
|
|
107
132
|
const key = textKeyMap.get(text);
|
|
108
133
|
if (!key) return;
|
|
109
|
-
|
|
110
|
-
t.callExpression(t.identifier("t"), [t.stringLiteral(key)])
|
|
111
|
-
);
|
|
134
|
+
nodePath2.node.value = t.jsxExpressionContainer(buildTranslationExpr(key));
|
|
112
135
|
replacedTexts++;
|
|
113
136
|
}
|
|
114
137
|
});
|
|
@@ -123,25 +146,21 @@ var ReactCodemod = class {
|
|
|
123
146
|
replacedTexts: 0
|
|
124
147
|
};
|
|
125
148
|
}
|
|
126
|
-
if (!
|
|
149
|
+
if (!hasImport) {
|
|
127
150
|
const importDecl = t.importDeclaration(
|
|
128
|
-
[t.importSpecifier(t.identifier(
|
|
129
|
-
t.stringLiteral(
|
|
151
|
+
[t.importSpecifier(t.identifier(hookName), t.identifier(hookName))],
|
|
152
|
+
t.stringLiteral(importSpecifier2)
|
|
130
153
|
);
|
|
131
154
|
ast.program.body.unshift(importDecl);
|
|
132
155
|
injectedImport = true;
|
|
133
156
|
}
|
|
134
|
-
if (!
|
|
135
|
-
this.
|
|
157
|
+
if (!hasHook) {
|
|
158
|
+
this.injectHook(ast);
|
|
136
159
|
injectedHook = true;
|
|
137
160
|
}
|
|
138
161
|
const output = generate(
|
|
139
162
|
ast,
|
|
140
|
-
{
|
|
141
|
-
retainLines: false,
|
|
142
|
-
comments: true,
|
|
143
|
-
compact: false
|
|
144
|
-
},
|
|
163
|
+
{ retainLines: false, comments: true, compact: false },
|
|
145
164
|
this.content
|
|
146
165
|
);
|
|
147
166
|
return {
|
|
@@ -154,19 +173,22 @@ var ReactCodemod = class {
|
|
|
154
173
|
replacedTexts
|
|
155
174
|
};
|
|
156
175
|
}
|
|
157
|
-
|
|
176
|
+
injectHook(ast) {
|
|
177
|
+
const hookName = this.hookName;
|
|
178
|
+
const translationFn = this.translationFn;
|
|
179
|
+
const namespace = this.namespace;
|
|
158
180
|
traverse(ast, {
|
|
159
|
-
FunctionDeclaration(
|
|
160
|
-
if (!isReactComponent(
|
|
161
|
-
injectHookIntoBody(
|
|
162
|
-
|
|
181
|
+
FunctionDeclaration(nodePath2) {
|
|
182
|
+
if (!isReactComponent(nodePath2.node.id?.name)) return;
|
|
183
|
+
injectHookIntoBody(nodePath2.node.body, hookName, translationFn, namespace);
|
|
184
|
+
nodePath2.stop();
|
|
163
185
|
},
|
|
164
|
-
ArrowFunctionExpression(
|
|
165
|
-
if (!isReactComponentParent(
|
|
166
|
-
const body =
|
|
186
|
+
ArrowFunctionExpression(nodePath2) {
|
|
187
|
+
if (!isReactComponentParent(nodePath2)) return;
|
|
188
|
+
const body = nodePath2.node.body;
|
|
167
189
|
if (t.isBlockStatement(body)) {
|
|
168
|
-
injectHookIntoBody(body);
|
|
169
|
-
|
|
190
|
+
injectHookIntoBody(body, hookName, translationFn, namespace);
|
|
191
|
+
nodePath2.stop();
|
|
170
192
|
}
|
|
171
193
|
}
|
|
172
194
|
});
|
|
@@ -176,28 +198,76 @@ function isReactComponent(name) {
|
|
|
176
198
|
if (!name) return false;
|
|
177
199
|
return /^[A-Z]/.test(name);
|
|
178
200
|
}
|
|
179
|
-
function isReactComponentParent(
|
|
180
|
-
const parent =
|
|
201
|
+
function isReactComponentParent(nodePath2) {
|
|
202
|
+
const parent = nodePath2.parent;
|
|
181
203
|
if (t.isVariableDeclarator(parent)) {
|
|
182
204
|
const id = parent.id;
|
|
183
205
|
if (t.isIdentifier(id) && /^[A-Z]/.test(id.name)) return true;
|
|
184
206
|
}
|
|
185
207
|
return false;
|
|
186
208
|
}
|
|
187
|
-
function injectHookIntoBody(body) {
|
|
209
|
+
function injectHookIntoBody(body, hookName, translationFn, namespace) {
|
|
210
|
+
const hookArgs = namespace ? [t.stringLiteral(namespace)] : [];
|
|
188
211
|
const hookDecl = t.variableDeclaration("const", [
|
|
189
212
|
t.variableDeclarator(
|
|
190
213
|
t.objectPattern([
|
|
191
|
-
t.objectProperty(t.identifier(
|
|
214
|
+
t.objectProperty(t.identifier(translationFn), t.identifier(translationFn), false, true)
|
|
192
215
|
]),
|
|
193
|
-
t.callExpression(t.identifier(
|
|
216
|
+
t.callExpression(t.identifier(hookName), hookArgs)
|
|
194
217
|
)
|
|
195
218
|
]);
|
|
196
219
|
body.body.unshift(hookDecl);
|
|
197
220
|
}
|
|
198
|
-
function
|
|
221
|
+
function resolveImportSpecifier(importPackage, filePath, cwd) {
|
|
222
|
+
if (isNpmPackage(importPackage)) {
|
|
223
|
+
return importPackage;
|
|
224
|
+
}
|
|
225
|
+
let absoluteHookPath;
|
|
226
|
+
if (nodePath.isAbsolute(importPackage)) {
|
|
227
|
+
absoluteHookPath = importPackage;
|
|
228
|
+
} else {
|
|
229
|
+
absoluteHookPath = nodePath.resolve(cwd, importPackage);
|
|
230
|
+
}
|
|
231
|
+
const fileDir = nodePath.dirname(filePath);
|
|
232
|
+
let rel = nodePath.relative(fileDir, absoluteHookPath).replace(/\\/g, "/");
|
|
233
|
+
if (!rel.startsWith(".")) {
|
|
234
|
+
rel = `./${rel}`;
|
|
235
|
+
}
|
|
236
|
+
return rel;
|
|
237
|
+
}
|
|
238
|
+
function isNpmPackage(pkg) {
|
|
239
|
+
if (pkg.startsWith(".") || pkg.startsWith("/")) return false;
|
|
240
|
+
if (pkg.startsWith("@") && pkg.includes("/")) {
|
|
241
|
+
if (pkg.startsWith("@/")) return false;
|
|
242
|
+
return true;
|
|
243
|
+
}
|
|
244
|
+
if (!pkg.includes("/")) return true;
|
|
245
|
+
const firstSegment = pkg.split("/")[0].toLowerCase();
|
|
246
|
+
const sourceDirectories = /* @__PURE__ */ new Set([
|
|
247
|
+
"src",
|
|
248
|
+
"app",
|
|
249
|
+
"lib",
|
|
250
|
+
"hooks",
|
|
251
|
+
"utils",
|
|
252
|
+
"pages",
|
|
253
|
+
"components",
|
|
254
|
+
"features",
|
|
255
|
+
"shared",
|
|
256
|
+
"common",
|
|
257
|
+
"locales",
|
|
258
|
+
"i18n",
|
|
259
|
+
"services",
|
|
260
|
+
"store",
|
|
261
|
+
"helpers",
|
|
262
|
+
"core",
|
|
263
|
+
"modules"
|
|
264
|
+
]);
|
|
265
|
+
if (sourceDirectories.has(firstSegment)) return false;
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
function applyReactCodemod(filePath, texts, dryRun = false, codemodConfig, cwd = process.cwd()) {
|
|
199
269
|
const content = fs.readFileSync(filePath, "utf-8");
|
|
200
|
-
const codemod = new ReactCodemod(filePath, content, texts);
|
|
270
|
+
const codemod = new ReactCodemod(filePath, content, texts, codemodConfig, cwd);
|
|
201
271
|
const result = codemod.transform();
|
|
202
272
|
if (result.changed && !dryRun) {
|
|
203
273
|
fs.writeFileSync(filePath, result.transformedContent, "utf-8");
|
|
@@ -207,14 +277,17 @@ function applyReactCodemod(filePath, texts, dryRun = false) {
|
|
|
207
277
|
|
|
208
278
|
// src/vue-codemod.ts
|
|
209
279
|
import * as fs2 from "fs";
|
|
280
|
+
var DEFAULT_TRANSLATION_FN2 = "$t";
|
|
210
281
|
var VueCodemod = class {
|
|
211
282
|
filePath;
|
|
212
283
|
content;
|
|
213
284
|
texts;
|
|
214
|
-
|
|
285
|
+
translationFn;
|
|
286
|
+
constructor(filePath, content, texts, codemodConfig) {
|
|
215
287
|
this.filePath = filePath;
|
|
216
288
|
this.content = content;
|
|
217
289
|
this.texts = texts;
|
|
290
|
+
this.translationFn = codemodConfig?.translationFunction ?? DEFAULT_TRANSLATION_FN2;
|
|
218
291
|
}
|
|
219
292
|
transform() {
|
|
220
293
|
if (this.texts.length === 0) {
|
|
@@ -231,14 +304,15 @@ var VueCodemod = class {
|
|
|
231
304
|
}
|
|
232
305
|
let transformedContent = this.content;
|
|
233
306
|
let replacedTexts = 0;
|
|
307
|
+
const fn = this.translationFn;
|
|
234
308
|
if (templateMatch) {
|
|
235
309
|
let template = templateMatch[1];
|
|
236
310
|
for (const [text, key] of textKeyMap) {
|
|
237
311
|
const escaped = text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
238
312
|
const re = new RegExp(`(>\\s*)${escaped}(\\s*<)`, "g");
|
|
239
|
-
template = template.replace(re, `$1{{ $
|
|
313
|
+
template = template.replace(re, `$1{{ ${fn}('${key}') }}$2`);
|
|
240
314
|
const attrRe = new RegExp(`((?:title|placeholder|alt|label)=)"(${escaped})"`, "g");
|
|
241
|
-
template = template.replace(attrRe, `:$1"$
|
|
315
|
+
template = template.replace(attrRe, `:$1"${fn}('${key}')"`);
|
|
242
316
|
replacedTexts++;
|
|
243
317
|
}
|
|
244
318
|
transformedContent = transformedContent.replace(templateMatch[1], template);
|
|
@@ -269,9 +343,9 @@ var VueCodemod = class {
|
|
|
269
343
|
};
|
|
270
344
|
}
|
|
271
345
|
};
|
|
272
|
-
function applyVueCodemod(filePath, texts, dryRun = false) {
|
|
346
|
+
function applyVueCodemod(filePath, texts, dryRun = false, codemodConfig) {
|
|
273
347
|
const content = fs2.readFileSync(filePath, "utf-8");
|
|
274
|
-
const codemod = new VueCodemod(filePath, content, texts);
|
|
348
|
+
const codemod = new VueCodemod(filePath, content, texts, codemodConfig);
|
|
275
349
|
const result = codemod.transform();
|
|
276
350
|
if (result.changed && !dryRun) {
|
|
277
351
|
fs2.writeFileSync(filePath, result.transformedContent, "utf-8");
|
|
@@ -281,14 +355,20 @@ function applyVueCodemod(filePath, texts, dryRun = false) {
|
|
|
281
355
|
|
|
282
356
|
// src/angular-codemod.ts
|
|
283
357
|
import * as fs3 from "fs";
|
|
358
|
+
var DEFAULT_TS_SERVICE = "this.translateService.instant";
|
|
359
|
+
var DEFAULT_TEMPLATE_PIPE = "translate";
|
|
284
360
|
var AngularCodemod = class {
|
|
285
361
|
filePath;
|
|
286
362
|
content;
|
|
287
363
|
texts;
|
|
288
|
-
|
|
364
|
+
templatePipe;
|
|
365
|
+
tsServiceCall;
|
|
366
|
+
constructor(filePath, content, texts, codemodConfig) {
|
|
289
367
|
this.filePath = filePath;
|
|
290
368
|
this.content = content;
|
|
291
369
|
this.texts = texts;
|
|
370
|
+
this.templatePipe = codemodConfig?.translationFunction ?? DEFAULT_TEMPLATE_PIPE;
|
|
371
|
+
this.tsServiceCall = codemodConfig?.hookName ?? DEFAULT_TS_SERVICE;
|
|
292
372
|
}
|
|
293
373
|
transform() {
|
|
294
374
|
if (this.texts.length === 0) return this.unchanged();
|
|
@@ -299,21 +379,23 @@ var AngularCodemod = class {
|
|
|
299
379
|
let transformedContent = this.content;
|
|
300
380
|
let replacedTexts = 0;
|
|
301
381
|
const isTemplate = this.filePath.endsWith(".html");
|
|
382
|
+
const pipe = this.templatePipe;
|
|
383
|
+
const svc = this.tsServiceCall;
|
|
302
384
|
for (const [text, key] of textKeyMap) {
|
|
303
385
|
const escaped = text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
304
386
|
if (isTemplate) {
|
|
305
387
|
const re = new RegExp(`(>\\s*)${escaped}(\\s*<)`, "g");
|
|
306
|
-
transformedContent = transformedContent.replace(re, `$1{{ '${key}' |
|
|
388
|
+
transformedContent = transformedContent.replace(re, `$1{{ '${key}' | ${pipe} }}$2`);
|
|
307
389
|
const attrRe = new RegExp(`\\[(?:placeholder|label|title|alt)\\]="'${escaped}'"`, "g");
|
|
308
390
|
transformedContent = transformedContent.replace(
|
|
309
391
|
attrRe,
|
|
310
|
-
`[placeholder]="'${key}' |
|
|
392
|
+
`[placeholder]="'${key}' | ${pipe}"`
|
|
311
393
|
);
|
|
312
394
|
} else {
|
|
313
395
|
const tsRe = new RegExp(`'${escaped}'`, "g");
|
|
314
396
|
transformedContent = transformedContent.replace(
|
|
315
397
|
tsRe,
|
|
316
|
-
|
|
398
|
+
`${svc}('${key}')`
|
|
317
399
|
);
|
|
318
400
|
}
|
|
319
401
|
replacedTexts++;
|
|
@@ -341,9 +423,9 @@ var AngularCodemod = class {
|
|
|
341
423
|
};
|
|
342
424
|
}
|
|
343
425
|
};
|
|
344
|
-
function applyAngularCodemod(filePath, texts, dryRun = false) {
|
|
426
|
+
function applyAngularCodemod(filePath, texts, dryRun = false, codemodConfig) {
|
|
345
427
|
const content = fs3.readFileSync(filePath, "utf-8");
|
|
346
|
-
const codemod = new AngularCodemod(filePath, content, texts);
|
|
428
|
+
const codemod = new AngularCodemod(filePath, content, texts, codemodConfig);
|
|
347
429
|
const result = codemod.transform();
|
|
348
430
|
if (result.changed && !dryRun) {
|
|
349
431
|
fs3.writeFileSync(filePath, result.transformedContent, "utf-8");
|
|
@@ -414,8 +496,10 @@ async function batchReplaceCdnUrls(fileUrlMap, assets, dryRun = false) {
|
|
|
414
496
|
import * as path2 from "path";
|
|
415
497
|
var CodemodRunner = class {
|
|
416
498
|
config;
|
|
417
|
-
|
|
499
|
+
cwd;
|
|
500
|
+
constructor(config, cwd = process.cwd()) {
|
|
418
501
|
this.config = config;
|
|
502
|
+
this.cwd = cwd;
|
|
419
503
|
}
|
|
420
504
|
async run(detectedTexts, options = {}) {
|
|
421
505
|
const startTime = Date.now();
|
|
@@ -464,13 +548,14 @@ var CodemodRunner = class {
|
|
|
464
548
|
}
|
|
465
549
|
applyCodemod(filePath, texts, framework, dryRun) {
|
|
466
550
|
const ext = path2.extname(filePath).toLowerCase();
|
|
551
|
+
const codemodConfig = this.config.codemods;
|
|
467
552
|
if (ext === ".vue") {
|
|
468
|
-
return applyVueCodemod(filePath, texts, dryRun);
|
|
553
|
+
return applyVueCodemod(filePath, texts, dryRun, codemodConfig);
|
|
469
554
|
}
|
|
470
555
|
if (framework === "angular" || framework === "angular-ngx" || framework === "angular-i18n") {
|
|
471
|
-
return applyAngularCodemod(filePath, texts, dryRun);
|
|
556
|
+
return applyAngularCodemod(filePath, texts, dryRun, codemodConfig);
|
|
472
557
|
}
|
|
473
|
-
return applyReactCodemod(filePath, texts, dryRun);
|
|
558
|
+
return applyReactCodemod(filePath, texts, dryRun, codemodConfig, this.cwd);
|
|
474
559
|
}
|
|
475
560
|
};
|
|
476
561
|
export {
|
|
@@ -482,5 +567,6 @@ export {
|
|
|
482
567
|
applyAngularCodemod,
|
|
483
568
|
applyReactCodemod,
|
|
484
569
|
applyVueCodemod,
|
|
485
|
-
batchReplaceCdnUrls
|
|
570
|
+
batchReplaceCdnUrls,
|
|
571
|
+
resolveImportSpecifier
|
|
486
572
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-localize-codemods",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"description": "AST-based codemods to inject i18n wrappers into frontend source code",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"@babel/generator": "^7.23.9",
|
|
20
20
|
"recast": "^0.23.4",
|
|
21
21
|
"jscodeshift": "^0.15.2",
|
|
22
|
-
"ai-localize-shared": "
|
|
22
|
+
"ai-localize-shared": "2.0.1"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"@types/babel__generator": "^7.6.8",
|
package/src/angular-codemod.ts
CHANGED
|
@@ -1,25 +1,39 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
|
|
3
|
-
import type { DetectedText } from 'ai-localize-shared';
|
|
3
|
+
import type { DetectedText, CodemodConfig } from 'ai-localize-shared';
|
|
4
4
|
import type { CodemodResult } from './react-codemod.js';
|
|
5
5
|
|
|
6
|
+
const DEFAULT_TS_SERVICE = 'this.translateService.instant';
|
|
7
|
+
const DEFAULT_TEMPLATE_PIPE = 'translate';
|
|
8
|
+
|
|
6
9
|
/**
|
|
7
10
|
* Angular ngx-translate codemod:
|
|
8
11
|
* - In templates: wraps text with {{ 'key' | translate }}
|
|
9
12
|
* - In TS: replaces string literals with this.translateService.instant('key')
|
|
13
|
+
*
|
|
14
|
+
* The pipe name used in templates can be overridden via
|
|
15
|
+
* `codemods.translationFunction` (default: "translate").
|
|
16
|
+
* The TS service call can be overridden via `codemods.hookName`
|
|
17
|
+
* (default: "this.translateService.instant").
|
|
10
18
|
*/
|
|
11
19
|
export class AngularCodemod {
|
|
12
20
|
private filePath: string;
|
|
13
21
|
private content: string;
|
|
14
22
|
private texts: DetectedText[];
|
|
23
|
+
private templatePipe: string;
|
|
24
|
+
private tsServiceCall: string;
|
|
15
25
|
|
|
16
|
-
constructor(filePath: string, content: string, texts: DetectedText[]) {
|
|
17
|
-
|
|
18
|
-
|
|
26
|
+
constructor(filePath: string, content: string, texts: DetectedText[], codemodConfig?: CodemodConfig) {
|
|
27
|
+
this.filePath = filePath;
|
|
28
|
+
this.content = content;
|
|
19
29
|
this.texts = texts;
|
|
30
|
+
// translationFunction maps to the Angular template pipe name
|
|
31
|
+
this.templatePipe = codemodConfig?.translationFunction ?? DEFAULT_TEMPLATE_PIPE;
|
|
32
|
+
// hookName maps to the TS service call expression
|
|
33
|
+
this.tsServiceCall = codemodConfig?.hookName ?? DEFAULT_TS_SERVICE;
|
|
20
34
|
}
|
|
21
35
|
|
|
22
|
-
|
|
36
|
+
transform(): CodemodResult {
|
|
23
37
|
if (this.texts.length === 0) return this.unchanged();
|
|
24
38
|
|
|
25
39
|
const textKeyMap = new Map<string, string>();
|
|
@@ -30,26 +44,28 @@ export class AngularCodemod {
|
|
|
30
44
|
let transformedContent = this.content;
|
|
31
45
|
let replacedTexts = 0;
|
|
32
46
|
const isTemplate = this.filePath.endsWith('.html');
|
|
47
|
+
const pipe = this.templatePipe;
|
|
48
|
+
const svc = this.tsServiceCall;
|
|
33
49
|
|
|
34
|
-
|
|
35
|
-
|
|
50
|
+
for (const [text, key] of textKeyMap) {
|
|
51
|
+
const escaped = text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
36
52
|
if (isTemplate) {
|
|
37
53
|
// Template: >Some Text< => >{{ 'key' | translate }}<
|
|
38
|
-
|
|
39
|
-
transformedContent = transformedContent.replace(re, `$1{{ '${key}' |
|
|
54
|
+
const re = new RegExp(`(>\\s*)${escaped}(\\s*<)`, 'g');
|
|
55
|
+
transformedContent = transformedContent.replace(re, `$1{{ '${key}' | ${pipe} }}$2`);
|
|
40
56
|
// attribute: [attr]="'Some Text'" => [attr]="'key' | translate"
|
|
41
57
|
const attrRe = new RegExp(`\\[(?:placeholder|label|title|alt)\\]="'${escaped}'"`, 'g');
|
|
42
58
|
transformedContent = transformedContent.replace(
|
|
43
|
-
|
|
44
|
-
`[placeholder]="'${key}' |
|
|
59
|
+
attrRe,
|
|
60
|
+
`[placeholder]="'${key}' | ${pipe}"`
|
|
45
61
|
);
|
|
46
62
|
} else {
|
|
47
|
-
|
|
48
|
-
|
|
63
|
+
// TS file: 'Some Text' => this.translateService.instant('key')
|
|
64
|
+
const tsRe = new RegExp(`'${escaped}'`, 'g');
|
|
49
65
|
transformedContent = transformedContent.replace(
|
|
50
66
|
tsRe,
|
|
51
|
-
|
|
52
|
-
|
|
67
|
+
`${svc}('${key}')`
|
|
68
|
+
);
|
|
53
69
|
}
|
|
54
70
|
replacedTexts++;
|
|
55
71
|
}
|
|
@@ -58,12 +74,12 @@ export class AngularCodemod {
|
|
|
58
74
|
|
|
59
75
|
return {
|
|
60
76
|
filePath: this.filePath,
|
|
61
|
-
|
|
77
|
+
originalContent: this.content,
|
|
62
78
|
transformedContent,
|
|
63
79
|
changed: true,
|
|
64
80
|
injectedImport: false,
|
|
65
81
|
injectedHook: false,
|
|
66
|
-
|
|
82
|
+
replacedTexts,
|
|
67
83
|
};
|
|
68
84
|
}
|
|
69
85
|
|
|
@@ -71,11 +87,11 @@ export class AngularCodemod {
|
|
|
71
87
|
return {
|
|
72
88
|
filePath: this.filePath,
|
|
73
89
|
originalContent: this.content,
|
|
74
|
-
|
|
75
|
-
|
|
90
|
+
transformedContent: this.content,
|
|
91
|
+
changed: false,
|
|
76
92
|
injectedImport: false,
|
|
77
93
|
injectedHook: false,
|
|
78
|
-
|
|
94
|
+
replacedTexts: 0,
|
|
79
95
|
};
|
|
80
96
|
}
|
|
81
97
|
}
|
|
@@ -83,10 +99,11 @@ export class AngularCodemod {
|
|
|
83
99
|
export function applyAngularCodemod(
|
|
84
100
|
filePath: string,
|
|
85
101
|
texts: DetectedText[],
|
|
86
|
-
dryRun = false
|
|
102
|
+
dryRun = false,
|
|
103
|
+
codemodConfig?: CodemodConfig
|
|
87
104
|
): CodemodResult {
|
|
88
105
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
89
|
-
const codemod = new AngularCodemod(filePath, content, texts);
|
|
106
|
+
const codemod = new AngularCodemod(filePath, content, texts, codemodConfig);
|
|
90
107
|
const result = codemod.transform();
|
|
91
108
|
if (result.changed && !dryRun) {
|
|
92
109
|
fs.writeFileSync(filePath, result.transformedContent, 'utf-8');
|