react-i18next-scanner 0.1.6 → 0.1.8
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/README.md +489 -132
- package/dist/core/actions/writeDynamicTranslationReport.js +1 -1
- package/dist/core/actions/writePossiblyDynamicallyUsedReport.js +1 -1
- package/dist/core/actions/writeResolvedImportedObjectUsageReport.js +13 -0
- package/dist/core/actions/writeUnusedTranslationsReport.js +4 -2
- package/dist/core/analysis/collectResolvedImportedObjectTranslationKeys.js +157 -78
- package/dist/core/analysis/deepVerifyUnusedKeys.js +4 -1
- package/dist/core/scanner.js +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
# react-i18next-scanner
|
|
2
2
|
|
|
3
|
-
CLI tool
|
|
3
|
+
CLI tool for React / `react-i18next` projects.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
It helps you:
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
7
|
+
- find missing translation keys
|
|
8
|
+
- find unused translation keys
|
|
9
|
+
- detect dynamic translation usages
|
|
10
|
+
- protect possibly used dynamic keys from removal
|
|
11
|
+
- generate reports
|
|
12
|
+
- sync missing keys
|
|
13
|
+
- remove definitely unused keys
|
|
14
|
+
- sort translation JSON files
|
|
15
15
|
|
|
16
16
|
---
|
|
17
17
|
|
|
18
|
-
##
|
|
18
|
+
## Quick Start
|
|
19
|
+
|
|
20
|
+
### 1. Install
|
|
19
21
|
|
|
20
22
|
```bash
|
|
21
23
|
npm install -D react-i18next-scanner
|
|
@@ -29,150 +31,139 @@ yarn add -D react-i18next-scanner
|
|
|
29
31
|
|
|
30
32
|
---
|
|
31
33
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
Create one of these config files in your project root:
|
|
34
|
+
### 2. Create config
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
```bash
|
|
37
|
+
npx react-i18next-scanner init
|
|
38
|
+
```
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
This will create a config file for your project.
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
Supported config formats:
|
|
43
43
|
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
"./src/i18n/locales/translations.en.json",
|
|
49
|
-
"./src/i18n/locales/translations.es.json",
|
|
50
|
-
],
|
|
51
|
-
sortJsonFiles: [
|
|
52
|
-
"./src/i18n/locales/translations.en.json",
|
|
53
|
-
"./src/i18n/locales/translations.es.json",
|
|
54
|
-
],
|
|
55
|
-
outputDir: "./unused",
|
|
56
|
-
syncMissing: false,
|
|
57
|
-
removeUnused: false,
|
|
58
|
-
};
|
|
44
|
+
```txt
|
|
45
|
+
i18n-scanner.config.js
|
|
46
|
+
i18n-scanner.config.cjs
|
|
47
|
+
i18n-scanner.config.mjs
|
|
59
48
|
```
|
|
60
49
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
## Config Options
|
|
50
|
+
> `.ts` config is not supported yet.
|
|
64
51
|
|
|
65
|
-
|
|
52
|
+
---
|
|
66
53
|
|
|
67
|
-
|
|
54
|
+
### 3. Run scan
|
|
68
55
|
|
|
69
|
-
```
|
|
70
|
-
|
|
56
|
+
```bash
|
|
57
|
+
npx react-i18next-scanner scan
|
|
71
58
|
```
|
|
72
59
|
|
|
73
|
-
|
|
60
|
+
The scanner will check your source files and translation JSON files.
|
|
74
61
|
|
|
75
|
-
|
|
62
|
+
Reports will be created inside:
|
|
76
63
|
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
"./src/i18n/locales/translations.en.json",
|
|
80
|
-
"./src/i18n/locales/translations.es.json",
|
|
81
|
-
];
|
|
64
|
+
```txt
|
|
65
|
+
reports/
|
|
82
66
|
```
|
|
83
67
|
|
|
84
|
-
|
|
68
|
+
---
|
|
85
69
|
|
|
86
|
-
|
|
70
|
+
## Example Config
|
|
87
71
|
|
|
88
72
|
```js
|
|
89
|
-
|
|
90
|
-
"./src
|
|
91
|
-
"./src/i18n/locales/translations.es.json",
|
|
92
|
-
];
|
|
93
|
-
```
|
|
73
|
+
export default {
|
|
74
|
+
srcPaths: ["./src/**/*.{js,jsx,ts,tsx}"],
|
|
94
75
|
|
|
95
|
-
|
|
76
|
+
jsonDir: [
|
|
77
|
+
"./src/i18n/locales/translations.en.json",
|
|
78
|
+
"./src/i18n/locales/translations.es.json",
|
|
79
|
+
],
|
|
96
80
|
|
|
97
|
-
|
|
81
|
+
sortJsonFiles: [
|
|
82
|
+
"./src/i18n/locales/translations.en.json",
|
|
83
|
+
"./src/i18n/locales/translations.es.json",
|
|
84
|
+
],
|
|
98
85
|
|
|
99
|
-
|
|
86
|
+
outputDir: "./reports",
|
|
100
87
|
|
|
101
|
-
|
|
102
|
-
|
|
88
|
+
syncMissing: false,
|
|
89
|
+
removeUnused: false,
|
|
90
|
+
sortJson: true,
|
|
91
|
+
};
|
|
103
92
|
```
|
|
104
93
|
|
|
105
|
-
### `syncMissing`
|
|
106
|
-
|
|
107
|
-
If `true`, missing keys found in code will be added to translation JSON files.
|
|
108
|
-
|
|
109
|
-
### `removeUnused`
|
|
110
|
-
|
|
111
|
-
If `true`, unused keys will be removed from translation JSON files.
|
|
112
|
-
|
|
113
94
|
---
|
|
114
95
|
|
|
115
96
|
## Commands
|
|
116
97
|
|
|
117
|
-
###
|
|
98
|
+
### `init`
|
|
99
|
+
|
|
100
|
+
Creates config file automatically.
|
|
118
101
|
|
|
119
102
|
```bash
|
|
120
103
|
npx react-i18next-scanner init
|
|
121
104
|
```
|
|
122
105
|
|
|
123
|
-
This command creates a config file automatically and lets you choose:
|
|
124
|
-
|
|
125
|
-
- `.js`
|
|
126
|
-
- `.mjs`
|
|
127
|
-
- `.cjs`
|
|
128
|
-
|
|
129
106
|
---
|
|
130
107
|
|
|
131
|
-
###
|
|
108
|
+
### `scan`
|
|
109
|
+
|
|
110
|
+
Scans your project and creates reports.
|
|
132
111
|
|
|
133
112
|
```bash
|
|
134
113
|
npx react-i18next-scanner scan
|
|
135
114
|
```
|
|
136
115
|
|
|
137
|
-
|
|
116
|
+
With custom config:
|
|
138
117
|
|
|
139
118
|
```bash
|
|
140
119
|
npx react-i18next-scanner scan --config ./i18n-scanner.config.mjs
|
|
141
120
|
```
|
|
142
121
|
|
|
143
|
-
|
|
122
|
+
The `scan` command:
|
|
144
123
|
|
|
145
124
|
- scans source files
|
|
146
|
-
- extracts translation keys
|
|
147
|
-
- compares
|
|
148
|
-
-
|
|
149
|
-
-
|
|
125
|
+
- extracts used translation keys
|
|
126
|
+
- compares used keys with translation JSON files
|
|
127
|
+
- detects missing keys
|
|
128
|
+
- detects unused keys
|
|
129
|
+
- detects dynamic translation usages
|
|
130
|
+
- writes reports
|
|
150
131
|
- optionally syncs missing keys
|
|
151
|
-
- optionally removes unused keys
|
|
132
|
+
- optionally removes definitely unused keys
|
|
152
133
|
|
|
153
134
|
---
|
|
154
135
|
|
|
155
|
-
###
|
|
136
|
+
### `sync-missing`
|
|
137
|
+
|
|
138
|
+
Finds missing keys and adds them to translation JSON files.
|
|
156
139
|
|
|
157
140
|
```bash
|
|
158
141
|
npx react-i18next-scanner sync-missing
|
|
159
142
|
```
|
|
160
143
|
|
|
161
|
-
|
|
144
|
+
Missing keys are added with empty values.
|
|
145
|
+
|
|
146
|
+
Example:
|
|
162
147
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
148
|
+
```json
|
|
149
|
+
{
|
|
150
|
+
"common": {
|
|
151
|
+
"Save": ""
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
```
|
|
166
155
|
|
|
167
156
|
---
|
|
168
157
|
|
|
169
|
-
###
|
|
158
|
+
### `sort`
|
|
159
|
+
|
|
160
|
+
Sorts translation JSON files alphabetically.
|
|
170
161
|
|
|
171
162
|
```bash
|
|
172
163
|
npx react-i18next-scanner sort
|
|
173
164
|
```
|
|
174
165
|
|
|
175
|
-
If no files are passed
|
|
166
|
+
If no files are passed, the command uses `sortJsonFiles` from config.
|
|
176
167
|
|
|
177
168
|
You can also pass files manually:
|
|
178
169
|
|
|
@@ -182,62 +173,387 @@ npx react-i18next-scanner sort ./src/i18n/locales/translations.en.json ./src/i18
|
|
|
182
173
|
|
|
183
174
|
---
|
|
184
175
|
|
|
176
|
+
## Report Structure
|
|
177
|
+
|
|
178
|
+
All reports are written inside one root folder:
|
|
179
|
+
|
|
180
|
+
```txt
|
|
181
|
+
reports/
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Each report type has its own folder:
|
|
185
|
+
|
|
186
|
+
```txt
|
|
187
|
+
reports/
|
|
188
|
+
unused-translations/
|
|
189
|
+
unused.translations.en.json
|
|
190
|
+
unused.translations.es.json
|
|
191
|
+
|
|
192
|
+
dynamic-translations/
|
|
193
|
+
dynamic-translation-usages.json
|
|
194
|
+
|
|
195
|
+
possibly-dynamically-used/
|
|
196
|
+
possibly-dynamically-used.json
|
|
197
|
+
|
|
198
|
+
imported-object-usages/
|
|
199
|
+
resolved-imported-object-usages.json
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
185
204
|
## Unused Translation Reports
|
|
186
205
|
|
|
187
|
-
|
|
206
|
+
Folder:
|
|
207
|
+
|
|
208
|
+
```txt
|
|
209
|
+
reports/unused-translations/
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
These reports contain translation keys that look unused after deep verification.
|
|
188
213
|
|
|
189
214
|
Example:
|
|
190
215
|
|
|
191
|
-
|
|
192
|
-
-
|
|
216
|
+
```txt
|
|
217
|
+
reports/unused-translations/unused.translations.en.json
|
|
218
|
+
```
|
|
193
219
|
|
|
194
|
-
|
|
220
|
+
Example content:
|
|
221
|
+
|
|
222
|
+
```json
|
|
223
|
+
{
|
|
224
|
+
"common": {
|
|
225
|
+
"Old button text": "Old button text"
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
You can review this file before removing keys.
|
|
231
|
+
|
|
232
|
+
If `removeUnused: true`, only definitely unused keys will be removed.
|
|
195
233
|
|
|
196
234
|
---
|
|
197
235
|
|
|
198
236
|
## Dynamic Translation Report
|
|
199
237
|
|
|
200
|
-
|
|
238
|
+
Folder:
|
|
201
239
|
|
|
202
|
-
```
|
|
203
|
-
|
|
204
|
-
t(labelKey);
|
|
205
|
-
t(transaction.descriptionKey);
|
|
240
|
+
```txt
|
|
241
|
+
reports/dynamic-translations/
|
|
206
242
|
```
|
|
207
243
|
|
|
208
|
-
|
|
244
|
+
This report shows places where the scanner found dynamic translation usage.
|
|
209
245
|
|
|
210
|
-
|
|
211
|
-
|
|
246
|
+
Examples:
|
|
247
|
+
|
|
248
|
+
```ts
|
|
249
|
+
t(labelKey);
|
|
250
|
+
t(getDescription());
|
|
251
|
+
t(`${section}.${name}`);
|
|
252
|
+
<Trans i18nKey={text} />;
|
|
253
|
+
<Trans i18nKey={`account.${text}`} />;
|
|
212
254
|
```
|
|
213
255
|
|
|
214
|
-
Example
|
|
256
|
+
Example report:
|
|
215
257
|
|
|
216
258
|
```json
|
|
217
259
|
{
|
|
218
|
-
"src/
|
|
260
|
+
"src/components/Profile.tsx": [
|
|
219
261
|
{
|
|
220
|
-
"namespaces": ["
|
|
221
|
-
"expression": "
|
|
222
|
-
"reason": "
|
|
223
|
-
"suggestion": "Translation key may come from
|
|
262
|
+
"namespaces": ["common"],
|
|
263
|
+
"expression": "labelKey",
|
|
264
|
+
"reason": "Non-literal translation key",
|
|
265
|
+
"suggestion": "Translation key may come from a variable or helper function. Manual or deeper analysis may be required."
|
|
224
266
|
}
|
|
225
267
|
]
|
|
226
268
|
}
|
|
227
269
|
```
|
|
228
270
|
|
|
271
|
+
This report answers:
|
|
272
|
+
|
|
273
|
+
```txt
|
|
274
|
+
Where do we use dynamic translation keys in source code?
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
## Possibly Dynamically Used Report
|
|
280
|
+
|
|
281
|
+
Folder:
|
|
282
|
+
|
|
283
|
+
```txt
|
|
284
|
+
reports/possibly-dynamically-used/
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
This report shows keys that looked unused at first, but were later detected as possibly used dynamically.
|
|
288
|
+
|
|
289
|
+
Example:
|
|
290
|
+
|
|
291
|
+
```ts
|
|
292
|
+
const titleKey = MODAL_DATA[type].TITLE;
|
|
293
|
+
|
|
294
|
+
t(String(titleKey));
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
The key may not appear directly as:
|
|
298
|
+
|
|
299
|
+
```ts
|
|
300
|
+
t("accountInformation.Account Number");
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
But the scanner can understand that it may come from an object config.
|
|
304
|
+
|
|
305
|
+
This report answers:
|
|
306
|
+
|
|
307
|
+
```txt
|
|
308
|
+
Which unused keys were saved from removal because they may be used dynamically?
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
---
|
|
312
|
+
|
|
313
|
+
## Imported Object Usages Report
|
|
314
|
+
|
|
315
|
+
Folder:
|
|
316
|
+
|
|
317
|
+
```txt
|
|
318
|
+
reports/imported-object-usages/
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
File:
|
|
322
|
+
|
|
323
|
+
```txt
|
|
324
|
+
resolved-imported-object-usages.json
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
This report shows where translation keys from imported object configs are used.
|
|
328
|
+
|
|
329
|
+
Example config object:
|
|
330
|
+
|
|
331
|
+
```ts
|
|
332
|
+
export const MODAL_DATA = {
|
|
333
|
+
accountNumber: {
|
|
334
|
+
TITLE: "accountInformation.Account Number",
|
|
335
|
+
TEXT: "accountInformation.Your Account Number is",
|
|
336
|
+
},
|
|
337
|
+
};
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
Example usage:
|
|
341
|
+
|
|
342
|
+
```ts
|
|
343
|
+
import { MODAL_DATA } from "./modalData";
|
|
344
|
+
|
|
345
|
+
const additionalInformationData = MODAL_DATA[additionalInformationModalType];
|
|
346
|
+
|
|
347
|
+
t(String(additionalInformationData.TITLE));
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
The scanner can resolve this as:
|
|
351
|
+
|
|
352
|
+
```txt
|
|
353
|
+
additionalInformationData.TITLE
|
|
354
|
+
→ MODAL_DATA[*].TITLE
|
|
355
|
+
→ accountInformation.Account Number
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
Example report:
|
|
359
|
+
|
|
360
|
+
```json
|
|
361
|
+
[
|
|
362
|
+
{
|
|
363
|
+
"key": "accountInformation.Account Number",
|
|
364
|
+
"sourceFilePath": "src/components/AdditionalInformationModal.tsx",
|
|
365
|
+
"importedFilePath": "src/config/modalData.ts",
|
|
366
|
+
"importedObjectName": "MODAL_DATA",
|
|
367
|
+
"accessPath": "MODAL_DATA.*.TITLE",
|
|
368
|
+
"usageKind": "t"
|
|
369
|
+
}
|
|
370
|
+
]
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
This report answers:
|
|
374
|
+
|
|
375
|
+
```txt
|
|
376
|
+
Where is this imported object translation key used?
|
|
377
|
+
```
|
|
378
|
+
|
|
229
379
|
---
|
|
230
380
|
|
|
231
381
|
## Supported Translation Patterns
|
|
232
382
|
|
|
233
|
-
The scanner
|
|
383
|
+
The scanner detects direct translation keys:
|
|
384
|
+
|
|
385
|
+
```ts
|
|
386
|
+
t("common.Save");
|
|
387
|
+
i18n.t("common.Save");
|
|
388
|
+
<Trans i18nKey="common.Save" />;
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
It also supports namespaces:
|
|
392
|
+
|
|
393
|
+
```ts
|
|
394
|
+
const { t } = useTranslation("common");
|
|
395
|
+
|
|
396
|
+
t("Save");
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
---
|
|
400
|
+
|
|
401
|
+
## Supported Dynamic Patterns
|
|
402
|
+
|
|
403
|
+
The scanner can detect and report dynamic usages:
|
|
404
|
+
|
|
405
|
+
```ts
|
|
406
|
+
t(labelKey);
|
|
407
|
+
t(getTitleKey());
|
|
408
|
+
t(`${section}.${name}`);
|
|
409
|
+
<Trans i18nKey={labelKey} />;
|
|
410
|
+
<Trans i18nKey={`account.${name}`} />;
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
It can also resolve some imported object config usages:
|
|
414
|
+
|
|
415
|
+
```ts
|
|
416
|
+
t(String(MODAL_DATA[type].TITLE));
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
```ts
|
|
420
|
+
const data = MODAL_DATA[type];
|
|
421
|
+
|
|
422
|
+
t(String(data.TITLE));
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
```tsx
|
|
426
|
+
<Trans i18nKey={MODAL_DATA[type].TITLE} />
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
This works not only for `MODAL_DATA`, but also for similar exported object configs:
|
|
430
|
+
|
|
431
|
+
```ts
|
|
432
|
+
export const WARNING_CONFIG = {
|
|
433
|
+
deleteAccount: {
|
|
434
|
+
TITLE: "settings.Delete account",
|
|
435
|
+
TEXT: "settings.This action cannot be undone",
|
|
436
|
+
},
|
|
437
|
+
};
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
---
|
|
441
|
+
|
|
442
|
+
## Config Options
|
|
443
|
+
|
|
444
|
+
### `srcPaths`
|
|
445
|
+
|
|
446
|
+
Source files to scan.
|
|
447
|
+
|
|
448
|
+
```js
|
|
449
|
+
srcPaths: ["./src/**/*.{js,jsx,ts,tsx}"];
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
---
|
|
453
|
+
|
|
454
|
+
### `jsonDir`
|
|
455
|
+
|
|
456
|
+
Translation JSON files to analyze.
|
|
457
|
+
|
|
458
|
+
```js
|
|
459
|
+
jsonDir: [
|
|
460
|
+
"./src/i18n/locales/translations.en.json",
|
|
461
|
+
"./src/i18n/locales/translations.es.json",
|
|
462
|
+
];
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
---
|
|
466
|
+
|
|
467
|
+
### `sortJsonFiles`
|
|
468
|
+
|
|
469
|
+
JSON files used by the `sort` command.
|
|
470
|
+
|
|
471
|
+
```js
|
|
472
|
+
sortJsonFiles: [
|
|
473
|
+
"./src/i18n/locales/translations.en.json",
|
|
474
|
+
"./src/i18n/locales/translations.es.json",
|
|
475
|
+
];
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
---
|
|
479
|
+
|
|
480
|
+
### `outputDir`
|
|
481
|
+
|
|
482
|
+
Root folder for scanner reports.
|
|
483
|
+
|
|
484
|
+
Default:
|
|
485
|
+
|
|
486
|
+
```js
|
|
487
|
+
outputDir: "./reports";
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
Reports will be grouped into subfolders:
|
|
491
|
+
|
|
492
|
+
```txt
|
|
493
|
+
reports/
|
|
494
|
+
unused-translations/
|
|
495
|
+
dynamic-translations/
|
|
496
|
+
possibly-dynamically-used/
|
|
497
|
+
imported-object-usages/
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
---
|
|
501
|
+
|
|
502
|
+
### `syncMissing`
|
|
503
|
+
|
|
504
|
+
If `true`, missing keys found in code will be added to translation JSON files.
|
|
505
|
+
|
|
506
|
+
```js
|
|
507
|
+
syncMissing: true;
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
Values are created as empty strings.
|
|
511
|
+
|
|
512
|
+
---
|
|
513
|
+
|
|
514
|
+
### `removeUnused`
|
|
515
|
+
|
|
516
|
+
If `true`, definitely unused keys will be removed from translation JSON files.
|
|
517
|
+
|
|
518
|
+
```js
|
|
519
|
+
removeUnused: true;
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
The scanner does not remove keys that are possibly dynamically used.
|
|
523
|
+
|
|
524
|
+
---
|
|
525
|
+
|
|
526
|
+
### `sortJson`
|
|
527
|
+
|
|
528
|
+
If `true`, JSON files will be sorted after sync or remove actions.
|
|
529
|
+
|
|
530
|
+
```js
|
|
531
|
+
sortJson: true;
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
---
|
|
535
|
+
|
|
536
|
+
## Safe Remove Behavior
|
|
234
537
|
|
|
235
|
-
|
|
236
|
-
- `i18n.t("key")`
|
|
237
|
-
- `<Trans i18nKey="key" />`
|
|
238
|
-
- `useTranslation("namespace")`
|
|
538
|
+
The scanner tries to avoid removing keys that may still be used dynamically.
|
|
239
539
|
|
|
240
|
-
|
|
540
|
+
For example:
|
|
541
|
+
|
|
542
|
+
```ts
|
|
543
|
+
const data = MODAL_DATA[type];
|
|
544
|
+
|
|
545
|
+
t(String(data.TITLE));
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
If `MODAL_DATA[type].TITLE` contains:
|
|
549
|
+
|
|
550
|
+
```ts
|
|
551
|
+
"accountInformation.Account Number";
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
then this key will not be treated as definitely unused.
|
|
555
|
+
|
|
556
|
+
It will be moved to a dynamic safety report instead.
|
|
241
557
|
|
|
242
558
|
---
|
|
243
559
|
|
|
@@ -245,20 +561,20 @@ It can also report dynamic usages where translation keys are not passed directly
|
|
|
245
561
|
|
|
246
562
|
### Source files
|
|
247
563
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
564
|
+
```txt
|
|
565
|
+
.js
|
|
566
|
+
.jsx
|
|
567
|
+
.ts
|
|
568
|
+
.tsx
|
|
569
|
+
```
|
|
254
570
|
|
|
255
571
|
### Config files
|
|
256
572
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
573
|
+
```txt
|
|
574
|
+
.js
|
|
575
|
+
.cjs
|
|
576
|
+
.mjs
|
|
577
|
+
```
|
|
262
578
|
|
|
263
579
|
> `.ts` config is not supported yet.
|
|
264
580
|
|
|
@@ -266,12 +582,15 @@ The scanner currently supports:
|
|
|
266
582
|
|
|
267
583
|
## Example Workflow
|
|
268
584
|
|
|
269
|
-
### 1. Scan
|
|
585
|
+
### 1. Scan only
|
|
586
|
+
|
|
587
|
+
Use this when you want reports without modifying translation JSON files.
|
|
270
588
|
|
|
271
589
|
```js
|
|
272
590
|
export default {
|
|
273
591
|
srcPaths: ["./src/**/*.{js,jsx,ts,tsx}"],
|
|
274
592
|
jsonDir: ["./src/i18n/locales/translations.en.json"],
|
|
593
|
+
outputDir: "./reports",
|
|
275
594
|
syncMissing: false,
|
|
276
595
|
removeUnused: false,
|
|
277
596
|
};
|
|
@@ -283,12 +602,15 @@ Run:
|
|
|
283
602
|
npx react-i18next-scanner scan
|
|
284
603
|
```
|
|
285
604
|
|
|
605
|
+
---
|
|
606
|
+
|
|
286
607
|
### 2. Sync missing keys
|
|
287
608
|
|
|
288
609
|
```js
|
|
289
610
|
export default {
|
|
290
611
|
srcPaths: ["./src/**/*.{js,jsx,ts,tsx}"],
|
|
291
612
|
jsonDir: ["./src/i18n/locales/translations.en.json"],
|
|
613
|
+
outputDir: "./reports",
|
|
292
614
|
syncMissing: true,
|
|
293
615
|
removeUnused: false,
|
|
294
616
|
};
|
|
@@ -300,18 +622,21 @@ Run:
|
|
|
300
622
|
npx react-i18next-scanner scan
|
|
301
623
|
```
|
|
302
624
|
|
|
303
|
-
or
|
|
625
|
+
or:
|
|
304
626
|
|
|
305
627
|
```bash
|
|
306
628
|
npx react-i18next-scanner sync-missing
|
|
307
629
|
```
|
|
308
630
|
|
|
309
|
-
|
|
631
|
+
---
|
|
632
|
+
|
|
633
|
+
### 3. Remove definitely unused keys
|
|
310
634
|
|
|
311
635
|
```js
|
|
312
636
|
export default {
|
|
313
637
|
srcPaths: ["./src/**/*.{js,jsx,ts,tsx}"],
|
|
314
638
|
jsonDir: ["./src/i18n/locales/translations.en.json"],
|
|
639
|
+
outputDir: "./reports",
|
|
315
640
|
syncMissing: false,
|
|
316
641
|
removeUnused: true,
|
|
317
642
|
};
|
|
@@ -323,14 +648,46 @@ Run:
|
|
|
323
648
|
npx react-i18next-scanner scan
|
|
324
649
|
```
|
|
325
650
|
|
|
651
|
+
Before using `removeUnused: true`, review reports first.
|
|
652
|
+
|
|
653
|
+
---
|
|
654
|
+
|
|
655
|
+
## Limitations
|
|
656
|
+
|
|
657
|
+
The scanner does not fully support every possible runtime data-flow case.
|
|
658
|
+
|
|
659
|
+
Currently not fully covered:
|
|
660
|
+
|
|
661
|
+
```ts
|
|
662
|
+
const data = getModalData(type);
|
|
663
|
+
t(data.TITLE);
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
```ts
|
|
667
|
+
const key = MODAL_DATA[type].TITLE + ".extra";
|
|
668
|
+
t(key);
|
|
669
|
+
```
|
|
670
|
+
|
|
671
|
+
```ts
|
|
672
|
+
const data = someWrapper(MODAL_DATA[type]);
|
|
673
|
+
t(data.TITLE);
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
```ts
|
|
677
|
+
export default MODAL_DATA;
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
The scanner is designed to be safe. If it is not sure, it reports dynamic usage instead of removing keys automatically.
|
|
681
|
+
|
|
326
682
|
---
|
|
327
683
|
|
|
328
684
|
## Notes
|
|
329
685
|
|
|
330
|
-
- `syncMissing` does not translate values automatically.
|
|
331
|
-
- `removeUnused` should be used carefully
|
|
332
|
-
-
|
|
686
|
+
- `syncMissing` does not translate values automatically.
|
|
687
|
+
- `removeUnused` should be used carefully.
|
|
688
|
+
- Possibly dynamic keys are not removed automatically.
|
|
333
689
|
- Config paths are resolved relative to the config file location.
|
|
690
|
+
- `.ts` config is not supported yet.
|
|
334
691
|
|
|
335
692
|
---
|
|
336
693
|
|
|
@@ -342,7 +699,7 @@ Build the project:
|
|
|
342
699
|
yarn build
|
|
343
700
|
```
|
|
344
701
|
|
|
345
|
-
Run
|
|
702
|
+
Run scanner in development:
|
|
346
703
|
|
|
347
704
|
```bash
|
|
348
705
|
yarn dev scan
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "fs/promises";
|
|
2
2
|
import path from "path";
|
|
3
3
|
import { sortObject } from "../utils/sortObject.js";
|
|
4
|
-
const DEFAULT_DYNAMIC_REPORT_DIR = path.resolve(process.cwd(), "reports-
|
|
4
|
+
const DEFAULT_DYNAMIC_REPORT_DIR = path.resolve(process.cwd(), "reports/dynamic-translations");
|
|
5
5
|
export async function writeDynamicTranslationReport(report, outputDir = DEFAULT_DYNAMIC_REPORT_DIR) {
|
|
6
6
|
if (Object.keys(report).length === 0) {
|
|
7
7
|
return;
|
|
@@ -3,7 +3,7 @@ import fs from "fs/promises";
|
|
|
3
3
|
import path from "path";
|
|
4
4
|
import { addKey } from "../utils/addKey.js";
|
|
5
5
|
import { sortObject } from "../utils/sortObject.js";
|
|
6
|
-
const DEFAULT_OUTPUT_DIR = path.resolve(process.cwd(), "
|
|
6
|
+
const DEFAULT_OUTPUT_DIR = path.resolve(process.cwd(), "dynamic-translations");
|
|
7
7
|
export async function writePossiblyDynamicallyUsedReport(keysByFile, jsonFiles, outputDir = DEFAULT_OUTPUT_DIR) {
|
|
8
8
|
const reportDataByFile = {};
|
|
9
9
|
for (const file of jsonFiles) {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// src/core/actions/writeResolvedImportedObjectUsagesReport.ts
|
|
2
|
+
import fs from "fs/promises";
|
|
3
|
+
import path from "path";
|
|
4
|
+
export async function writeResolvedImportedObjectUsagesReport(usages, outputDir) {
|
|
5
|
+
if (usages.length === 0) {
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
const rootReportDir = outputDir ?? "reports";
|
|
9
|
+
const reportDir = path.join(rootReportDir, "imported-object-usages");
|
|
10
|
+
await fs.mkdir(reportDir, { recursive: true });
|
|
11
|
+
const reportPath = path.join(reportDir, "resolved-imported-object-usages.json");
|
|
12
|
+
await fs.writeFile(reportPath, JSON.stringify(usages, null, 2), "utf-8");
|
|
13
|
+
}
|
|
@@ -10,7 +10,9 @@ export async function writeUnusedTranslationsReport(keysByFile, jsonFiles, outpu
|
|
|
10
10
|
if (filesWithUnused.length === 0) {
|
|
11
11
|
return;
|
|
12
12
|
}
|
|
13
|
-
|
|
13
|
+
const rootReportDir = outputDir ?? "reports";
|
|
14
|
+
const reportDir = path.join(rootReportDir, "unused-translations");
|
|
15
|
+
await fs.mkdir(reportDir, { recursive: true });
|
|
14
16
|
for (const file of filesWithUnused) {
|
|
15
17
|
const unusedKeys = keysByFile[file.filePath] ?? [];
|
|
16
18
|
const reportData = {};
|
|
@@ -22,7 +24,7 @@ export async function writeUnusedTranslationsReport(keysByFile, jsonFiles, outpu
|
|
|
22
24
|
}
|
|
23
25
|
const sorted = sortObject(reportData);
|
|
24
26
|
const reportFileName = `unused.${path.basename(file.filePath)}`;
|
|
25
|
-
const reportPath = path.join(
|
|
27
|
+
const reportPath = path.join(reportDir, reportFileName);
|
|
26
28
|
await fs.writeFile(reportPath, JSON.stringify(sorted, null, 2), "utf-8");
|
|
27
29
|
console.log(`📝 Unused report created: ${reportPath}`);
|
|
28
30
|
}
|
|
@@ -20,6 +20,15 @@ function isProbablyTranslationKey(value) {
|
|
|
20
20
|
!normalized.includes("\r") &&
|
|
21
21
|
(normalized.includes(".") || normalized.includes(":")));
|
|
22
22
|
}
|
|
23
|
+
function getPropertyName(name) {
|
|
24
|
+
if (ts.isIdentifier(name)) {
|
|
25
|
+
return name.text;
|
|
26
|
+
}
|
|
27
|
+
if (ts.isStringLiteral(name) || ts.isNumericLiteral(name)) {
|
|
28
|
+
return name.text;
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
23
32
|
function flattenObjectLiteral(node, prefix = "", result = new Map()) {
|
|
24
33
|
for (const property of node.properties) {
|
|
25
34
|
if (!ts.isPropertyAssignment(property)) {
|
|
@@ -45,15 +54,6 @@ function flattenObjectLiteral(node, prefix = "", result = new Map()) {
|
|
|
45
54
|
}
|
|
46
55
|
return result;
|
|
47
56
|
}
|
|
48
|
-
function getPropertyName(name) {
|
|
49
|
-
if (ts.isIdentifier(name)) {
|
|
50
|
-
return name.text;
|
|
51
|
-
}
|
|
52
|
-
if (ts.isStringLiteral(name) || ts.isNumericLiteral(name)) {
|
|
53
|
-
return name.text;
|
|
54
|
-
}
|
|
55
|
-
return null;
|
|
56
|
-
}
|
|
57
57
|
function hasExportModifier(node) {
|
|
58
58
|
return !!node.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword);
|
|
59
59
|
}
|
|
@@ -103,7 +103,10 @@ function collectImportBindings(sourceFile) {
|
|
|
103
103
|
const importedName = element.propertyName
|
|
104
104
|
? element.propertyName.text
|
|
105
105
|
: element.name.text;
|
|
106
|
-
bindings.set(localName, {
|
|
106
|
+
bindings.set(localName, {
|
|
107
|
+
importedName,
|
|
108
|
+
source,
|
|
109
|
+
});
|
|
107
110
|
}
|
|
108
111
|
}
|
|
109
112
|
});
|
|
@@ -222,6 +225,47 @@ function extractPropertyChain(expression) {
|
|
|
222
225
|
}
|
|
223
226
|
return null;
|
|
224
227
|
}
|
|
228
|
+
function matchObjectPath(pattern, actual) {
|
|
229
|
+
if (pattern.length !== actual.length) {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
return pattern.every((part, index) => {
|
|
233
|
+
return part === "*" || part === actual[index];
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
function resolveObjectValuesByPath(objectMap, pathPattern) {
|
|
237
|
+
const result = new Set();
|
|
238
|
+
for (const [objectPath, translationKey] of objectMap.entries()) {
|
|
239
|
+
const actualPath = objectPath.split(".");
|
|
240
|
+
if (matchObjectPath(pathPattern, actualPath)) {
|
|
241
|
+
result.add(translationKey);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return result;
|
|
245
|
+
}
|
|
246
|
+
function resolveImportedObjectAccess(expression, currentFilePath, importBindings, objectRegistry, availableFiles) {
|
|
247
|
+
const chain = extractPropertyChain(expression);
|
|
248
|
+
if (!chain) {
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
const binding = importBindings.get(chain.root);
|
|
252
|
+
if (!binding) {
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
const importedFilePath = resolveImportSource(currentFilePath, binding.source, availableFiles);
|
|
256
|
+
if (!importedFilePath) {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
const objectMap = objectRegistry.get(`${importedFilePath}::${binding.importedName}`);
|
|
260
|
+
if (!objectMap) {
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
return {
|
|
264
|
+
importedFilePath,
|
|
265
|
+
importedName: binding.importedName,
|
|
266
|
+
path: chain.path,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
225
269
|
function resolvePropertyChainToTranslationKeys(expression, currentFilePath, importBindings, objectRegistry, availableFiles) {
|
|
226
270
|
const access = resolveImportedObjectAccess(expression, currentFilePath, importBindings, objectRegistry, availableFiles);
|
|
227
271
|
if (!access) {
|
|
@@ -245,7 +289,10 @@ function collectLocalAliases(sourceFile, currentFilePath, importBindings, object
|
|
|
245
289
|
if (importedAccess) {
|
|
246
290
|
const keys = resolvePropertyChainToTranslationKeys(initializer, currentFilePath, importBindings, objectRegistry, availableFiles);
|
|
247
291
|
if (keys.size > 0) {
|
|
248
|
-
keyAliases.set(node.name.text,
|
|
292
|
+
keyAliases.set(node.name.text, {
|
|
293
|
+
keys,
|
|
294
|
+
access: importedAccess,
|
|
295
|
+
});
|
|
249
296
|
}
|
|
250
297
|
else {
|
|
251
298
|
objectAliases.set(node.name.text, importedAccess);
|
|
@@ -255,12 +302,15 @@ function collectLocalAliases(sourceFile, currentFilePath, importBindings, object
|
|
|
255
302
|
ts.forEachChild(node, visit);
|
|
256
303
|
}
|
|
257
304
|
visit(sourceFile);
|
|
258
|
-
return {
|
|
305
|
+
return {
|
|
306
|
+
objectAliases,
|
|
307
|
+
keyAliases,
|
|
308
|
+
};
|
|
259
309
|
}
|
|
260
310
|
function resolveAliasPropertyAccessToTranslationKeys(expression, objectAliases, keyAliases, objectRegistry) {
|
|
261
311
|
const unwrapped = unwrapExpression(expression);
|
|
262
312
|
if (ts.isIdentifier(unwrapped)) {
|
|
263
|
-
return keyAliases.get(unwrapped.text) ?? new Set();
|
|
313
|
+
return keyAliases.get(unwrapped.text)?.keys ?? new Set();
|
|
264
314
|
}
|
|
265
315
|
const chain = extractPropertyChain(unwrapped);
|
|
266
316
|
if (!chain) {
|
|
@@ -277,11 +327,62 @@ function resolveAliasPropertyAccessToTranslationKeys(expression, objectAliases,
|
|
|
277
327
|
const fullPath = [...objectAlias.path, ...chain.path];
|
|
278
328
|
return resolveObjectValuesByPath(objectMap, fullPath);
|
|
279
329
|
}
|
|
330
|
+
function resolveAliasObjectAccess(expression, objectAliases, keyAliases) {
|
|
331
|
+
const unwrapped = unwrapExpression(expression);
|
|
332
|
+
if (ts.isIdentifier(unwrapped)) {
|
|
333
|
+
return keyAliases.get(unwrapped.text)?.access ?? null;
|
|
334
|
+
}
|
|
335
|
+
const chain = extractPropertyChain(unwrapped);
|
|
336
|
+
if (!chain) {
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
const objectAlias = objectAliases.get(chain.root);
|
|
340
|
+
if (!objectAlias) {
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
return {
|
|
344
|
+
importedFilePath: objectAlias.importedFilePath,
|
|
345
|
+
importedName: objectAlias.importedName,
|
|
346
|
+
path: [...objectAlias.path, ...chain.path],
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
function formatAccessPath(importedName, pathParts) {
|
|
350
|
+
if (pathParts.length === 0) {
|
|
351
|
+
return importedName;
|
|
352
|
+
}
|
|
353
|
+
return `${importedName}.${pathParts.join(".")}`;
|
|
354
|
+
}
|
|
355
|
+
function addResolvedUsages(usages, usageIds, keys, params) {
|
|
356
|
+
for (const key of keys) {
|
|
357
|
+
const usageId = [
|
|
358
|
+
key,
|
|
359
|
+
params.sourceFilePath,
|
|
360
|
+
params.importedFilePath,
|
|
361
|
+
params.importedObjectName,
|
|
362
|
+
params.accessPath,
|
|
363
|
+
params.usageKind,
|
|
364
|
+
].join("::");
|
|
365
|
+
if (usageIds.has(usageId)) {
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
usageIds.add(usageId);
|
|
369
|
+
usages.push({
|
|
370
|
+
key,
|
|
371
|
+
sourceFilePath: params.sourceFilePath,
|
|
372
|
+
importedFilePath: params.importedFilePath,
|
|
373
|
+
importedObjectName: params.importedObjectName,
|
|
374
|
+
accessPath: params.accessPath,
|
|
375
|
+
usageKind: params.usageKind,
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
}
|
|
280
379
|
export async function collectResolvedImportedObjectTranslationKeys(filePaths) {
|
|
281
380
|
const normalizedFilePaths = filePaths.map((filePath) => path.resolve(filePath));
|
|
282
381
|
const sourceFiles = new Map();
|
|
283
382
|
const availableFiles = new Set(normalizedFilePaths);
|
|
284
383
|
const resolvedKeys = new Set();
|
|
384
|
+
const usages = [];
|
|
385
|
+
const usageIds = new Set();
|
|
285
386
|
for (const filePath of normalizedFilePaths) {
|
|
286
387
|
const content = await fs.readFile(filePath, "utf-8");
|
|
287
388
|
sourceFiles.set(filePath, createSourceFile(filePath, content));
|
|
@@ -294,27 +395,36 @@ export async function collectResolvedImportedObjectTranslationKeys(filePaths) {
|
|
|
294
395
|
}
|
|
295
396
|
const importBindings = collectImportBindings(sourceFile);
|
|
296
397
|
const { objectAliases, keyAliases } = collectLocalAliases(sourceFile, filePath, importBindings, objectRegistry, availableFiles);
|
|
297
|
-
function
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
if (firstArg) {
|
|
301
|
-
const directResolved = resolvePropertyChainToTranslationKeys(firstArg, filePath, importBindings, objectRegistry, availableFiles);
|
|
302
|
-
for (const key of directResolved) {
|
|
303
|
-
resolvedKeys.add(key);
|
|
304
|
-
}
|
|
305
|
-
const aliasResolved = resolveAliasPropertyAccessToTranslationKeys(firstArg, objectAliases, keyAliases, objectRegistry);
|
|
306
|
-
for (const key of aliasResolved) {
|
|
307
|
-
resolvedKeys.add(key);
|
|
308
|
-
}
|
|
309
|
-
}
|
|
398
|
+
function addKeys(keys) {
|
|
399
|
+
for (const key of keys) {
|
|
400
|
+
resolvedKeys.add(key);
|
|
310
401
|
}
|
|
311
|
-
|
|
312
|
-
|
|
402
|
+
}
|
|
403
|
+
function collectFromExpression(expression, usageKind) {
|
|
404
|
+
const directResolved = resolvePropertyChainToTranslationKeys(expression, filePath, importBindings, objectRegistry, availableFiles);
|
|
405
|
+
addKeys(directResolved);
|
|
406
|
+
const directAccess = resolveImportedObjectAccess(expression, filePath, importBindings, objectRegistry, availableFiles);
|
|
407
|
+
if (directAccess && directResolved.size > 0) {
|
|
408
|
+
addResolvedUsages(usages, usageIds, directResolved, {
|
|
409
|
+
sourceFilePath: filePath,
|
|
410
|
+
importedFilePath: directAccess.importedFilePath,
|
|
411
|
+
importedObjectName: directAccess.importedName,
|
|
412
|
+
accessPath: formatAccessPath(directAccess.importedName, directAccess.path),
|
|
413
|
+
usageKind,
|
|
414
|
+
});
|
|
313
415
|
}
|
|
314
|
-
|
|
315
|
-
|
|
416
|
+
const aliasResolved = resolveAliasPropertyAccessToTranslationKeys(expression, objectAliases, keyAliases, objectRegistry);
|
|
417
|
+
addKeys(aliasResolved);
|
|
418
|
+
const aliasAccess = resolveAliasObjectAccess(expression, objectAliases, keyAliases);
|
|
419
|
+
if (aliasAccess && aliasResolved.size > 0) {
|
|
420
|
+
addResolvedUsages(usages, usageIds, aliasResolved, {
|
|
421
|
+
sourceFilePath: filePath,
|
|
422
|
+
importedFilePath: aliasAccess.importedFilePath,
|
|
423
|
+
importedObjectName: aliasAccess.importedName,
|
|
424
|
+
accessPath: formatAccessPath(aliasAccess.importedName, aliasAccess.path),
|
|
425
|
+
usageKind,
|
|
426
|
+
});
|
|
316
427
|
}
|
|
317
|
-
ts.forEachChild(node, visit);
|
|
318
428
|
}
|
|
319
429
|
function collectFromTrans(attributes) {
|
|
320
430
|
const i18nKeyAttr = getJsxAttribute(attributes, "i18nKey");
|
|
@@ -323,58 +433,27 @@ export async function collectResolvedImportedObjectTranslationKeys(filePaths) {
|
|
|
323
433
|
!i18nKeyAttr.initializer.expression) {
|
|
324
434
|
return;
|
|
325
435
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
436
|
+
collectFromExpression(i18nKeyAttr.initializer.expression, "Trans");
|
|
437
|
+
}
|
|
438
|
+
function visit(node) {
|
|
439
|
+
if (ts.isCallExpression(node) && isTranslationCall(node)) {
|
|
440
|
+
const firstArg = node.arguments[0];
|
|
441
|
+
if (firstArg) {
|
|
442
|
+
collectFromExpression(firstArg, "t");
|
|
443
|
+
}
|
|
330
444
|
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
445
|
+
if (ts.isJsxSelfClosingElement(node) && isTransTagName(node.tagName)) {
|
|
446
|
+
collectFromTrans(node.attributes);
|
|
447
|
+
}
|
|
448
|
+
if (ts.isJsxOpeningElement(node) && isTransTagName(node.tagName)) {
|
|
449
|
+
collectFromTrans(node.attributes);
|
|
334
450
|
}
|
|
451
|
+
ts.forEachChild(node, visit);
|
|
335
452
|
}
|
|
336
453
|
visit(sourceFile);
|
|
337
454
|
}
|
|
338
|
-
return resolvedKeys;
|
|
339
|
-
}
|
|
340
|
-
function matchObjectPath(pattern, actual) {
|
|
341
|
-
if (pattern.length !== actual.length) {
|
|
342
|
-
return false;
|
|
343
|
-
}
|
|
344
|
-
return pattern.every((part, index) => {
|
|
345
|
-
return part === "*" || part === actual[index];
|
|
346
|
-
});
|
|
347
|
-
}
|
|
348
|
-
function resolveObjectValuesByPath(objectMap, pathPattern) {
|
|
349
|
-
const result = new Set();
|
|
350
|
-
for (const [objectPath, translationKey] of objectMap.entries()) {
|
|
351
|
-
const actualPath = objectPath.split(".");
|
|
352
|
-
if (matchObjectPath(pathPattern, actualPath)) {
|
|
353
|
-
result.add(translationKey);
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
return result;
|
|
357
|
-
}
|
|
358
|
-
function resolveImportedObjectAccess(expression, currentFilePath, importBindings, objectRegistry, availableFiles) {
|
|
359
|
-
const chain = extractPropertyChain(expression);
|
|
360
|
-
if (!chain) {
|
|
361
|
-
return null;
|
|
362
|
-
}
|
|
363
|
-
const binding = importBindings.get(chain.root);
|
|
364
|
-
if (!binding) {
|
|
365
|
-
return null;
|
|
366
|
-
}
|
|
367
|
-
const importedFilePath = resolveImportSource(currentFilePath, binding.source, availableFiles);
|
|
368
|
-
if (!importedFilePath) {
|
|
369
|
-
return null;
|
|
370
|
-
}
|
|
371
|
-
const objectMap = objectRegistry.get(`${importedFilePath}::${binding.importedName}`);
|
|
372
|
-
if (!objectMap) {
|
|
373
|
-
return null;
|
|
374
|
-
}
|
|
375
455
|
return {
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
path: chain.path,
|
|
456
|
+
keys: resolvedKeys,
|
|
457
|
+
usages,
|
|
379
458
|
};
|
|
380
459
|
}
|
|
@@ -246,7 +246,9 @@ export async function deepVerifyUnusedKeys(sourceFiles, unusedKeysByFile) {
|
|
|
246
246
|
const definitelyUnusedByFile = {};
|
|
247
247
|
const possiblyDynamicallyUsedByFile = {};
|
|
248
248
|
const evidenceList = await Promise.all(sourceFiles.map((file) => collectFileEvidence(file)));
|
|
249
|
-
const
|
|
249
|
+
const resolvedImportedObjectResult = await collectResolvedImportedObjectTranslationKeys(sourceFiles);
|
|
250
|
+
const resolvedImportedObjectKeys = resolvedImportedObjectResult.keys;
|
|
251
|
+
const resolvedImportedObjectUsages = resolvedImportedObjectResult.usages;
|
|
250
252
|
for (const [translationFilePath, unusedKeys] of Object.entries(unusedKeysByFile)) {
|
|
251
253
|
const definitelyUnused = [];
|
|
252
254
|
const possiblyDynamic = [];
|
|
@@ -264,5 +266,6 @@ export async function deepVerifyUnusedKeys(sourceFiles, unusedKeysByFile) {
|
|
|
264
266
|
return {
|
|
265
267
|
definitelyUnusedByFile,
|
|
266
268
|
possiblyDynamicallyUsedByFile,
|
|
269
|
+
resolvedImportedObjectUsages,
|
|
267
270
|
};
|
|
268
271
|
}
|
package/dist/core/scanner.js
CHANGED
|
@@ -7,6 +7,7 @@ import { writeDynamicTranslationReport } from "./actions/writeDynamicTranslation
|
|
|
7
7
|
import { syncMissingTranslations } from "./actions/syncMissingTranslations.js";
|
|
8
8
|
import { removeUnusedTranslations } from "./actions/removeUnusedTranslations.js";
|
|
9
9
|
import { writeUnusedTranslationsReport } from "./actions/writeUnusedTranslationsReport.js";
|
|
10
|
+
import { writeResolvedImportedObjectUsagesReport } from "./actions/writeResolvedImportedObjectUsageReport.js";
|
|
10
11
|
import { deepVerifyUnusedKeys } from "./analysis/deepVerifyUnusedKeys.js";
|
|
11
12
|
export async function runScanner(config) {
|
|
12
13
|
const files = await getFiles(config.srcPaths);
|
|
@@ -16,6 +17,7 @@ export async function runScanner(config) {
|
|
|
16
17
|
const jsonData = await loadJsonKeys(config.jsonDir);
|
|
17
18
|
const result = analyzeKeys(usedKeys, jsonData);
|
|
18
19
|
const verification = await deepVerifyUnusedKeys(files, result.unusedKeysByFile);
|
|
20
|
+
await writeResolvedImportedObjectUsagesReport(verification.resolvedImportedObjectUsages ?? [], config.outputDir);
|
|
19
21
|
await writeUnusedTranslationsReport(verification.definitelyUnusedByFile, jsonData, config.outputDir);
|
|
20
22
|
if (config.syncMissing) {
|
|
21
23
|
await syncMissingTranslations(result, jsonData, config.sortJson);
|