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