@lingual/i18n-check 0.1.5 → 0.3.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/README.md +156 -79
- package/dist/bin/index.js +133 -63
- package/dist/bin/index.test.d.ts +1 -0
- package/dist/bin/index.test.js +266 -0
- package/dist/errorReporters.d.ts +9 -3
- package/dist/errorReporters.js +29 -5
- package/dist/index.d.ts +2 -1
- package/dist/index.js +43 -1
- package/dist/utils/findInvalidi18nTranslations.js +0 -3
- package/package.json +17 -9
package/README.md
CHANGED
|
@@ -1,6 +1,28 @@
|
|
|
1
|
-
# Lingual i18n
|
|
1
|
+
# Lingual i18n-check
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**i18n-check** validates your [ICU](https://github.com/unicode-org/icu) and [i18next](https://www.i18next.com/) translation files and checks for missing and broken translations.
|
|
4
|
+
It compares the defined source language with all target translation files and finds inconsistencies between source and target files.
|
|
5
|
+
You can run these checks as a pre-commit hook or on the CI depending on your use-case and setup.
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+

|
|
10
|
+
|
|
11
|
+
## Table of Contents
|
|
12
|
+
|
|
13
|
+
- [Installation](#installation)
|
|
14
|
+
- [General Usage](#general-usage)
|
|
15
|
+
- [CLI Options](#options)
|
|
16
|
+
- [Examples](#examples)
|
|
17
|
+
- [Single folder](#single-folder)
|
|
18
|
+
- [Folder per locale](#folder-per-locale)
|
|
19
|
+
- [Folder per locale with multiple files](#folder-per-locale-with-multiple-files)
|
|
20
|
+
- [Github Action](#as-github-action)
|
|
21
|
+
- [API](#api)
|
|
22
|
+
- [Development](#development)
|
|
23
|
+
- [Links](#links)
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
4
26
|
|
|
5
27
|
Using **yarn**:
|
|
6
28
|
|
|
@@ -20,9 +42,9 @@ Using **pnpm**:
|
|
|
20
42
|
pnpm add --save-dev @lingual/i18n-check
|
|
21
43
|
```
|
|
22
44
|
|
|
23
|
-
After the installation,
|
|
45
|
+
After the installation, i18n-check can either be accessed via defining a command in the `package.json` file or directly in the CLI.
|
|
24
46
|
|
|
25
|
-
Update
|
|
47
|
+
Update your `package.json` and add a new command:
|
|
26
48
|
|
|
27
49
|
```bash
|
|
28
50
|
"scripts": {
|
|
@@ -39,160 +61,197 @@ Alternatively you can also access the library directly:
|
|
|
39
61
|
node_modules/.bin/i18n-check
|
|
40
62
|
```
|
|
41
63
|
|
|
64
|
+
## General Usage
|
|
65
|
+
|
|
66
|
+
For i18n-check to work you need to provide it at a minimum the source locale (`--source, -s`) for the primary language and the path to the locale translation files (`--locales, -l`).
|
|
67
|
+
|
|
68
|
+
Example:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
yarn i18n:check -s en-US --locales translations/
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Instead of a single source file you can also pass a directory:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
yarn i18n:check -s en-US --locales translations/
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
See the [examples](#examples) for more details.
|
|
81
|
+
|
|
42
82
|
## Options
|
|
43
83
|
|
|
44
|
-
### --
|
|
84
|
+
### --locales, -l
|
|
45
85
|
|
|
46
|
-
With the `-
|
|
86
|
+
With the `-l` or `--locales` option you define which folder or multiple folders you want to run the i18n checks against. It is a **required** option. i18n-check will try to find all target locale files and compare these files against the defined source file(s).
|
|
47
87
|
Check the [example](#examples) to see how different locale translation files are organised and how they can be addressed.
|
|
48
88
|
|
|
49
89
|
```bash
|
|
50
|
-
yarn i18n:check
|
|
90
|
+
yarn i18n:check --locales translations/messageExamples -s en-US
|
|
51
91
|
```
|
|
52
92
|
|
|
53
|
-
### --source
|
|
93
|
+
### --source, -s
|
|
54
94
|
|
|
55
|
-
With the `-s` or `--source` option you define
|
|
56
|
-
Check the [
|
|
95
|
+
With the `-s` or `--source` option you define the source locale to compare all target files against. It is a **required** option. i18n-check will try to find all target locale files and compare these files against the applicable source file(s).
|
|
96
|
+
Check the [examples](#examples) to see how different locale translation files are organised and how they can be addressed.
|
|
57
97
|
|
|
58
98
|
```bash
|
|
59
|
-
yarn i18n:check
|
|
99
|
+
yarn i18n:check --locales translations/messageExamples -s en-US
|
|
60
100
|
```
|
|
61
101
|
|
|
62
|
-
### --format
|
|
102
|
+
### --format, -f
|
|
63
103
|
|
|
64
|
-
By default
|
|
104
|
+
By default i18n-check will validate against any [ICU](https://github.com/unicode-org/icu) compliant translations.
|
|
65
105
|
Additionally the `i18next` format is supported and can be set via the `-f` or `--format` option.
|
|
66
106
|
|
|
67
|
-
There are i18n libraries that have their own specific format, which might not be based on
|
|
107
|
+
There are i18n libraries that have their own specific format, which might not be based on ICU and therefore can not be validated against currently. On a side-note: there might be future support for more specific formats.
|
|
108
|
+
|
|
109
|
+
Hint: If you want to use the `--unused` flag, you should provide react-intl as the format. Also see the [`unused` section](#--unused) for more details.
|
|
68
110
|
|
|
69
111
|
```bash
|
|
70
|
-
yarn i18n:check
|
|
112
|
+
yarn i18n:check --locales translations/i18NextMessageExamples -s en-US -f i18next
|
|
71
113
|
```
|
|
72
114
|
|
|
73
|
-
### --check
|
|
115
|
+
### --check, -c
|
|
74
116
|
|
|
75
|
-
By default
|
|
117
|
+
By default i18n-check will perform a validation against any **missing** and/or **invalid** keys. There are situations where only a specific check should run. By using the `-c` or `--check` option you can specify a specific check to run.
|
|
76
118
|
|
|
77
119
|
The available options are `missingKeys`, which will check against any missing keys in the target files and `invalidKeys` will check for invalid keys, where the target translations has a different type then the one defined in the source file.
|
|
78
120
|
|
|
79
121
|
Check for missing keys:
|
|
80
122
|
|
|
81
123
|
```bash
|
|
82
|
-
yarn i18n:check
|
|
124
|
+
yarn i18n:check --locales translations/messageExamples -s en-US -c missingKeys
|
|
83
125
|
```
|
|
84
126
|
|
|
85
127
|
Check for invalid keys:
|
|
86
128
|
|
|
87
129
|
```bash
|
|
88
|
-
yarn i18n:check
|
|
130
|
+
yarn i18n:check --locales translations/messageExamples -s en-US -c invalidKeys
|
|
89
131
|
```
|
|
90
132
|
|
|
91
|
-
Check for missing
|
|
133
|
+
Check for missing and invalid keys (which is the default):
|
|
92
134
|
|
|
93
135
|
```bash
|
|
94
|
-
yarn i18n:check
|
|
136
|
+
yarn i18n:check --locales translations/messageExamples -s en-US -c missingKeys,invalidKeys
|
|
95
137
|
```
|
|
96
138
|
|
|
97
|
-
### --
|
|
139
|
+
### --unused, -u
|
|
140
|
+
|
|
141
|
+
This feature is currently only supported for react-intl and is useful if you need to know which keys exist in your translation files but not in your codebase. Via the `-u` or `--unused` option you provide a source path to the code, which will be parsed to find all unused keys in the primary target language.
|
|
142
|
+
|
|
143
|
+
It is important to note that you must also provide the `-f` or `--format` option with `react-intl` as value. See the [`format` section](#--format) for more information.
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
yarn i18n:check --locales translations/messageExamples -s en-US -u client/ -f react-intl
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### --reporter, -r
|
|
98
150
|
|
|
99
151
|
The standard reporting prints out all the missing or invalid keys.
|
|
100
152
|
Using the `-r` or `--reporter` option enables to override the standard error reporting. Passing the `summary` option will print a summary of the missing or invalid keys.
|
|
101
153
|
|
|
102
154
|
```bash
|
|
103
|
-
yarn i18n:check
|
|
155
|
+
yarn i18n:check --locales translations/messageExamples -s en-US -r summary
|
|
104
156
|
```
|
|
105
157
|
|
|
106
|
-
### --exclude
|
|
158
|
+
### --exclude, -e
|
|
107
159
|
|
|
108
|
-
There are situations where we want to exclude a single or multiple files or a single folder or a group of folders. A typical scenario would be that some keys are missing in a specific folder, as they are being work in progress for example. To exclude this or these files/folders you can use the `-e` or `--exclude` option. It expects
|
|
160
|
+
There are situations where we want to exclude a single or multiple files or a single folder or a group of folders. A typical scenario would be that some keys are missing in a specific folder, as they are being work in progress for example. To exclude this or these files/folders you can use the `-e` or `--exclude` option. It expects one or more files and/or folders.
|
|
109
161
|
|
|
110
162
|
To exclude a single file:
|
|
111
163
|
|
|
112
164
|
```bash
|
|
113
|
-
yarn i18n:check
|
|
165
|
+
yarn i18n:check --locales translations/messageExamples -s en-US -e translations/messageExamples/fr-fr.json
|
|
114
166
|
```
|
|
115
167
|
|
|
116
|
-
To exclude multiple files
|
|
168
|
+
To exclude multiple files provide all files:
|
|
117
169
|
|
|
118
170
|
```bash
|
|
119
|
-
yarn i18n:check
|
|
171
|
+
yarn i18n:check --locales translations/messageExamples -s en-US -e translations/messageExamples/fr-fr.json translations/messageExamples/de-at.json
|
|
120
172
|
```
|
|
121
173
|
|
|
122
174
|
To exclude a single folder:
|
|
123
175
|
|
|
124
176
|
```bash
|
|
125
|
-
yarn i18n:check
|
|
177
|
+
yarn i18n:check --locales translations/folderExamples -s en-US -e translations/folderExamples/fr/*
|
|
126
178
|
```
|
|
127
179
|
|
|
128
|
-
Alternatively you can exclude multiple folders by providing
|
|
180
|
+
Alternatively you can exclude multiple folders by providing the folders to be excluded:
|
|
129
181
|
|
|
130
182
|
```bash
|
|
131
|
-
yarn i18n:check
|
|
183
|
+
yarn i18n:check --locales translations/folderExamples -s en-US -e translations/folderExamples/fr/* translations/folderExample/it/*
|
|
132
184
|
```
|
|
133
185
|
|
|
134
|
-
The `--exclude` option also accepts a mix of files and folders, which follows the same pattern as above
|
|
135
|
-
`-e translations/folderExamples/fr
|
|
186
|
+
The `--exclude` option also accepts a mix of files and folders, which follows the same pattern as above, i.e.
|
|
187
|
+
`-e translations/folderExamples/fr/* translations/messageExamples/it.json`
|
|
136
188
|
|
|
137
189
|
## Examples
|
|
138
190
|
|
|
139
|
-
|
|
191
|
+
i18n-check is able to load and validate against different locale folder structures. Depending on how the locale files are organized, there are different configuration options.
|
|
140
192
|
|
|
141
|
-
|
|
193
|
+
### Single folder
|
|
142
194
|
|
|
143
195
|
If all the locales are organized in a **single folder**:
|
|
144
196
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
-
|
|
197
|
+
```
|
|
198
|
+
locales/
|
|
199
|
+
en-en.json
|
|
200
|
+
de-de.json
|
|
201
|
+
```
|
|
148
202
|
|
|
149
|
-
Use the `
|
|
203
|
+
Use the `t` or `target` option to define the directory that should be checked for target files. With the `s` or `source` option you can specify the base/reference file to compare the target files against.
|
|
150
204
|
|
|
151
205
|
```bash
|
|
152
|
-
yarn i18n:check
|
|
206
|
+
yarn i18n:check --locales locales -s locales/en-us.json
|
|
153
207
|
```
|
|
154
208
|
|
|
155
|
-
|
|
209
|
+
### Folder per locale
|
|
156
210
|
|
|
157
211
|
If the locales are **organised as folders** containing a single json file:
|
|
158
212
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
213
|
+
```
|
|
214
|
+
locales/
|
|
215
|
+
en-US/
|
|
216
|
+
index.json
|
|
217
|
+
de-DE/
|
|
218
|
+
index.json
|
|
219
|
+
```
|
|
164
220
|
|
|
165
221
|
Define the `locales` folder as the directory to look for target files.
|
|
166
222
|
|
|
167
223
|
```bash
|
|
168
|
-
yarn i18n:check
|
|
224
|
+
yarn i18n:check --locales locales -s en-US
|
|
169
225
|
```
|
|
170
226
|
|
|
171
|
-
|
|
227
|
+
### Folder per locale with multiple files
|
|
172
228
|
|
|
173
229
|
If the locales are **organised as folders** containing multiple json files:
|
|
174
230
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
231
|
+
```
|
|
232
|
+
locales/
|
|
233
|
+
en-US/
|
|
234
|
+
one.json
|
|
235
|
+
two.json
|
|
236
|
+
three.json
|
|
237
|
+
de-DE/
|
|
238
|
+
one.json
|
|
239
|
+
two.json
|
|
240
|
+
three.json
|
|
241
|
+
```
|
|
184
242
|
|
|
185
|
-
Define the `locales` folder as the directory to look for target files and pass `locales/en-US/` as the `source` option.
|
|
243
|
+
Define the `locales` folder as the directory to look for target files and pass `locales/en-US/` as the `source` option. i18n-check will try to collect all the files in the provided source directory and compare each one against the corresponding files in the target locales.
|
|
186
244
|
|
|
187
245
|
```bash
|
|
188
|
-
yarn i18n:check
|
|
246
|
+
yarn i18n:check --locales locales -s en-US
|
|
189
247
|
```
|
|
190
248
|
|
|
191
249
|
#### Multiple folders containing locales
|
|
192
250
|
|
|
193
251
|
If the locales are **organised as folders** containing multiple json files:
|
|
194
252
|
|
|
195
|
-
|
|
253
|
+
```
|
|
254
|
+
- spaceOne
|
|
196
255
|
- locales/
|
|
197
256
|
- en-US/
|
|
198
257
|
- one.json
|
|
@@ -202,28 +261,29 @@ If the locales are **organised as folders** containing multiple json files:
|
|
|
202
261
|
- one.json
|
|
203
262
|
- two.json
|
|
204
263
|
- three.json
|
|
205
|
-
-
|
|
264
|
+
- spaceTwo
|
|
206
265
|
- locales/
|
|
207
|
-
- en/
|
|
266
|
+
- en-US/
|
|
208
267
|
- one.json
|
|
209
268
|
- two.json
|
|
210
269
|
- three.json
|
|
211
|
-
- de/
|
|
270
|
+
- de-DE/
|
|
212
271
|
- one.json
|
|
213
272
|
- two.json
|
|
214
273
|
- three.json
|
|
274
|
+
```
|
|
215
275
|
|
|
216
|
-
Define the `locales` folder as the directory to look for target files and pass `
|
|
276
|
+
Define the `locales` folder as the directory to look for target files and pass `en-US` as the `source` option. i18n-check will try to collect all the files in the provided source directory and compare each one against the corresponding files in the target locales.
|
|
217
277
|
|
|
218
278
|
```bash
|
|
219
|
-
yarn i18n:check -
|
|
279
|
+
yarn i18n:check -l spaceOne spaceTwo -s en-US
|
|
220
280
|
```
|
|
221
281
|
|
|
222
282
|
## As Github Action
|
|
223
283
|
|
|
224
|
-
We currently do not offer an explicit **Github Action** you can use out of the box, but if you have
|
|
284
|
+
We currently do not offer an explicit **Github Action** you can use out of the box, but if you have i18n-check already installed, you can define your own **YAML** file. The following example can be seen as a starting point that you can adapt to your current setup:
|
|
225
285
|
|
|
226
|
-
```
|
|
286
|
+
```yml
|
|
227
287
|
name: i18n Check
|
|
228
288
|
on:
|
|
229
289
|
pull_request:
|
|
@@ -247,13 +307,17 @@ jobs:
|
|
|
247
307
|
|
|
248
308
|
- name: yarn i18n-check
|
|
249
309
|
run: |
|
|
250
|
-
yarn i18n-check
|
|
310
|
+
yarn i18n-check --locales translations/messageExamples --source en-US
|
|
251
311
|
```
|
|
252
312
|
|
|
313
|
+
The above workflow will return any missing or invalid keys and the action would fail if missing/invalid keys are found:
|
|
314
|
+
|
|
315
|
+

|
|
316
|
+
|
|
253
317
|
## API
|
|
254
318
|
|
|
255
|
-
Aside from using the CLI,
|
|
256
|
-
Start by importing
|
|
319
|
+
Aside from using the CLI, i18n-check also exposes a set of check functions that can be accessed programmatically.
|
|
320
|
+
Start by importing i18n-check:
|
|
257
321
|
|
|
258
322
|
```ts
|
|
259
323
|
import * as i18nCheck from "@lingual/i18n-check";
|
|
@@ -360,43 +424,56 @@ Run `yarn build`, `pnpm run build` or `npm run build` and then depending on the
|
|
|
360
424
|
Basic icu translation example:
|
|
361
425
|
|
|
362
426
|
```bash
|
|
363
|
-
node dist/bin/index.js
|
|
427
|
+
node dist/bin/index.js --locales translations/messageExamples -s en-US
|
|
364
428
|
```
|
|
365
429
|
|
|
366
430
|
Flatted translation keys example:
|
|
367
431
|
|
|
368
432
|
```bash
|
|
369
|
-
node dist/bin/index.js
|
|
433
|
+
node dist/bin/index.js --locales translations/flattenExamples -s en-US
|
|
370
434
|
```
|
|
371
435
|
|
|
372
436
|
i18next translation example:
|
|
373
437
|
|
|
374
438
|
```bash
|
|
375
|
-
node dist/bin/index.js
|
|
439
|
+
node dist/bin/index.js --locales translations/i18NextMessageExamples -s en-US -f i18next
|
|
376
440
|
```
|
|
377
441
|
|
|
378
442
|
Single file translation example:
|
|
379
443
|
|
|
380
444
|
```bash
|
|
381
|
-
node dist/bin/index.js
|
|
445
|
+
node dist/bin/index.js --locales translations/folderExample -s en-US
|
|
382
446
|
```
|
|
383
447
|
|
|
384
448
|
Multiple files per folder translation example:
|
|
385
449
|
|
|
386
450
|
```bash
|
|
387
|
-
node dist/bin/index.js
|
|
451
|
+
node dist/bin/index.js --locales translations/multipleFilesFolderExample/ -s en-US
|
|
388
452
|
```
|
|
389
453
|
|
|
390
454
|
Multiple folders containing locales translation example:
|
|
391
455
|
|
|
392
456
|
```bash
|
|
393
|
-
node dist/bin/index.js
|
|
457
|
+
node dist/bin/index.js --locales translations/folderExample,translations/messageExamples -s en-US
|
|
394
458
|
```
|
|
395
459
|
|
|
396
|
-
|
|
460
|
+
### Tests
|
|
397
461
|
|
|
398
|
-
To run the tests use the following
|
|
462
|
+
To run the tests use one of the following commands:
|
|
463
|
+
|
|
464
|
+
```bash
|
|
465
|
+
pnpm test
|
|
466
|
+
```
|
|
399
467
|
|
|
400
468
|
```bash
|
|
401
|
-
|
|
469
|
+
yarn test
|
|
402
470
|
```
|
|
471
|
+
|
|
472
|
+
```bash
|
|
473
|
+
npm test
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
## Links
|
|
477
|
+
|
|
478
|
+
- [Introducing i18n-check](https://lingual.dev/blog/introducing-i18n-check/)
|
|
479
|
+
- [Twitter](https://twitter.com/lingualdev)
|
package/dist/bin/index.js
CHANGED
|
@@ -22,13 +22,14 @@ const __1 = require("..");
|
|
|
22
22
|
const errorReporters_1 = require("../errorReporters");
|
|
23
23
|
const flattenTranslations_1 = require("../utils/flattenTranslations");
|
|
24
24
|
commander_1.program
|
|
25
|
-
.version("0.
|
|
26
|
-
.option("-
|
|
27
|
-
.option("-s, --source [source
|
|
25
|
+
.version("0.3.0")
|
|
26
|
+
.option("-l, --locales <locales...>", "name of the directory containing the locales to validate")
|
|
27
|
+
.option("-s, --source [source locale]", "the source locale to validate against")
|
|
28
28
|
.option("-f, --format [format type]", "define the specific format, i.e. i18next")
|
|
29
29
|
.option("-c, --check [checks]", "define the specific checks you want to run: invalid, missing. By default the check will validate against missing and invalid keys, i.e. --check invalidKeys,missingKeys")
|
|
30
30
|
.option("-r, --reporter [error reporting style]", "define the reporting style: standard or summary")
|
|
31
|
-
.option("-e, --exclude
|
|
31
|
+
.option("-e, --exclude <exclude...>", "define the file(s) and/or folders(s) that should be excluded from the check")
|
|
32
|
+
.option("-u, --unused [folder]", "define the source path to find all unused keys")
|
|
32
33
|
.parse();
|
|
33
34
|
const getCheckOptions = () => {
|
|
34
35
|
const checkOption = commander_1.program.getOptionValue("check");
|
|
@@ -40,54 +41,37 @@ const getCheckOptions = () => {
|
|
|
40
41
|
.filter((check) => ["invalidKeys", "missingKeys"].includes(check.trim()));
|
|
41
42
|
return checks.length > 0 ? checks : ["invalidKeys", "missingKeys"];
|
|
42
43
|
};
|
|
43
|
-
const getSourcePath = (
|
|
44
|
-
return
|
|
45
|
-
return fileName.includes(basePathName);
|
|
46
|
-
});
|
|
47
|
-
};
|
|
48
|
-
const getTargetPath = (paths, sourcePaths, fileName) => {
|
|
49
|
-
const basePath = paths.find((path) => {
|
|
50
|
-
return fileName.includes(path);
|
|
51
|
-
});
|
|
52
|
-
if (!basePath) {
|
|
53
|
-
return null;
|
|
54
|
-
}
|
|
55
|
-
return sourcePaths.find((path) => path.includes(basePath));
|
|
56
|
-
};
|
|
57
|
-
const toArray = (input) => {
|
|
58
|
-
return input
|
|
59
|
-
.trim()
|
|
60
|
-
.split(",")
|
|
61
|
-
.filter((a) => a);
|
|
44
|
+
const getSourcePath = (source, fileName) => {
|
|
45
|
+
return fileName.toLowerCase().includes(source.toLowerCase());
|
|
62
46
|
};
|
|
63
47
|
const main = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
64
48
|
const start = performance.now();
|
|
65
49
|
const srcPath = commander_1.program.getOptionValue("source");
|
|
66
|
-
const
|
|
50
|
+
const localePath = commander_1.program.getOptionValue("locales");
|
|
67
51
|
const format = commander_1.program.getOptionValue("format");
|
|
68
52
|
const exclude = commander_1.program.getOptionValue("exclude");
|
|
53
|
+
const unusedSrcPath = commander_1.program.getOptionValue("unused");
|
|
69
54
|
if (!srcPath) {
|
|
70
|
-
console.log(chalk_1.default.red("Source
|
|
55
|
+
console.log(chalk_1.default.red("Source not found. Please provide a valid source locale, i.e. -s en-US"));
|
|
71
56
|
(0, process_1.exit)(1);
|
|
72
57
|
}
|
|
73
|
-
if (!
|
|
74
|
-
console.log(chalk_1.default.red("
|
|
58
|
+
if (!localePath || localePath.length === 0) {
|
|
59
|
+
console.log(chalk_1.default.red("Locale file(s) not found. Please provide valid locale file(s), i.e. -locales translations/"));
|
|
75
60
|
(0, process_1.exit)(1);
|
|
76
61
|
}
|
|
77
|
-
const excludedPaths = exclude ?
|
|
78
|
-
const
|
|
79
|
-
const
|
|
80
|
-
const isMultiFolders = targetPathFolders.length > 1;
|
|
62
|
+
const excludedPaths = exclude !== null && exclude !== void 0 ? exclude : [];
|
|
63
|
+
const localePathFolders = localePath;
|
|
64
|
+
const isMultiFolders = localePathFolders.length > 1;
|
|
81
65
|
let srcFiles = [];
|
|
82
|
-
let
|
|
66
|
+
let localeFiles = [];
|
|
83
67
|
const pattern = isMultiFolders
|
|
84
|
-
? `{${
|
|
85
|
-
: `${
|
|
68
|
+
? `{${localePath.join(",").trim()}}/**/*.json`
|
|
69
|
+
: `${localePath.join(",").trim()}/**/*.json`;
|
|
86
70
|
const files = yield (0, glob_1.glob)(pattern, {
|
|
87
71
|
ignore: ["node_modules/**"].concat(excludedPaths),
|
|
88
72
|
});
|
|
89
|
-
console.log(
|
|
90
|
-
console.log(chalk_1.default.
|
|
73
|
+
console.log("i18n translations checker");
|
|
74
|
+
console.log(chalk_1.default.gray(`Source: ${srcPath}`));
|
|
91
75
|
if (format) {
|
|
92
76
|
console.log(chalk_1.default.blackBright(`Selected format is: ${format}`));
|
|
93
77
|
}
|
|
@@ -95,39 +79,82 @@ const main = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
|
95
79
|
checks: getCheckOptions(),
|
|
96
80
|
format: format !== null && format !== void 0 ? format : undefined,
|
|
97
81
|
};
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
82
|
+
const fileInfos = [];
|
|
83
|
+
files.sort().forEach((file) => {
|
|
84
|
+
var _a;
|
|
85
|
+
const path = file.split("/");
|
|
86
|
+
const name = (_a = path.pop()) !== null && _a !== void 0 ? _a : "";
|
|
87
|
+
fileInfos.push({
|
|
88
|
+
file,
|
|
89
|
+
path,
|
|
90
|
+
name,
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
fileInfos.forEach(({ file, name, path }) => {
|
|
94
|
+
const rawContent = JSON.parse(fs_1.default.readFileSync(file, "utf-8"));
|
|
95
|
+
const content = (0, flattenTranslations_1.flattenTranslations)(rawContent);
|
|
96
|
+
const sourcePath = getSourcePath(srcPath, file);
|
|
101
97
|
if (sourcePath) {
|
|
102
98
|
srcFiles.push({
|
|
103
99
|
reference: null,
|
|
104
100
|
name: file,
|
|
105
|
-
content
|
|
101
|
+
content,
|
|
106
102
|
});
|
|
107
103
|
}
|
|
108
104
|
else {
|
|
109
|
-
const
|
|
110
|
-
const reference = (
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
105
|
+
const fullPath = path.join("-");
|
|
106
|
+
const reference = fileInfos.find((fileInfo) => {
|
|
107
|
+
if (!fileInfo.file.toLowerCase().includes(srcPath.toLowerCase())) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
if (fileInfo.path.join("-") === fullPath) {
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
// Check if the folder path matches - ignoring the last folder
|
|
114
|
+
// Then check if the file names are the same
|
|
115
|
+
// Example folder structure:
|
|
116
|
+
// path/to/locales/
|
|
117
|
+
// en-US/
|
|
118
|
+
// one.json
|
|
119
|
+
// two.json
|
|
120
|
+
// three.json
|
|
121
|
+
// de-DE/
|
|
122
|
+
// one.json
|
|
123
|
+
// two.json
|
|
124
|
+
// three.json
|
|
125
|
+
//
|
|
126
|
+
// Referencing: `path/to/locales/en-US/one.json`, `path/to/locales/de-DE/one.json`
|
|
127
|
+
// Non Referencing: `path/to/locales/en-US/one.json`, `path/to/other/locales/de-DE/one.json`
|
|
128
|
+
if (fileInfo.path.slice(0, fileInfo.path.length - 1).join("-") ===
|
|
129
|
+
path.slice(0, path.length - 1).join("-")) {
|
|
130
|
+
return fileInfo.name === name;
|
|
131
|
+
}
|
|
132
|
+
return false;
|
|
117
133
|
});
|
|
134
|
+
if (reference) {
|
|
135
|
+
localeFiles.push({
|
|
136
|
+
reference: reference.file,
|
|
137
|
+
name: file,
|
|
138
|
+
content,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
118
141
|
}
|
|
119
142
|
});
|
|
120
143
|
if (srcFiles.length === 0) {
|
|
121
|
-
console.log(chalk_1.default.red("Source
|
|
144
|
+
console.log(chalk_1.default.red("Source not found. Please provide a valid source locale, i.e. -s en-US"));
|
|
122
145
|
(0, process_1.exit)(1);
|
|
123
146
|
}
|
|
124
|
-
if (
|
|
125
|
-
console.log(chalk_1.default.red("
|
|
147
|
+
if (localeFiles.length === 0) {
|
|
148
|
+
console.log(chalk_1.default.red("Locale file(s) not found. Please provide valid locale file(s), i.e. --locales translations/"));
|
|
126
149
|
(0, process_1.exit)(1);
|
|
127
150
|
}
|
|
128
151
|
try {
|
|
129
|
-
const result = (0, __1.checkTranslations)(srcFiles,
|
|
130
|
-
|
|
152
|
+
const result = (0, __1.checkTranslations)(srcFiles, localeFiles, options);
|
|
153
|
+
printTranslationResult(result);
|
|
154
|
+
if (unusedSrcPath) {
|
|
155
|
+
const unusedKeys = yield (0, __1.checkUnusedKeys)(srcFiles, unusedSrcPath, options);
|
|
156
|
+
printUnusedKeysResult({ unusedKeys });
|
|
157
|
+
}
|
|
131
158
|
const end = performance.now();
|
|
132
159
|
console.log(chalk_1.default.green(`\nDone in ${Math.round(((end - start) * 100) / 1000) / 100}s.`));
|
|
133
160
|
if ((result.missingKeys && Object.keys(result.missingKeys).length > 0) ||
|
|
@@ -143,28 +170,71 @@ const main = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
|
143
170
|
(0, process_1.exit)(1);
|
|
144
171
|
}
|
|
145
172
|
});
|
|
146
|
-
const
|
|
173
|
+
const printTranslationResult = ({ missingKeys, invalidKeys, }) => {
|
|
147
174
|
const reporter = commander_1.program.getOptionValue("reporter");
|
|
148
|
-
const
|
|
175
|
+
const isSummary = reporter === "summary";
|
|
149
176
|
if (missingKeys && Object.keys(missingKeys).length > 0) {
|
|
150
|
-
console.log(chalk_1.default.
|
|
151
|
-
|
|
152
|
-
console.log(chalk_1.default.red(
|
|
153
|
-
|
|
177
|
+
console.log(chalk_1.default.red("\nFound missing keys!"));
|
|
178
|
+
if (isSummary) {
|
|
179
|
+
console.log(chalk_1.default.red((0, errorReporters_1.summaryReporter)(getSummaryRows(missingKeys))));
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
console.log(chalk_1.default.red((0, errorReporters_1.standardReporter)(getStandardRows(missingKeys))));
|
|
154
183
|
}
|
|
155
184
|
}
|
|
156
185
|
else if (missingKeys) {
|
|
157
186
|
console.log(chalk_1.default.green("\nNo missing keys found!"));
|
|
158
187
|
}
|
|
159
188
|
if (invalidKeys && Object.keys(invalidKeys).length > 0) {
|
|
160
|
-
console.log(chalk_1.default.
|
|
161
|
-
|
|
162
|
-
console.log(chalk_1.default.red(
|
|
163
|
-
|
|
189
|
+
console.log(chalk_1.default.red("\nFound invalid keys!"));
|
|
190
|
+
if (isSummary) {
|
|
191
|
+
console.log(chalk_1.default.red((0, errorReporters_1.summaryReporter)(getSummaryRows(invalidKeys))));
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
console.log(chalk_1.default.red((0, errorReporters_1.standardReporter)(getStandardRows(invalidKeys))));
|
|
164
195
|
}
|
|
165
196
|
}
|
|
166
197
|
else if (invalidKeys) {
|
|
167
198
|
console.log(chalk_1.default.green("\nNo invalid translations found!"));
|
|
168
199
|
}
|
|
169
200
|
};
|
|
201
|
+
const printUnusedKeysResult = ({ unusedKeys, }) => {
|
|
202
|
+
const reporter = commander_1.program.getOptionValue("reporter");
|
|
203
|
+
const isSummary = reporter === "summary";
|
|
204
|
+
if (unusedKeys && Object.keys(unusedKeys).length > 0) {
|
|
205
|
+
console.log(chalk_1.default.red("\nFound unused keys!"));
|
|
206
|
+
if (isSummary) {
|
|
207
|
+
console.log(chalk_1.default.red((0, errorReporters_1.summaryReporter)(getSummaryRows(unusedKeys))));
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
console.log(chalk_1.default.red((0, errorReporters_1.standardReporter)(getStandardRows(unusedKeys))));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
else if (unusedKeys) {
|
|
214
|
+
console.log(chalk_1.default.green("\nNo unused found!"));
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
const truncate = (chars) => chars.length > 80 ? `${chars.substring(0, 80)}...` : chars;
|
|
218
|
+
const getSummaryRows = (checkResult) => {
|
|
219
|
+
const formattedRows = [];
|
|
220
|
+
for (const [file, keys] of Object.entries(checkResult)) {
|
|
221
|
+
formattedRows.push({
|
|
222
|
+
file: truncate(file),
|
|
223
|
+
total: keys.length,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
return formattedRows;
|
|
227
|
+
};
|
|
228
|
+
const getStandardRows = (checkResult) => {
|
|
229
|
+
const formattedRows = [];
|
|
230
|
+
for (const [file, keys] of Object.entries(checkResult)) {
|
|
231
|
+
for (const key of keys) {
|
|
232
|
+
formattedRows.push({
|
|
233
|
+
file: truncate(file),
|
|
234
|
+
key: truncate(key),
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return formattedRows;
|
|
239
|
+
};
|
|
170
240
|
main();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const child_process_1 = require("child_process");
|
|
4
|
+
describe("CLI", () => {
|
|
5
|
+
it("should return the missing keys for single folder translations", (done) => {
|
|
6
|
+
(0, child_process_1.exec)("node dist/bin/index.js -s en-US -l translations/flattenExamples", (_error, stdout, _stderr) => {
|
|
7
|
+
const result = stdout.split("Done")[0];
|
|
8
|
+
expect(result).toEqual(`i18n translations checker
|
|
9
|
+
Source: en-US
|
|
10
|
+
|
|
11
|
+
Found missing keys!
|
|
12
|
+
┌───────────────────────────────────────────┬────────────────────────────────┐
|
|
13
|
+
│ file │ key │
|
|
14
|
+
├───────────────────────────────────────────┼────────────────────────────────┤
|
|
15
|
+
│ translations/flattenExamples/de-de.json │ other.nested.three │
|
|
16
|
+
│ translations/flattenExamples/de-de.json │ other.nested.deep.more.final │
|
|
17
|
+
└───────────────────────────────────────────┴────────────────────────────────┘
|
|
18
|
+
|
|
19
|
+
No invalid translations found!
|
|
20
|
+
|
|
21
|
+
`);
|
|
22
|
+
done();
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
it("should return the missing/invalid keys for folder per locale with single file", (done) => {
|
|
26
|
+
(0, child_process_1.exec)("node dist/bin/index.js -l translations/folderExample/ -s en-US", (_error, stdout, _stderr) => {
|
|
27
|
+
const result = stdout.split("Done")[0];
|
|
28
|
+
expect(result).toEqual(`i18n translations checker
|
|
29
|
+
Source: en-US
|
|
30
|
+
|
|
31
|
+
Found missing keys!
|
|
32
|
+
┌───────────────────────────────────────────────┬───────────────────────┐
|
|
33
|
+
│ file │ key │
|
|
34
|
+
├───────────────────────────────────────────────┼───────────────────────┤
|
|
35
|
+
│ translations/folderExample/de-DE/index.json │ message.text-format │
|
|
36
|
+
└───────────────────────────────────────────────┴───────────────────────┘
|
|
37
|
+
|
|
38
|
+
Found invalid keys!
|
|
39
|
+
┌───────────────────────────────────────────────┬──────────────────┐
|
|
40
|
+
│ file │ key │
|
|
41
|
+
├───────────────────────────────────────────────┼──────────────────┤
|
|
42
|
+
│ translations/folderExample/de-DE/index.json │ message.select │
|
|
43
|
+
└───────────────────────────────────────────────┴──────────────────┘
|
|
44
|
+
|
|
45
|
+
`);
|
|
46
|
+
done();
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
it("should return the missing/invalid keys for folder per locale with multiple files", (done) => {
|
|
50
|
+
(0, child_process_1.exec)("node dist/bin/index.js -l translations/multipleFilesFolderExample/ -s en-US", (_error, stdout, _stderr) => {
|
|
51
|
+
const result = stdout.split("Done")[0];
|
|
52
|
+
expect(result).toEqual(`i18n translations checker
|
|
53
|
+
Source: en-US
|
|
54
|
+
|
|
55
|
+
Found missing keys!
|
|
56
|
+
┌──────────────────────────────────────────────────────────┬───────────────────────┐
|
|
57
|
+
│ file │ key │
|
|
58
|
+
├──────────────────────────────────────────────────────────┼───────────────────────┤
|
|
59
|
+
│ translations/multipleFilesFolderExample/de-DE/one.json │ message.text-format │
|
|
60
|
+
│ translations/multipleFilesFolderExample/de-DE/two.json │ test.drive.four │
|
|
61
|
+
└──────────────────────────────────────────────────────────┴───────────────────────┘
|
|
62
|
+
|
|
63
|
+
Found invalid keys!
|
|
64
|
+
┌────────────────────────────────────────────────────────────┬─────────────────────┐
|
|
65
|
+
│ file │ key │
|
|
66
|
+
├────────────────────────────────────────────────────────────┼─────────────────────┤
|
|
67
|
+
│ translations/multipleFilesFolderExample/de-DE/one.json │ message.select │
|
|
68
|
+
│ translations/multipleFilesFolderExample/de-DE/three.json │ multipleVariables │
|
|
69
|
+
└────────────────────────────────────────────────────────────┴─────────────────────┘
|
|
70
|
+
|
|
71
|
+
`);
|
|
72
|
+
done();
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
it("should return the missing/invalid keys for folder containing multiple locale folders", (done) => {
|
|
76
|
+
(0, child_process_1.exec)("node dist/bin/index.js -l translations/multipleFoldersExample -s en-US", (_error, stdout, _stderr) => {
|
|
77
|
+
const result = stdout.split("Done")[0];
|
|
78
|
+
expect(result).toEqual(`i18n translations checker
|
|
79
|
+
Source: en-US
|
|
80
|
+
|
|
81
|
+
Found missing keys!
|
|
82
|
+
┌───────────────────────────────────────────────────────────────────────┬───────────────────────┐
|
|
83
|
+
│ file │ key │
|
|
84
|
+
├───────────────────────────────────────────────────────────────────────┼───────────────────────┤
|
|
85
|
+
│ translations/multipleFoldersExample/spaceOne/locales/de-DE/one.json │ message.text-format │
|
|
86
|
+
│ translations/multipleFoldersExample/spaceOne/locales/de-DE/two.json │ test.drive.four │
|
|
87
|
+
│ translations/multipleFoldersExample/spaceTwo/locales/de-DE/one.json │ message.plural │
|
|
88
|
+
│ translations/multipleFoldersExample/spaceTwo/locales/de-DE/two.json │ test.drive.two │
|
|
89
|
+
└───────────────────────────────────────────────────────────────────────┴───────────────────────┘
|
|
90
|
+
|
|
91
|
+
Found invalid keys!
|
|
92
|
+
┌─────────────────────────────────────────────────────────────────────────┬───────────────────────┐
|
|
93
|
+
│ file │ key │
|
|
94
|
+
├─────────────────────────────────────────────────────────────────────────┼───────────────────────┤
|
|
95
|
+
│ translations/multipleFoldersExample/spaceOne/locales/de-DE/one.json │ message.select │
|
|
96
|
+
│ translations/multipleFoldersExample/spaceOne/locales/de-DE/three.json │ multipleVariables │
|
|
97
|
+
│ translations/multipleFoldersExample/spaceTwo/locales/de-DE/one.json │ message.text-format │
|
|
98
|
+
│ translations/multipleFoldersExample/spaceTwo/locales/de-DE/three.json │ numberFormat │
|
|
99
|
+
└─────────────────────────────────────────────────────────────────────────┴───────────────────────┘
|
|
100
|
+
|
|
101
|
+
`);
|
|
102
|
+
done();
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
it("should return the missing/invalid keys for multiple locale folders", (done) => {
|
|
106
|
+
(0, child_process_1.exec)("node dist/bin/index.js -l translations/multipleFoldersExample/spaceOne translations/multipleFoldersExample/spaceTwo -s en-US", (_error, stdout, _stderr) => {
|
|
107
|
+
const result = stdout.split("Done")[0];
|
|
108
|
+
expect(result).toEqual(`i18n translations checker
|
|
109
|
+
Source: en-US
|
|
110
|
+
|
|
111
|
+
Found missing keys!
|
|
112
|
+
┌───────────────────────────────────────────────────────────────────────┬───────────────────────┐
|
|
113
|
+
│ file │ key │
|
|
114
|
+
├───────────────────────────────────────────────────────────────────────┼───────────────────────┤
|
|
115
|
+
│ translations/multipleFoldersExample/spaceOne/locales/de-DE/one.json │ message.text-format │
|
|
116
|
+
│ translations/multipleFoldersExample/spaceOne/locales/de-DE/two.json │ test.drive.four │
|
|
117
|
+
│ translations/multipleFoldersExample/spaceTwo/locales/de-DE/one.json │ message.plural │
|
|
118
|
+
│ translations/multipleFoldersExample/spaceTwo/locales/de-DE/two.json │ test.drive.two │
|
|
119
|
+
└───────────────────────────────────────────────────────────────────────┴───────────────────────┘
|
|
120
|
+
|
|
121
|
+
Found invalid keys!
|
|
122
|
+
┌─────────────────────────────────────────────────────────────────────────┬───────────────────────┐
|
|
123
|
+
│ file │ key │
|
|
124
|
+
├─────────────────────────────────────────────────────────────────────────┼───────────────────────┤
|
|
125
|
+
│ translations/multipleFoldersExample/spaceOne/locales/de-DE/one.json │ message.select │
|
|
126
|
+
│ translations/multipleFoldersExample/spaceOne/locales/de-DE/three.json │ multipleVariables │
|
|
127
|
+
│ translations/multipleFoldersExample/spaceTwo/locales/de-DE/one.json │ message.text-format │
|
|
128
|
+
│ translations/multipleFoldersExample/spaceTwo/locales/de-DE/three.json │ numberFormat │
|
|
129
|
+
└─────────────────────────────────────────────────────────────────────────┴───────────────────────┘
|
|
130
|
+
|
|
131
|
+
`);
|
|
132
|
+
done();
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
it("should return the missing/invalid keys for all files in the provided locale folders", (done) => {
|
|
136
|
+
(0, child_process_1.exec)("node dist/bin/index.js --source en-US --locales translations/flattenExamples translations/messageExamples", (_error, stdout, _stderr) => {
|
|
137
|
+
const result = stdout.split("Done")[0];
|
|
138
|
+
expect(result).toEqual(`i18n translations checker
|
|
139
|
+
Source: en-US
|
|
140
|
+
|
|
141
|
+
Found missing keys!
|
|
142
|
+
┌───────────────────────────────────────────┬────────────────────────────────┐
|
|
143
|
+
│ file │ key │
|
|
144
|
+
├───────────────────────────────────────────┼────────────────────────────────┤
|
|
145
|
+
│ translations/flattenExamples/de-de.json │ other.nested.three │
|
|
146
|
+
│ translations/flattenExamples/de-de.json │ other.nested.deep.more.final │
|
|
147
|
+
│ translations/messageExamples/de-de.json │ richText │
|
|
148
|
+
│ translations/messageExamples/de-de.json │ yo │
|
|
149
|
+
│ translations/messageExamples/de-de.json │ nesting1 │
|
|
150
|
+
│ translations/messageExamples/de-de.json │ nesting2 │
|
|
151
|
+
│ translations/messageExamples/de-de.json │ nesting3 │
|
|
152
|
+
│ translations/messageExamples/de-de.json │ key1 │
|
|
153
|
+
└───────────────────────────────────────────┴────────────────────────────────┘
|
|
154
|
+
|
|
155
|
+
Found invalid keys!
|
|
156
|
+
┌───────────────────────────────────────────┬─────────────────────┐
|
|
157
|
+
│ file │ key │
|
|
158
|
+
├───────────────────────────────────────────┼─────────────────────┤
|
|
159
|
+
│ translations/messageExamples/de-de.json │ multipleVariables │
|
|
160
|
+
└───────────────────────────────────────────┴─────────────────────┘
|
|
161
|
+
|
|
162
|
+
`);
|
|
163
|
+
done();
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
it("should return the missing/invalid keys for all files with source matching folder and source matching file", (done) => {
|
|
167
|
+
(0, child_process_1.exec)("node dist/bin/index.js -l translations/multipleFilesFolderExample translations/flattenExamples -s en-US", (_error, stdout, _stderr) => {
|
|
168
|
+
const result = stdout.split("Done")[0];
|
|
169
|
+
expect(result).toEqual(`i18n translations checker
|
|
170
|
+
Source: en-US
|
|
171
|
+
|
|
172
|
+
Found missing keys!
|
|
173
|
+
┌──────────────────────────────────────────────────────────┬────────────────────────────────┐
|
|
174
|
+
│ file │ key │
|
|
175
|
+
├──────────────────────────────────────────────────────────┼────────────────────────────────┤
|
|
176
|
+
│ translations/flattenExamples/de-de.json │ other.nested.three │
|
|
177
|
+
│ translations/flattenExamples/de-de.json │ other.nested.deep.more.final │
|
|
178
|
+
│ translations/multipleFilesFolderExample/de-DE/one.json │ message.text-format │
|
|
179
|
+
│ translations/multipleFilesFolderExample/de-DE/two.json │ test.drive.four │
|
|
180
|
+
└──────────────────────────────────────────────────────────┴────────────────────────────────┘
|
|
181
|
+
|
|
182
|
+
Found invalid keys!
|
|
183
|
+
┌────────────────────────────────────────────────────────────┬─────────────────────┐
|
|
184
|
+
│ file │ key │
|
|
185
|
+
├────────────────────────────────────────────────────────────┼─────────────────────┤
|
|
186
|
+
│ translations/multipleFilesFolderExample/de-DE/one.json │ message.select │
|
|
187
|
+
│ translations/multipleFilesFolderExample/de-DE/three.json │ multipleVariables │
|
|
188
|
+
└────────────────────────────────────────────────────────────┴─────────────────────┘
|
|
189
|
+
|
|
190
|
+
`);
|
|
191
|
+
done();
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
it("should ignore the excluded file", (done) => {
|
|
195
|
+
(0, child_process_1.exec)("node dist/bin/index.js --source en-US --locales translations/flattenExamples translations/messageExamples --exclude translations/flattenExamples/de-de.json", (_error, stdout, _stderr) => {
|
|
196
|
+
const result = stdout.split("Done")[0];
|
|
197
|
+
expect(result).toEqual(`i18n translations checker
|
|
198
|
+
Source: en-US
|
|
199
|
+
|
|
200
|
+
Found missing keys!
|
|
201
|
+
┌───────────────────────────────────────────┬────────────┐
|
|
202
|
+
│ file │ key │
|
|
203
|
+
├───────────────────────────────────────────┼────────────┤
|
|
204
|
+
│ translations/messageExamples/de-de.json │ richText │
|
|
205
|
+
│ translations/messageExamples/de-de.json │ yo │
|
|
206
|
+
│ translations/messageExamples/de-de.json │ nesting1 │
|
|
207
|
+
│ translations/messageExamples/de-de.json │ nesting2 │
|
|
208
|
+
│ translations/messageExamples/de-de.json │ nesting3 │
|
|
209
|
+
│ translations/messageExamples/de-de.json │ key1 │
|
|
210
|
+
└───────────────────────────────────────────┴────────────┘
|
|
211
|
+
|
|
212
|
+
Found invalid keys!
|
|
213
|
+
┌───────────────────────────────────────────┬─────────────────────┐
|
|
214
|
+
│ file │ key │
|
|
215
|
+
├───────────────────────────────────────────┼─────────────────────┤
|
|
216
|
+
│ translations/messageExamples/de-de.json │ multipleVariables │
|
|
217
|
+
└───────────────────────────────────────────┴─────────────────────┘
|
|
218
|
+
|
|
219
|
+
`);
|
|
220
|
+
done();
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
it("should ignore the excluded folder", (done) => {
|
|
224
|
+
(0, child_process_1.exec)("node dist/bin/index.js --source en-US --locales translations/flattenExamples translations/messageExamples --exclude translations/flattenExamples/*", (_error, stdout, _stderr) => {
|
|
225
|
+
const result = stdout.split("Done")[0];
|
|
226
|
+
expect(result).toEqual(`i18n translations checker
|
|
227
|
+
Source: en-US
|
|
228
|
+
|
|
229
|
+
Found missing keys!
|
|
230
|
+
┌───────────────────────────────────────────┬────────────┐
|
|
231
|
+
│ file │ key │
|
|
232
|
+
├───────────────────────────────────────────┼────────────┤
|
|
233
|
+
│ translations/messageExamples/de-de.json │ richText │
|
|
234
|
+
│ translations/messageExamples/de-de.json │ yo │
|
|
235
|
+
│ translations/messageExamples/de-de.json │ nesting1 │
|
|
236
|
+
│ translations/messageExamples/de-de.json │ nesting2 │
|
|
237
|
+
│ translations/messageExamples/de-de.json │ nesting3 │
|
|
238
|
+
│ translations/messageExamples/de-de.json │ key1 │
|
|
239
|
+
└───────────────────────────────────────────┴────────────┘
|
|
240
|
+
|
|
241
|
+
Found invalid keys!
|
|
242
|
+
┌───────────────────────────────────────────┬─────────────────────┐
|
|
243
|
+
│ file │ key │
|
|
244
|
+
├───────────────────────────────────────────┼─────────────────────┤
|
|
245
|
+
│ translations/messageExamples/de-de.json │ multipleVariables │
|
|
246
|
+
└───────────────────────────────────────────┴─────────────────────┘
|
|
247
|
+
|
|
248
|
+
`);
|
|
249
|
+
done();
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
it("should ignore the excluded multiple files", (done) => {
|
|
253
|
+
(0, child_process_1.exec)("node dist/bin/index.js --source en-US --locales translations/flattenExamples translations/messageExamples --exclude translations/flattenExamples/de-de.json translations/messageExamples/de-de.json", (_error, stdout, _stderr) => {
|
|
254
|
+
const result = stdout.split("Done")[0];
|
|
255
|
+
expect(result).toEqual(`i18n translations checker
|
|
256
|
+
Source: en-US
|
|
257
|
+
|
|
258
|
+
No missing keys found!
|
|
259
|
+
|
|
260
|
+
No invalid translations found!
|
|
261
|
+
|
|
262
|
+
`);
|
|
263
|
+
done();
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
});
|
package/dist/errorReporters.d.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
export type Context = "missingKeys" | "invalidKeys";
|
|
2
|
-
export type ErrorReporter = (result: string[], context: Context) => void;
|
|
3
2
|
export declare const contextMapping: Record<Context, string>;
|
|
4
|
-
export declare const standardReporter:
|
|
5
|
-
|
|
3
|
+
export declare const standardReporter: (result: {
|
|
4
|
+
file: string;
|
|
5
|
+
key: string;
|
|
6
|
+
}[]) => string;
|
|
7
|
+
export declare const summaryReporter: (result: {
|
|
8
|
+
file: string;
|
|
9
|
+
total: number;
|
|
10
|
+
}[]) => string;
|
|
11
|
+
export declare const createTable: (input: unknown[]) => string;
|
package/dist/errorReporters.js
CHANGED
|
@@ -1,16 +1,40 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.summaryReporter = exports.standardReporter = exports.contextMapping = void 0;
|
|
3
|
+
exports.createTable = exports.summaryReporter = exports.standardReporter = exports.contextMapping = void 0;
|
|
4
|
+
const console_1 = require("console");
|
|
5
|
+
const stream_1 = require("stream");
|
|
4
6
|
exports.contextMapping = {
|
|
5
7
|
invalidKeys: "invalid",
|
|
6
8
|
missingKeys: "missing",
|
|
7
9
|
};
|
|
8
10
|
const standardReporter = (result) => {
|
|
9
|
-
return result.map((key) =>
|
|
11
|
+
return (0, exports.createTable)(result.map(({ file, key }) => ({ file, key })));
|
|
10
12
|
};
|
|
11
13
|
exports.standardReporter = standardReporter;
|
|
12
|
-
const summaryReporter = (result
|
|
13
|
-
|
|
14
|
-
return `Found ${count} ${exports.contextMapping[context]} ${count === 1 ? "key" : "keys"}.`;
|
|
14
|
+
const summaryReporter = (result) => {
|
|
15
|
+
return (0, exports.createTable)(result.map(({ file, total }) => ({ file, total })));
|
|
15
16
|
};
|
|
16
17
|
exports.summaryReporter = summaryReporter;
|
|
18
|
+
const createTable = (input) => {
|
|
19
|
+
// https://stackoverflow.com/a/67859384
|
|
20
|
+
const ts = new stream_1.Transform({
|
|
21
|
+
transform(chunk, enc, cb) {
|
|
22
|
+
cb(null, chunk);
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
const logger = new console_1.Console({ stdout: ts });
|
|
26
|
+
logger.table(input);
|
|
27
|
+
const table = (ts.read() || "").toString();
|
|
28
|
+
// https://stackoverflow.com/a/69874540
|
|
29
|
+
let output = "";
|
|
30
|
+
for (let line of table.split(/[\r\n]+/)) {
|
|
31
|
+
output += `${line
|
|
32
|
+
.replace(/[^┬]*┬/, "┌")
|
|
33
|
+
.replace(/^├─*┼/, "├")
|
|
34
|
+
.replace(/│[^│]*/, "")
|
|
35
|
+
.replace(/^└─*┴/, "└")
|
|
36
|
+
.replace(/'/g, " ")}\n`;
|
|
37
|
+
}
|
|
38
|
+
return output.replace(/\n\n$/, "");
|
|
39
|
+
};
|
|
40
|
+
exports.createTable = createTable;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { CheckResult, Translation, TranslationFile } from "./types";
|
|
2
2
|
import { Context } from "./errorReporters";
|
|
3
3
|
export type Options = {
|
|
4
|
-
format?: "icu" | "i18next";
|
|
4
|
+
format?: "icu" | "i18next" | "react-intl" | "react-i18next";
|
|
5
5
|
checks?: Context[];
|
|
6
6
|
};
|
|
7
7
|
export declare const checkInvalidTranslations: (source: Translation, targets: Record<string, Translation>, options?: Options) => CheckResult;
|
|
@@ -10,3 +10,4 @@ export declare const checkTranslations: (source: TranslationFile[], targets: Tra
|
|
|
10
10
|
missingKeys: CheckResult | undefined;
|
|
11
11
|
invalidKeys: CheckResult | undefined;
|
|
12
12
|
};
|
|
13
|
+
export declare const checkUnusedKeys: (source: TranslationFile[], codebaseSrc: string, options?: Options) => Promise<CheckResult | undefined>;
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
2
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.checkTranslations = exports.checkMissingTranslations = exports.checkInvalidTranslations = void 0;
|
|
12
|
+
exports.checkUnusedKeys = exports.checkTranslations = exports.checkMissingTranslations = exports.checkInvalidTranslations = void 0;
|
|
4
13
|
const findMissingKeys_1 = require("./utils/findMissingKeys");
|
|
5
14
|
const findInvalidTranslations_1 = require("./utils/findInvalidTranslations");
|
|
6
15
|
const findInvalidi18nTranslations_1 = require("./utils/findInvalidi18nTranslations");
|
|
16
|
+
const glob_1 = require("glob");
|
|
17
|
+
const cli_lib_1 = require("@formatjs/cli-lib");
|
|
7
18
|
const checkInvalidTranslations = (source, targets, options = { format: "icu" }) => {
|
|
8
19
|
return options.format === "i18next"
|
|
9
20
|
? (0, findInvalidi18nTranslations_1.findInvalid18nTranslations)(source, targets)
|
|
@@ -39,3 +50,34 @@ const checkTranslations = (source, targets, options = { format: "icu", checks: [
|
|
|
39
50
|
};
|
|
40
51
|
};
|
|
41
52
|
exports.checkTranslations = checkTranslations;
|
|
53
|
+
const checkUnusedKeys = (source_1, codebaseSrc_1, ...args_1) => __awaiter(void 0, [source_1, codebaseSrc_1, ...args_1], void 0, function* (source, codebaseSrc, options = {
|
|
54
|
+
format: "react-intl",
|
|
55
|
+
}) {
|
|
56
|
+
if (!options.format || !["react-intl", "i18next"].includes(options.format)) {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
return options.format === "react-intl"
|
|
60
|
+
? findUnusedReactIntlTranslations(source, codebaseSrc)
|
|
61
|
+
: undefined;
|
|
62
|
+
});
|
|
63
|
+
exports.checkUnusedKeys = checkUnusedKeys;
|
|
64
|
+
const findUnusedReactIntlTranslations = (source, codebaseSrc) => __awaiter(void 0, void 0, void 0, function* () {
|
|
65
|
+
let unusedKeys = {};
|
|
66
|
+
// find any unused keys in a react-intl code base
|
|
67
|
+
const unsuedKeysFiles = (0, glob_1.globSync)(codebaseSrc, {
|
|
68
|
+
ignore: ["node_modules/**"],
|
|
69
|
+
});
|
|
70
|
+
const extracted = yield (0, cli_lib_1.extract)(unsuedKeysFiles, {});
|
|
71
|
+
const extractedResultSet = new Set(Object.keys(JSON.parse(extracted)));
|
|
72
|
+
source.forEach(({ name, content }) => {
|
|
73
|
+
const keysInSource = Object.keys(content);
|
|
74
|
+
const found = [];
|
|
75
|
+
for (const keyInSource of keysInSource) {
|
|
76
|
+
if (!extractedResultSet.has(keyInSource)) {
|
|
77
|
+
found.push(keyInSource);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
Object.assign(unusedKeys, { [name]: found });
|
|
81
|
+
});
|
|
82
|
+
return unusedKeys;
|
|
83
|
+
});
|
|
@@ -65,9 +65,6 @@ const hasDiff = (a, b) => {
|
|
|
65
65
|
if (formatElementA.type !== formatElementB.type) {
|
|
66
66
|
return true;
|
|
67
67
|
}
|
|
68
|
-
if (formatElementA.type === "text" && formatElementB.type === "text") {
|
|
69
|
-
return false;
|
|
70
|
-
}
|
|
71
68
|
if (formatElementA.type === "tag" && formatElementB.type === "tag") {
|
|
72
69
|
return (formatElementA.raw !== formatElementB.raw ||
|
|
73
70
|
formatElementA.voidElement !== formatElementB.voidElement);
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lingual/i18n-check",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "i18n translation messages check",
|
|
4
5
|
"license": "MIT",
|
|
5
6
|
"main": "dist/index.js",
|
|
6
7
|
"module": "dist/index.js",
|
|
@@ -10,22 +11,29 @@
|
|
|
10
11
|
"types": "dist/index.d.ts",
|
|
11
12
|
"scripts": {
|
|
12
13
|
"build": "tsc",
|
|
13
|
-
"test": "jest"
|
|
14
|
+
"test": "jest src/ --testPathIgnorePatterns src/bin/*",
|
|
15
|
+
"test:cli": "tsc && jest src/bin/index.test.ts"
|
|
14
16
|
},
|
|
15
17
|
"files": [
|
|
16
18
|
"dist/"
|
|
17
19
|
],
|
|
18
20
|
"dependencies": {
|
|
19
|
-
"@formatjs/
|
|
21
|
+
"@formatjs/cli-lib": "^6.4.2",
|
|
22
|
+
"@formatjs/icu-messageformat-parser": "^2.7.8",
|
|
20
23
|
"chalk": "^4.1.2",
|
|
21
|
-
"commander": "^
|
|
22
|
-
"glob": "^
|
|
23
|
-
"typescript": "^5.
|
|
24
|
+
"commander": "^12.1.0",
|
|
25
|
+
"glob": "^11.0.0",
|
|
26
|
+
"typescript": "^5.6.2"
|
|
24
27
|
},
|
|
25
28
|
"devDependencies": {
|
|
26
|
-
"@types/jest": "^29.5.
|
|
27
|
-
"@types/node": "^
|
|
29
|
+
"@types/jest": "^29.5.13",
|
|
30
|
+
"@types/node": "^22.5.5",
|
|
31
|
+
"braces": "^3.0.3",
|
|
28
32
|
"jest": "^29.7.0",
|
|
29
|
-
"ts-jest": "^29.
|
|
33
|
+
"ts-jest": "^29.2.5"
|
|
34
|
+
},
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "https://github.com/lingualdev/i18n-check.git"
|
|
30
38
|
}
|
|
31
39
|
}
|