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/dist/index.js CHANGED
@@ -38,26 +38,42 @@ __export(index_exports, {
38
38
  applyAngularCodemod: () => applyAngularCodemod,
39
39
  applyReactCodemod: () => applyReactCodemod,
40
40
  applyVueCodemod: () => applyVueCodemod,
41
- batchReplaceCdnUrls: () => batchReplaceCdnUrls
41
+ batchReplaceCdnUrls: () => batchReplaceCdnUrls,
42
+ resolveImportSpecifier: () => resolveImportSpecifier
42
43
  });
43
44
  module.exports = __toCommonJS(index_exports);
44
45
 
45
46
  // src/react-codemod.ts
46
47
  var fs = __toESM(require("fs"));
48
+ var nodePath = __toESM(require("path"));
47
49
  var parser = __toESM(require("@babel/parser"));
48
50
  var import_traverse = __toESM(require("@babel/traverse"));
49
51
  var import_generator = __toESM(require("@babel/generator"));
50
52
  var t = __toESM(require("@babel/types"));
51
- var USE_TRANSLATION_IMPORT = "react-i18next";
52
- var USE_TRANSLATION_HOOK = "useTranslation";
53
+ var DEFAULT_IMPORT_PACKAGE = "react-i18next";
54
+ var DEFAULT_HOOK_NAME = "useTranslation";
55
+ var DEFAULT_TRANSLATION_FN = "t";
56
+ var DEFAULT_ACCESSOR_STYLE = "function";
53
57
  var ReactCodemod = class {
54
58
  filePath;
55
59
  content;
56
60
  texts;
57
- constructor(filePath, content, texts) {
61
+ /** Resolved import specifier string (may differ per file for local paths) */
62
+ importSpecifier;
63
+ hookName;
64
+ translationFn;
65
+ namespace;
66
+ accessorStyle;
67
+ constructor(filePath, content, texts, codemodConfig, cwd = process.cwd()) {
58
68
  this.filePath = filePath;
59
69
  this.content = content;
60
70
  this.texts = texts;
71
+ this.hookName = codemodConfig?.hookName ?? DEFAULT_HOOK_NAME;
72
+ this.translationFn = codemodConfig?.translationFunction ?? DEFAULT_TRANSLATION_FN;
73
+ this.namespace = codemodConfig?.namespace;
74
+ this.accessorStyle = codemodConfig?.accessorStyle ?? DEFAULT_ACCESSOR_STYLE;
75
+ const pkg = codemodConfig?.importPackage ?? DEFAULT_IMPORT_PACKAGE;
76
+ this.importSpecifier = resolveImportSpecifier(pkg, filePath, cwd);
61
77
  }
62
78
  transform() {
63
79
  if (this.texts.length === 0) {
@@ -101,58 +117,66 @@ var ReactCodemod = class {
101
117
  textKeyMap.set(dt.text, dt.suggestedKey);
102
118
  }
103
119
  let replacedTexts = 0;
104
- let hasUseTranslationImport = false;
105
- let hasUseTranslationHook = false;
120
+ let hasImport = false;
121
+ let hasHook = false;
106
122
  let injectedImport = false;
107
123
  let injectedHook = false;
124
+ const translationFn = this.translationFn;
125
+ const hookName = this.hookName;
126
+ const importSpecifier2 = this.importSpecifier;
127
+ const accessorStyle = this.accessorStyle;
128
+ const buildTranslationExpr = (key) => {
129
+ if (accessorStyle === "bracket") {
130
+ return t.memberExpression(
131
+ t.identifier(translationFn),
132
+ t.stringLiteral(key),
133
+ true
134
+ /* computed */
135
+ );
136
+ }
137
+ return t.callExpression(t.identifier(translationFn), [t.stringLiteral(key)]);
138
+ };
108
139
  (0, import_traverse.default)(ast, {
109
- ImportDeclaration(nodePath) {
110
- if (nodePath.node.source.value === USE_TRANSLATION_IMPORT) {
111
- hasUseTranslationImport = true;
140
+ ImportDeclaration(nodePath2) {
141
+ if (nodePath2.node.source.value === importSpecifier2) {
142
+ hasImport = true;
112
143
  }
113
144
  },
114
- // Check if useTranslation hook already destructured
115
- VariableDeclarator(nodePath) {
116
- const id = nodePath.node.id;
145
+ VariableDeclarator(nodePath2) {
146
+ const id = nodePath2.node.id;
117
147
  if (t.isObjectPattern(id) && id.properties.some(
118
- (p) => t.isObjectProperty(p) && t.isIdentifier(p.key) && p.key.name === "t"
148
+ (p) => t.isObjectProperty(p) && t.isIdentifier(p.key) && p.key.name === translationFn
119
149
  )) {
120
- hasUseTranslationHook = true;
150
+ hasHook = true;
121
151
  }
122
152
  }
123
153
  });
124
154
  (0, import_traverse.default)(ast, {
125
- JSXText(nodePath) {
126
- const text = nodePath.node.value.trim();
155
+ JSXText(nodePath2) {
156
+ const text = nodePath2.node.value.trim();
127
157
  if (!text) return;
128
158
  const key = textKeyMap.get(text);
129
159
  if (!key) return;
130
- const tCall = t.jsxExpressionContainer(
131
- t.callExpression(t.identifier("t"), [t.stringLiteral(key)])
132
- );
133
- nodePath.replaceWith(tCall);
160
+ nodePath2.replaceWith(t.jsxExpressionContainer(buildTranslationExpr(key)));
134
161
  replacedTexts++;
135
162
  },
136
- StringLiteral(nodePath) {
137
- const text = nodePath.node.value;
163
+ StringLiteral(nodePath2) {
164
+ const text = nodePath2.node.value;
138
165
  const key = textKeyMap.get(text);
139
166
  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);
167
+ if (t.isImportDeclaration(nodePath2.parent)) return;
168
+ if (t.isObjectProperty(nodePath2.parent) && nodePath2.parent.key === nodePath2.node) return;
169
+ if (t.isJSXAttribute(nodePath2.parent)) return;
170
+ nodePath2.replaceWith(buildTranslationExpr(key));
145
171
  replacedTexts++;
146
172
  },
147
- JSXAttribute(nodePath) {
148
- const valueNode = nodePath.node.value;
173
+ JSXAttribute(nodePath2) {
174
+ const valueNode = nodePath2.node.value;
149
175
  if (!t.isStringLiteral(valueNode)) return;
150
176
  const text = valueNode.value;
151
177
  const key = textKeyMap.get(text);
152
178
  if (!key) return;
153
- nodePath.node.value = t.jsxExpressionContainer(
154
- t.callExpression(t.identifier("t"), [t.stringLiteral(key)])
155
- );
179
+ nodePath2.node.value = t.jsxExpressionContainer(buildTranslationExpr(key));
156
180
  replacedTexts++;
157
181
  }
158
182
  });
@@ -167,25 +191,21 @@ var ReactCodemod = class {
167
191
  replacedTexts: 0
168
192
  };
169
193
  }
170
- if (!hasUseTranslationImport) {
194
+ if (!hasImport) {
171
195
  const importDecl = t.importDeclaration(
172
- [t.importSpecifier(t.identifier(USE_TRANSLATION_HOOK), t.identifier(USE_TRANSLATION_HOOK))],
173
- t.stringLiteral(USE_TRANSLATION_IMPORT)
196
+ [t.importSpecifier(t.identifier(hookName), t.identifier(hookName))],
197
+ t.stringLiteral(importSpecifier2)
174
198
  );
175
199
  ast.program.body.unshift(importDecl);
176
200
  injectedImport = true;
177
201
  }
178
- if (!hasUseTranslationHook) {
179
- this.injectUseTranslationHook(ast);
202
+ if (!hasHook) {
203
+ this.injectHook(ast);
180
204
  injectedHook = true;
181
205
  }
182
206
  const output = (0, import_generator.default)(
183
207
  ast,
184
- {
185
- retainLines: false,
186
- comments: true,
187
- compact: false
188
- },
208
+ { retainLines: false, comments: true, compact: false },
189
209
  this.content
190
210
  );
191
211
  return {
@@ -198,19 +218,22 @@ var ReactCodemod = class {
198
218
  replacedTexts
199
219
  };
200
220
  }
201
- injectUseTranslationHook(ast) {
221
+ injectHook(ast) {
222
+ const hookName = this.hookName;
223
+ const translationFn = this.translationFn;
224
+ const namespace = this.namespace;
202
225
  (0, import_traverse.default)(ast, {
203
- FunctionDeclaration(nodePath) {
204
- if (!isReactComponent(nodePath.node.id?.name)) return;
205
- injectHookIntoBody(nodePath.node.body);
206
- nodePath.stop();
226
+ FunctionDeclaration(nodePath2) {
227
+ if (!isReactComponent(nodePath2.node.id?.name)) return;
228
+ injectHookIntoBody(nodePath2.node.body, hookName, translationFn, namespace);
229
+ nodePath2.stop();
207
230
  },
208
- ArrowFunctionExpression(nodePath) {
209
- if (!isReactComponentParent(nodePath)) return;
210
- const body = nodePath.node.body;
231
+ ArrowFunctionExpression(nodePath2) {
232
+ if (!isReactComponentParent(nodePath2)) return;
233
+ const body = nodePath2.node.body;
211
234
  if (t.isBlockStatement(body)) {
212
- injectHookIntoBody(body);
213
- nodePath.stop();
235
+ injectHookIntoBody(body, hookName, translationFn, namespace);
236
+ nodePath2.stop();
214
237
  }
215
238
  }
216
239
  });
@@ -220,28 +243,76 @@ function isReactComponent(name) {
220
243
  if (!name) return false;
221
244
  return /^[A-Z]/.test(name);
222
245
  }
223
- function isReactComponentParent(nodePath) {
224
- const parent = nodePath.parent;
246
+ function isReactComponentParent(nodePath2) {
247
+ const parent = nodePath2.parent;
225
248
  if (t.isVariableDeclarator(parent)) {
226
249
  const id = parent.id;
227
250
  if (t.isIdentifier(id) && /^[A-Z]/.test(id.name)) return true;
228
251
  }
229
252
  return false;
230
253
  }
231
- function injectHookIntoBody(body) {
254
+ function injectHookIntoBody(body, hookName, translationFn, namespace) {
255
+ const hookArgs = namespace ? [t.stringLiteral(namespace)] : [];
232
256
  const hookDecl = t.variableDeclaration("const", [
233
257
  t.variableDeclarator(
234
258
  t.objectPattern([
235
- t.objectProperty(t.identifier("t"), t.identifier("t"), false, true)
259
+ t.objectProperty(t.identifier(translationFn), t.identifier(translationFn), false, true)
236
260
  ]),
237
- t.callExpression(t.identifier("useTranslation"), [])
261
+ t.callExpression(t.identifier(hookName), hookArgs)
238
262
  )
239
263
  ]);
240
264
  body.body.unshift(hookDecl);
241
265
  }
242
- function applyReactCodemod(filePath, texts, dryRun = false) {
266
+ function resolveImportSpecifier(importPackage, filePath, cwd) {
267
+ if (isNpmPackage(importPackage)) {
268
+ return importPackage;
269
+ }
270
+ let absoluteHookPath;
271
+ if (nodePath.isAbsolute(importPackage)) {
272
+ absoluteHookPath = importPackage;
273
+ } else {
274
+ absoluteHookPath = nodePath.resolve(cwd, importPackage);
275
+ }
276
+ const fileDir = nodePath.dirname(filePath);
277
+ let rel = nodePath.relative(fileDir, absoluteHookPath).replace(/\\/g, "/");
278
+ if (!rel.startsWith(".")) {
279
+ rel = `./${rel}`;
280
+ }
281
+ return rel;
282
+ }
283
+ function isNpmPackage(pkg) {
284
+ if (pkg.startsWith(".") || pkg.startsWith("/")) return false;
285
+ if (pkg.startsWith("@") && pkg.includes("/")) {
286
+ if (pkg.startsWith("@/")) return false;
287
+ return true;
288
+ }
289
+ if (!pkg.includes("/")) return true;
290
+ const firstSegment = pkg.split("/")[0].toLowerCase();
291
+ const sourceDirectories = /* @__PURE__ */ new Set([
292
+ "src",
293
+ "app",
294
+ "lib",
295
+ "hooks",
296
+ "utils",
297
+ "pages",
298
+ "components",
299
+ "features",
300
+ "shared",
301
+ "common",
302
+ "locales",
303
+ "i18n",
304
+ "services",
305
+ "store",
306
+ "helpers",
307
+ "core",
308
+ "modules"
309
+ ]);
310
+ if (sourceDirectories.has(firstSegment)) return false;
311
+ return true;
312
+ }
313
+ function applyReactCodemod(filePath, texts, dryRun = false, codemodConfig, cwd = process.cwd()) {
243
314
  const content = fs.readFileSync(filePath, "utf-8");
244
- const codemod = new ReactCodemod(filePath, content, texts);
315
+ const codemod = new ReactCodemod(filePath, content, texts, codemodConfig, cwd);
245
316
  const result = codemod.transform();
246
317
  if (result.changed && !dryRun) {
247
318
  fs.writeFileSync(filePath, result.transformedContent, "utf-8");
@@ -251,14 +322,17 @@ function applyReactCodemod(filePath, texts, dryRun = false) {
251
322
 
252
323
  // src/vue-codemod.ts
253
324
  var fs2 = __toESM(require("fs"));
325
+ var DEFAULT_TRANSLATION_FN2 = "$t";
254
326
  var VueCodemod = class {
255
327
  filePath;
256
328
  content;
257
329
  texts;
258
- constructor(filePath, content, texts) {
330
+ translationFn;
331
+ constructor(filePath, content, texts, codemodConfig) {
259
332
  this.filePath = filePath;
260
333
  this.content = content;
261
334
  this.texts = texts;
335
+ this.translationFn = codemodConfig?.translationFunction ?? DEFAULT_TRANSLATION_FN2;
262
336
  }
263
337
  transform() {
264
338
  if (this.texts.length === 0) {
@@ -275,14 +349,15 @@ var VueCodemod = class {
275
349
  }
276
350
  let transformedContent = this.content;
277
351
  let replacedTexts = 0;
352
+ const fn = this.translationFn;
278
353
  if (templateMatch) {
279
354
  let template = templateMatch[1];
280
355
  for (const [text, key] of textKeyMap) {
281
356
  const escaped = text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
282
357
  const re = new RegExp(`(>\\s*)${escaped}(\\s*<)`, "g");
283
- template = template.replace(re, `$1{{ $t('${key}') }}$2`);
358
+ template = template.replace(re, `$1{{ ${fn}('${key}') }}$2`);
284
359
  const attrRe = new RegExp(`((?:title|placeholder|alt|label)=)"(${escaped})"`, "g");
285
- template = template.replace(attrRe, `:$1"$t('${key}')"`);
360
+ template = template.replace(attrRe, `:$1"${fn}('${key}')"`);
286
361
  replacedTexts++;
287
362
  }
288
363
  transformedContent = transformedContent.replace(templateMatch[1], template);
@@ -313,9 +388,9 @@ var VueCodemod = class {
313
388
  };
314
389
  }
315
390
  };
316
- function applyVueCodemod(filePath, texts, dryRun = false) {
391
+ function applyVueCodemod(filePath, texts, dryRun = false, codemodConfig) {
317
392
  const content = fs2.readFileSync(filePath, "utf-8");
318
- const codemod = new VueCodemod(filePath, content, texts);
393
+ const codemod = new VueCodemod(filePath, content, texts, codemodConfig);
319
394
  const result = codemod.transform();
320
395
  if (result.changed && !dryRun) {
321
396
  fs2.writeFileSync(filePath, result.transformedContent, "utf-8");
@@ -325,14 +400,20 @@ function applyVueCodemod(filePath, texts, dryRun = false) {
325
400
 
326
401
  // src/angular-codemod.ts
327
402
  var fs3 = __toESM(require("fs"));
403
+ var DEFAULT_TS_SERVICE = "this.translateService.instant";
404
+ var DEFAULT_TEMPLATE_PIPE = "translate";
328
405
  var AngularCodemod = class {
329
406
  filePath;
330
407
  content;
331
408
  texts;
332
- constructor(filePath, content, texts) {
409
+ templatePipe;
410
+ tsServiceCall;
411
+ constructor(filePath, content, texts, codemodConfig) {
333
412
  this.filePath = filePath;
334
413
  this.content = content;
335
414
  this.texts = texts;
415
+ this.templatePipe = codemodConfig?.translationFunction ?? DEFAULT_TEMPLATE_PIPE;
416
+ this.tsServiceCall = codemodConfig?.hookName ?? DEFAULT_TS_SERVICE;
336
417
  }
337
418
  transform() {
338
419
  if (this.texts.length === 0) return this.unchanged();
@@ -343,21 +424,23 @@ var AngularCodemod = class {
343
424
  let transformedContent = this.content;
344
425
  let replacedTexts = 0;
345
426
  const isTemplate = this.filePath.endsWith(".html");
427
+ const pipe = this.templatePipe;
428
+ const svc = this.tsServiceCall;
346
429
  for (const [text, key] of textKeyMap) {
347
430
  const escaped = text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
348
431
  if (isTemplate) {
349
432
  const re = new RegExp(`(>\\s*)${escaped}(\\s*<)`, "g");
350
- transformedContent = transformedContent.replace(re, `$1{{ '${key}' | translate }}$2`);
433
+ transformedContent = transformedContent.replace(re, `$1{{ '${key}' | ${pipe} }}$2`);
351
434
  const attrRe = new RegExp(`\\[(?:placeholder|label|title|alt)\\]="'${escaped}'"`, "g");
352
435
  transformedContent = transformedContent.replace(
353
436
  attrRe,
354
- `[placeholder]="'${key}' | translate"`
437
+ `[placeholder]="'${key}' | ${pipe}"`
355
438
  );
356
439
  } else {
357
440
  const tsRe = new RegExp(`'${escaped}'`, "g");
358
441
  transformedContent = transformedContent.replace(
359
442
  tsRe,
360
- `this.translateService.instant('${key}')`
443
+ `${svc}('${key}')`
361
444
  );
362
445
  }
363
446
  replacedTexts++;
@@ -385,9 +468,9 @@ var AngularCodemod = class {
385
468
  };
386
469
  }
387
470
  };
388
- function applyAngularCodemod(filePath, texts, dryRun = false) {
471
+ function applyAngularCodemod(filePath, texts, dryRun = false, codemodConfig) {
389
472
  const content = fs3.readFileSync(filePath, "utf-8");
390
- const codemod = new AngularCodemod(filePath, content, texts);
473
+ const codemod = new AngularCodemod(filePath, content, texts, codemodConfig);
391
474
  const result = codemod.transform();
392
475
  if (result.changed && !dryRun) {
393
476
  fs3.writeFileSync(filePath, result.transformedContent, "utf-8");
@@ -458,8 +541,10 @@ async function batchReplaceCdnUrls(fileUrlMap, assets, dryRun = false) {
458
541
  var path2 = __toESM(require("path"));
459
542
  var CodemodRunner = class {
460
543
  config;
461
- constructor(config) {
544
+ cwd;
545
+ constructor(config, cwd = process.cwd()) {
462
546
  this.config = config;
547
+ this.cwd = cwd;
463
548
  }
464
549
  async run(detectedTexts, options = {}) {
465
550
  const startTime = Date.now();
@@ -508,13 +593,14 @@ var CodemodRunner = class {
508
593
  }
509
594
  applyCodemod(filePath, texts, framework, dryRun) {
510
595
  const ext = path2.extname(filePath).toLowerCase();
596
+ const codemodConfig = this.config.codemods;
511
597
  if (ext === ".vue") {
512
- return applyVueCodemod(filePath, texts, dryRun);
598
+ return applyVueCodemod(filePath, texts, dryRun, codemodConfig);
513
599
  }
514
600
  if (framework === "angular" || framework === "angular-ngx" || framework === "angular-i18n") {
515
- return applyAngularCodemod(filePath, texts, dryRun);
601
+ return applyAngularCodemod(filePath, texts, dryRun, codemodConfig);
516
602
  }
517
- return applyReactCodemod(filePath, texts, dryRun);
603
+ return applyReactCodemod(filePath, texts, dryRun, codemodConfig, this.cwd);
518
604
  }
519
605
  };
520
606
  // Annotate the CommonJS export names for ESM import in node:
@@ -527,5 +613,6 @@ var CodemodRunner = class {
527
613
  applyAngularCodemod,
528
614
  applyReactCodemod,
529
615
  applyVueCodemod,
530
- batchReplaceCdnUrls
616
+ batchReplaceCdnUrls,
617
+ resolveImportSpecifier
531
618
  });