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/src/vue-codemod.ts
CHANGED
|
@@ -1,60 +1,68 @@
|
|
|
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_TRANSLATION_FN = '$t';
|
|
7
|
+
|
|
6
8
|
/**
|
|
7
|
-
* Vue i18n codemod: wraps hardcoded text with $t() calls in Vue SFCs.
|
|
8
|
-
* Handles both Options API and Composition API.
|
|
9
|
+
* Vue i18n codemod: wraps hardcoded text with $t() / t() calls in Vue SFCs.
|
|
10
|
+
* Handles both Options API (global $t) and Composition API (useI18n / useTranslation).
|
|
11
|
+
*
|
|
12
|
+
* The translation function name can be overridden via the `codemods.translationFunction`
|
|
13
|
+
* config field (default: "$t").
|
|
9
14
|
*/
|
|
10
15
|
export class VueCodemod {
|
|
11
16
|
private filePath: string;
|
|
12
17
|
private content: string;
|
|
13
18
|
private texts: DetectedText[];
|
|
19
|
+
private translationFn: string;
|
|
14
20
|
|
|
15
|
-
constructor(filePath: string, content: string, texts: DetectedText[]) {
|
|
21
|
+
constructor(filePath: string, content: string, texts: DetectedText[], codemodConfig?: CodemodConfig) {
|
|
16
22
|
this.filePath = filePath;
|
|
17
23
|
this.content = content;
|
|
18
|
-
|
|
24
|
+
this.texts = texts;
|
|
25
|
+
this.translationFn = codemodConfig?.translationFunction ?? DEFAULT_TRANSLATION_FN;
|
|
19
26
|
}
|
|
20
27
|
|
|
21
28
|
transform(): CodemodResult {
|
|
22
29
|
if (this.texts.length === 0) {
|
|
23
|
-
|
|
24
|
-
|
|
30
|
+
return this.unchanged();
|
|
31
|
+
}
|
|
25
32
|
|
|
26
33
|
// Parse <template> and <script> sections separately
|
|
27
34
|
const templateMatch = this.content.match(/<template[^>]*>([\s\S]*?)<\/template>/);
|
|
28
35
|
const scriptMatch = this.content.match(/<script[^>]*>([\s\S]*?)<\/script>/);
|
|
29
36
|
|
|
30
|
-
|
|
37
|
+
if (!templateMatch && !scriptMatch) {
|
|
31
38
|
return this.unchanged();
|
|
32
39
|
}
|
|
33
40
|
|
|
34
41
|
const textKeyMap = new Map<string, string>();
|
|
35
42
|
for (const dt of this.texts) {
|
|
36
|
-
|
|
37
|
-
|
|
43
|
+
textKeyMap.set(dt.text, dt.suggestedKey);
|
|
44
|
+
}
|
|
38
45
|
|
|
39
46
|
let transformedContent = this.content;
|
|
40
47
|
let replacedTexts = 0;
|
|
48
|
+
const fn = this.translationFn;
|
|
41
49
|
|
|
42
|
-
|
|
50
|
+
// Replace in template: >text< => >{{ $t('key') }}<
|
|
43
51
|
if (templateMatch) {
|
|
44
52
|
let template = templateMatch[1];
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
template = template.replace(attrRe, `:$1"$
|
|
52
|
-
|
|
53
|
+
for (const [text, key] of textKeyMap) {
|
|
54
|
+
const escaped = text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
55
|
+
const re = new RegExp(`(>\\s*)${escaped}(\\s*<)`, 'g');
|
|
56
|
+
template = template.replace(re, `$1{{ ${fn}('${key}') }}$2`);
|
|
57
|
+
// Also replace in attribute values: title="text" => :title="$t('key')"
|
|
58
|
+
const attrRe = new RegExp(`((?:title|placeholder|alt|label)=)"(${escaped})"`, 'g');
|
|
59
|
+
template = template.replace(attrRe, `:$1"${fn}('${key}')"`);
|
|
60
|
+
replacedTexts++;
|
|
53
61
|
}
|
|
54
62
|
transformedContent = transformedContent.replace(templateMatch[1], template);
|
|
55
63
|
}
|
|
56
64
|
|
|
57
|
-
|
|
65
|
+
if (replacedTexts === 0) {
|
|
58
66
|
return this.unchanged();
|
|
59
67
|
}
|
|
60
68
|
|
|
@@ -65,16 +73,16 @@ export class VueCodemod {
|
|
|
65
73
|
changed: true,
|
|
66
74
|
injectedImport: false, // vue-i18n is globally injected
|
|
67
75
|
injectedHook: false,
|
|
68
|
-
|
|
76
|
+
replacedTexts,
|
|
69
77
|
};
|
|
70
78
|
}
|
|
71
79
|
|
|
72
80
|
private unchanged(): CodemodResult {
|
|
73
81
|
return {
|
|
74
|
-
|
|
75
|
-
|
|
82
|
+
filePath: this.filePath,
|
|
83
|
+
originalContent: this.content,
|
|
76
84
|
transformedContent: this.content,
|
|
77
|
-
|
|
85
|
+
changed: false,
|
|
78
86
|
injectedImport: false,
|
|
79
87
|
injectedHook: false,
|
|
80
88
|
replacedTexts: 0,
|
|
@@ -85,10 +93,11 @@ export class VueCodemod {
|
|
|
85
93
|
export function applyVueCodemod(
|
|
86
94
|
filePath: string,
|
|
87
95
|
texts: DetectedText[],
|
|
88
|
-
dryRun = false
|
|
96
|
+
dryRun = false,
|
|
97
|
+
codemodConfig?: CodemodConfig
|
|
89
98
|
): CodemodResult {
|
|
90
99
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
91
|
-
const codemod = new VueCodemod(filePath, content, texts);
|
|
100
|
+
const codemod = new VueCodemod(filePath, content, texts, codemodConfig);
|
|
92
101
|
const result = codemod.transform();
|
|
93
102
|
if (result.changed && !dryRun) {
|
|
94
103
|
fs.writeFileSync(filePath, result.transformedContent, 'utf-8');
|