ai-localize-scanner 2.0.6 → 2.0.7
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 +20 -0
- package/LICENSE +21 -0
- package/dist/index.d.mts +37 -20
- package/dist/index.d.ts +37 -20
- package/dist/index.js +358 -33
- package/dist/index.mjs +359 -33
- package/package.json +15 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# ai-localize-scanner
|
|
2
2
|
|
|
3
|
+
## 2.0.7
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- **CSS/utility class filtering in `AstScanner`**:
|
|
8
|
+
- New `NON_TRANSLATABLE_ATTR_NAMES` set: JSX attributes like `type`, `role`, `method`,
|
|
9
|
+
`href`, `src`, `rel`, `target`, `id`, `name`, and 20+ others are now skipped.
|
|
10
|
+
- New `NON_TRANSLATABLE_PROP_KEYS` set: object property values are skipped when the key
|
|
11
|
+
is a CSS-in-JS property (`fontFamily`, `color`, `display`, etc.) or structural prop.
|
|
12
|
+
- New `CSS_UTILITY_FN_NAMES` set: string literals inside calls to `clsx`, `cx`, `cn`,
|
|
13
|
+
`twMerge`, `twJoin`, `classnames`, `styled`, `css`, etc. are now skipped.
|
|
14
|
+
- `isCssClassString()` applied to every candidate string.
|
|
15
|
+
- Centralised `isTranslatableText()` private method applies all checks + `ignoreTextPatterns`.
|
|
16
|
+
- **`ignoreTextPatterns` config support** — user-defined regex patterns applied per scan.
|
|
17
|
+
- `regexFallbackScan()` updated to use the same `isTranslatableText()` pipeline.
|
|
18
|
+
- `ProjectScanner` passes `config.ignoreTextPatterns` to `AstScanner`.
|
|
19
|
+
- Added `repository`, `homepage`, `bugs`, `author` fields to `package.json` for npm registry display.
|
|
20
|
+
- Added `sideEffects: false` to enable tree-shaking in bundlers.
|
|
21
|
+
- Added `prepublishOnly` script to ensure the package is built before publishing.
|
|
22
|
+
|
|
3
23
|
## 2.0.6
|
|
4
24
|
|
|
5
25
|
### Patch Changes
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024-2026 ai-localize-core contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.d.mts
CHANGED
|
@@ -6,38 +6,27 @@ interface AstScanOptions {
|
|
|
6
6
|
sourceRoot?: string;
|
|
7
7
|
/**
|
|
8
8
|
* Controls the format of the generated locale key for each detected text.
|
|
9
|
-
|
|
9
|
+
*
|
|
10
10
|
* - `"path"` (default) — hierarchical dot-notation key derived from file path + text:
|
|
11
11
|
* `settings.settings_page.save_changes`
|
|
12
12
|
*
|
|
13
13
|
* - `"screaming_snake"` — UPPER_SNAKE_CASE key derived solely from the text value:
|
|
14
14
|
* "Save Changes" → `SAVE_CHANGES`
|
|
15
|
-
* "Max Count"
|
|
15
|
+
* "Max Count" → `MAX_COUNT`
|
|
16
16
|
*/
|
|
17
17
|
keyStyle?: KeyStyle;
|
|
18
18
|
/**
|
|
19
19
|
* Optional codemod config from ai-localize.config.json.
|
|
20
20
|
*
|
|
21
21
|
* The scanner uses this to recognise already-translated strings even when
|
|
22
|
-
* the project uses a custom i18n library or a locally-defined hook
|
|
23
|
-
*
|
|
24
|
-
* importPackage — matched against import source strings. Supports:
|
|
25
|
-
* - npm package names: "react-i18next", "my-i18n-lib"
|
|
26
|
-
* - path aliases: "@/hooks/useTranslation", "@/i18n"
|
|
27
|
-
* - relative paths: "../../hooks/useTranslation"
|
|
28
|
-
* Matching is done by checking whether the import source equals the value
|
|
29
|
-
* OR ends with the last path segment(s) of the value (normalised).
|
|
30
|
-
*
|
|
31
|
-
* hookName — the hook identifier (e.g. "useTranslation", "useI18n").
|
|
32
|
-
* Added directly to the translation-function names set regardless of
|
|
33
|
-
* how the hook is imported. This means even default imports, re-exports
|
|
34
|
-
* or barrel aliases are handled correctly:
|
|
35
|
-
* import useT from '../../hooks/useT' (default import, hookName="useT")
|
|
36
|
-
*
|
|
37
|
-
* translationFunction — the accessor returned by the hook (e.g. "t").
|
|
38
|
-
* Added directly to the translation-function names set.
|
|
22
|
+
* the project uses a custom i18n library or a locally-defined hook.
|
|
39
23
|
*/
|
|
40
24
|
codemodConfig?: CodemodConfig;
|
|
25
|
+
/**
|
|
26
|
+
* Additional regex patterns (as strings) from config.ignoreTextPatterns.
|
|
27
|
+
* Any scanned string matching at least one pattern is excluded.
|
|
28
|
+
*/
|
|
29
|
+
ignoreTextPatterns?: string[];
|
|
41
30
|
}
|
|
42
31
|
/**
|
|
43
32
|
* Scans a JS/TS/JSX/TSX file using Babel AST to find hardcoded text.
|
|
@@ -45,6 +34,7 @@ interface AstScanOptions {
|
|
|
45
34
|
declare class AstScanner {
|
|
46
35
|
private options;
|
|
47
36
|
private detectedTexts;
|
|
37
|
+
private compiledIgnorePatterns;
|
|
48
38
|
/** Identifiers whose call/bracket expressions contain already-translated strings. */
|
|
49
39
|
private translationFunctionNames;
|
|
50
40
|
/**
|
|
@@ -55,6 +45,16 @@ declare class AstScanner {
|
|
|
55
45
|
private importSourceMatchers;
|
|
56
46
|
constructor(options: AstScanOptions);
|
|
57
47
|
scan(): DetectedText[];
|
|
48
|
+
/**
|
|
49
|
+
* Central check: is this text worth extracting as a locale key?
|
|
50
|
+
* Applies isHumanReadableText(), isCssClassString(), and user ignoreTextPatterns.
|
|
51
|
+
*/
|
|
52
|
+
private isTranslatableText;
|
|
53
|
+
/**
|
|
54
|
+
* Returns true when the node path is inside a CSS utility function call:
|
|
55
|
+
* clsx("a", "b"), cn("x"), twMerge("foo", "bar"), styled("div"), etc.
|
|
56
|
+
*/
|
|
57
|
+
private isInsideCssUtilityCall;
|
|
58
58
|
/**
|
|
59
59
|
* Walk import declarations; when the source matches a known translation
|
|
60
60
|
* import, collect all named/default imports as translation function names.
|
|
@@ -83,7 +83,14 @@ declare class AssetScanner {
|
|
|
83
83
|
declare class IncrementalScanCache {
|
|
84
84
|
private cachePath;
|
|
85
85
|
private cache;
|
|
86
|
-
|
|
86
|
+
/**
|
|
87
|
+
* @param cacheDir Directory where `scan-cache.json` is stored.
|
|
88
|
+
* @param configHash SHA-256 hash of the resolved config object.
|
|
89
|
+
* When this differs from the persisted value the entire
|
|
90
|
+
* cache is invalidated so that config changes (keyStyle,
|
|
91
|
+
* ignoreTextPatterns, codemods, etc.) are always reflected.
|
|
92
|
+
*/
|
|
93
|
+
constructor(cacheDir: string, configHash?: string);
|
|
87
94
|
private load;
|
|
88
95
|
isFileChanged(filePath: string): boolean;
|
|
89
96
|
getCachedResult(filePath: string): DetectedText[] | null;
|
|
@@ -103,6 +110,16 @@ declare class ProjectScanner {
|
|
|
103
110
|
private cache?;
|
|
104
111
|
private assetScanner;
|
|
105
112
|
constructor(config: LocalizationConfig);
|
|
113
|
+
/**
|
|
114
|
+
* Produces a stable SHA-256 fingerprint of the config fields that affect
|
|
115
|
+
* scan output. When any of these change the incremental cache is fully
|
|
116
|
+
* invalidated so the next run re-scans every file with the new settings.
|
|
117
|
+
*
|
|
118
|
+
* Fields intentionally excluded: `incrementalCache`, `cacheDir`, `aws`,
|
|
119
|
+
* `plugins` — none of those influence what text the AST scanner detects
|
|
120
|
+
* or how keys are generated.
|
|
121
|
+
*/
|
|
122
|
+
private hashConfig;
|
|
106
123
|
scan(options?: ScanOptions): Promise<ScanResult>;
|
|
107
124
|
private scanFile;
|
|
108
125
|
private chunkArray;
|
package/dist/index.d.ts
CHANGED
|
@@ -6,38 +6,27 @@ interface AstScanOptions {
|
|
|
6
6
|
sourceRoot?: string;
|
|
7
7
|
/**
|
|
8
8
|
* Controls the format of the generated locale key for each detected text.
|
|
9
|
-
|
|
9
|
+
*
|
|
10
10
|
* - `"path"` (default) — hierarchical dot-notation key derived from file path + text:
|
|
11
11
|
* `settings.settings_page.save_changes`
|
|
12
12
|
*
|
|
13
13
|
* - `"screaming_snake"` — UPPER_SNAKE_CASE key derived solely from the text value:
|
|
14
14
|
* "Save Changes" → `SAVE_CHANGES`
|
|
15
|
-
* "Max Count"
|
|
15
|
+
* "Max Count" → `MAX_COUNT`
|
|
16
16
|
*/
|
|
17
17
|
keyStyle?: KeyStyle;
|
|
18
18
|
/**
|
|
19
19
|
* Optional codemod config from ai-localize.config.json.
|
|
20
20
|
*
|
|
21
21
|
* The scanner uses this to recognise already-translated strings even when
|
|
22
|
-
* the project uses a custom i18n library or a locally-defined hook
|
|
23
|
-
*
|
|
24
|
-
* importPackage — matched against import source strings. Supports:
|
|
25
|
-
* - npm package names: "react-i18next", "my-i18n-lib"
|
|
26
|
-
* - path aliases: "@/hooks/useTranslation", "@/i18n"
|
|
27
|
-
* - relative paths: "../../hooks/useTranslation"
|
|
28
|
-
* Matching is done by checking whether the import source equals the value
|
|
29
|
-
* OR ends with the last path segment(s) of the value (normalised).
|
|
30
|
-
*
|
|
31
|
-
* hookName — the hook identifier (e.g. "useTranslation", "useI18n").
|
|
32
|
-
* Added directly to the translation-function names set regardless of
|
|
33
|
-
* how the hook is imported. This means even default imports, re-exports
|
|
34
|
-
* or barrel aliases are handled correctly:
|
|
35
|
-
* import useT from '../../hooks/useT' (default import, hookName="useT")
|
|
36
|
-
*
|
|
37
|
-
* translationFunction — the accessor returned by the hook (e.g. "t").
|
|
38
|
-
* Added directly to the translation-function names set.
|
|
22
|
+
* the project uses a custom i18n library or a locally-defined hook.
|
|
39
23
|
*/
|
|
40
24
|
codemodConfig?: CodemodConfig;
|
|
25
|
+
/**
|
|
26
|
+
* Additional regex patterns (as strings) from config.ignoreTextPatterns.
|
|
27
|
+
* Any scanned string matching at least one pattern is excluded.
|
|
28
|
+
*/
|
|
29
|
+
ignoreTextPatterns?: string[];
|
|
41
30
|
}
|
|
42
31
|
/**
|
|
43
32
|
* Scans a JS/TS/JSX/TSX file using Babel AST to find hardcoded text.
|
|
@@ -45,6 +34,7 @@ interface AstScanOptions {
|
|
|
45
34
|
declare class AstScanner {
|
|
46
35
|
private options;
|
|
47
36
|
private detectedTexts;
|
|
37
|
+
private compiledIgnorePatterns;
|
|
48
38
|
/** Identifiers whose call/bracket expressions contain already-translated strings. */
|
|
49
39
|
private translationFunctionNames;
|
|
50
40
|
/**
|
|
@@ -55,6 +45,16 @@ declare class AstScanner {
|
|
|
55
45
|
private importSourceMatchers;
|
|
56
46
|
constructor(options: AstScanOptions);
|
|
57
47
|
scan(): DetectedText[];
|
|
48
|
+
/**
|
|
49
|
+
* Central check: is this text worth extracting as a locale key?
|
|
50
|
+
* Applies isHumanReadableText(), isCssClassString(), and user ignoreTextPatterns.
|
|
51
|
+
*/
|
|
52
|
+
private isTranslatableText;
|
|
53
|
+
/**
|
|
54
|
+
* Returns true when the node path is inside a CSS utility function call:
|
|
55
|
+
* clsx("a", "b"), cn("x"), twMerge("foo", "bar"), styled("div"), etc.
|
|
56
|
+
*/
|
|
57
|
+
private isInsideCssUtilityCall;
|
|
58
58
|
/**
|
|
59
59
|
* Walk import declarations; when the source matches a known translation
|
|
60
60
|
* import, collect all named/default imports as translation function names.
|
|
@@ -83,7 +83,14 @@ declare class AssetScanner {
|
|
|
83
83
|
declare class IncrementalScanCache {
|
|
84
84
|
private cachePath;
|
|
85
85
|
private cache;
|
|
86
|
-
|
|
86
|
+
/**
|
|
87
|
+
* @param cacheDir Directory where `scan-cache.json` is stored.
|
|
88
|
+
* @param configHash SHA-256 hash of the resolved config object.
|
|
89
|
+
* When this differs from the persisted value the entire
|
|
90
|
+
* cache is invalidated so that config changes (keyStyle,
|
|
91
|
+
* ignoreTextPatterns, codemods, etc.) are always reflected.
|
|
92
|
+
*/
|
|
93
|
+
constructor(cacheDir: string, configHash?: string);
|
|
87
94
|
private load;
|
|
88
95
|
isFileChanged(filePath: string): boolean;
|
|
89
96
|
getCachedResult(filePath: string): DetectedText[] | null;
|
|
@@ -103,6 +110,16 @@ declare class ProjectScanner {
|
|
|
103
110
|
private cache?;
|
|
104
111
|
private assetScanner;
|
|
105
112
|
constructor(config: LocalizationConfig);
|
|
113
|
+
/**
|
|
114
|
+
* Produces a stable SHA-256 fingerprint of the config fields that affect
|
|
115
|
+
* scan output. When any of these change the incremental cache is fully
|
|
116
|
+
* invalidated so the next run re-scans every file with the new settings.
|
|
117
|
+
*
|
|
118
|
+
* Fields intentionally excluded: `incrementalCache`, `cacheDir`, `aws`,
|
|
119
|
+
* `plugins` — none of those influence what text the AST scanner detects
|
|
120
|
+
* or how keys are generated.
|
|
121
|
+
*/
|
|
122
|
+
private hashConfig;
|
|
106
123
|
scan(options?: ScanOptions): Promise<ScanResult>;
|
|
107
124
|
private scanFile;
|
|
108
125
|
private chunkArray;
|
package/dist/index.js
CHANGED
|
@@ -49,9 +49,239 @@ var BUILTIN_TRANSLATION_IMPORT_SOURCES = /* @__PURE__ */ new Set([
|
|
|
49
49
|
"vue-i18n",
|
|
50
50
|
"@ngx-translate/core"
|
|
51
51
|
]);
|
|
52
|
+
var NON_TRANSLATABLE_ATTR_NAMES = /* @__PURE__ */ new Set([
|
|
53
|
+
"classname",
|
|
54
|
+
"class",
|
|
55
|
+
"id",
|
|
56
|
+
"name",
|
|
57
|
+
"key",
|
|
58
|
+
"ref",
|
|
59
|
+
"type",
|
|
60
|
+
"method",
|
|
61
|
+
"action",
|
|
62
|
+
"enctype",
|
|
63
|
+
"href",
|
|
64
|
+
"src",
|
|
65
|
+
"srcset",
|
|
66
|
+
"action",
|
|
67
|
+
"to",
|
|
68
|
+
"as",
|
|
69
|
+
"path",
|
|
70
|
+
"url",
|
|
71
|
+
"rel",
|
|
72
|
+
"target",
|
|
73
|
+
"referrerpolicy",
|
|
74
|
+
"crossorigin",
|
|
75
|
+
"fetchpriority",
|
|
76
|
+
"loading",
|
|
77
|
+
"decoding",
|
|
78
|
+
"sizes",
|
|
79
|
+
"media",
|
|
80
|
+
"role",
|
|
81
|
+
"tabindex",
|
|
82
|
+
"autocomplete",
|
|
83
|
+
"inputmode",
|
|
84
|
+
"dir",
|
|
85
|
+
"lang",
|
|
86
|
+
"charset",
|
|
87
|
+
"for",
|
|
88
|
+
"htmlfor",
|
|
89
|
+
"htmlFor",
|
|
90
|
+
"accept",
|
|
91
|
+
"capture",
|
|
92
|
+
"pattern",
|
|
93
|
+
"autocorrect",
|
|
94
|
+
"autocapitalize",
|
|
95
|
+
"spellcheck",
|
|
96
|
+
"style",
|
|
97
|
+
"css",
|
|
98
|
+
"data-testid",
|
|
99
|
+
"data-cy",
|
|
100
|
+
"data-test",
|
|
101
|
+
"data-id",
|
|
102
|
+
"variant",
|
|
103
|
+
"color",
|
|
104
|
+
"size",
|
|
105
|
+
"shape",
|
|
106
|
+
"align",
|
|
107
|
+
"valign",
|
|
108
|
+
"justify",
|
|
109
|
+
"orientation",
|
|
110
|
+
"direction",
|
|
111
|
+
"placement",
|
|
112
|
+
"position",
|
|
113
|
+
"icon",
|
|
114
|
+
"iconname",
|
|
115
|
+
"lefticon",
|
|
116
|
+
"righticon"
|
|
117
|
+
]);
|
|
118
|
+
var NON_TRANSLATABLE_PROP_KEYS = /* @__PURE__ */ new Set([
|
|
119
|
+
// HTML/JSX structural
|
|
120
|
+
"className",
|
|
121
|
+
"class",
|
|
122
|
+
"id",
|
|
123
|
+
"name",
|
|
124
|
+
"key",
|
|
125
|
+
"ref",
|
|
126
|
+
"type",
|
|
127
|
+
"method",
|
|
128
|
+
"action",
|
|
129
|
+
"href",
|
|
130
|
+
"src",
|
|
131
|
+
"rel",
|
|
132
|
+
"target",
|
|
133
|
+
"role",
|
|
134
|
+
"style",
|
|
135
|
+
"css",
|
|
136
|
+
"to",
|
|
137
|
+
"as",
|
|
138
|
+
"path",
|
|
139
|
+
"url",
|
|
140
|
+
"link",
|
|
141
|
+
"icon",
|
|
142
|
+
"variant",
|
|
143
|
+
"color",
|
|
144
|
+
"size",
|
|
145
|
+
// CSS-in-JS style properties
|
|
146
|
+
"fontFamily",
|
|
147
|
+
"fontWeight",
|
|
148
|
+
"fontStyle",
|
|
149
|
+
"fontVariant",
|
|
150
|
+
"fontSize",
|
|
151
|
+
"fontStretch",
|
|
152
|
+
"color",
|
|
153
|
+
"backgroundColor",
|
|
154
|
+
"borderColor",
|
|
155
|
+
"outlineColor",
|
|
156
|
+
"textDecorationColor",
|
|
157
|
+
"display",
|
|
158
|
+
"position",
|
|
159
|
+
"visibility",
|
|
160
|
+
"overflow",
|
|
161
|
+
"overflowX",
|
|
162
|
+
"overflowY",
|
|
163
|
+
"cursor",
|
|
164
|
+
"pointerEvents",
|
|
165
|
+
"userSelect",
|
|
166
|
+
"appearance",
|
|
167
|
+
"resize",
|
|
168
|
+
"textAlign",
|
|
169
|
+
"verticalAlign",
|
|
170
|
+
"textDecoration",
|
|
171
|
+
"textTransform",
|
|
172
|
+
"textOverflow",
|
|
173
|
+
"whiteSpace",
|
|
174
|
+
"wordBreak",
|
|
175
|
+
"wordWrap",
|
|
176
|
+
"overflowWrap",
|
|
177
|
+
"lineBreak",
|
|
178
|
+
"hyphens",
|
|
179
|
+
"flexDirection",
|
|
180
|
+
"flexWrap",
|
|
181
|
+
"alignItems",
|
|
182
|
+
"alignContent",
|
|
183
|
+
"alignSelf",
|
|
184
|
+
"justifyContent",
|
|
185
|
+
"justifyItems",
|
|
186
|
+
"justifySelf",
|
|
187
|
+
"flexFlow",
|
|
188
|
+
"gridAutoFlow",
|
|
189
|
+
"gridAutoColumns",
|
|
190
|
+
"gridAutoRows",
|
|
191
|
+
"float",
|
|
192
|
+
"clear",
|
|
193
|
+
"objectFit",
|
|
194
|
+
"objectPosition",
|
|
195
|
+
"listStyle",
|
|
196
|
+
"listStyleType",
|
|
197
|
+
"borderStyle",
|
|
198
|
+
"outlineStyle",
|
|
199
|
+
"backgroundRepeat",
|
|
200
|
+
"backgroundAttachment",
|
|
201
|
+
"backgroundPosition",
|
|
202
|
+
"backgroundSize",
|
|
203
|
+
"backgroundBlendMode",
|
|
204
|
+
"mixBlendMode",
|
|
205
|
+
"isolation",
|
|
206
|
+
"boxSizing",
|
|
207
|
+
"tableLayout",
|
|
208
|
+
"captionSide",
|
|
209
|
+
"borderCollapse",
|
|
210
|
+
"imageRendering",
|
|
211
|
+
"shapeOutside",
|
|
212
|
+
"shapeBox",
|
|
213
|
+
"writingMode",
|
|
214
|
+
"direction",
|
|
215
|
+
"speak",
|
|
216
|
+
"contentVisibility",
|
|
217
|
+
// Animation / transition
|
|
218
|
+
"animationName",
|
|
219
|
+
"animationTimingFunction",
|
|
220
|
+
"animationFillMode",
|
|
221
|
+
"animationDirection",
|
|
222
|
+
"animationPlayState",
|
|
223
|
+
"transitionTimingFunction",
|
|
224
|
+
"transitionProperty",
|
|
225
|
+
"transformOrigin",
|
|
226
|
+
"transformBox",
|
|
227
|
+
"transformStyle",
|
|
228
|
+
// Event handler patterns
|
|
229
|
+
"onChange",
|
|
230
|
+
"onClick",
|
|
231
|
+
"onSubmit",
|
|
232
|
+
"onFocus",
|
|
233
|
+
"onBlur",
|
|
234
|
+
"onKeyDown",
|
|
235
|
+
"onKeyUp",
|
|
236
|
+
"onKeyPress",
|
|
237
|
+
"onMouseEnter",
|
|
238
|
+
"onMouseLeave",
|
|
239
|
+
"onMouseOver",
|
|
240
|
+
"onMouseOut",
|
|
241
|
+
"onMouseDown",
|
|
242
|
+
"onMouseUp",
|
|
243
|
+
"onDragStart",
|
|
244
|
+
"onDrop",
|
|
245
|
+
"onTouchStart",
|
|
246
|
+
"onTouchEnd",
|
|
247
|
+
"onTouchMove",
|
|
248
|
+
"onScroll",
|
|
249
|
+
"onResize",
|
|
250
|
+
"onLoad",
|
|
251
|
+
"onError",
|
|
252
|
+
"onAbort",
|
|
253
|
+
"onContextMenu",
|
|
254
|
+
"onDoubleClick",
|
|
255
|
+
"onSelect",
|
|
256
|
+
"onInput",
|
|
257
|
+
"onPaste",
|
|
258
|
+
"onCopy",
|
|
259
|
+
"onCut",
|
|
260
|
+
"onWheel"
|
|
261
|
+
]);
|
|
262
|
+
var CSS_UTILITY_FN_NAMES = /* @__PURE__ */ new Set([
|
|
263
|
+
"clsx",
|
|
264
|
+
"cx",
|
|
265
|
+
"classnames",
|
|
266
|
+
"classNames",
|
|
267
|
+
"cn",
|
|
268
|
+
"cc",
|
|
269
|
+
"twMerge",
|
|
270
|
+
"twJoin",
|
|
271
|
+
"tw",
|
|
272
|
+
"cva",
|
|
273
|
+
"ctl",
|
|
274
|
+
"classes",
|
|
275
|
+
"makeClasses",
|
|
276
|
+
"styled",
|
|
277
|
+
// styled-components / @emotion tag
|
|
278
|
+
"css"
|
|
279
|
+
// @emotion/css or linaria
|
|
280
|
+
]);
|
|
52
281
|
var AstScanner = class {
|
|
53
282
|
options;
|
|
54
283
|
detectedTexts = [];
|
|
284
|
+
compiledIgnorePatterns = [];
|
|
55
285
|
/** Identifiers whose call/bracket expressions contain already-translated strings. */
|
|
56
286
|
translationFunctionNames;
|
|
57
287
|
/**
|
|
@@ -62,6 +292,13 @@ var AstScanner = class {
|
|
|
62
292
|
importSourceMatchers;
|
|
63
293
|
constructor(options) {
|
|
64
294
|
this.options = options;
|
|
295
|
+
this.compiledIgnorePatterns = (options.ignoreTextPatterns ?? []).flatMap((p) => {
|
|
296
|
+
try {
|
|
297
|
+
return [new RegExp(p)];
|
|
298
|
+
} catch {
|
|
299
|
+
return [];
|
|
300
|
+
}
|
|
301
|
+
});
|
|
65
302
|
this.translationFunctionNames = /* @__PURE__ */ new Set(["t", "$t", "i18n", "translate"]);
|
|
66
303
|
this.importSourceMatchers = Array.from(BUILTIN_TRANSLATION_IMPORT_SOURCES).map(
|
|
67
304
|
(pkg) => (src) => src === pkg
|
|
@@ -102,9 +339,10 @@ var AstScanner = class {
|
|
|
102
339
|
}
|
|
103
340
|
this.collectTranslationImports(ast);
|
|
104
341
|
(0, import_traverse.default)(ast, {
|
|
342
|
+
// ── JSX text nodes: <h1>Welcome</h1> ───────────────────────────────────
|
|
105
343
|
JSXText: (nodePath) => {
|
|
106
344
|
const text = (0, import_ai_localize_shared.normalizeText)(nodePath.node.value);
|
|
107
|
-
if (!
|
|
345
|
+
if (!this.isTranslatableText(text)) return;
|
|
108
346
|
if (this.isInsideTranslationCall(nodePath)) return;
|
|
109
347
|
this.addDetected(
|
|
110
348
|
text,
|
|
@@ -114,13 +352,15 @@ var AstScanner = class {
|
|
|
114
352
|
"JSXText"
|
|
115
353
|
);
|
|
116
354
|
},
|
|
355
|
+
// ── JSX text-content attributes: placeholder="...", alt="..." ──────────
|
|
117
356
|
JSXAttribute: (nodePath) => {
|
|
118
357
|
const attrName = t.isJSXIdentifier(nodePath.node.name) ? nodePath.node.name.name : "";
|
|
119
358
|
if (!import_ai_localize_shared.TEXT_ATTRIBUTE_NAMES.has(attrName.toLowerCase())) return;
|
|
359
|
+
if (NON_TRANSLATABLE_ATTR_NAMES.has(attrName.toLowerCase())) return;
|
|
120
360
|
const valueNode = nodePath.node.value;
|
|
121
361
|
if (!t.isStringLiteral(valueNode)) return;
|
|
122
362
|
const text = (0, import_ai_localize_shared.normalizeText)(valueNode.value);
|
|
123
|
-
if (!
|
|
363
|
+
if (!this.isTranslatableText(text)) return;
|
|
124
364
|
if (this.isInsideTranslationCall(nodePath)) return;
|
|
125
365
|
const context = this.mapAttrToContext(attrName);
|
|
126
366
|
this.addDetected(
|
|
@@ -131,21 +371,23 @@ var AstScanner = class {
|
|
|
131
371
|
"JSXAttribute"
|
|
132
372
|
);
|
|
133
373
|
},
|
|
374
|
+
// ── StringLiteral in JS/TS expressions ─────────────────────────────────
|
|
134
375
|
StringLiteral: (nodePath) => {
|
|
135
376
|
if (t.isImportDeclaration(nodePath.parent)) return;
|
|
136
377
|
if (t.isObjectProperty(nodePath.parent) && nodePath.parent.key === nodePath.node) return;
|
|
137
|
-
if (t.isJSXAttribute(nodePath.parent))
|
|
138
|
-
|
|
139
|
-
if (
|
|
140
|
-
return;
|
|
378
|
+
if (t.isJSXAttribute(nodePath.parent)) return;
|
|
379
|
+
if (t.isObjectProperty(nodePath.parent) && t.isIdentifier(nodePath.parent.key)) {
|
|
380
|
+
if (NON_TRANSLATABLE_PROP_KEYS.has(nodePath.parent.key.name)) return;
|
|
141
381
|
}
|
|
382
|
+
if (this.isInsideCssUtilityCall(nodePath)) return;
|
|
142
383
|
if (this.isInsideTranslationCall(nodePath)) return;
|
|
143
384
|
const val = nodePath.node.value;
|
|
144
|
-
if (/^[a-z][a-z0-9_
|
|
385
|
+
if (/^[a-z][a-z0-9_.\-]*$/.test(val) || // all-lowercase token (CSS class / id / key)
|
|
386
|
+
/^#?[0-9a-fA-F]+$/.test(val)) {
|
|
145
387
|
return;
|
|
146
388
|
}
|
|
147
|
-
const text = (0, import_ai_localize_shared.normalizeText)(
|
|
148
|
-
if (!
|
|
389
|
+
const text = (0, import_ai_localize_shared.normalizeText)(val);
|
|
390
|
+
if (!this.isTranslatableText(text)) return;
|
|
149
391
|
this.addDetected(
|
|
150
392
|
text,
|
|
151
393
|
nodePath.node.loc?.start.line ?? 0,
|
|
@@ -154,11 +396,12 @@ var AstScanner = class {
|
|
|
154
396
|
"StringLiteral"
|
|
155
397
|
);
|
|
156
398
|
},
|
|
399
|
+
// ── Template literals with no expressions: `Hello world` ───────────────
|
|
157
400
|
TemplateLiteral: (nodePath) => {
|
|
158
401
|
if (nodePath.node.expressions.length > 0) return;
|
|
159
402
|
if (this.isInsideTranslationCall(nodePath)) return;
|
|
160
403
|
const text = (0, import_ai_localize_shared.normalizeText)(nodePath.node.quasis[0]?.value.cooked ?? "");
|
|
161
|
-
if (!
|
|
404
|
+
if (!this.isTranslatableText(text)) return;
|
|
162
405
|
this.addDetected(
|
|
163
406
|
text,
|
|
164
407
|
nodePath.node.loc?.start.line ?? 0,
|
|
@@ -170,6 +413,39 @@ var AstScanner = class {
|
|
|
170
413
|
});
|
|
171
414
|
return this.detectedTexts;
|
|
172
415
|
}
|
|
416
|
+
/**
|
|
417
|
+
* Central check: is this text worth extracting as a locale key?
|
|
418
|
+
* Applies isHumanReadableText(), isCssClassString(), and user ignoreTextPatterns.
|
|
419
|
+
*/
|
|
420
|
+
isTranslatableText(text) {
|
|
421
|
+
if (!(0, import_ai_localize_shared.isHumanReadableText)(text)) return false;
|
|
422
|
+
if ((0, import_ai_localize_shared.isCssClassString)(text)) return false;
|
|
423
|
+
if (this.compiledIgnorePatterns.some((re) => re.test(text))) return false;
|
|
424
|
+
return true;
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Returns true when the node path is inside a CSS utility function call:
|
|
428
|
+
* clsx("a", "b"), cn("x"), twMerge("foo", "bar"), styled("div"), etc.
|
|
429
|
+
*/
|
|
430
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
431
|
+
isInsideCssUtilityCall(nodePath) {
|
|
432
|
+
let current = nodePath.parentPath;
|
|
433
|
+
while (current) {
|
|
434
|
+
const node = current.node;
|
|
435
|
+
if (t.isCallExpression(node)) {
|
|
436
|
+
const callee = node.callee;
|
|
437
|
+
if (t.isIdentifier(callee) && CSS_UTILITY_FN_NAMES.has(callee.name)) return true;
|
|
438
|
+
if (t.isMemberExpression(callee) && t.isIdentifier(callee.property) && CSS_UTILITY_FN_NAMES.has(callee.property.name)) return true;
|
|
439
|
+
}
|
|
440
|
+
if (t.isTaggedTemplateExpression(node)) {
|
|
441
|
+
const tag = node.tag;
|
|
442
|
+
if (t.isIdentifier(tag) && CSS_UTILITY_FN_NAMES.has(tag.name)) return true;
|
|
443
|
+
if (t.isMemberExpression(tag) && t.isIdentifier(tag.object) && CSS_UTILITY_FN_NAMES.has(tag.object.name)) return true;
|
|
444
|
+
}
|
|
445
|
+
current = current.parentPath;
|
|
446
|
+
}
|
|
447
|
+
return false;
|
|
448
|
+
}
|
|
173
449
|
/**
|
|
174
450
|
* Walk import declarations; when the source matches a known translation
|
|
175
451
|
* import, collect all named/default imports as translation function names.
|
|
@@ -255,7 +531,7 @@ var AstScanner = class {
|
|
|
255
531
|
jsxTextRegex.lastIndex = 0;
|
|
256
532
|
while ((m = jsxTextRegex.exec(line)) !== null) {
|
|
257
533
|
const text = (0, import_ai_localize_shared.normalizeText)(m[1]);
|
|
258
|
-
if (!
|
|
534
|
+
if (!this.isTranslatableText(text)) continue;
|
|
259
535
|
const key = (0, import_ai_localize_shared.generateKeyByStyle)(
|
|
260
536
|
this.options.filePath,
|
|
261
537
|
text,
|
|
@@ -278,22 +554,22 @@ var AstScanner = class {
|
|
|
278
554
|
}
|
|
279
555
|
};
|
|
280
556
|
function buildImportMatcher(importPackage) {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
if (
|
|
290
|
-
const
|
|
291
|
-
|
|
557
|
+
if (!importPackage.startsWith(".") && !importPackage.startsWith("/") && !importPackage.startsWith("@/")) {
|
|
558
|
+
return (src) => src === importPackage;
|
|
559
|
+
}
|
|
560
|
+
const normalise = (s) => s.replace(/^[@./]+/, "").replace(/\\/g, "/").toLowerCase();
|
|
561
|
+
const normTarget = normalise(importPackage);
|
|
562
|
+
const targetSegments = normTarget.split("/").filter(Boolean);
|
|
563
|
+
const segCount = targetSegments.length;
|
|
564
|
+
return (src) => {
|
|
565
|
+
if (src === importPackage) return true;
|
|
566
|
+
const normSrc = normalise(src);
|
|
567
|
+
const srcSegments = normSrc.split("/").filter(Boolean);
|
|
568
|
+
if (srcSegments.length < segCount) return false;
|
|
569
|
+
const tail = srcSegments.slice(-segCount);
|
|
570
|
+
return tail.join("/") === targetSegments.join("/");
|
|
292
571
|
};
|
|
293
572
|
}
|
|
294
|
-
function normalisePath(p) {
|
|
295
|
-
return p.replace(/\\/g, "/").replace(/^(@\/|\.{1,2}\/)+/, "").replace(/^@/, "");
|
|
296
|
-
}
|
|
297
573
|
|
|
298
574
|
// src/asset-scanner.ts
|
|
299
575
|
var fs = __toESM(require("fs"));
|
|
@@ -403,15 +679,27 @@ var import_ai_localize_shared3 = require("ai-localize-shared");
|
|
|
403
679
|
var IncrementalScanCache = class {
|
|
404
680
|
cachePath;
|
|
405
681
|
cache;
|
|
406
|
-
|
|
682
|
+
/**
|
|
683
|
+
* @param cacheDir Directory where `scan-cache.json` is stored.
|
|
684
|
+
* @param configHash SHA-256 hash of the resolved config object.
|
|
685
|
+
* When this differs from the persisted value the entire
|
|
686
|
+
* cache is invalidated so that config changes (keyStyle,
|
|
687
|
+
* ignoreTextPatterns, codemods, etc.) are always reflected.
|
|
688
|
+
*/
|
|
689
|
+
constructor(cacheDir, configHash) {
|
|
407
690
|
(0, import_ai_localize_shared3.ensureDir)(cacheDir);
|
|
408
691
|
this.cachePath = path2.join(cacheDir, "scan-cache.json");
|
|
409
|
-
this.cache = this.load();
|
|
692
|
+
this.cache = this.load(configHash);
|
|
410
693
|
}
|
|
411
|
-
load() {
|
|
694
|
+
load(configHash) {
|
|
412
695
|
const existing = (0, import_ai_localize_shared3.readJsonSafe)(this.cachePath);
|
|
413
|
-
if (existing?.version === "1")
|
|
414
|
-
|
|
696
|
+
if (existing?.version === "1") {
|
|
697
|
+
if (configHash && existing.configHash !== configHash) {
|
|
698
|
+
return { version: "1", lastRun: (/* @__PURE__ */ new Date()).toISOString(), configHash, fileHashes: {}, processedFiles: {} };
|
|
699
|
+
}
|
|
700
|
+
return existing;
|
|
701
|
+
}
|
|
702
|
+
return { version: "1", lastRun: (/* @__PURE__ */ new Date()).toISOString(), configHash, fileHashes: {}, processedFiles: {} };
|
|
415
703
|
}
|
|
416
704
|
isFileChanged(filePath) {
|
|
417
705
|
return this.hashFile(filePath) !== this.cache.fileHashes[filePath];
|
|
@@ -439,7 +727,13 @@ var IncrementalScanCache = class {
|
|
|
439
727
|
}
|
|
440
728
|
}
|
|
441
729
|
clear() {
|
|
442
|
-
this.cache = {
|
|
730
|
+
this.cache = {
|
|
731
|
+
version: "1",
|
|
732
|
+
lastRun: (/* @__PURE__ */ new Date()).toISOString(),
|
|
733
|
+
configHash: this.cache.configHash,
|
|
734
|
+
fileHashes: {},
|
|
735
|
+
processedFiles: {}
|
|
736
|
+
};
|
|
443
737
|
this.persist();
|
|
444
738
|
}
|
|
445
739
|
};
|
|
@@ -447,6 +741,7 @@ var IncrementalScanCache = class {
|
|
|
447
741
|
// src/project-scanner.ts
|
|
448
742
|
var path3 = __toESM(require("path"));
|
|
449
743
|
var os = __toESM(require("os"));
|
|
744
|
+
var crypto2 = __toESM(require("crypto"));
|
|
450
745
|
var import_ai_localize_shared4 = require("ai-localize-shared");
|
|
451
746
|
var ProjectScanner = class {
|
|
452
747
|
config;
|
|
@@ -459,10 +754,39 @@ var ProjectScanner = class {
|
|
|
459
754
|
this.assetScanner = new AssetScanner(config.aws?.legacyCdnPattern);
|
|
460
755
|
if (config.incrementalCache) {
|
|
461
756
|
this.cache = new IncrementalScanCache(
|
|
462
|
-
path3.join(process.cwd(), config.cacheDir || ".ai-localize-cache")
|
|
757
|
+
path3.join(process.cwd(), config.cacheDir || ".ai-localize-cache"),
|
|
758
|
+
this.hashConfig(config)
|
|
463
759
|
);
|
|
464
760
|
}
|
|
465
761
|
}
|
|
762
|
+
/**
|
|
763
|
+
* Produces a stable SHA-256 fingerprint of the config fields that affect
|
|
764
|
+
* scan output. When any of these change the incremental cache is fully
|
|
765
|
+
* invalidated so the next run re-scans every file with the new settings.
|
|
766
|
+
*
|
|
767
|
+
* Fields intentionally excluded: `incrementalCache`, `cacheDir`, `aws`,
|
|
768
|
+
* `plugins` — none of those influence what text the AST scanner detects
|
|
769
|
+
* or how keys are generated.
|
|
770
|
+
*/
|
|
771
|
+
hashConfig(config) {
|
|
772
|
+
const relevant = {
|
|
773
|
+
framework: config.framework,
|
|
774
|
+
defaultLanguage: config.defaultLanguage,
|
|
775
|
+
targetLanguages: config.targetLanguages,
|
|
776
|
+
sourceDir: config.sourceDir,
|
|
777
|
+
localesDir: config.localesDir,
|
|
778
|
+
keyPrefix: config.keyPrefix,
|
|
779
|
+
namespaces: config.namespaces,
|
|
780
|
+
ignorePatterns: config.ignorePatterns,
|
|
781
|
+
includePatterns: config.includePatterns,
|
|
782
|
+
localeStructure: config.localeStructure,
|
|
783
|
+
keyStyle: config.keyStyle,
|
|
784
|
+
staticKeys: config.staticKeys,
|
|
785
|
+
ignoreTextPatterns: config.ignoreTextPatterns,
|
|
786
|
+
codemods: config.codemods
|
|
787
|
+
};
|
|
788
|
+
return crypto2.createHash("sha256").update(JSON.stringify(relevant)).digest("hex");
|
|
789
|
+
}
|
|
466
790
|
async scan(options = {}) {
|
|
467
791
|
const startTime = Date.now();
|
|
468
792
|
let filesToScan = [];
|
|
@@ -528,7 +852,8 @@ var ProjectScanner = class {
|
|
|
528
852
|
content,
|
|
529
853
|
sourceRoot: this.sourceRoot,
|
|
530
854
|
codemodConfig: this.config.codemods,
|
|
531
|
-
keyStyle: this.config.keyStyle ?? "path"
|
|
855
|
+
keyStyle: this.config.keyStyle ?? "path",
|
|
856
|
+
ignoreTextPatterns: this.config.ignoreTextPatterns ?? []
|
|
532
857
|
});
|
|
533
858
|
const texts = scanner.scan();
|
|
534
859
|
const { assets, legacyCdnUrls } = this.assetScanner.scanFile(filePath);
|
package/dist/index.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import traverse from "@babel/traverse";
|
|
|
4
4
|
import * as t from "@babel/types";
|
|
5
5
|
import {
|
|
6
6
|
isHumanReadableText,
|
|
7
|
+
isCssClassString,
|
|
7
8
|
normalizeText,
|
|
8
9
|
TEXT_ATTRIBUTE_NAMES,
|
|
9
10
|
generateKeyByStyle
|
|
@@ -14,9 +15,239 @@ var BUILTIN_TRANSLATION_IMPORT_SOURCES = /* @__PURE__ */ new Set([
|
|
|
14
15
|
"vue-i18n",
|
|
15
16
|
"@ngx-translate/core"
|
|
16
17
|
]);
|
|
18
|
+
var NON_TRANSLATABLE_ATTR_NAMES = /* @__PURE__ */ new Set([
|
|
19
|
+
"classname",
|
|
20
|
+
"class",
|
|
21
|
+
"id",
|
|
22
|
+
"name",
|
|
23
|
+
"key",
|
|
24
|
+
"ref",
|
|
25
|
+
"type",
|
|
26
|
+
"method",
|
|
27
|
+
"action",
|
|
28
|
+
"enctype",
|
|
29
|
+
"href",
|
|
30
|
+
"src",
|
|
31
|
+
"srcset",
|
|
32
|
+
"action",
|
|
33
|
+
"to",
|
|
34
|
+
"as",
|
|
35
|
+
"path",
|
|
36
|
+
"url",
|
|
37
|
+
"rel",
|
|
38
|
+
"target",
|
|
39
|
+
"referrerpolicy",
|
|
40
|
+
"crossorigin",
|
|
41
|
+
"fetchpriority",
|
|
42
|
+
"loading",
|
|
43
|
+
"decoding",
|
|
44
|
+
"sizes",
|
|
45
|
+
"media",
|
|
46
|
+
"role",
|
|
47
|
+
"tabindex",
|
|
48
|
+
"autocomplete",
|
|
49
|
+
"inputmode",
|
|
50
|
+
"dir",
|
|
51
|
+
"lang",
|
|
52
|
+
"charset",
|
|
53
|
+
"for",
|
|
54
|
+
"htmlfor",
|
|
55
|
+
"htmlFor",
|
|
56
|
+
"accept",
|
|
57
|
+
"capture",
|
|
58
|
+
"pattern",
|
|
59
|
+
"autocorrect",
|
|
60
|
+
"autocapitalize",
|
|
61
|
+
"spellcheck",
|
|
62
|
+
"style",
|
|
63
|
+
"css",
|
|
64
|
+
"data-testid",
|
|
65
|
+
"data-cy",
|
|
66
|
+
"data-test",
|
|
67
|
+
"data-id",
|
|
68
|
+
"variant",
|
|
69
|
+
"color",
|
|
70
|
+
"size",
|
|
71
|
+
"shape",
|
|
72
|
+
"align",
|
|
73
|
+
"valign",
|
|
74
|
+
"justify",
|
|
75
|
+
"orientation",
|
|
76
|
+
"direction",
|
|
77
|
+
"placement",
|
|
78
|
+
"position",
|
|
79
|
+
"icon",
|
|
80
|
+
"iconname",
|
|
81
|
+
"lefticon",
|
|
82
|
+
"righticon"
|
|
83
|
+
]);
|
|
84
|
+
var NON_TRANSLATABLE_PROP_KEYS = /* @__PURE__ */ new Set([
|
|
85
|
+
// HTML/JSX structural
|
|
86
|
+
"className",
|
|
87
|
+
"class",
|
|
88
|
+
"id",
|
|
89
|
+
"name",
|
|
90
|
+
"key",
|
|
91
|
+
"ref",
|
|
92
|
+
"type",
|
|
93
|
+
"method",
|
|
94
|
+
"action",
|
|
95
|
+
"href",
|
|
96
|
+
"src",
|
|
97
|
+
"rel",
|
|
98
|
+
"target",
|
|
99
|
+
"role",
|
|
100
|
+
"style",
|
|
101
|
+
"css",
|
|
102
|
+
"to",
|
|
103
|
+
"as",
|
|
104
|
+
"path",
|
|
105
|
+
"url",
|
|
106
|
+
"link",
|
|
107
|
+
"icon",
|
|
108
|
+
"variant",
|
|
109
|
+
"color",
|
|
110
|
+
"size",
|
|
111
|
+
// CSS-in-JS style properties
|
|
112
|
+
"fontFamily",
|
|
113
|
+
"fontWeight",
|
|
114
|
+
"fontStyle",
|
|
115
|
+
"fontVariant",
|
|
116
|
+
"fontSize",
|
|
117
|
+
"fontStretch",
|
|
118
|
+
"color",
|
|
119
|
+
"backgroundColor",
|
|
120
|
+
"borderColor",
|
|
121
|
+
"outlineColor",
|
|
122
|
+
"textDecorationColor",
|
|
123
|
+
"display",
|
|
124
|
+
"position",
|
|
125
|
+
"visibility",
|
|
126
|
+
"overflow",
|
|
127
|
+
"overflowX",
|
|
128
|
+
"overflowY",
|
|
129
|
+
"cursor",
|
|
130
|
+
"pointerEvents",
|
|
131
|
+
"userSelect",
|
|
132
|
+
"appearance",
|
|
133
|
+
"resize",
|
|
134
|
+
"textAlign",
|
|
135
|
+
"verticalAlign",
|
|
136
|
+
"textDecoration",
|
|
137
|
+
"textTransform",
|
|
138
|
+
"textOverflow",
|
|
139
|
+
"whiteSpace",
|
|
140
|
+
"wordBreak",
|
|
141
|
+
"wordWrap",
|
|
142
|
+
"overflowWrap",
|
|
143
|
+
"lineBreak",
|
|
144
|
+
"hyphens",
|
|
145
|
+
"flexDirection",
|
|
146
|
+
"flexWrap",
|
|
147
|
+
"alignItems",
|
|
148
|
+
"alignContent",
|
|
149
|
+
"alignSelf",
|
|
150
|
+
"justifyContent",
|
|
151
|
+
"justifyItems",
|
|
152
|
+
"justifySelf",
|
|
153
|
+
"flexFlow",
|
|
154
|
+
"gridAutoFlow",
|
|
155
|
+
"gridAutoColumns",
|
|
156
|
+
"gridAutoRows",
|
|
157
|
+
"float",
|
|
158
|
+
"clear",
|
|
159
|
+
"objectFit",
|
|
160
|
+
"objectPosition",
|
|
161
|
+
"listStyle",
|
|
162
|
+
"listStyleType",
|
|
163
|
+
"borderStyle",
|
|
164
|
+
"outlineStyle",
|
|
165
|
+
"backgroundRepeat",
|
|
166
|
+
"backgroundAttachment",
|
|
167
|
+
"backgroundPosition",
|
|
168
|
+
"backgroundSize",
|
|
169
|
+
"backgroundBlendMode",
|
|
170
|
+
"mixBlendMode",
|
|
171
|
+
"isolation",
|
|
172
|
+
"boxSizing",
|
|
173
|
+
"tableLayout",
|
|
174
|
+
"captionSide",
|
|
175
|
+
"borderCollapse",
|
|
176
|
+
"imageRendering",
|
|
177
|
+
"shapeOutside",
|
|
178
|
+
"shapeBox",
|
|
179
|
+
"writingMode",
|
|
180
|
+
"direction",
|
|
181
|
+
"speak",
|
|
182
|
+
"contentVisibility",
|
|
183
|
+
// Animation / transition
|
|
184
|
+
"animationName",
|
|
185
|
+
"animationTimingFunction",
|
|
186
|
+
"animationFillMode",
|
|
187
|
+
"animationDirection",
|
|
188
|
+
"animationPlayState",
|
|
189
|
+
"transitionTimingFunction",
|
|
190
|
+
"transitionProperty",
|
|
191
|
+
"transformOrigin",
|
|
192
|
+
"transformBox",
|
|
193
|
+
"transformStyle",
|
|
194
|
+
// Event handler patterns
|
|
195
|
+
"onChange",
|
|
196
|
+
"onClick",
|
|
197
|
+
"onSubmit",
|
|
198
|
+
"onFocus",
|
|
199
|
+
"onBlur",
|
|
200
|
+
"onKeyDown",
|
|
201
|
+
"onKeyUp",
|
|
202
|
+
"onKeyPress",
|
|
203
|
+
"onMouseEnter",
|
|
204
|
+
"onMouseLeave",
|
|
205
|
+
"onMouseOver",
|
|
206
|
+
"onMouseOut",
|
|
207
|
+
"onMouseDown",
|
|
208
|
+
"onMouseUp",
|
|
209
|
+
"onDragStart",
|
|
210
|
+
"onDrop",
|
|
211
|
+
"onTouchStart",
|
|
212
|
+
"onTouchEnd",
|
|
213
|
+
"onTouchMove",
|
|
214
|
+
"onScroll",
|
|
215
|
+
"onResize",
|
|
216
|
+
"onLoad",
|
|
217
|
+
"onError",
|
|
218
|
+
"onAbort",
|
|
219
|
+
"onContextMenu",
|
|
220
|
+
"onDoubleClick",
|
|
221
|
+
"onSelect",
|
|
222
|
+
"onInput",
|
|
223
|
+
"onPaste",
|
|
224
|
+
"onCopy",
|
|
225
|
+
"onCut",
|
|
226
|
+
"onWheel"
|
|
227
|
+
]);
|
|
228
|
+
var CSS_UTILITY_FN_NAMES = /* @__PURE__ */ new Set([
|
|
229
|
+
"clsx",
|
|
230
|
+
"cx",
|
|
231
|
+
"classnames",
|
|
232
|
+
"classNames",
|
|
233
|
+
"cn",
|
|
234
|
+
"cc",
|
|
235
|
+
"twMerge",
|
|
236
|
+
"twJoin",
|
|
237
|
+
"tw",
|
|
238
|
+
"cva",
|
|
239
|
+
"ctl",
|
|
240
|
+
"classes",
|
|
241
|
+
"makeClasses",
|
|
242
|
+
"styled",
|
|
243
|
+
// styled-components / @emotion tag
|
|
244
|
+
"css"
|
|
245
|
+
// @emotion/css or linaria
|
|
246
|
+
]);
|
|
17
247
|
var AstScanner = class {
|
|
18
248
|
options;
|
|
19
249
|
detectedTexts = [];
|
|
250
|
+
compiledIgnorePatterns = [];
|
|
20
251
|
/** Identifiers whose call/bracket expressions contain already-translated strings. */
|
|
21
252
|
translationFunctionNames;
|
|
22
253
|
/**
|
|
@@ -27,6 +258,13 @@ var AstScanner = class {
|
|
|
27
258
|
importSourceMatchers;
|
|
28
259
|
constructor(options) {
|
|
29
260
|
this.options = options;
|
|
261
|
+
this.compiledIgnorePatterns = (options.ignoreTextPatterns ?? []).flatMap((p) => {
|
|
262
|
+
try {
|
|
263
|
+
return [new RegExp(p)];
|
|
264
|
+
} catch {
|
|
265
|
+
return [];
|
|
266
|
+
}
|
|
267
|
+
});
|
|
30
268
|
this.translationFunctionNames = /* @__PURE__ */ new Set(["t", "$t", "i18n", "translate"]);
|
|
31
269
|
this.importSourceMatchers = Array.from(BUILTIN_TRANSLATION_IMPORT_SOURCES).map(
|
|
32
270
|
(pkg) => (src) => src === pkg
|
|
@@ -67,9 +305,10 @@ var AstScanner = class {
|
|
|
67
305
|
}
|
|
68
306
|
this.collectTranslationImports(ast);
|
|
69
307
|
traverse(ast, {
|
|
308
|
+
// ── JSX text nodes: <h1>Welcome</h1> ───────────────────────────────────
|
|
70
309
|
JSXText: (nodePath) => {
|
|
71
310
|
const text = normalizeText(nodePath.node.value);
|
|
72
|
-
if (!
|
|
311
|
+
if (!this.isTranslatableText(text)) return;
|
|
73
312
|
if (this.isInsideTranslationCall(nodePath)) return;
|
|
74
313
|
this.addDetected(
|
|
75
314
|
text,
|
|
@@ -79,13 +318,15 @@ var AstScanner = class {
|
|
|
79
318
|
"JSXText"
|
|
80
319
|
);
|
|
81
320
|
},
|
|
321
|
+
// ── JSX text-content attributes: placeholder="...", alt="..." ──────────
|
|
82
322
|
JSXAttribute: (nodePath) => {
|
|
83
323
|
const attrName = t.isJSXIdentifier(nodePath.node.name) ? nodePath.node.name.name : "";
|
|
84
324
|
if (!TEXT_ATTRIBUTE_NAMES.has(attrName.toLowerCase())) return;
|
|
325
|
+
if (NON_TRANSLATABLE_ATTR_NAMES.has(attrName.toLowerCase())) return;
|
|
85
326
|
const valueNode = nodePath.node.value;
|
|
86
327
|
if (!t.isStringLiteral(valueNode)) return;
|
|
87
328
|
const text = normalizeText(valueNode.value);
|
|
88
|
-
if (!
|
|
329
|
+
if (!this.isTranslatableText(text)) return;
|
|
89
330
|
if (this.isInsideTranslationCall(nodePath)) return;
|
|
90
331
|
const context = this.mapAttrToContext(attrName);
|
|
91
332
|
this.addDetected(
|
|
@@ -96,21 +337,23 @@ var AstScanner = class {
|
|
|
96
337
|
"JSXAttribute"
|
|
97
338
|
);
|
|
98
339
|
},
|
|
340
|
+
// ── StringLiteral in JS/TS expressions ─────────────────────────────────
|
|
99
341
|
StringLiteral: (nodePath) => {
|
|
100
342
|
if (t.isImportDeclaration(nodePath.parent)) return;
|
|
101
343
|
if (t.isObjectProperty(nodePath.parent) && nodePath.parent.key === nodePath.node) return;
|
|
102
|
-
if (t.isJSXAttribute(nodePath.parent))
|
|
103
|
-
|
|
104
|
-
if (
|
|
105
|
-
return;
|
|
344
|
+
if (t.isJSXAttribute(nodePath.parent)) return;
|
|
345
|
+
if (t.isObjectProperty(nodePath.parent) && t.isIdentifier(nodePath.parent.key)) {
|
|
346
|
+
if (NON_TRANSLATABLE_PROP_KEYS.has(nodePath.parent.key.name)) return;
|
|
106
347
|
}
|
|
348
|
+
if (this.isInsideCssUtilityCall(nodePath)) return;
|
|
107
349
|
if (this.isInsideTranslationCall(nodePath)) return;
|
|
108
350
|
const val = nodePath.node.value;
|
|
109
|
-
if (/^[a-z][a-z0-9_
|
|
351
|
+
if (/^[a-z][a-z0-9_.\-]*$/.test(val) || // all-lowercase token (CSS class / id / key)
|
|
352
|
+
/^#?[0-9a-fA-F]+$/.test(val)) {
|
|
110
353
|
return;
|
|
111
354
|
}
|
|
112
|
-
const text = normalizeText(
|
|
113
|
-
if (!
|
|
355
|
+
const text = normalizeText(val);
|
|
356
|
+
if (!this.isTranslatableText(text)) return;
|
|
114
357
|
this.addDetected(
|
|
115
358
|
text,
|
|
116
359
|
nodePath.node.loc?.start.line ?? 0,
|
|
@@ -119,11 +362,12 @@ var AstScanner = class {
|
|
|
119
362
|
"StringLiteral"
|
|
120
363
|
);
|
|
121
364
|
},
|
|
365
|
+
// ── Template literals with no expressions: `Hello world` ───────────────
|
|
122
366
|
TemplateLiteral: (nodePath) => {
|
|
123
367
|
if (nodePath.node.expressions.length > 0) return;
|
|
124
368
|
if (this.isInsideTranslationCall(nodePath)) return;
|
|
125
369
|
const text = normalizeText(nodePath.node.quasis[0]?.value.cooked ?? "");
|
|
126
|
-
if (!
|
|
370
|
+
if (!this.isTranslatableText(text)) return;
|
|
127
371
|
this.addDetected(
|
|
128
372
|
text,
|
|
129
373
|
nodePath.node.loc?.start.line ?? 0,
|
|
@@ -135,6 +379,39 @@ var AstScanner = class {
|
|
|
135
379
|
});
|
|
136
380
|
return this.detectedTexts;
|
|
137
381
|
}
|
|
382
|
+
/**
|
|
383
|
+
* Central check: is this text worth extracting as a locale key?
|
|
384
|
+
* Applies isHumanReadableText(), isCssClassString(), and user ignoreTextPatterns.
|
|
385
|
+
*/
|
|
386
|
+
isTranslatableText(text) {
|
|
387
|
+
if (!isHumanReadableText(text)) return false;
|
|
388
|
+
if (isCssClassString(text)) return false;
|
|
389
|
+
if (this.compiledIgnorePatterns.some((re) => re.test(text))) return false;
|
|
390
|
+
return true;
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Returns true when the node path is inside a CSS utility function call:
|
|
394
|
+
* clsx("a", "b"), cn("x"), twMerge("foo", "bar"), styled("div"), etc.
|
|
395
|
+
*/
|
|
396
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
397
|
+
isInsideCssUtilityCall(nodePath) {
|
|
398
|
+
let current = nodePath.parentPath;
|
|
399
|
+
while (current) {
|
|
400
|
+
const node = current.node;
|
|
401
|
+
if (t.isCallExpression(node)) {
|
|
402
|
+
const callee = node.callee;
|
|
403
|
+
if (t.isIdentifier(callee) && CSS_UTILITY_FN_NAMES.has(callee.name)) return true;
|
|
404
|
+
if (t.isMemberExpression(callee) && t.isIdentifier(callee.property) && CSS_UTILITY_FN_NAMES.has(callee.property.name)) return true;
|
|
405
|
+
}
|
|
406
|
+
if (t.isTaggedTemplateExpression(node)) {
|
|
407
|
+
const tag = node.tag;
|
|
408
|
+
if (t.isIdentifier(tag) && CSS_UTILITY_FN_NAMES.has(tag.name)) return true;
|
|
409
|
+
if (t.isMemberExpression(tag) && t.isIdentifier(tag.object) && CSS_UTILITY_FN_NAMES.has(tag.object.name)) return true;
|
|
410
|
+
}
|
|
411
|
+
current = current.parentPath;
|
|
412
|
+
}
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
138
415
|
/**
|
|
139
416
|
* Walk import declarations; when the source matches a known translation
|
|
140
417
|
* import, collect all named/default imports as translation function names.
|
|
@@ -220,7 +497,7 @@ var AstScanner = class {
|
|
|
220
497
|
jsxTextRegex.lastIndex = 0;
|
|
221
498
|
while ((m = jsxTextRegex.exec(line)) !== null) {
|
|
222
499
|
const text = normalizeText(m[1]);
|
|
223
|
-
if (!
|
|
500
|
+
if (!this.isTranslatableText(text)) continue;
|
|
224
501
|
const key = generateKeyByStyle(
|
|
225
502
|
this.options.filePath,
|
|
226
503
|
text,
|
|
@@ -243,22 +520,22 @@ var AstScanner = class {
|
|
|
243
520
|
}
|
|
244
521
|
};
|
|
245
522
|
function buildImportMatcher(importPackage) {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
if (
|
|
255
|
-
const
|
|
256
|
-
|
|
523
|
+
if (!importPackage.startsWith(".") && !importPackage.startsWith("/") && !importPackage.startsWith("@/")) {
|
|
524
|
+
return (src) => src === importPackage;
|
|
525
|
+
}
|
|
526
|
+
const normalise = (s) => s.replace(/^[@./]+/, "").replace(/\\/g, "/").toLowerCase();
|
|
527
|
+
const normTarget = normalise(importPackage);
|
|
528
|
+
const targetSegments = normTarget.split("/").filter(Boolean);
|
|
529
|
+
const segCount = targetSegments.length;
|
|
530
|
+
return (src) => {
|
|
531
|
+
if (src === importPackage) return true;
|
|
532
|
+
const normSrc = normalise(src);
|
|
533
|
+
const srcSegments = normSrc.split("/").filter(Boolean);
|
|
534
|
+
if (srcSegments.length < segCount) return false;
|
|
535
|
+
const tail = srcSegments.slice(-segCount);
|
|
536
|
+
return tail.join("/") === targetSegments.join("/");
|
|
257
537
|
};
|
|
258
538
|
}
|
|
259
|
-
function normalisePath(p) {
|
|
260
|
-
return p.replace(/\\/g, "/").replace(/^(@\/|\.{1,2}\/)+/, "").replace(/^@/, "");
|
|
261
|
-
}
|
|
262
539
|
|
|
263
540
|
// src/asset-scanner.ts
|
|
264
541
|
import * as fs from "fs";
|
|
@@ -368,15 +645,27 @@ import { readJsonSafe, writeJson, ensureDir } from "ai-localize-shared";
|
|
|
368
645
|
var IncrementalScanCache = class {
|
|
369
646
|
cachePath;
|
|
370
647
|
cache;
|
|
371
|
-
|
|
648
|
+
/**
|
|
649
|
+
* @param cacheDir Directory where `scan-cache.json` is stored.
|
|
650
|
+
* @param configHash SHA-256 hash of the resolved config object.
|
|
651
|
+
* When this differs from the persisted value the entire
|
|
652
|
+
* cache is invalidated so that config changes (keyStyle,
|
|
653
|
+
* ignoreTextPatterns, codemods, etc.) are always reflected.
|
|
654
|
+
*/
|
|
655
|
+
constructor(cacheDir, configHash) {
|
|
372
656
|
ensureDir(cacheDir);
|
|
373
657
|
this.cachePath = path2.join(cacheDir, "scan-cache.json");
|
|
374
|
-
this.cache = this.load();
|
|
658
|
+
this.cache = this.load(configHash);
|
|
375
659
|
}
|
|
376
|
-
load() {
|
|
660
|
+
load(configHash) {
|
|
377
661
|
const existing = readJsonSafe(this.cachePath);
|
|
378
|
-
if (existing?.version === "1")
|
|
379
|
-
|
|
662
|
+
if (existing?.version === "1") {
|
|
663
|
+
if (configHash && existing.configHash !== configHash) {
|
|
664
|
+
return { version: "1", lastRun: (/* @__PURE__ */ new Date()).toISOString(), configHash, fileHashes: {}, processedFiles: {} };
|
|
665
|
+
}
|
|
666
|
+
return existing;
|
|
667
|
+
}
|
|
668
|
+
return { version: "1", lastRun: (/* @__PURE__ */ new Date()).toISOString(), configHash, fileHashes: {}, processedFiles: {} };
|
|
380
669
|
}
|
|
381
670
|
isFileChanged(filePath) {
|
|
382
671
|
return this.hashFile(filePath) !== this.cache.fileHashes[filePath];
|
|
@@ -404,7 +693,13 @@ var IncrementalScanCache = class {
|
|
|
404
693
|
}
|
|
405
694
|
}
|
|
406
695
|
clear() {
|
|
407
|
-
this.cache = {
|
|
696
|
+
this.cache = {
|
|
697
|
+
version: "1",
|
|
698
|
+
lastRun: (/* @__PURE__ */ new Date()).toISOString(),
|
|
699
|
+
configHash: this.cache.configHash,
|
|
700
|
+
fileHashes: {},
|
|
701
|
+
processedFiles: {}
|
|
702
|
+
};
|
|
408
703
|
this.persist();
|
|
409
704
|
}
|
|
410
705
|
};
|
|
@@ -412,6 +707,7 @@ var IncrementalScanCache = class {
|
|
|
412
707
|
// src/project-scanner.ts
|
|
413
708
|
import * as path3 from "path";
|
|
414
709
|
import * as os from "os";
|
|
710
|
+
import * as crypto2 from "crypto";
|
|
415
711
|
import { collectFiles, DEFAULT_IGNORE_DIRS, SOURCE_EXTENSIONS } from "ai-localize-shared";
|
|
416
712
|
var ProjectScanner = class {
|
|
417
713
|
config;
|
|
@@ -424,10 +720,39 @@ var ProjectScanner = class {
|
|
|
424
720
|
this.assetScanner = new AssetScanner(config.aws?.legacyCdnPattern);
|
|
425
721
|
if (config.incrementalCache) {
|
|
426
722
|
this.cache = new IncrementalScanCache(
|
|
427
|
-
path3.join(process.cwd(), config.cacheDir || ".ai-localize-cache")
|
|
723
|
+
path3.join(process.cwd(), config.cacheDir || ".ai-localize-cache"),
|
|
724
|
+
this.hashConfig(config)
|
|
428
725
|
);
|
|
429
726
|
}
|
|
430
727
|
}
|
|
728
|
+
/**
|
|
729
|
+
* Produces a stable SHA-256 fingerprint of the config fields that affect
|
|
730
|
+
* scan output. When any of these change the incremental cache is fully
|
|
731
|
+
* invalidated so the next run re-scans every file with the new settings.
|
|
732
|
+
*
|
|
733
|
+
* Fields intentionally excluded: `incrementalCache`, `cacheDir`, `aws`,
|
|
734
|
+
* `plugins` — none of those influence what text the AST scanner detects
|
|
735
|
+
* or how keys are generated.
|
|
736
|
+
*/
|
|
737
|
+
hashConfig(config) {
|
|
738
|
+
const relevant = {
|
|
739
|
+
framework: config.framework,
|
|
740
|
+
defaultLanguage: config.defaultLanguage,
|
|
741
|
+
targetLanguages: config.targetLanguages,
|
|
742
|
+
sourceDir: config.sourceDir,
|
|
743
|
+
localesDir: config.localesDir,
|
|
744
|
+
keyPrefix: config.keyPrefix,
|
|
745
|
+
namespaces: config.namespaces,
|
|
746
|
+
ignorePatterns: config.ignorePatterns,
|
|
747
|
+
includePatterns: config.includePatterns,
|
|
748
|
+
localeStructure: config.localeStructure,
|
|
749
|
+
keyStyle: config.keyStyle,
|
|
750
|
+
staticKeys: config.staticKeys,
|
|
751
|
+
ignoreTextPatterns: config.ignoreTextPatterns,
|
|
752
|
+
codemods: config.codemods
|
|
753
|
+
};
|
|
754
|
+
return crypto2.createHash("sha256").update(JSON.stringify(relevant)).digest("hex");
|
|
755
|
+
}
|
|
431
756
|
async scan(options = {}) {
|
|
432
757
|
const startTime = Date.now();
|
|
433
758
|
let filesToScan = [];
|
|
@@ -493,7 +818,8 @@ var ProjectScanner = class {
|
|
|
493
818
|
content,
|
|
494
819
|
sourceRoot: this.sourceRoot,
|
|
495
820
|
codemodConfig: this.config.codemods,
|
|
496
|
-
keyStyle: this.config.keyStyle ?? "path"
|
|
821
|
+
keyStyle: this.config.keyStyle ?? "path",
|
|
822
|
+
ignoreTextPatterns: this.config.ignoreTextPatterns ?? []
|
|
497
823
|
});
|
|
498
824
|
const texts = scanner.scan();
|
|
499
825
|
const { assets, legacyCdnUrls } = this.assetScanner.scanFile(filePath);
|
package/package.json
CHANGED
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-localize-scanner",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.7",
|
|
4
4
|
"description": "AST-based hardcoded text scanner for frontend applications",
|
|
5
|
+
"author": "ai-localize-core contributors",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/ai-localize/ai-localize-core.git",
|
|
10
|
+
"directory": "packages/scanner"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/ai-localize/ai-localize-core/tree/main/packages/scanner#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/ai-localize/ai-localize-core/issues"
|
|
15
|
+
},
|
|
5
16
|
"main": "./dist/index.js",
|
|
6
17
|
"module": "./dist/index.mjs",
|
|
7
18
|
"types": "./dist/index.d.ts",
|
|
19
|
+
"sideEffects": false,
|
|
8
20
|
"files": [
|
|
9
21
|
"dist",
|
|
10
22
|
"README.md",
|
|
@@ -39,8 +51,8 @@
|
|
|
39
51
|
"@babel/traverse": "^7.23.9",
|
|
40
52
|
"@babel/types": "^7.23.9",
|
|
41
53
|
"glob": "^10.3.10",
|
|
42
|
-
"ai-localize-shared": "2.0.
|
|
43
|
-
"ai-localize-config": "2.0.
|
|
54
|
+
"ai-localize-shared": "2.0.7",
|
|
55
|
+
"ai-localize-config": "2.0.7"
|
|
44
56
|
},
|
|
45
57
|
"devDependencies": {
|
|
46
58
|
"@types/babel__traverse": "^7.20.5",
|
|
@@ -48,7 +60,6 @@
|
|
|
48
60
|
"typescript": "^5.3.3",
|
|
49
61
|
"vitest": "^1.2.1"
|
|
50
62
|
},
|
|
51
|
-
"license": "MIT",
|
|
52
63
|
"publishConfig": {
|
|
53
64
|
"access": "public"
|
|
54
65
|
},
|