@lingual/i18n-check 0.1.5 → 0.2.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 +28 -3
- package/dist/bin/index.js +62 -13
- 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/package.json +8 -1
package/README.md
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
# Lingual i18n
|
|
1
|
+
# Lingual i18n-check
|
|
2
|
+
|
|
3
|
+
**i18n-check** will help with validating your translation files, including
|
|
4
|
+
checking for missing and/or invalid/broken translations.
|
|
5
|
+
It will compare the defined source language with all target translation files and try to find inconsistencies between source and target files.
|
|
6
|
+
You can run these checks as a pre-commit hook or on the CI depending on your use-case and setup.
|
|
7
|
+
|
|
8
|
+

|
|
9
|
+
|
|
10
|
+

|
|
2
11
|
|
|
3
12
|
## Installation and Usage
|
|
4
13
|
|
|
@@ -66,6 +75,8 @@ Additionally the `i18next` format is supported and can be set via the `-f` or `-
|
|
|
66
75
|
|
|
67
76
|
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.
|
|
68
77
|
|
|
78
|
+
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.
|
|
79
|
+
|
|
69
80
|
```bash
|
|
70
81
|
yarn i18n:check -t translations/i18NextMessageExamples -s translations/i18NextMessageExamples/en-us.json -f i18next
|
|
71
82
|
```
|
|
@@ -94,6 +105,16 @@ Check for missing an invalid keys (which is the default):
|
|
|
94
105
|
yarn i18n:check -t translations/messageExamples -s translations/messageExamples/en-us.json -c missingKeys,invalidKeys
|
|
95
106
|
```
|
|
96
107
|
|
|
108
|
+
### --unused
|
|
109
|
+
|
|
110
|
+
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 primay target language.
|
|
111
|
+
|
|
112
|
+
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.
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
yarn i18n:check -t translations/messageExamples -s translations/messageExamples/en-us.json -u client/ -f react-intl
|
|
116
|
+
```
|
|
117
|
+
|
|
97
118
|
### --reporter
|
|
98
119
|
|
|
99
120
|
The standard reporting prints out all the missing or invalid keys.
|
|
@@ -146,7 +167,7 @@ If all the locales are organized in a **single folder**:
|
|
|
146
167
|
- en-en.json
|
|
147
168
|
- de-de.json
|
|
148
169
|
|
|
149
|
-
Use the `
|
|
170
|
+
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
171
|
|
|
151
172
|
```bash
|
|
152
173
|
yarn i18n:check -t locales -s locales/en-us.json
|
|
@@ -216,7 +237,7 @@ If the locales are **organised as folders** containing multiple json files:
|
|
|
216
237
|
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.
|
|
217
238
|
|
|
218
239
|
```bash
|
|
219
|
-
yarn i18n:check -t dirOne,dirTwo -s dirOne/en/,dirTwo/
|
|
240
|
+
yarn i18n:check -t dirOne,dirTwo -s dirOne/en/,dirTwo/en
|
|
220
241
|
```
|
|
221
242
|
|
|
222
243
|
## As Github Action
|
|
@@ -400,3 +421,7 @@ To run the tests use the following command:
|
|
|
400
421
|
```bash
|
|
401
422
|
pnpm run test // yarn test or npm test
|
|
402
423
|
```
|
|
424
|
+
|
|
425
|
+
## Links
|
|
426
|
+
|
|
427
|
+
[Twitter](https://twitter.com/lingualdev)
|
package/dist/bin/index.js
CHANGED
|
@@ -29,6 +29,7 @@ commander_1.program
|
|
|
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
31
|
.option("-e, --exclude [file(s), folder(s)]", "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");
|
|
@@ -66,6 +67,7 @@ const main = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
|
66
67
|
const targetPath = commander_1.program.getOptionValue("target");
|
|
67
68
|
const format = commander_1.program.getOptionValue("format");
|
|
68
69
|
const exclude = commander_1.program.getOptionValue("exclude");
|
|
70
|
+
const unusedSrcPath = commander_1.program.getOptionValue("unused");
|
|
69
71
|
if (!srcPath) {
|
|
70
72
|
console.log(chalk_1.default.red("Source file(s) not found. Please provide valid source file(s), i.e. -s translations/en-us.json"));
|
|
71
73
|
(0, process_1.exit)(1);
|
|
@@ -86,8 +88,8 @@ const main = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
|
86
88
|
const files = yield (0, glob_1.glob)(pattern, {
|
|
87
89
|
ignore: ["node_modules/**"].concat(excludedPaths),
|
|
88
90
|
});
|
|
89
|
-
console.log(
|
|
90
|
-
console.log(chalk_1.default.
|
|
91
|
+
console.log("i18n translations checker");
|
|
92
|
+
console.log(chalk_1.default.gray(`Source file(s): ${srcPath}`));
|
|
91
93
|
if (format) {
|
|
92
94
|
console.log(chalk_1.default.blackBright(`Selected format is: ${format}`));
|
|
93
95
|
}
|
|
@@ -127,7 +129,11 @@ const main = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
|
127
129
|
}
|
|
128
130
|
try {
|
|
129
131
|
const result = (0, __1.checkTranslations)(srcFiles, targetFiles, options);
|
|
130
|
-
|
|
132
|
+
printTranslationResult(result);
|
|
133
|
+
if (unusedSrcPath) {
|
|
134
|
+
const unusedKeys = yield (0, __1.checkUnusedKeys)(srcFiles, unusedSrcPath, options);
|
|
135
|
+
printUnusedKeysResult({ unusedKeys });
|
|
136
|
+
}
|
|
131
137
|
const end = performance.now();
|
|
132
138
|
console.log(chalk_1.default.green(`\nDone in ${Math.round(((end - start) * 100) / 1000) / 100}s.`));
|
|
133
139
|
if ((result.missingKeys && Object.keys(result.missingKeys).length > 0) ||
|
|
@@ -143,28 +149,71 @@ const main = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
|
143
149
|
(0, process_1.exit)(1);
|
|
144
150
|
}
|
|
145
151
|
});
|
|
146
|
-
const
|
|
152
|
+
const printTranslationResult = ({ missingKeys, invalidKeys, }) => {
|
|
147
153
|
const reporter = commander_1.program.getOptionValue("reporter");
|
|
148
|
-
const
|
|
154
|
+
const isSummary = reporter === "summary";
|
|
149
155
|
if (missingKeys && Object.keys(missingKeys).length > 0) {
|
|
150
|
-
console.log(chalk_1.default.
|
|
151
|
-
|
|
152
|
-
console.log(chalk_1.default.red(
|
|
153
|
-
|
|
156
|
+
console.log(chalk_1.default.red("\nFound missing keys!"));
|
|
157
|
+
if (isSummary) {
|
|
158
|
+
console.log(chalk_1.default.red((0, errorReporters_1.summaryReporter)(getSummaryRows(missingKeys))));
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
console.log(chalk_1.default.red((0, errorReporters_1.standardReporter)(getStandardRows(missingKeys))));
|
|
154
162
|
}
|
|
155
163
|
}
|
|
156
164
|
else if (missingKeys) {
|
|
157
165
|
console.log(chalk_1.default.green("\nNo missing keys found!"));
|
|
158
166
|
}
|
|
159
167
|
if (invalidKeys && Object.keys(invalidKeys).length > 0) {
|
|
160
|
-
console.log(chalk_1.default.
|
|
161
|
-
|
|
162
|
-
console.log(chalk_1.default.red(
|
|
163
|
-
|
|
168
|
+
console.log(chalk_1.default.red("\nFound invalid keys!"));
|
|
169
|
+
if (isSummary) {
|
|
170
|
+
console.log(chalk_1.default.red((0, errorReporters_1.summaryReporter)(getSummaryRows(invalidKeys))));
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
console.log(chalk_1.default.red((0, errorReporters_1.standardReporter)(getStandardRows(invalidKeys))));
|
|
164
174
|
}
|
|
165
175
|
}
|
|
166
176
|
else if (invalidKeys) {
|
|
167
177
|
console.log(chalk_1.default.green("\nNo invalid translations found!"));
|
|
168
178
|
}
|
|
169
179
|
};
|
|
180
|
+
const printUnusedKeysResult = ({ unusedKeys, }) => {
|
|
181
|
+
const reporter = commander_1.program.getOptionValue("reporter");
|
|
182
|
+
const isSummary = reporter === "summary";
|
|
183
|
+
if (unusedKeys && Object.keys(unusedKeys).length > 0) {
|
|
184
|
+
console.log(chalk_1.default.red("\nFound unused keys!"));
|
|
185
|
+
if (isSummary) {
|
|
186
|
+
console.log(chalk_1.default.red((0, errorReporters_1.summaryReporter)(getSummaryRows(unusedKeys))));
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
console.log(chalk_1.default.red((0, errorReporters_1.standardReporter)(getStandardRows(unusedKeys))));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
else if (unusedKeys) {
|
|
193
|
+
console.log(chalk_1.default.green("\nNo unused found!"));
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
const truncate = (chars) => chars.length > 80 ? `${chars.substring(0, 80)}...` : chars;
|
|
197
|
+
const getSummaryRows = (checkResult) => {
|
|
198
|
+
const formattedRows = [];
|
|
199
|
+
for (const [file, keys] of Object.entries(checkResult)) {
|
|
200
|
+
formattedRows.push({
|
|
201
|
+
file: truncate(file),
|
|
202
|
+
total: keys.length,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
return formattedRows;
|
|
206
|
+
};
|
|
207
|
+
const getStandardRows = (checkResult) => {
|
|
208
|
+
const formattedRows = [];
|
|
209
|
+
for (const [file, keys] of Object.entries(checkResult)) {
|
|
210
|
+
for (const key of keys) {
|
|
211
|
+
formattedRows.push({
|
|
212
|
+
file: truncate(file),
|
|
213
|
+
key: truncate(key),
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return formattedRows;
|
|
218
|
+
};
|
|
170
219
|
main();
|
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;
|
|
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, codebaseSrc, options = {
|
|
54
|
+
format: "react-intl",
|
|
55
|
+
}) => __awaiter(void 0, void 0, void 0, function* () {
|
|
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
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lingual/i18n-check",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "i18n translation messages check",
|
|
4
5
|
"license": "MIT",
|
|
5
6
|
"main": "dist/index.js",
|
|
6
7
|
"module": "dist/index.js",
|
|
@@ -16,6 +17,7 @@
|
|
|
16
17
|
"dist/"
|
|
17
18
|
],
|
|
18
19
|
"dependencies": {
|
|
20
|
+
"@formatjs/cli-lib": "^6.4.2",
|
|
19
21
|
"@formatjs/icu-messageformat-parser": "^2.7.6",
|
|
20
22
|
"chalk": "^4.1.2",
|
|
21
23
|
"commander": "^11.0.0",
|
|
@@ -25,7 +27,12 @@
|
|
|
25
27
|
"devDependencies": {
|
|
26
28
|
"@types/jest": "^29.5.12",
|
|
27
29
|
"@types/node": "^20.12.12",
|
|
30
|
+
"braces": ">=3.0.3",
|
|
28
31
|
"jest": "^29.7.0",
|
|
29
32
|
"ts-jest": "^29.1.2"
|
|
33
|
+
},
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "https://github.com/lingualdev/i18n-check.git"
|
|
30
37
|
}
|
|
31
38
|
}
|