@lingual/i18n-check 0.5.5 → 0.6.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 +3 -1
- package/dist/bin/index.js +27 -4
- package/dist/bin/index.test.js +38 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +88 -43
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -138,7 +138,9 @@ yarn i18n:check --locales translations/messageExamples -s en-US -o missingKeys i
|
|
|
138
138
|
|
|
139
139
|
### --unused, -u
|
|
140
140
|
|
|
141
|
-
This feature is currently only supported for `react-intl` and `i18next` based React applications and is useful when you need to know which keys exist in your translation files but not in your codebase.
|
|
141
|
+
This feature is currently only supported for `react-intl` and `i18next` based React applications and is useful when you need to know which keys exist in your translation files but not in your codebase. Additionally an inverse check is run to find any keys that exist in the codebase but not in the translation files.
|
|
142
|
+
|
|
143
|
+
Via the `-u` or `--unused` option you provide a source path to the code, which will be parsed to find all unused as well as undefined keys in the primary target language.
|
|
142
144
|
|
|
143
145
|
It is important to note that you must also provide the `-f` or `--format` option with `react-intl` or `i18next` as value. See the [`format` section](#--format) for more information.
|
|
144
146
|
|
package/dist/bin/index.js
CHANGED
|
@@ -32,7 +32,7 @@ commander_1.program
|
|
|
32
32
|
.option("-o, --only <only...>", "define the specific checks you want to run: invalid, missing. By default the check will validate against missing and invalid keys, i.e. --only invalidKeys,missingKeys")
|
|
33
33
|
.option("-r, --reporter <style>", "define the reporting style: standard or summary")
|
|
34
34
|
.option("-e, --exclude <exclude...>", "define the file(s) and/or folders(s) that should be excluded from the check")
|
|
35
|
-
.option("-u, --unused <path>", "define the source path to find all unused keys")
|
|
35
|
+
.option("-u, --unused <path>", "define the source path to find all unused and undefined keys")
|
|
36
36
|
.option("--parser-component-functions <components...>", "a list of component names to parse when using the --unused option")
|
|
37
37
|
.parse();
|
|
38
38
|
const getCheckOptions = () => {
|
|
@@ -165,8 +165,15 @@ const main = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
|
165
165
|
const result = (0, __1.checkTranslations)(srcFiles, localeFiles, options);
|
|
166
166
|
printTranslationResult(result);
|
|
167
167
|
if (unusedSrcPath) {
|
|
168
|
-
const
|
|
168
|
+
const filesToParse = (0, glob_1.globSync)(`${unusedSrcPath}/**/*.{ts,tsx}`, {
|
|
169
|
+
ignore: ["node_modules/**"],
|
|
170
|
+
});
|
|
171
|
+
const unusedKeys = yield (0, __1.checkUnusedKeys)(srcFiles, filesToParse, options, componentFunctions);
|
|
169
172
|
printUnusedKeysResult({ unusedKeys });
|
|
173
|
+
const undefinedKeys = yield (0, __1.checkUndefinedKeys)(srcFiles, filesToParse, options, componentFunctions);
|
|
174
|
+
printUndefinedKeysResult({
|
|
175
|
+
undefinedKeys,
|
|
176
|
+
});
|
|
170
177
|
}
|
|
171
178
|
const end = performance.now();
|
|
172
179
|
console.log(chalk_1.default.green(`\nDone in ${Math.round(((end - start) * 100) / 1000) / 100}s.`));
|
|
@@ -214,7 +221,7 @@ const printTranslationResult = ({ missingKeys, invalidKeys, }) => {
|
|
|
214
221
|
const printUnusedKeysResult = ({ unusedKeys, }) => {
|
|
215
222
|
const reporter = commander_1.program.getOptionValue("reporter");
|
|
216
223
|
const isSummary = reporter === "summary";
|
|
217
|
-
if (unusedKeys &&
|
|
224
|
+
if (unusedKeys && hasKeys(unusedKeys)) {
|
|
218
225
|
console.log(chalk_1.default.red("\nFound unused keys!"));
|
|
219
226
|
if (isSummary) {
|
|
220
227
|
console.log(chalk_1.default.red((0, errorReporters_1.summaryReporter)(getSummaryRows(unusedKeys))));
|
|
@@ -227,6 +234,22 @@ const printUnusedKeysResult = ({ unusedKeys, }) => {
|
|
|
227
234
|
console.log(chalk_1.default.green("\nNo unused keys found!"));
|
|
228
235
|
}
|
|
229
236
|
};
|
|
237
|
+
const printUndefinedKeysResult = ({ undefinedKeys, }) => {
|
|
238
|
+
const reporter = commander_1.program.getOptionValue("reporter");
|
|
239
|
+
const isSummary = reporter === "summary";
|
|
240
|
+
if (undefinedKeys && hasKeys(undefinedKeys)) {
|
|
241
|
+
console.log(chalk_1.default.red("\nFound undefined keys!"));
|
|
242
|
+
if (isSummary) {
|
|
243
|
+
console.log(chalk_1.default.red((0, errorReporters_1.summaryReporter)(getSummaryRows(undefinedKeys))));
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
console.log(chalk_1.default.red((0, errorReporters_1.standardReporter)(getStandardRows(undefinedKeys))));
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
else if (undefinedKeys) {
|
|
250
|
+
console.log(chalk_1.default.green("\nNo undefined keys found!"));
|
|
251
|
+
}
|
|
252
|
+
};
|
|
230
253
|
const truncate = (chars) => chars.length > 80 ? `${chars.substring(0, 80)}...` : chars;
|
|
231
254
|
const getSummaryRows = (checkResult) => {
|
|
232
255
|
const formattedRows = [];
|
|
@@ -250,7 +273,7 @@ const getStandardRows = (checkResult) => {
|
|
|
250
273
|
}
|
|
251
274
|
return formattedRows;
|
|
252
275
|
};
|
|
253
|
-
const
|
|
276
|
+
const hasKeys = (checkResult) => {
|
|
254
277
|
for (const [_, keys] of Object.entries(checkResult)) {
|
|
255
278
|
if (keys.length > 0) {
|
|
256
279
|
return true;
|
package/dist/bin/index.test.js
CHANGED
|
@@ -264,7 +264,7 @@ No invalid translations found!
|
|
|
264
264
|
done();
|
|
265
265
|
});
|
|
266
266
|
});
|
|
267
|
-
it("should find unused keys for react-i18next applications", (done) => {
|
|
267
|
+
it("should find unused and undefined keys for react-i18next applications", (done) => {
|
|
268
268
|
(0, child_process_1.exec)("node dist/bin/index.js --source en --locales translations/codeExamples/reacti18next/locales -f i18next -u translations/codeExamples/reacti18next/src --parser-component-functions WrappedTransComponent", (_error, stdout, _stderr) => {
|
|
269
269
|
const result = stdout.split("Done")[0];
|
|
270
270
|
expect(result).toEqual(`i18n translations checker
|
|
@@ -283,6 +283,43 @@ Found unused keys!
|
|
|
283
283
|
│ translations/codeExamples/reacti18next/locales/en/translation.json │ nonExistentKey │
|
|
284
284
|
└──────────────────────────────────────────────────────────────────────┴──────────────────┘
|
|
285
285
|
|
|
286
|
+
Found undefined keys!
|
|
287
|
+
┌──────────────────────────────────────────────────────┬────────────────────────────────┐
|
|
288
|
+
│ file │ key │
|
|
289
|
+
├──────────────────────────────────────────────────────┼────────────────────────────────┤
|
|
290
|
+
│ translations/codeExamples/reacti18next/src/App.tsx │ some.key.that.is.not.defined │
|
|
291
|
+
└──────────────────────────────────────────────────────┴────────────────────────────────┘
|
|
292
|
+
|
|
293
|
+
`);
|
|
294
|
+
done();
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
it("should find unused and undefined keys for react-intl applications", (done) => {
|
|
298
|
+
(0, child_process_1.exec)(" node dist/bin/index.js --source en-US --locales translations/codeExamples/react-intl/locales -f react-intl -u translations/codeExamples/react-intl/src", (_error, stdout, _stderr) => {
|
|
299
|
+
const result = stdout.split("Done")[0];
|
|
300
|
+
expect(result).toEqual(`i18n translations checker
|
|
301
|
+
Source: en-US
|
|
302
|
+
Selected format is: react-intl
|
|
303
|
+
|
|
304
|
+
No missing keys found!
|
|
305
|
+
|
|
306
|
+
No invalid translations found!
|
|
307
|
+
|
|
308
|
+
Found unused keys!
|
|
309
|
+
┌─────────────────────────────────────────────────────────────────┬─────────────────────────┐
|
|
310
|
+
│ file │ key │
|
|
311
|
+
├─────────────────────────────────────────────────────────────────┼─────────────────────────┤
|
|
312
|
+
│ translations/codeExamples/react-intl/locales/en-US/one.json │ message.number-format │
|
|
313
|
+
│ translations/codeExamples/react-intl/locales/en-US/three.json │ multipleVariables │
|
|
314
|
+
└─────────────────────────────────────────────────────────────────┴─────────────────────────┘
|
|
315
|
+
|
|
316
|
+
Found undefined keys!
|
|
317
|
+
┌────────────────────────────────────────────────────┬────────────────────────────────┐
|
|
318
|
+
│ file │ key │
|
|
319
|
+
├────────────────────────────────────────────────────┼────────────────────────────────┤
|
|
320
|
+
│ translations/codeExamples/react-intl/src/App.tsx │ some.key.that.is.not.defined │
|
|
321
|
+
└────────────────────────────────────────────────────┴────────────────────────────────┘
|
|
322
|
+
|
|
286
323
|
`);
|
|
287
324
|
done();
|
|
288
325
|
});
|
package/dist/index.d.ts
CHANGED
|
@@ -10,4 +10,5 @@ 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: (
|
|
13
|
+
export declare const checkUnusedKeys: (translationFiles: TranslationFile[], filesToParse: string[], options?: Options, componentFunctions?: never[]) => Promise<CheckResult | undefined>;
|
|
14
|
+
export declare const checkUndefinedKeys: (source: TranslationFile[], filesToParse: string[], options?: Options, componentFunctions?: never[]) => Promise<CheckResult | undefined>;
|
package/dist/index.js
CHANGED
|
@@ -12,11 +12,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
12
12
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
13
|
};
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
-
exports.checkUnusedKeys = exports.checkTranslations = exports.checkMissingTranslations = exports.checkInvalidTranslations = void 0;
|
|
15
|
+
exports.checkUndefinedKeys = exports.checkUnusedKeys = exports.checkTranslations = exports.checkMissingTranslations = exports.checkInvalidTranslations = void 0;
|
|
16
16
|
const findMissingKeys_1 = require("./utils/findMissingKeys");
|
|
17
17
|
const findInvalidTranslations_1 = require("./utils/findInvalidTranslations");
|
|
18
18
|
const findInvalidi18nTranslations_1 = require("./utils/findInvalidi18nTranslations");
|
|
19
|
-
const glob_1 = require("glob");
|
|
20
19
|
const cli_lib_1 = require("@formatjs/cli-lib");
|
|
21
20
|
const fs_1 = __importDefault(require("fs"));
|
|
22
21
|
const checkInvalidTranslations = (source, targets, options = { format: "icu" }) => {
|
|
@@ -54,26 +53,22 @@ const checkTranslations = (source, targets, options = { format: "icu", checks: [
|
|
|
54
53
|
};
|
|
55
54
|
};
|
|
56
55
|
exports.checkTranslations = checkTranslations;
|
|
57
|
-
const checkUnusedKeys = (
|
|
56
|
+
const checkUnusedKeys = (translationFiles_1, filesToParse_1, ...args_1) => __awaiter(void 0, [translationFiles_1, filesToParse_1, ...args_1], void 0, function* (translationFiles, filesToParse, options = {
|
|
58
57
|
format: "react-intl",
|
|
59
58
|
}, componentFunctions = []) {
|
|
60
59
|
if (!options.format || !["react-intl", "i18next"].includes(options.format)) {
|
|
61
60
|
return undefined;
|
|
62
61
|
}
|
|
63
62
|
return options.format === "react-intl"
|
|
64
|
-
? findUnusedReactIntlTranslations(
|
|
65
|
-
: findUnusedI18NextTranslations(
|
|
63
|
+
? findUnusedReactIntlTranslations(translationFiles, filesToParse)
|
|
64
|
+
: findUnusedI18NextTranslations(translationFiles, filesToParse, componentFunctions);
|
|
66
65
|
});
|
|
67
66
|
exports.checkUnusedKeys = checkUnusedKeys;
|
|
68
|
-
const findUnusedReactIntlTranslations = (
|
|
67
|
+
const findUnusedReactIntlTranslations = (translationFiles, keysInCode) => __awaiter(void 0, void 0, void 0, function* () {
|
|
69
68
|
let unusedKeys = {};
|
|
70
|
-
|
|
71
|
-
const unusedKeysFiles = (0, glob_1.globSync)(codebaseSrc, {
|
|
72
|
-
ignore: ["node_modules/**"],
|
|
73
|
-
});
|
|
74
|
-
const extracted = yield (0, cli_lib_1.extract)(unusedKeysFiles, {});
|
|
69
|
+
const extracted = yield (0, cli_lib_1.extract)(keysInCode, {});
|
|
75
70
|
const extractedResultSet = new Set(Object.keys(JSON.parse(extracted)));
|
|
76
|
-
|
|
71
|
+
translationFiles.forEach(({ name, content }) => {
|
|
77
72
|
const keysInSource = Object.keys(content);
|
|
78
73
|
const found = [];
|
|
79
74
|
for (const keyInSource of keysInSource) {
|
|
@@ -85,13 +80,85 @@ const findUnusedReactIntlTranslations = (source, codebaseSrc) => __awaiter(void
|
|
|
85
80
|
});
|
|
86
81
|
return unusedKeys;
|
|
87
82
|
});
|
|
88
|
-
const findUnusedI18NextTranslations = (source_1,
|
|
83
|
+
const findUnusedI18NextTranslations = (source_1, filesToParse_1, ...args_1) => __awaiter(void 0, [source_1, filesToParse_1, ...args_1], void 0, function* (source, filesToParse, componentFunctions = []) {
|
|
89
84
|
let unusedKeys = {};
|
|
90
|
-
|
|
91
|
-
const
|
|
92
|
-
|
|
85
|
+
const { extractedResult, skippableKeys } = yield getI18NextKeysInCode(filesToParse, componentFunctions);
|
|
86
|
+
const extractedResultSet = new Set(extractedResult.map(({ key }) => key));
|
|
87
|
+
source.forEach(({ name, content }) => {
|
|
88
|
+
const keysInSource = Object.keys(content);
|
|
89
|
+
const found = [];
|
|
90
|
+
for (const keyInSource of keysInSource) {
|
|
91
|
+
const isSkippable = skippableKeys.find((skippableKey) => {
|
|
92
|
+
return keyInSource.includes(skippableKey);
|
|
93
|
+
});
|
|
94
|
+
if (isSkippable !== undefined) {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (!extractedResultSet.has(keyInSource)) {
|
|
98
|
+
found.push(keyInSource);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
Object.assign(unusedKeys, { [name]: found });
|
|
93
102
|
});
|
|
94
|
-
|
|
103
|
+
return unusedKeys;
|
|
104
|
+
});
|
|
105
|
+
const checkUndefinedKeys = (source_1, filesToParse_1, ...args_1) => __awaiter(void 0, [source_1, filesToParse_1, ...args_1], void 0, function* (source, filesToParse, options = {
|
|
106
|
+
format: "react-intl",
|
|
107
|
+
}, componentFunctions = []) {
|
|
108
|
+
if (!options.format || !["react-intl", "i18next"].includes(options.format)) {
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
return options.format === "react-intl"
|
|
112
|
+
? findUndefinedReactIntlKeys(source, filesToParse)
|
|
113
|
+
: findUndefinedI18NextKeys(source, filesToParse, componentFunctions);
|
|
114
|
+
});
|
|
115
|
+
exports.checkUndefinedKeys = checkUndefinedKeys;
|
|
116
|
+
const findUndefinedReactIntlKeys = (translationFiles, keysInCode) => __awaiter(void 0, void 0, void 0, function* () {
|
|
117
|
+
const sourceKeys = new Set(translationFiles.flatMap(({ content }) => {
|
|
118
|
+
return Object.keys(content);
|
|
119
|
+
}));
|
|
120
|
+
const extractedResult = yield (0, cli_lib_1.extract)(keysInCode, {
|
|
121
|
+
extractSourceLocation: true,
|
|
122
|
+
});
|
|
123
|
+
let undefinedKeys = {};
|
|
124
|
+
Object.entries(JSON.parse(extractedResult)).forEach(([key, meta]) => {
|
|
125
|
+
if (!sourceKeys.has(key)) {
|
|
126
|
+
// @ts-ignore
|
|
127
|
+
const file = meta.file;
|
|
128
|
+
if (!undefinedKeys[file]) {
|
|
129
|
+
undefinedKeys[file] = [];
|
|
130
|
+
}
|
|
131
|
+
undefinedKeys[file].push(key);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
return undefinedKeys;
|
|
135
|
+
});
|
|
136
|
+
const findUndefinedI18NextKeys = (source_1, filesToParse_1, ...args_1) => __awaiter(void 0, [source_1, filesToParse_1, ...args_1], void 0, function* (source, filesToParse, componentFunctions = []) {
|
|
137
|
+
const { extractedResult, skippableKeys } = yield getI18NextKeysInCode(filesToParse, componentFunctions);
|
|
138
|
+
const sourceKeys = new Set(source.flatMap(({ content }) => {
|
|
139
|
+
return Object.keys(content);
|
|
140
|
+
}));
|
|
141
|
+
let undefinedKeys = {};
|
|
142
|
+
extractedResult.forEach(({ file, key }) => {
|
|
143
|
+
const isSkippable = skippableKeys.find((skippableKey) => {
|
|
144
|
+
return key.includes(skippableKey);
|
|
145
|
+
});
|
|
146
|
+
if (isSkippable === undefined && !sourceKeys.has(key)) {
|
|
147
|
+
if (!undefinedKeys[file]) {
|
|
148
|
+
undefinedKeys[file] = [];
|
|
149
|
+
}
|
|
150
|
+
undefinedKeys[file].push(key);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
return undefinedKeys;
|
|
154
|
+
});
|
|
155
|
+
const isRecord = (data) => {
|
|
156
|
+
return (typeof data === "object" &&
|
|
157
|
+
!Array.isArray(data) &&
|
|
158
|
+
data !== null &&
|
|
159
|
+
data !== undefined);
|
|
160
|
+
};
|
|
161
|
+
const getI18NextKeysInCode = (filesToParse_1, ...args_1) => __awaiter(void 0, [filesToParse_1, ...args_1], void 0, function* (filesToParse, componentFunctions = []) {
|
|
95
162
|
// @ts-ignore
|
|
96
163
|
const { transform } = yield import("i18next-parser");
|
|
97
164
|
const i18nextParser = new transform({
|
|
@@ -113,8 +180,9 @@ const findUnusedI18NextTranslations = (source_1, codebaseSrc_1, ...args_1) => __
|
|
|
113
180
|
// Skip any parsed keys that have the `returnObjects` property set to true
|
|
114
181
|
// As these are used dynamically, they will be skipped to prevent
|
|
115
182
|
// these keys from being marked as unused.
|
|
183
|
+
let extractedResult = [];
|
|
116
184
|
const skippableKeys = [];
|
|
117
|
-
|
|
185
|
+
filesToParse.forEach((file) => {
|
|
118
186
|
const rawContent = fs_1.default.readFileSync(file, "utf-8");
|
|
119
187
|
const entries = i18nextParser.parser.parse(rawContent, file);
|
|
120
188
|
// Intermediate solution to retrieve all keys from the parser.
|
|
@@ -126,35 +194,12 @@ const findUnusedI18NextTranslations = (source_1, codebaseSrc_1, ...args_1) => __
|
|
|
126
194
|
skippableKeys.push(entry.key);
|
|
127
195
|
}
|
|
128
196
|
else {
|
|
129
|
-
extractedResult.push(entry.key);
|
|
197
|
+
extractedResult.push({ file, key: entry.key });
|
|
130
198
|
}
|
|
131
199
|
}
|
|
132
200
|
});
|
|
133
|
-
|
|
134
|
-
source.forEach(({ name, content }) => {
|
|
135
|
-
const keysInSource = Object.keys(content);
|
|
136
|
-
const found = [];
|
|
137
|
-
for (const keyInSource of keysInSource) {
|
|
138
|
-
const isSkippable = skippableKeys.find((skippableKey) => {
|
|
139
|
-
return keyInSource.includes(skippableKey);
|
|
140
|
-
});
|
|
141
|
-
if (isSkippable !== undefined) {
|
|
142
|
-
continue;
|
|
143
|
-
}
|
|
144
|
-
if (!extractedResultSet.has(keyInSource)) {
|
|
145
|
-
found.push(keyInSource);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
Object.assign(unusedKeys, { [name]: found });
|
|
149
|
-
});
|
|
150
|
-
return unusedKeys;
|
|
201
|
+
return { extractedResult, skippableKeys };
|
|
151
202
|
});
|
|
152
|
-
const isRecord = (data) => {
|
|
153
|
-
return (typeof data === "object" &&
|
|
154
|
-
!Array.isArray(data) &&
|
|
155
|
-
data !== null &&
|
|
156
|
-
data !== undefined);
|
|
157
|
-
};
|
|
158
203
|
function flatten(object, prefix = null, result = {}) {
|
|
159
204
|
for (let key in object) {
|
|
160
205
|
let propName = prefix ? `${prefix}.${key}` : key;
|