ai-localize-codemods 1.0.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/dist/index.js ADDED
@@ -0,0 +1,531 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ AngularCodemod: () => AngularCodemod,
34
+ CdnReplacer: () => CdnReplacer,
35
+ CodemodRunner: () => CodemodRunner,
36
+ ReactCodemod: () => ReactCodemod,
37
+ VueCodemod: () => VueCodemod,
38
+ applyAngularCodemod: () => applyAngularCodemod,
39
+ applyReactCodemod: () => applyReactCodemod,
40
+ applyVueCodemod: () => applyVueCodemod,
41
+ batchReplaceCdnUrls: () => batchReplaceCdnUrls
42
+ });
43
+ module.exports = __toCommonJS(index_exports);
44
+
45
+ // src/react-codemod.ts
46
+ var fs = __toESM(require("fs"));
47
+ var parser = __toESM(require("@babel/parser"));
48
+ var import_traverse = __toESM(require("@babel/traverse"));
49
+ var import_generator = __toESM(require("@babel/generator"));
50
+ var t = __toESM(require("@babel/types"));
51
+ var USE_TRANSLATION_IMPORT = "react-i18next";
52
+ var USE_TRANSLATION_HOOK = "useTranslation";
53
+ var ReactCodemod = class {
54
+ filePath;
55
+ content;
56
+ texts;
57
+ constructor(filePath, content, texts) {
58
+ this.filePath = filePath;
59
+ this.content = content;
60
+ this.texts = texts;
61
+ }
62
+ transform() {
63
+ if (this.texts.length === 0) {
64
+ return {
65
+ filePath: this.filePath,
66
+ originalContent: this.content,
67
+ transformedContent: this.content,
68
+ changed: false,
69
+ injectedImport: false,
70
+ injectedHook: false,
71
+ replacedTexts: 0
72
+ };
73
+ }
74
+ let ast;
75
+ try {
76
+ ast = parser.parse(this.content, {
77
+ sourceType: "module",
78
+ plugins: [
79
+ "jsx",
80
+ "typescript",
81
+ "decorators-legacy",
82
+ "classProperties",
83
+ "optionalChaining",
84
+ "nullishCoalescingOperator"
85
+ ],
86
+ errorRecovery: true
87
+ });
88
+ } catch {
89
+ return {
90
+ filePath: this.filePath,
91
+ originalContent: this.content,
92
+ transformedContent: this.content,
93
+ changed: false,
94
+ injectedImport: false,
95
+ injectedHook: false,
96
+ replacedTexts: 0
97
+ };
98
+ }
99
+ const textKeyMap = /* @__PURE__ */ new Map();
100
+ for (const dt of this.texts) {
101
+ textKeyMap.set(dt.text, dt.suggestedKey);
102
+ }
103
+ let replacedTexts = 0;
104
+ let hasUseTranslationImport = false;
105
+ let hasUseTranslationHook = false;
106
+ let injectedImport = false;
107
+ let injectedHook = false;
108
+ (0, import_traverse.default)(ast, {
109
+ ImportDeclaration(nodePath) {
110
+ if (nodePath.node.source.value === USE_TRANSLATION_IMPORT) {
111
+ hasUseTranslationImport = true;
112
+ }
113
+ },
114
+ // Check if useTranslation hook already destructured
115
+ VariableDeclarator(nodePath) {
116
+ const id = nodePath.node.id;
117
+ if (t.isObjectPattern(id) && id.properties.some(
118
+ (p) => t.isObjectProperty(p) && t.isIdentifier(p.key) && p.key.name === "t"
119
+ )) {
120
+ hasUseTranslationHook = true;
121
+ }
122
+ }
123
+ });
124
+ (0, import_traverse.default)(ast, {
125
+ JSXText(nodePath) {
126
+ const text = nodePath.node.value.trim();
127
+ if (!text) return;
128
+ const key = textKeyMap.get(text);
129
+ if (!key) return;
130
+ const tCall = t.jsxExpressionContainer(
131
+ t.callExpression(t.identifier("t"), [t.stringLiteral(key)])
132
+ );
133
+ nodePath.replaceWith(tCall);
134
+ replacedTexts++;
135
+ },
136
+ StringLiteral(nodePath) {
137
+ const text = nodePath.node.value;
138
+ const key = textKeyMap.get(text);
139
+ if (!key) return;
140
+ if (t.isImportDeclaration(nodePath.parent)) return;
141
+ if (t.isObjectProperty(nodePath.parent) && nodePath.parent.key === nodePath.node) return;
142
+ if (t.isJSXAttribute(nodePath.parent)) return;
143
+ const tCall = t.callExpression(t.identifier("t"), [t.stringLiteral(key)]);
144
+ nodePath.replaceWith(tCall);
145
+ replacedTexts++;
146
+ },
147
+ JSXAttribute(nodePath) {
148
+ const valueNode = nodePath.node.value;
149
+ if (!t.isStringLiteral(valueNode)) return;
150
+ const text = valueNode.value;
151
+ const key = textKeyMap.get(text);
152
+ if (!key) return;
153
+ nodePath.node.value = t.jsxExpressionContainer(
154
+ t.callExpression(t.identifier("t"), [t.stringLiteral(key)])
155
+ );
156
+ replacedTexts++;
157
+ }
158
+ });
159
+ if (replacedTexts === 0) {
160
+ return {
161
+ filePath: this.filePath,
162
+ originalContent: this.content,
163
+ transformedContent: this.content,
164
+ changed: false,
165
+ injectedImport: false,
166
+ injectedHook: false,
167
+ replacedTexts: 0
168
+ };
169
+ }
170
+ if (!hasUseTranslationImport) {
171
+ const importDecl = t.importDeclaration(
172
+ [t.importSpecifier(t.identifier(USE_TRANSLATION_HOOK), t.identifier(USE_TRANSLATION_HOOK))],
173
+ t.stringLiteral(USE_TRANSLATION_IMPORT)
174
+ );
175
+ ast.program.body.unshift(importDecl);
176
+ injectedImport = true;
177
+ }
178
+ if (!hasUseTranslationHook) {
179
+ this.injectUseTranslationHook(ast);
180
+ injectedHook = true;
181
+ }
182
+ const output = (0, import_generator.default)(
183
+ ast,
184
+ {
185
+ retainLines: false,
186
+ comments: true,
187
+ compact: false
188
+ },
189
+ this.content
190
+ );
191
+ return {
192
+ filePath: this.filePath,
193
+ originalContent: this.content,
194
+ transformedContent: output.code,
195
+ changed: true,
196
+ injectedImport,
197
+ injectedHook,
198
+ replacedTexts
199
+ };
200
+ }
201
+ injectUseTranslationHook(ast) {
202
+ (0, import_traverse.default)(ast, {
203
+ FunctionDeclaration(nodePath) {
204
+ if (!isReactComponent(nodePath.node.id?.name)) return;
205
+ injectHookIntoBody(nodePath.node.body);
206
+ nodePath.stop();
207
+ },
208
+ ArrowFunctionExpression(nodePath) {
209
+ if (!isReactComponentParent(nodePath)) return;
210
+ const body = nodePath.node.body;
211
+ if (t.isBlockStatement(body)) {
212
+ injectHookIntoBody(body);
213
+ nodePath.stop();
214
+ }
215
+ }
216
+ });
217
+ }
218
+ };
219
+ function isReactComponent(name) {
220
+ if (!name) return false;
221
+ return /^[A-Z]/.test(name);
222
+ }
223
+ function isReactComponentParent(nodePath) {
224
+ const parent = nodePath.parent;
225
+ if (t.isVariableDeclarator(parent)) {
226
+ const id = parent.id;
227
+ if (t.isIdentifier(id) && /^[A-Z]/.test(id.name)) return true;
228
+ }
229
+ return false;
230
+ }
231
+ function injectHookIntoBody(body) {
232
+ const hookDecl = t.variableDeclaration("const", [
233
+ t.variableDeclarator(
234
+ t.objectPattern([
235
+ t.objectProperty(t.identifier("t"), t.identifier("t"), false, true)
236
+ ]),
237
+ t.callExpression(t.identifier("useTranslation"), [])
238
+ )
239
+ ]);
240
+ body.body.unshift(hookDecl);
241
+ }
242
+ function applyReactCodemod(filePath, texts, dryRun = false) {
243
+ const content = fs.readFileSync(filePath, "utf-8");
244
+ const codemod = new ReactCodemod(filePath, content, texts);
245
+ const result = codemod.transform();
246
+ if (result.changed && !dryRun) {
247
+ fs.writeFileSync(filePath, result.transformedContent, "utf-8");
248
+ }
249
+ return result;
250
+ }
251
+
252
+ // src/vue-codemod.ts
253
+ var fs2 = __toESM(require("fs"));
254
+ var VueCodemod = class {
255
+ filePath;
256
+ content;
257
+ texts;
258
+ constructor(filePath, content, texts) {
259
+ this.filePath = filePath;
260
+ this.content = content;
261
+ this.texts = texts;
262
+ }
263
+ transform() {
264
+ if (this.texts.length === 0) {
265
+ return this.unchanged();
266
+ }
267
+ const templateMatch = this.content.match(/<template[^>]*>([\s\S]*?)<\/template>/);
268
+ const scriptMatch = this.content.match(/<script[^>]*>([\s\S]*?)<\/script>/);
269
+ if (!templateMatch && !scriptMatch) {
270
+ return this.unchanged();
271
+ }
272
+ const textKeyMap = /* @__PURE__ */ new Map();
273
+ for (const dt of this.texts) {
274
+ textKeyMap.set(dt.text, dt.suggestedKey);
275
+ }
276
+ let transformedContent = this.content;
277
+ let replacedTexts = 0;
278
+ if (templateMatch) {
279
+ let template = templateMatch[1];
280
+ for (const [text, key] of textKeyMap) {
281
+ const escaped = text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
282
+ const re = new RegExp(`(>\\s*)${escaped}(\\s*<)`, "g");
283
+ template = template.replace(re, `$1{{ $t('${key}') }}$2`);
284
+ const attrRe = new RegExp(`((?:title|placeholder|alt|label)=)"(${escaped})"`, "g");
285
+ template = template.replace(attrRe, `:$1"$t('${key}')"`);
286
+ replacedTexts++;
287
+ }
288
+ transformedContent = transformedContent.replace(templateMatch[1], template);
289
+ }
290
+ if (replacedTexts === 0) {
291
+ return this.unchanged();
292
+ }
293
+ return {
294
+ filePath: this.filePath,
295
+ originalContent: this.content,
296
+ transformedContent,
297
+ changed: true,
298
+ injectedImport: false,
299
+ // vue-i18n is globally injected
300
+ injectedHook: false,
301
+ replacedTexts
302
+ };
303
+ }
304
+ unchanged() {
305
+ return {
306
+ filePath: this.filePath,
307
+ originalContent: this.content,
308
+ transformedContent: this.content,
309
+ changed: false,
310
+ injectedImport: false,
311
+ injectedHook: false,
312
+ replacedTexts: 0
313
+ };
314
+ }
315
+ };
316
+ function applyVueCodemod(filePath, texts, dryRun = false) {
317
+ const content = fs2.readFileSync(filePath, "utf-8");
318
+ const codemod = new VueCodemod(filePath, content, texts);
319
+ const result = codemod.transform();
320
+ if (result.changed && !dryRun) {
321
+ fs2.writeFileSync(filePath, result.transformedContent, "utf-8");
322
+ }
323
+ return result;
324
+ }
325
+
326
+ // src/angular-codemod.ts
327
+ var fs3 = __toESM(require("fs"));
328
+ var AngularCodemod = class {
329
+ filePath;
330
+ content;
331
+ texts;
332
+ constructor(filePath, content, texts) {
333
+ this.filePath = filePath;
334
+ this.content = content;
335
+ this.texts = texts;
336
+ }
337
+ transform() {
338
+ if (this.texts.length === 0) return this.unchanged();
339
+ const textKeyMap = /* @__PURE__ */ new Map();
340
+ for (const dt of this.texts) {
341
+ textKeyMap.set(dt.text, dt.suggestedKey);
342
+ }
343
+ let transformedContent = this.content;
344
+ let replacedTexts = 0;
345
+ const isTemplate = this.filePath.endsWith(".html");
346
+ for (const [text, key] of textKeyMap) {
347
+ const escaped = text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
348
+ if (isTemplate) {
349
+ const re = new RegExp(`(>\\s*)${escaped}(\\s*<)`, "g");
350
+ transformedContent = transformedContent.replace(re, `$1{{ '${key}' | translate }}$2`);
351
+ const attrRe = new RegExp(`\\[(?:placeholder|label|title|alt)\\]="'${escaped}'"`, "g");
352
+ transformedContent = transformedContent.replace(
353
+ attrRe,
354
+ `[placeholder]="'${key}' | translate"`
355
+ );
356
+ } else {
357
+ const tsRe = new RegExp(`'${escaped}'`, "g");
358
+ transformedContent = transformedContent.replace(
359
+ tsRe,
360
+ `this.translateService.instant('${key}')`
361
+ );
362
+ }
363
+ replacedTexts++;
364
+ }
365
+ if (replacedTexts === 0) return this.unchanged();
366
+ return {
367
+ filePath: this.filePath,
368
+ originalContent: this.content,
369
+ transformedContent,
370
+ changed: true,
371
+ injectedImport: false,
372
+ injectedHook: false,
373
+ replacedTexts
374
+ };
375
+ }
376
+ unchanged() {
377
+ return {
378
+ filePath: this.filePath,
379
+ originalContent: this.content,
380
+ transformedContent: this.content,
381
+ changed: false,
382
+ injectedImport: false,
383
+ injectedHook: false,
384
+ replacedTexts: 0
385
+ };
386
+ }
387
+ };
388
+ function applyAngularCodemod(filePath, texts, dryRun = false) {
389
+ const content = fs3.readFileSync(filePath, "utf-8");
390
+ const codemod = new AngularCodemod(filePath, content, texts);
391
+ const result = codemod.transform();
392
+ if (result.changed && !dryRun) {
393
+ fs3.writeFileSync(filePath, result.transformedContent, "utf-8");
394
+ }
395
+ return result;
396
+ }
397
+
398
+ // src/cdn-replacer.ts
399
+ var fs4 = __toESM(require("fs"));
400
+ var path = __toESM(require("path"));
401
+ var CdnReplacer = class {
402
+ assetMap;
403
+ constructor(assets) {
404
+ this.assetMap = /* @__PURE__ */ new Map();
405
+ for (const asset of assets) {
406
+ this.assetMap.set(asset.localPath, asset);
407
+ this.assetMap.set(asset.s3Key, asset);
408
+ }
409
+ }
410
+ replaceInFile(filePath, legacyUrls, dryRun = false) {
411
+ let content;
412
+ try {
413
+ content = fs4.readFileSync(filePath, "utf-8");
414
+ } catch {
415
+ return { filePath, replacedCount: 0, changed: false, originalContent: "", transformedContent: "" };
416
+ }
417
+ let transformedContent = content;
418
+ let replacedCount = 0;
419
+ for (const legacyUrl of legacyUrls) {
420
+ const asset = this.findAsset(legacyUrl.assetPath);
421
+ if (!asset) continue;
422
+ const escapedUrl = legacyUrl.url.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
423
+ const re = new RegExp(escapedUrl, "g");
424
+ const newCount = (transformedContent.match(re) || []).length;
425
+ transformedContent = transformedContent.replace(re, asset.cloudfrontUrl);
426
+ replacedCount += newCount;
427
+ }
428
+ if (replacedCount > 0 && !dryRun) {
429
+ fs4.writeFileSync(filePath, transformedContent, "utf-8");
430
+ }
431
+ return {
432
+ filePath,
433
+ replacedCount,
434
+ changed: replacedCount > 0,
435
+ originalContent: content,
436
+ transformedContent
437
+ };
438
+ }
439
+ findAsset(assetPath) {
440
+ const normalized = assetPath.replace(/^\//, "");
441
+ return this.assetMap.get(assetPath) || this.assetMap.get(normalized) || this.assetMap.get("/" + normalized) || // fuzzy match by filename
442
+ Array.from(this.assetMap.values()).find(
443
+ (a) => path.basename(a.localPath) === path.basename(assetPath)
444
+ );
445
+ }
446
+ };
447
+ async function batchReplaceCdnUrls(fileUrlMap, assets, dryRun = false) {
448
+ const replacer = new CdnReplacer(assets);
449
+ const results = [];
450
+ for (const [filePath, legacyUrls] of fileUrlMap) {
451
+ const result = replacer.replaceInFile(filePath, legacyUrls, dryRun);
452
+ results.push(result);
453
+ }
454
+ return results;
455
+ }
456
+
457
+ // src/codemod-runner.ts
458
+ var path2 = __toESM(require("path"));
459
+ var CodemodRunner = class {
460
+ config;
461
+ constructor(config) {
462
+ this.config = config;
463
+ }
464
+ async run(detectedTexts, options = {}) {
465
+ const startTime = Date.now();
466
+ const { dryRun = false, onProgress } = options;
467
+ const fileTextMap = /* @__PURE__ */ new Map();
468
+ for (const dt of detectedTexts) {
469
+ const existing = fileTextMap.get(dt.filePath) || [];
470
+ existing.push(dt);
471
+ fileTextMap.set(dt.filePath, existing);
472
+ }
473
+ const results = [];
474
+ let totalReplacements = 0;
475
+ let importInjections = 0;
476
+ let hookInjections = 0;
477
+ for (const [filePath, texts] of fileTextMap) {
478
+ let result;
479
+ const framework = this.config.framework;
480
+ try {
481
+ result = this.applyCodemod(filePath, texts, framework, dryRun);
482
+ } catch (err) {
483
+ result = {
484
+ filePath,
485
+ originalContent: "",
486
+ transformedContent: "",
487
+ changed: false,
488
+ injectedImport: false,
489
+ injectedHook: false,
490
+ replacedTexts: 0
491
+ };
492
+ }
493
+ results.push(result);
494
+ totalReplacements += result.replacedTexts;
495
+ if (result.injectedImport) importInjections++;
496
+ if (result.injectedHook) hookInjections++;
497
+ onProgress?.(filePath, result);
498
+ }
499
+ return {
500
+ totalFiles: fileTextMap.size,
501
+ changedFiles: results.filter((r) => r.changed).length,
502
+ totalReplacements,
503
+ importInjections,
504
+ hookInjections,
505
+ results,
506
+ duration: Date.now() - startTime
507
+ };
508
+ }
509
+ applyCodemod(filePath, texts, framework, dryRun) {
510
+ const ext = path2.extname(filePath).toLowerCase();
511
+ if (ext === ".vue") {
512
+ return applyVueCodemod(filePath, texts, dryRun);
513
+ }
514
+ if (framework === "angular" || framework === "angular-ngx" || framework === "angular-i18n") {
515
+ return applyAngularCodemod(filePath, texts, dryRun);
516
+ }
517
+ return applyReactCodemod(filePath, texts, dryRun);
518
+ }
519
+ };
520
+ // Annotate the CommonJS export names for ESM import in node:
521
+ 0 && (module.exports = {
522
+ AngularCodemod,
523
+ CdnReplacer,
524
+ CodemodRunner,
525
+ ReactCodemod,
526
+ VueCodemod,
527
+ applyAngularCodemod,
528
+ applyReactCodemod,
529
+ applyVueCodemod,
530
+ batchReplaceCdnUrls
531
+ });