@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 CHANGED
@@ -1,6 +1,28 @@
1
- # Lingual i18n Check
1
+ # Lingual i18n-check
2
2
 
3
- ## Installation and Usage
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
+ ![example 1](./assets/i18n-check-screenshot-full.png)
8
+
9
+ ![example 2](./assets/i18n-check-screenshot-summary.png)
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, `i18n-check` can either be accessed via defining a command in the `package.json` file or directly in the CLI.
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 the your `package.json` and add a new command:
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
- ### --target
84
+ ### --locales, -l
45
85
 
46
- With the `-t` or `--target` 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).
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 -t translations/messageExamples -s translations/messageExamples/en-us.json
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 which file(s) or folder(s) you want to use as the source 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 defined source file(s).
56
- Check the [example](#examples) to see how different locale translation files are organised and how they can be addressed.
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 -t translations/messageExamples -s translations/messageExamples/en-us.json
99
+ yarn i18n:check --locales translations/messageExamples -s en-US
60
100
  ```
61
101
 
62
- ### --format
102
+ ### --format, -f
63
103
 
64
- By default `i18n-check` will validate against any [`icu`](https://github.com/unicode-org/icu) compliant translations.
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 `icu` and therefore can not be validated against currently. On a side-note: there might be future support for more specific formats.
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 -t translations/i18NextMessageExamples -s translations/i18NextMessageExamples/en-us.json -f i18next
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 the `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.
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 -t translations/messageExamples -s translations/messageExamples/en-us.json -c missingKeys
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 -t translations/messageExamples -s translations/messageExamples/en-us.json -c invalidKeys
130
+ yarn i18n:check --locales translations/messageExamples -s en-US -c invalidKeys
89
131
  ```
90
132
 
91
- Check for missing an invalid keys (which is the default):
133
+ Check for missing and invalid keys (which is the default):
92
134
 
93
135
  ```bash
94
- yarn i18n:check -t translations/messageExamples -s translations/messageExamples/en-us.json -c missingKeys,invalidKeys
136
+ yarn i18n:check --locales translations/messageExamples -s en-US -c missingKeys,invalidKeys
95
137
  ```
96
138
 
97
- ### --reporter
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 -t translations/messageExamples -s translations/messageExamples/en-us.json -r summary
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 a comma separated string of files and/or folders.
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 -t translations/messageExamples -s translations/messageExamples/en-us.json -e translations/messageExamples/fr-fr.json
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, provide a comma-separated list:
168
+ To exclude multiple files provide all files:
117
169
 
118
170
  ```bash
119
- yarn i18n:check -t translations/messageExamples -s translations/messageExamples/en-us.json -e translations/messageExamples/fr-fr.json,translations/messageExamples/de-at.json
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 -t translations/folderExamples -s translations/folderExamples/en -e translations/folderExamples/fr/*
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 a comma-separated list of folders to be excluded:
180
+ Alternatively you can exclude multiple folders by providing the folders to be excluded:
129
181
 
130
182
  ```bash
131
- yarn i18n:check -t translations/folderExamples -s translations/folderExamples/en -e translations/folderExamples/fr/*,translations/folderExample/it/*
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 and can be defined as a comma-separated list, i.e.
135
- `-e translations/folderExamples/fr/*,translations/messageExamples/it.json`
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
- `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.
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
- #### Single folder
193
+ ### Single folder
142
194
 
143
195
  If all the locales are organized in a **single folder**:
144
196
 
145
- - locales/
146
- - en-en.json
147
- - de-de.json
197
+ ```
198
+ locales/
199
+ en-en.json
200
+ de-de.json
201
+ ```
148
202
 
149
- Use the `d` or `dir` 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.
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 -t locales -s locales/en-us.json
206
+ yarn i18n:check --locales locales -s locales/en-us.json
153
207
  ```
154
208
 
155
- #### Folder per locale
209
+ ### Folder per locale
156
210
 
157
211
  If the locales are **organised as folders** containing a single json file:
158
212
 
159
- - locales/
160
- - en-US/
161
- - index.json
162
- - de-DE/
163
- - index.json
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 -t locales -s locales/en-US/index.json
224
+ yarn i18n:check --locales locales -s en-US
169
225
  ```
170
226
 
171
- #### Folder per locale with multiple files
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
- - locales/
176
- - en-US/
177
- - one.json
178
- - two.json
179
- - three.json
180
- - de-DE/
181
- - one.json
182
- - two.json
183
- - three.json
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. `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.
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 -t locales -s locales/en-US/
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
- - dirOne
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
- - dirTwo
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 `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.
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 -t dirOne,dirTwo -s dirOne/en/,dirTwo/de
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 `i18n-check` already installed, you can define your own **YAML** file. The following example can be starting point that you can adapt to your current setup:
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 -t translations/messageExamples -s translations/messageExamples/en-us.json
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
+ ![i18n-check Github workflow example out](./assets/i18n-check-workflow-example.png)
316
+
253
317
  ## API
254
318
 
255
- Aside from using the CLI, `i18n-check` also exposes a set of check functions that can be accessed programmatically.
256
- Start by importing `i18n-check`:
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 -t translations/messageExamples -s translations/messageExamples/en-us.json
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 -t translations/flattenExamples -s translations/flattenExamples/en-us.json
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 -t translations/i18NextMessageExamples -s translations/i18NextMessageExamples/en-us.json -f i18next
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 -t translations/folderExample -s translations/folderExample/en-US/
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 -t translations/multipleFilesFolderExample/ -s translations/multipleFilesFolderExample/en-US/
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 -t translations/folderExample,translations/messageExamples -s translations/folderExample/en-US/,translations/messageExamples/en-us.json
457
+ node dist/bin/index.js --locales translations/folderExample,translations/messageExamples -s en-US
394
458
  ```
395
459
 
396
- ## Test
460
+ ### Tests
397
461
 
398
- To run the tests use the following command:
462
+ To run the tests use one of the following commands:
463
+
464
+ ```bash
465
+ pnpm test
466
+ ```
399
467
 
400
468
  ```bash
401
- pnpm run test // yarn test or npm test
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.1.0")
26
- .option("-t, --target [directory]", "name of the directory containing the JSON files to validate")
27
- .option("-s, --source [source file(s)]", "path to the reference file(s)")
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 [file(s), folder(s)]", "define the file(s) and/or folders(s) that should be excluded from the check")
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 = (sourcePaths, fileName) => {
44
- return sourcePaths.find((basePathName) => {
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 targetPath = commander_1.program.getOptionValue("target");
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 file(s) not found. Please provide valid source file(s), i.e. -s translations/en-us.json"));
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 (!targetPath) {
74
- console.log(chalk_1.default.red("Target file(s) not found. Please provide valid target file(s), i.e. -t translations/"));
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 ? toArray(exclude) : [];
78
- const targetPathFolders = toArray(targetPath);
79
- const srcPaths = toArray(srcPath);
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 targetFiles = [];
66
+ let localeFiles = [];
83
67
  const pattern = isMultiFolders
84
- ? `{${targetPath.trim()}}/**/*.json`
85
- : `${targetPath.trim()}/**/*.json`;
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(chalk_1.default.blue("i18n translations checker"));
90
- console.log(chalk_1.default.blackBright(`Source file(s): ${srcPath}`));
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
- files.forEach((file) => {
99
- const content = JSON.parse(fs_1.default.readFileSync(file, "utf-8"));
100
- const sourcePath = getSourcePath(srcPaths, file);
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: (0, flattenTranslations_1.flattenTranslations)(content),
101
+ content,
106
102
  });
107
103
  }
108
104
  else {
109
- const targetPath = getTargetPath(targetPathFolders, srcPaths, file);
110
- const reference = (targetPath === null || targetPath === void 0 ? void 0 : targetPath.includes(".json"))
111
- ? targetPath
112
- : `${targetPath}${file.split("/").pop()}`;
113
- targetFiles.push({
114
- reference,
115
- name: file,
116
- content: (0, flattenTranslations_1.flattenTranslations)(content),
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 file(s) not found. Please provide valid source file(s), i.e. -s translations/en-us.json"));
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 (targetFiles.length === 0) {
125
- console.log(chalk_1.default.red("Target file(s) not found. Please provide valid target file(s), i.e. -t translations/"));
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, targetFiles, options);
130
- print(result);
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 print = ({ missingKeys, invalidKeys, }) => {
173
+ const printTranslationResult = ({ missingKeys, invalidKeys, }) => {
147
174
  const reporter = commander_1.program.getOptionValue("reporter");
148
- const errorReporter = reporter === "summary" ? errorReporters_1.summaryReporter : errorReporters_1.standardReporter;
175
+ const isSummary = reporter === "summary";
149
176
  if (missingKeys && Object.keys(missingKeys).length > 0) {
150
- console.log(chalk_1.default.bgRed(chalk_1.default.white("\nFound missing keys!")));
151
- for (const [lang, missingMessageKeys] of Object.entries(missingKeys)) {
152
- console.log(chalk_1.default.red(`\nIn ${lang}:\n`));
153
- console.log(chalk_1.default.red(errorReporter(missingMessageKeys, "missingKeys")));
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.bgRed(chalk_1.default.white("\nFound invalid keys!")));
161
- for (const [lang, invalidMessageKeys] of Object.entries(invalidKeys)) {
162
- console.log(chalk_1.default.red(`\nIn ${lang}:\n`));
163
- console.log(chalk_1.default.red(errorReporter(invalidMessageKeys, "invalidKeys")));
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
+ });
@@ -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: ErrorReporter;
5
- export declare const summaryReporter: ErrorReporter;
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;
@@ -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) => `◯ ${key}`).join("\n");
11
+ return (0, exports.createTable)(result.map(({ file, key }) => ({ file, key })));
10
12
  };
11
13
  exports.standardReporter = standardReporter;
12
- const summaryReporter = (result, context) => {
13
- const count = result.length;
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.1.5",
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/icu-messageformat-parser": "^2.7.6",
21
+ "@formatjs/cli-lib": "^6.4.2",
22
+ "@formatjs/icu-messageformat-parser": "^2.7.8",
20
23
  "chalk": "^4.1.2",
21
- "commander": "^11.0.0",
22
- "glob": "^10.3.10",
23
- "typescript": "^5.2.2"
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.12",
27
- "@types/node": "^20.12.12",
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.1.2"
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
  }