@lingual/i18n-check 0.5.5 → 0.7.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 +10 -2
- package/dist/bin/index.js +35 -22
- package/dist/bin/index.test.js +68 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +141 -57
- package/dist/utils/findInvalidTranslations.test.js +14 -5
- package/dist/utils/findInvalidi18nTranslations.test.js +15 -3
- package/dist/utils/findMissingKeys.test.js +3 -3
- package/dist/utils/i18NextParser.js +1 -1
- package/dist/utils/nextIntlSrcParser.d.ts +6 -0
- package/dist/utils/nextIntlSrcParser.js +202 -0
- package/dist/utils/nextIntlSrcParser.test.d.ts +1 -0
- package/dist/utils/nextIntlSrcParser.test.js +306 -0
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -138,9 +138,11 @@ 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` as well as `next-intl` (experimental at the moment) 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
142
|
|
|
143
|
-
|
|
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.
|
|
144
|
+
|
|
145
|
+
It is important to note that you must also provide the `-f` or `--format` option with `react-intl`, `i18next` or `next-intl` as value. See the [`format` section](#--format) for more information.
|
|
144
146
|
|
|
145
147
|
```bash
|
|
146
148
|
yarn i18n:check --locales translations/messageExamples -s en-US -u client/ -f react-intl
|
|
@@ -152,6 +154,12 @@ or
|
|
|
152
154
|
yarn i18n:check --locales translations/messageExamples -s en-US -u client/ -f i18next
|
|
153
155
|
```
|
|
154
156
|
|
|
157
|
+
or
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
yarn i18n:check --locales translations/messageExamples -s en-US -u client/ -f next-intl
|
|
161
|
+
```
|
|
162
|
+
|
|
155
163
|
### --reporter, -r
|
|
156
164
|
|
|
157
165
|
The standard reporting prints out all the missing or invalid keys.
|
package/dist/bin/index.js
CHANGED
|
@@ -1,14 +1,5 @@
|
|
|
1
1
|
#! /usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
|
-
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
4
|
-
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
5
|
-
return new (P || (P = Promise))(function (resolve, reject) {
|
|
6
|
-
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
7
|
-
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
8
|
-
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
9
|
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
10
|
-
});
|
|
11
|
-
};
|
|
12
3
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
13
4
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
14
5
|
};
|
|
@@ -27,12 +18,12 @@ commander_1.program
|
|
|
27
18
|
.version(version)
|
|
28
19
|
.option("-l, --locales <locales...>", "name of the directory containing the locales to validate")
|
|
29
20
|
.option("-s, --source <locale>", "the source locale to validate against")
|
|
30
|
-
.option("-f, --format <type>", "define the specific format: i18next or
|
|
21
|
+
.option("-f, --format <type>", "define the specific format: i18next, react-intl or next-intl")
|
|
31
22
|
.option("-c, --check <checks...>", "this option is deprecated - use -o or --only instead")
|
|
32
23
|
.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
24
|
.option("-r, --reporter <style>", "define the reporting style: standard or summary")
|
|
34
25
|
.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")
|
|
26
|
+
.option("-u, --unused <path>", "define the source path to find all unused and undefined keys")
|
|
36
27
|
.option("--parser-component-functions <components...>", "a list of component names to parse when using the --unused option")
|
|
37
28
|
.parse();
|
|
38
29
|
const getCheckOptions = () => {
|
|
@@ -49,7 +40,7 @@ const getCheckOptions = () => {
|
|
|
49
40
|
const isSource = (fileInfo, srcPath) => {
|
|
50
41
|
return (fileInfo.path.some((path) => path.toLowerCase() === srcPath.toLowerCase()) || fileInfo.name.toLowerCase().slice(0, -5) === srcPath.toLowerCase());
|
|
51
42
|
};
|
|
52
|
-
const main = () =>
|
|
43
|
+
const main = async () => {
|
|
53
44
|
const start = performance.now();
|
|
54
45
|
const srcPath = commander_1.program.getOptionValue("source");
|
|
55
46
|
const localePath = commander_1.program.getOptionValue("locales");
|
|
@@ -65,7 +56,7 @@ const main = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
|
65
56
|
console.log(chalk_1.default.red("Locale file(s) not found. Please provide valid locale file(s), i.e. -locales translations/"));
|
|
66
57
|
(0, node_process_1.exit)(1);
|
|
67
58
|
}
|
|
68
|
-
const excludedPaths = exclude
|
|
59
|
+
const excludedPaths = exclude ?? [];
|
|
69
60
|
const localePathFolders = localePath;
|
|
70
61
|
const isMultiFolders = localePathFolders.length > 1;
|
|
71
62
|
let srcFiles = [];
|
|
@@ -73,7 +64,7 @@ const main = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
|
73
64
|
const pattern = isMultiFolders
|
|
74
65
|
? `{${localePath.join(",").trim()}}/**/*.{json,yaml,yml}`
|
|
75
66
|
: `${localePath.join(",").trim()}/**/*.{json,yaml,yml}`;
|
|
76
|
-
const files =
|
|
67
|
+
const files = await (0, glob_1.glob)(pattern, {
|
|
77
68
|
ignore: ["node_modules/**"].concat(excludedPaths),
|
|
78
69
|
});
|
|
79
70
|
console.log("i18n translations checker");
|
|
@@ -83,14 +74,13 @@ const main = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
|
83
74
|
}
|
|
84
75
|
const options = {
|
|
85
76
|
checks: getCheckOptions(),
|
|
86
|
-
format: format
|
|
77
|
+
format: format ?? undefined,
|
|
87
78
|
};
|
|
88
79
|
const fileInfos = [];
|
|
89
80
|
files.sort().forEach((file) => {
|
|
90
|
-
var _a, _b;
|
|
91
81
|
const path = file.split("/");
|
|
92
|
-
const name =
|
|
93
|
-
const extension =
|
|
82
|
+
const name = path.pop() ?? "";
|
|
83
|
+
const extension = name.split(".").pop() ?? "json";
|
|
94
84
|
fileInfos.push({
|
|
95
85
|
extension,
|
|
96
86
|
file,
|
|
@@ -165,8 +155,15 @@ const main = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
|
165
155
|
const result = (0, __1.checkTranslations)(srcFiles, localeFiles, options);
|
|
166
156
|
printTranslationResult(result);
|
|
167
157
|
if (unusedSrcPath) {
|
|
168
|
-
const
|
|
158
|
+
const filesToParse = (0, glob_1.globSync)(`${unusedSrcPath}/**/*.{ts,tsx}`, {
|
|
159
|
+
ignore: ["node_modules/**"],
|
|
160
|
+
});
|
|
161
|
+
const unusedKeys = await (0, __1.checkUnusedKeys)(srcFiles, filesToParse, options, componentFunctions);
|
|
169
162
|
printUnusedKeysResult({ unusedKeys });
|
|
163
|
+
const undefinedKeys = await (0, __1.checkUndefinedKeys)(srcFiles, filesToParse, options, componentFunctions);
|
|
164
|
+
printUndefinedKeysResult({
|
|
165
|
+
undefinedKeys,
|
|
166
|
+
});
|
|
170
167
|
}
|
|
171
168
|
const end = performance.now();
|
|
172
169
|
console.log(chalk_1.default.green(`\nDone in ${Math.round(((end - start) * 100) / 1000) / 100}s.`));
|
|
@@ -182,7 +179,7 @@ const main = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
|
182
179
|
console.log(chalk_1.default.red("\nError: Can't validate translations. Check if the format is supported or specify the translation format i.e. -f i18next"));
|
|
183
180
|
(0, node_process_1.exit)(1);
|
|
184
181
|
}
|
|
185
|
-
}
|
|
182
|
+
};
|
|
186
183
|
const printTranslationResult = ({ missingKeys, invalidKeys, }) => {
|
|
187
184
|
const reporter = commander_1.program.getOptionValue("reporter");
|
|
188
185
|
const isSummary = reporter === "summary";
|
|
@@ -214,7 +211,7 @@ const printTranslationResult = ({ missingKeys, invalidKeys, }) => {
|
|
|
214
211
|
const printUnusedKeysResult = ({ unusedKeys, }) => {
|
|
215
212
|
const reporter = commander_1.program.getOptionValue("reporter");
|
|
216
213
|
const isSummary = reporter === "summary";
|
|
217
|
-
if (unusedKeys &&
|
|
214
|
+
if (unusedKeys && hasKeys(unusedKeys)) {
|
|
218
215
|
console.log(chalk_1.default.red("\nFound unused keys!"));
|
|
219
216
|
if (isSummary) {
|
|
220
217
|
console.log(chalk_1.default.red((0, errorReporters_1.summaryReporter)(getSummaryRows(unusedKeys))));
|
|
@@ -227,6 +224,22 @@ const printUnusedKeysResult = ({ unusedKeys, }) => {
|
|
|
227
224
|
console.log(chalk_1.default.green("\nNo unused keys found!"));
|
|
228
225
|
}
|
|
229
226
|
};
|
|
227
|
+
const printUndefinedKeysResult = ({ undefinedKeys, }) => {
|
|
228
|
+
const reporter = commander_1.program.getOptionValue("reporter");
|
|
229
|
+
const isSummary = reporter === "summary";
|
|
230
|
+
if (undefinedKeys && hasKeys(undefinedKeys)) {
|
|
231
|
+
console.log(chalk_1.default.red("\nFound undefined keys!"));
|
|
232
|
+
if (isSummary) {
|
|
233
|
+
console.log(chalk_1.default.red((0, errorReporters_1.summaryReporter)(getSummaryRows(undefinedKeys))));
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
console.log(chalk_1.default.red((0, errorReporters_1.standardReporter)(getStandardRows(undefinedKeys))));
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
else if (undefinedKeys) {
|
|
240
|
+
console.log(chalk_1.default.green("\nNo undefined keys found!"));
|
|
241
|
+
}
|
|
242
|
+
};
|
|
230
243
|
const truncate = (chars) => chars.length > 80 ? `${chars.substring(0, 80)}...` : chars;
|
|
231
244
|
const getSummaryRows = (checkResult) => {
|
|
232
245
|
const formattedRows = [];
|
|
@@ -250,7 +263,7 @@ const getStandardRows = (checkResult) => {
|
|
|
250
263
|
}
|
|
251
264
|
return formattedRows;
|
|
252
265
|
};
|
|
253
|
-
const
|
|
266
|
+
const hasKeys = (checkResult) => {
|
|
254
267
|
for (const [_, keys] of Object.entries(checkResult)) {
|
|
255
268
|
if (keys.length > 0) {
|
|
256
269
|
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,73 @@ 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
|
+
|
|
323
|
+
`);
|
|
324
|
+
done();
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
it("should find unused and undefined keys for next-intl applications", (done) => {
|
|
328
|
+
(0, child_process_1.exec)("node dist/bin/index.js --source en --locales translations/codeExamples/next-intl/locales/ -f next-intl -u translations/codeExamples/next-intl/src", (_error, stdout, _stderr) => {
|
|
329
|
+
const result = stdout.split("Done")[0];
|
|
330
|
+
expect(result).toEqual(`i18n translations checker
|
|
331
|
+
Source: en
|
|
332
|
+
Selected format is: next-intl
|
|
333
|
+
|
|
334
|
+
No missing keys found!
|
|
335
|
+
|
|
336
|
+
No invalid translations found!
|
|
337
|
+
|
|
338
|
+
Found unused keys!
|
|
339
|
+
┌───────────────────────────────────────────────────────────────────┬──────────────────┐
|
|
340
|
+
│ file │ key │
|
|
341
|
+
├───────────────────────────────────────────────────────────────────┼──────────────────┤
|
|
342
|
+
│ translations/codeExamples/next-intl/locales/en/translation.json │ message.plural │
|
|
343
|
+
│ translations/codeExamples/next-intl/locales/en/translation.json │ notUsedKey │
|
|
344
|
+
└───────────────────────────────────────────────────────────────────┴──────────────────┘
|
|
345
|
+
|
|
346
|
+
Found undefined keys!
|
|
347
|
+
┌─────────────────────────────────────────────────────┬──────────────────┐
|
|
348
|
+
│ file │ key │
|
|
349
|
+
├─────────────────────────────────────────────────────┼──────────────────┤
|
|
350
|
+
│ translations/codeExamples/next-intl/src/Basic.tsx │ message.select │
|
|
351
|
+
└─────────────────────────────────────────────────────┴──────────────────┘
|
|
352
|
+
|
|
286
353
|
`);
|
|
287
354
|
done();
|
|
288
355
|
});
|
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" | "react-intl" | "
|
|
4
|
+
format?: "icu" | "i18next" | "react-intl" | "next-intl";
|
|
5
5
|
checks?: Context[];
|
|
6
6
|
};
|
|
7
7
|
export declare const checkInvalidTranslations: (source: Translation, targets: Record<string, Translation>, options?: Options) => CheckResult;
|
|
@@ -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
|
@@ -1,24 +1,16 @@
|
|
|
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
|
-
};
|
|
11
2
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
4
|
};
|
|
14
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
-
exports.checkUnusedKeys = exports.checkTranslations = exports.checkMissingTranslations = exports.checkInvalidTranslations = void 0;
|
|
6
|
+
exports.checkUndefinedKeys = exports.checkUnusedKeys = exports.checkTranslations = exports.checkMissingTranslations = exports.checkInvalidTranslations = void 0;
|
|
16
7
|
const findMissingKeys_1 = require("./utils/findMissingKeys");
|
|
17
8
|
const findInvalidTranslations_1 = require("./utils/findInvalidTranslations");
|
|
18
9
|
const findInvalidi18nTranslations_1 = require("./utils/findInvalidi18nTranslations");
|
|
19
|
-
const glob_1 = require("glob");
|
|
20
10
|
const cli_lib_1 = require("@formatjs/cli-lib");
|
|
11
|
+
const nextIntlSrcParser_1 = require("./utils/nextIntlSrcParser");
|
|
21
12
|
const fs_1 = __importDefault(require("fs"));
|
|
13
|
+
const ParseFormats = ["react-intl", "i18next", "next-intl"];
|
|
22
14
|
const checkInvalidTranslations = (source, targets, options = { format: "icu" }) => {
|
|
23
15
|
return options.format === "i18next"
|
|
24
16
|
? (0, findInvalidi18nTranslations_1.findInvalid18nTranslations)(source, targets)
|
|
@@ -54,29 +46,53 @@ const checkTranslations = (source, targets, options = { format: "icu", checks: [
|
|
|
54
46
|
};
|
|
55
47
|
};
|
|
56
48
|
exports.checkTranslations = checkTranslations;
|
|
57
|
-
const checkUnusedKeys =
|
|
49
|
+
const checkUnusedKeys = async (translationFiles, filesToParse, options = {
|
|
58
50
|
format: "react-intl",
|
|
59
|
-
}, componentFunctions = []) {
|
|
60
|
-
if (!options.format || !
|
|
51
|
+
}, componentFunctions = []) => {
|
|
52
|
+
if (!options.format || !ParseFormats.includes(options.format)) {
|
|
61
53
|
return undefined;
|
|
62
54
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
55
|
+
if (options.format === "react-intl") {
|
|
56
|
+
return findUnusedReactIntlTranslations(translationFiles, filesToParse);
|
|
57
|
+
}
|
|
58
|
+
else if (options.format === "i18next") {
|
|
59
|
+
return findUnusedI18NextTranslations(translationFiles, filesToParse, componentFunctions);
|
|
60
|
+
}
|
|
61
|
+
else if (options.format === "next-intl") {
|
|
62
|
+
return findUnusedNextIntlTranslations(translationFiles, filesToParse);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
67
65
|
exports.checkUnusedKeys = checkUnusedKeys;
|
|
68
|
-
const findUnusedReactIntlTranslations = (
|
|
66
|
+
const findUnusedReactIntlTranslations = async (translationFiles, filesToParse) => {
|
|
69
67
|
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, {});
|
|
68
|
+
const extracted = await (0, cli_lib_1.extract)(filesToParse, {});
|
|
75
69
|
const extractedResultSet = new Set(Object.keys(JSON.parse(extracted)));
|
|
70
|
+
translationFiles.forEach(({ name, content }) => {
|
|
71
|
+
const keysInSource = Object.keys(content);
|
|
72
|
+
const found = [];
|
|
73
|
+
for (const keyInSource of keysInSource) {
|
|
74
|
+
if (!extractedResultSet.has(keyInSource)) {
|
|
75
|
+
found.push(keyInSource);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
Object.assign(unusedKeys, { [name]: found });
|
|
79
|
+
});
|
|
80
|
+
return unusedKeys;
|
|
81
|
+
};
|
|
82
|
+
const findUnusedI18NextTranslations = async (source, filesToParse, componentFunctions = []) => {
|
|
83
|
+
let unusedKeys = {};
|
|
84
|
+
const { extractedResult, skippableKeys } = await getI18NextKeysInCode(filesToParse, componentFunctions);
|
|
85
|
+
const extractedResultSet = new Set(extractedResult.map(({ key }) => key));
|
|
76
86
|
source.forEach(({ name, content }) => {
|
|
77
87
|
const keysInSource = Object.keys(content);
|
|
78
88
|
const found = [];
|
|
79
89
|
for (const keyInSource of keysInSource) {
|
|
90
|
+
const isSkippable = skippableKeys.find((skippableKey) => {
|
|
91
|
+
return keyInSource.includes(skippableKey);
|
|
92
|
+
});
|
|
93
|
+
if (isSkippable !== undefined) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
80
96
|
if (!extractedResultSet.has(keyInSource)) {
|
|
81
97
|
found.push(keyInSource);
|
|
82
98
|
}
|
|
@@ -84,16 +100,106 @@ const findUnusedReactIntlTranslations = (source, codebaseSrc) => __awaiter(void
|
|
|
84
100
|
Object.assign(unusedKeys, { [name]: found });
|
|
85
101
|
});
|
|
86
102
|
return unusedKeys;
|
|
87
|
-
}
|
|
88
|
-
const
|
|
103
|
+
};
|
|
104
|
+
const findUnusedNextIntlTranslations = async (translationFiles, filesToParse) => {
|
|
89
105
|
let unusedKeys = {};
|
|
90
|
-
|
|
91
|
-
const
|
|
92
|
-
|
|
106
|
+
const extracted = (0, nextIntlSrcParser_1.extract)(filesToParse);
|
|
107
|
+
const extractedResultSet = new Set(extracted.map(({ key }) => key));
|
|
108
|
+
translationFiles.forEach(({ name, content }) => {
|
|
109
|
+
const keysInSource = Object.keys(content);
|
|
110
|
+
const found = [];
|
|
111
|
+
for (const keyInSource of keysInSource) {
|
|
112
|
+
if (!extractedResultSet.has(keyInSource)) {
|
|
113
|
+
found.push(keyInSource);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
Object.assign(unusedKeys, { [name]: found });
|
|
93
117
|
});
|
|
94
|
-
|
|
118
|
+
return unusedKeys;
|
|
119
|
+
};
|
|
120
|
+
const checkUndefinedKeys = async (source, filesToParse, options = {
|
|
121
|
+
format: "react-intl",
|
|
122
|
+
}, componentFunctions = []) => {
|
|
123
|
+
if (!options.format || !ParseFormats.includes(options.format)) {
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
if (options.format === "react-intl") {
|
|
127
|
+
return findUndefinedReactIntlKeys(source, filesToParse);
|
|
128
|
+
}
|
|
129
|
+
else if (options.format === "i18next") {
|
|
130
|
+
return findUndefinedI18NextKeys(source, filesToParse, componentFunctions);
|
|
131
|
+
}
|
|
132
|
+
else if (options.format === "next-intl") {
|
|
133
|
+
return findUndefinedNextIntlKeys(source, filesToParse);
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
exports.checkUndefinedKeys = checkUndefinedKeys;
|
|
137
|
+
const findUndefinedReactIntlKeys = async (translationFiles, filesToParse) => {
|
|
138
|
+
const sourceKeys = new Set(translationFiles.flatMap(({ content }) => {
|
|
139
|
+
return Object.keys(content);
|
|
140
|
+
}));
|
|
141
|
+
const extractedResult = await (0, cli_lib_1.extract)(filesToParse, {
|
|
142
|
+
extractSourceLocation: true,
|
|
143
|
+
});
|
|
144
|
+
let undefinedKeys = {};
|
|
145
|
+
Object.entries(JSON.parse(extractedResult)).forEach(([key, meta]) => {
|
|
146
|
+
if (!sourceKeys.has(key)) {
|
|
147
|
+
// @ts-ignore
|
|
148
|
+
const file = meta.file;
|
|
149
|
+
if (!undefinedKeys[file]) {
|
|
150
|
+
undefinedKeys[file] = [];
|
|
151
|
+
}
|
|
152
|
+
undefinedKeys[file].push(key);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
return undefinedKeys;
|
|
156
|
+
};
|
|
157
|
+
const findUndefinedI18NextKeys = async (source, filesToParse, componentFunctions = []) => {
|
|
158
|
+
const { extractedResult, skippableKeys } = await getI18NextKeysInCode(filesToParse, componentFunctions);
|
|
159
|
+
const sourceKeys = new Set(source.flatMap(({ content }) => {
|
|
160
|
+
return Object.keys(content);
|
|
161
|
+
}));
|
|
162
|
+
let undefinedKeys = {};
|
|
163
|
+
extractedResult.forEach(({ file, key }) => {
|
|
164
|
+
const isSkippable = skippableKeys.find((skippableKey) => {
|
|
165
|
+
return key.includes(skippableKey);
|
|
166
|
+
});
|
|
167
|
+
if (isSkippable === undefined && !sourceKeys.has(key)) {
|
|
168
|
+
if (!undefinedKeys[file]) {
|
|
169
|
+
undefinedKeys[file] = [];
|
|
170
|
+
}
|
|
171
|
+
undefinedKeys[file].push(key);
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
return undefinedKeys;
|
|
175
|
+
};
|
|
176
|
+
const findUndefinedNextIntlKeys = async (translationFiles, filesToParse) => {
|
|
177
|
+
const sourceKeys = new Set(translationFiles.flatMap(({ content }) => {
|
|
178
|
+
return Object.keys(content);
|
|
179
|
+
}));
|
|
180
|
+
const extractedResult = (0, nextIntlSrcParser_1.extract)(filesToParse);
|
|
181
|
+
let undefinedKeys = {};
|
|
182
|
+
extractedResult.forEach(({ key, meta }) => {
|
|
183
|
+
if (!sourceKeys.has(key)) {
|
|
184
|
+
// @ts-ignore
|
|
185
|
+
const file = meta.file;
|
|
186
|
+
if (!undefinedKeys[file]) {
|
|
187
|
+
undefinedKeys[file] = [];
|
|
188
|
+
}
|
|
189
|
+
undefinedKeys[file].push(key);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
return undefinedKeys;
|
|
193
|
+
};
|
|
194
|
+
const isRecord = (data) => {
|
|
195
|
+
return (typeof data === "object" &&
|
|
196
|
+
!Array.isArray(data) &&
|
|
197
|
+
data !== null &&
|
|
198
|
+
data !== undefined);
|
|
199
|
+
};
|
|
200
|
+
const getI18NextKeysInCode = async (filesToParse, componentFunctions = []) => {
|
|
95
201
|
// @ts-ignore
|
|
96
|
-
const { transform } =
|
|
202
|
+
const { transform } = await import("i18next-parser");
|
|
97
203
|
const i18nextParser = new transform({
|
|
98
204
|
lexers: {
|
|
99
205
|
jsx: [
|
|
@@ -113,8 +219,9 @@ const findUnusedI18NextTranslations = (source_1, codebaseSrc_1, ...args_1) => __
|
|
|
113
219
|
// Skip any parsed keys that have the `returnObjects` property set to true
|
|
114
220
|
// As these are used dynamically, they will be skipped to prevent
|
|
115
221
|
// these keys from being marked as unused.
|
|
222
|
+
let extractedResult = [];
|
|
116
223
|
const skippableKeys = [];
|
|
117
|
-
|
|
224
|
+
filesToParse.forEach((file) => {
|
|
118
225
|
const rawContent = fs_1.default.readFileSync(file, "utf-8");
|
|
119
226
|
const entries = i18nextParser.parser.parse(rawContent, file);
|
|
120
227
|
// Intermediate solution to retrieve all keys from the parser.
|
|
@@ -126,34 +233,11 @@ const findUnusedI18NextTranslations = (source_1, codebaseSrc_1, ...args_1) => __
|
|
|
126
233
|
skippableKeys.push(entry.key);
|
|
127
234
|
}
|
|
128
235
|
else {
|
|
129
|
-
extractedResult.push(entry.key);
|
|
236
|
+
extractedResult.push({ file, key: entry.key });
|
|
130
237
|
}
|
|
131
238
|
}
|
|
132
239
|
});
|
|
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;
|
|
151
|
-
});
|
|
152
|
-
const isRecord = (data) => {
|
|
153
|
-
return (typeof data === "object" &&
|
|
154
|
-
!Array.isArray(data) &&
|
|
155
|
-
data !== null &&
|
|
156
|
-
data !== undefined);
|
|
240
|
+
return { extractedResult, skippableKeys };
|
|
157
241
|
};
|
|
158
242
|
function flatten(object, prefix = null, result = {}) {
|
|
159
243
|
for (let key in object) {
|
|
@@ -9,7 +9,10 @@ describe("findInvalidTranslations:compareTranslationFiles", () => {
|
|
|
9
9
|
expect((0, findInvalidTranslations_1.compareTranslationFiles)((0, flattenTranslations_1.flattenTranslations)(sourceFile), (0, flattenTranslations_1.flattenTranslations)(sourceFile))).toEqual([]);
|
|
10
10
|
});
|
|
11
11
|
it("should return the invalid keys in the target file", () => {
|
|
12
|
-
expect((0, findInvalidTranslations_1.compareTranslationFiles)((0, flattenTranslations_1.flattenTranslations)(
|
|
12
|
+
expect((0, findInvalidTranslations_1.compareTranslationFiles)((0, flattenTranslations_1.flattenTranslations)({
|
|
13
|
+
...sourceFile,
|
|
14
|
+
"ten.eleven.twelve": "ten eleven twelve",
|
|
15
|
+
}), (0, flattenTranslations_1.flattenTranslations)(secondaryFile))).toEqual(["multipleVariables"]);
|
|
13
16
|
});
|
|
14
17
|
it("should return empty array if placeholders are identical but in different positions", () => {
|
|
15
18
|
expect((0, findInvalidTranslations_1.compareTranslationFiles)({
|
|
@@ -24,10 +27,10 @@ describe("findInvalidTranslations", () => {
|
|
|
24
27
|
expect((0, findInvalidTranslations_1.findInvalidTranslations)(sourceFile, { de: sourceFile })).toEqual({});
|
|
25
28
|
});
|
|
26
29
|
it("should return an object containing the keys for the missing language", () => {
|
|
27
|
-
expect((0, findInvalidTranslations_1.findInvalidTranslations)(
|
|
30
|
+
expect((0, findInvalidTranslations_1.findInvalidTranslations)({ ...sourceFile, "ten.eleven.twelve": "ten eleven twelve" }, { de: secondaryFile })).toEqual({ de: ["multipleVariables"] });
|
|
28
31
|
});
|
|
29
32
|
it("should return an object containing the keys for every language with missing key", () => {
|
|
30
|
-
expect((0, findInvalidTranslations_1.findInvalidTranslations)(
|
|
33
|
+
expect((0, findInvalidTranslations_1.findInvalidTranslations)({ ...sourceFile, "ten.eleven.twelve": "ten eleven twelve" }, {
|
|
31
34
|
de: secondaryFile,
|
|
32
35
|
fr: {
|
|
33
36
|
"four.five.six": "four five six",
|
|
@@ -41,14 +44,20 @@ describe("findInvalidTranslations", () => {
|
|
|
41
44
|
});
|
|
42
45
|
it("should allow for different types of keys per locale", () => {
|
|
43
46
|
expect((0, findInvalidTranslations_1.findInvalidTranslations)(sourceFile, {
|
|
44
|
-
de:
|
|
47
|
+
de: {
|
|
48
|
+
...secondaryFile,
|
|
49
|
+
"message.plural": "{count, plural, other {# of {total} items}}",
|
|
50
|
+
}
|
|
45
51
|
})).toEqual({
|
|
46
52
|
de: ["multipleVariables"]
|
|
47
53
|
});
|
|
48
54
|
});
|
|
49
55
|
it("should fail if a variable is changed in one of the translations", () => {
|
|
50
56
|
expect((0, findInvalidTranslations_1.findInvalidTranslations)(sourceFile, {
|
|
51
|
-
de:
|
|
57
|
+
de: {
|
|
58
|
+
...secondaryFile,
|
|
59
|
+
"message.plural": "{count, plural, other {# of {cargado} items}}",
|
|
60
|
+
}
|
|
52
61
|
})).toEqual({
|
|
53
62
|
de: ["message.plural", "multipleVariables"]
|
|
54
63
|
});
|
|
@@ -9,7 +9,10 @@ describe("findInvalid18nTranslations:compareTranslationFiles", () => {
|
|
|
9
9
|
expect((0, findInvalidi18nTranslations_1.compareTranslationFiles)((0, flattenTranslations_1.flattenTranslations)(sourceFile), (0, flattenTranslations_1.flattenTranslations)(sourceFile))).toEqual([]);
|
|
10
10
|
});
|
|
11
11
|
it("should return the invalid keys in the target file", () => {
|
|
12
|
-
expect((0, findInvalidi18nTranslations_1.compareTranslationFiles)((0, flattenTranslations_1.flattenTranslations)(
|
|
12
|
+
expect((0, findInvalidi18nTranslations_1.compareTranslationFiles)((0, flattenTranslations_1.flattenTranslations)({
|
|
13
|
+
...sourceFile,
|
|
14
|
+
"ten.eleven.twelve": "ten eleven twelve",
|
|
15
|
+
}), (0, flattenTranslations_1.flattenTranslations)(targetFile))).toEqual(["key_with_broken_de", "intlNumber_broken_de"]);
|
|
13
16
|
});
|
|
14
17
|
it("should return an empty array if the strings contain paranthesis that have different content", () => {
|
|
15
18
|
expect((0, findInvalidi18nTranslations_1.compareTranslationFiles)((0, flattenTranslations_1.flattenTranslations)({
|
|
@@ -43,10 +46,10 @@ describe("findInvalidTranslations", () => {
|
|
|
43
46
|
expect((0, findInvalidi18nTranslations_1.findInvalid18nTranslations)(sourceFile, { de: sourceFile })).toEqual({});
|
|
44
47
|
});
|
|
45
48
|
it("should return an object containing the keys for the missing language", () => {
|
|
46
|
-
expect((0, findInvalidi18nTranslations_1.findInvalid18nTranslations)(
|
|
49
|
+
expect((0, findInvalidi18nTranslations_1.findInvalid18nTranslations)({ ...sourceFile, "ten.eleven.twelve": "ten eleven twelve" }, { de: targetFile })).toEqual({ de: ["key_with_broken_de", "intlNumber_broken_de"] });
|
|
47
50
|
});
|
|
48
51
|
it("should return an object containing the keys for every language with missing key", () => {
|
|
49
|
-
expect((0, findInvalidi18nTranslations_1.findInvalid18nTranslations)(
|
|
52
|
+
expect((0, findInvalidi18nTranslations_1.findInvalid18nTranslations)({ ...sourceFile, "ten.eleven.twelve": "ten eleven twelve" }, {
|
|
50
53
|
de: targetFile,
|
|
51
54
|
fr: {
|
|
52
55
|
key_with_broken_de: "Some format {{value, formatname}} and some other format {{value, formatname}}",
|
|
@@ -122,4 +125,13 @@ describe("findInvalidTranslations", () => {
|
|
|
122
125
|
de: ["tag"],
|
|
123
126
|
});
|
|
124
127
|
});
|
|
128
|
+
it("should recognize special characters", () => {
|
|
129
|
+
expect((0, findInvalidi18nTranslations_1.findInvalid18nTranslations)({
|
|
130
|
+
key: "Test < {{a}} and > {{max_a}}",
|
|
131
|
+
}, {
|
|
132
|
+
de: {
|
|
133
|
+
key: "Test < {{a}} und > {{max_a}}",
|
|
134
|
+
},
|
|
135
|
+
})).toEqual({});
|
|
136
|
+
});
|
|
125
137
|
});
|
|
@@ -16,7 +16,7 @@ describe("findMissingKeys:compareTranslationFiles", () => {
|
|
|
16
16
|
expect((0, findMissingKeys_1.compareTranslationFiles)(sourceFile, secondaryFile)).toEqual([]);
|
|
17
17
|
});
|
|
18
18
|
it("should return the missing keys in the secondary file", () => {
|
|
19
|
-
expect((0, findMissingKeys_1.compareTranslationFiles)(
|
|
19
|
+
expect((0, findMissingKeys_1.compareTranslationFiles)({ ...sourceFile, "ten.eleven.twelve": "ten eleven twelve" }, secondaryFile)).toEqual(["ten.eleven.twelve"]);
|
|
20
20
|
});
|
|
21
21
|
});
|
|
22
22
|
describe("findMissingKeys", () => {
|
|
@@ -24,10 +24,10 @@ describe("findMissingKeys", () => {
|
|
|
24
24
|
expect((0, findMissingKeys_1.findMissingKeys)(sourceFile, { de: secondaryFile })).toEqual({});
|
|
25
25
|
});
|
|
26
26
|
it("should return an object containing the keys for the missing language", () => {
|
|
27
|
-
expect((0, findMissingKeys_1.findMissingKeys)(
|
|
27
|
+
expect((0, findMissingKeys_1.findMissingKeys)({ ...sourceFile, "ten.eleven.twelve": "ten eleven twelve" }, { de: secondaryFile })).toEqual({ de: ["ten.eleven.twelve"] });
|
|
28
28
|
});
|
|
29
29
|
it("should return an object containing the keys for every language with missing key", () => {
|
|
30
|
-
expect((0, findMissingKeys_1.findMissingKeys)(
|
|
30
|
+
expect((0, findMissingKeys_1.findMissingKeys)({ ...sourceFile, "ten.eleven.twelve": "ten eleven twelve" }, {
|
|
31
31
|
de: secondaryFile,
|
|
32
32
|
fr: {
|
|
33
33
|
"four.five.six": "four five six",
|
|
@@ -86,7 +86,7 @@ const parseInput = (input) => {
|
|
|
86
86
|
voidElement: match.substring(match.length - 2) === "/>",
|
|
87
87
|
});
|
|
88
88
|
}
|
|
89
|
-
else if (match.indexOf(OPEN_TAG) === 0) {
|
|
89
|
+
else if (match.indexOf(OPEN_TAG) === 0 && /<[^\s]+/.test(match)) {
|
|
90
90
|
acc.push({
|
|
91
91
|
type: "tag",
|
|
92
92
|
raw: match,
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.extract = void 0;
|
|
40
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
41
|
+
const ts = __importStar(require("typescript"));
|
|
42
|
+
const USE_TRANSLATIONS = "useTranslations";
|
|
43
|
+
const GET_TRANSLATIONS = "getTranslations";
|
|
44
|
+
const COMMENT_CONTAINS_STATIC_KEY_REGEX = /t\((["'])(.*?[^\\])(["'])\)/;
|
|
45
|
+
const extract = (filesPaths) => {
|
|
46
|
+
return filesPaths.flatMap(getKeys).sort((a, b) => {
|
|
47
|
+
return a > b ? 1 : -1;
|
|
48
|
+
});
|
|
49
|
+
};
|
|
50
|
+
exports.extract = extract;
|
|
51
|
+
const getKeys = (path) => {
|
|
52
|
+
const content = node_fs_1.default.readFileSync(path, "utf-8");
|
|
53
|
+
const sourceFile = ts.createSourceFile(path, content, ts.ScriptTarget.Latest, true);
|
|
54
|
+
const foundKeys = [];
|
|
55
|
+
let namespaces = [];
|
|
56
|
+
let variable = "t";
|
|
57
|
+
const getCurrentNamespace = () => {
|
|
58
|
+
if (namespaces.length > 0) {
|
|
59
|
+
return namespaces[namespaces.length - 1];
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
};
|
|
63
|
+
const pushNamespace = (name) => {
|
|
64
|
+
namespaces.push(name);
|
|
65
|
+
};
|
|
66
|
+
const removeNamespace = () => {
|
|
67
|
+
if (namespaces.length > 0) {
|
|
68
|
+
namespaces.pop();
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
const visit = (node) => {
|
|
72
|
+
let key = null;
|
|
73
|
+
let current = namespaces.length;
|
|
74
|
+
if (node === undefined) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (ts.isVariableDeclaration(node)) {
|
|
78
|
+
if (node.initializer && ts.isCallExpression(node.initializer)) {
|
|
79
|
+
if (ts.isIdentifier(node.initializer.expression)) {
|
|
80
|
+
// Search for `useTranslations` calls and extract the namespace
|
|
81
|
+
// Additionally check for assigned variable name, as it might differ
|
|
82
|
+
// from the default `t`, i.e.: const other = useTranslations("namespace1");
|
|
83
|
+
if (node.initializer.expression.text === USE_TRANSLATIONS) {
|
|
84
|
+
const [argument] = node.initializer.arguments;
|
|
85
|
+
if (argument && ts.isStringLiteral(argument)) {
|
|
86
|
+
pushNamespace(argument.text);
|
|
87
|
+
}
|
|
88
|
+
else if (argument === undefined) {
|
|
89
|
+
pushNamespace("");
|
|
90
|
+
}
|
|
91
|
+
if (ts.isIdentifier(node.name)) {
|
|
92
|
+
variable = node.name.text;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// Search for `getTranslations` calls and extract the namespace
|
|
98
|
+
// There are two different ways `getTranslations` can be used:
|
|
99
|
+
//
|
|
100
|
+
// import {getTranslations} from 'next-intl/server';
|
|
101
|
+
// const t = await getTranslations(namespace?);
|
|
102
|
+
// const t = await getTranslations({locale, namespace});
|
|
103
|
+
//
|
|
104
|
+
// Additionally check for assigned variable name, as it might differ
|
|
105
|
+
// from the default `t`, i.e.: const other = getTranslations("namespace1");
|
|
106
|
+
// Simplified usage in async components
|
|
107
|
+
if (node.initializer && ts.isAwaitExpression(node.initializer)) {
|
|
108
|
+
if (ts.isCallExpression(node.initializer.expression) &&
|
|
109
|
+
ts.isIdentifier(node.initializer.expression.expression)) {
|
|
110
|
+
if (node.initializer.expression.expression.text === GET_TRANSLATIONS) {
|
|
111
|
+
const [argument] = node.initializer.expression.arguments;
|
|
112
|
+
if (argument && ts.isObjectLiteralExpression(argument)) {
|
|
113
|
+
argument.properties.forEach((property) => {
|
|
114
|
+
if (property &&
|
|
115
|
+
ts.isPropertyAssignment(property) &&
|
|
116
|
+
property.name &&
|
|
117
|
+
ts.isIdentifier(property.name) &&
|
|
118
|
+
property.name.text === "namespace" &&
|
|
119
|
+
ts.isStringLiteral(property.initializer)) {
|
|
120
|
+
pushNamespace(property.initializer.text);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
else if (argument && ts.isStringLiteral(argument)) {
|
|
125
|
+
pushNamespace(argument.text);
|
|
126
|
+
}
|
|
127
|
+
else if (argument === undefined) {
|
|
128
|
+
pushNamespace("");
|
|
129
|
+
}
|
|
130
|
+
if (ts.isIdentifier(node.name)) {
|
|
131
|
+
variable = node.name.text;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// Search for `t()` calls
|
|
138
|
+
if (getCurrentNamespace() !== null &&
|
|
139
|
+
ts.isCallExpression(node) &&
|
|
140
|
+
ts.isIdentifier(node.expression)) {
|
|
141
|
+
const expressionName = node.expression.text;
|
|
142
|
+
if (expressionName === variable) {
|
|
143
|
+
const [argument] = node.arguments;
|
|
144
|
+
if (argument && ts.isStringLiteral(argument)) {
|
|
145
|
+
key = argument.text;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// Search for `t.*()` calls, i.e. t.html() or t.rich()
|
|
150
|
+
if (getCurrentNamespace() !== null &&
|
|
151
|
+
ts.isCallExpression(node) &&
|
|
152
|
+
ts.isPropertyAccessExpression(node.expression) &&
|
|
153
|
+
ts.isIdentifier(node.expression.expression)) {
|
|
154
|
+
const expressionName = node.expression.expression.text;
|
|
155
|
+
if (expressionName === variable) {
|
|
156
|
+
const [argument] = node.arguments;
|
|
157
|
+
if (argument && ts.isStringLiteral(argument)) {
|
|
158
|
+
key = argument.text;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (key) {
|
|
163
|
+
const namespace = getCurrentNamespace();
|
|
164
|
+
foundKeys.push({
|
|
165
|
+
key: namespace ? `${namespace}.${key}` : key,
|
|
166
|
+
meta: { file: path },
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
// Search for single-line comments that contain the static values of a dynamic key
|
|
170
|
+
// Example:
|
|
171
|
+
// const someKeys = messages[selectedOption];
|
|
172
|
+
// Define as a single-line comment all the possible static keys for that dynamic key
|
|
173
|
+
// t('some.static.key.we.want.to.extract');
|
|
174
|
+
// t('some.other.key.we.want.to.extract.without.semicolons')
|
|
175
|
+
const commentRanges = ts.getLeadingCommentRanges(sourceFile.getFullText(), node.getFullStart());
|
|
176
|
+
if (commentRanges?.length && commentRanges.length > 0) {
|
|
177
|
+
commentRanges.forEach((range) => {
|
|
178
|
+
const comment = sourceFile.getFullText().slice(range.pos, range.end);
|
|
179
|
+
// parse the string and check if it includes the following format:
|
|
180
|
+
// t('someString')
|
|
181
|
+
const hasStaticKeyComment = COMMENT_CONTAINS_STATIC_KEY_REGEX.test(comment);
|
|
182
|
+
if (hasStaticKeyComment) {
|
|
183
|
+
// capture the string comment
|
|
184
|
+
const commentKey = COMMENT_CONTAINS_STATIC_KEY_REGEX.exec(comment)?.[2];
|
|
185
|
+
if (commentKey) {
|
|
186
|
+
const namespace = getCurrentNamespace();
|
|
187
|
+
foundKeys.push({
|
|
188
|
+
key: namespace ? `${namespace}.${commentKey}` : commentKey,
|
|
189
|
+
meta: { file: path },
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
ts.forEachChild(node, visit);
|
|
196
|
+
if (ts.isFunctionLike(node) && namespaces.length > current) {
|
|
197
|
+
removeNamespace();
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
ts.forEachChild(sourceFile, visit);
|
|
201
|
+
return foundKeys;
|
|
202
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const nextIntlSrcParser_1 = require("./nextIntlSrcParser");
|
|
7
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
8
|
+
const srcPath = "./translations/codeExamples/next-intl/src/";
|
|
9
|
+
const basicFile = node_path_1.default.join(srcPath, "Basic.tsx");
|
|
10
|
+
const counterFile = node_path_1.default.join(srcPath, "Counter.tsx");
|
|
11
|
+
const clientCounterFile = node_path_1.default.join(srcPath, "ClientCounter.tsx");
|
|
12
|
+
const nestedExampleFile = node_path_1.default.join(srcPath, "NestedExample.tsx");
|
|
13
|
+
const asyncExampleFile = node_path_1.default.join(srcPath, "AsyncExample.tsx");
|
|
14
|
+
const dynamicKeysExamplFile = node_path_1.default.join(srcPath, "DynamicKeysExample.tsx");
|
|
15
|
+
describe("nextIntlSrcParser", () => {
|
|
16
|
+
it("should find all the translation keys", () => {
|
|
17
|
+
const keys = (0, nextIntlSrcParser_1.extract)([basicFile, counterFile, clientCounterFile]);
|
|
18
|
+
expect(keys).toEqual([
|
|
19
|
+
{
|
|
20
|
+
key: "ClientCounter2.increment",
|
|
21
|
+
meta: {
|
|
22
|
+
file: clientCounterFile,
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
key: "ClientCounter2.count",
|
|
27
|
+
meta: {
|
|
28
|
+
file: clientCounterFile,
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
key: "ClientCounter.increment",
|
|
33
|
+
meta: {
|
|
34
|
+
file: clientCounterFile,
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
key: "ClientCounter.count",
|
|
39
|
+
meta: {
|
|
40
|
+
file: clientCounterFile,
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
key: "Counter.increment",
|
|
45
|
+
meta: {
|
|
46
|
+
file: counterFile,
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
key: "Counter.count",
|
|
51
|
+
meta: {
|
|
52
|
+
file: counterFile,
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
key: "testKeyWithoutNamespace",
|
|
57
|
+
meta: {
|
|
58
|
+
file: basicFile,
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
key: "message.argument",
|
|
63
|
+
meta: {
|
|
64
|
+
file: basicFile,
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
key: "message.select",
|
|
69
|
+
meta: {
|
|
70
|
+
file: basicFile,
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
key: "message.simple",
|
|
75
|
+
meta: {
|
|
76
|
+
file: basicFile,
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
key: "Navigation.news",
|
|
81
|
+
meta: {
|
|
82
|
+
file: basicFile,
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
key: "Navigation.nested",
|
|
87
|
+
meta: {
|
|
88
|
+
file: basicFile,
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
key: "Navigation.about",
|
|
93
|
+
meta: {
|
|
94
|
+
file: basicFile,
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
key: "Navigation.client",
|
|
99
|
+
meta: {
|
|
100
|
+
file: basicFile,
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
key: "Navigation.home",
|
|
105
|
+
meta: {
|
|
106
|
+
file: basicFile,
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
]);
|
|
110
|
+
});
|
|
111
|
+
it("should find all the nested translation keys", () => {
|
|
112
|
+
const keys = (0, nextIntlSrcParser_1.extract)([nestedExampleFile]);
|
|
113
|
+
expect(keys).toEqual([
|
|
114
|
+
{
|
|
115
|
+
key: "nested.three.htmlKey",
|
|
116
|
+
meta: {
|
|
117
|
+
file: "translations/codeExamples/next-intl/src/NestedExample.tsx",
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
key: "nested.three.markupKey",
|
|
122
|
+
meta: {
|
|
123
|
+
file: "translations/codeExamples/next-intl/src/NestedExample.tsx",
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
key: "nested.three.richTextKey",
|
|
128
|
+
meta: {
|
|
129
|
+
file: "translations/codeExamples/next-intl/src/NestedExample.tsx",
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
key: "nested.three.hasKeyCheck",
|
|
134
|
+
meta: {
|
|
135
|
+
file: "translations/codeExamples/next-intl/src/NestedExample.tsx",
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
key: "nested.three.basicKey",
|
|
140
|
+
meta: {
|
|
141
|
+
file: "translations/codeExamples/next-intl/src/NestedExample.tsx",
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
key: "deepNested.level1.one",
|
|
146
|
+
meta: {
|
|
147
|
+
file: "translations/codeExamples/next-intl/src/NestedExample.tsx",
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
key: "deepNested.level2.two",
|
|
152
|
+
meta: {
|
|
153
|
+
file: "translations/codeExamples/next-intl/src/NestedExample.tsx",
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
key: "deepNested.level3.three",
|
|
158
|
+
meta: {
|
|
159
|
+
file: "translations/codeExamples/next-intl/src/NestedExample.tsx",
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
key: "deepNested.level4.four",
|
|
164
|
+
meta: {
|
|
165
|
+
file: "translations/codeExamples/next-intl/src/NestedExample.tsx",
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
key: "nested.two.regularKey",
|
|
170
|
+
meta: {
|
|
171
|
+
file: "translations/codeExamples/next-intl/src/NestedExample.tsx",
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
key: "nested.two.nestedKey",
|
|
176
|
+
meta: {
|
|
177
|
+
file: "translations/codeExamples/next-intl/src/NestedExample.tsx",
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
key: "nested.nested.two.nestedTwoKey",
|
|
182
|
+
meta: {
|
|
183
|
+
file: "translations/codeExamples/next-intl/src/NestedExample.tsx",
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
key: "nested.one.regularKey",
|
|
188
|
+
meta: {
|
|
189
|
+
file: "translations/codeExamples/next-intl/src/NestedExample.tsx",
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
key: "nested.one.nestedKey",
|
|
194
|
+
meta: {
|
|
195
|
+
file: "translations/codeExamples/next-intl/src/NestedExample.tsx",
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
]);
|
|
199
|
+
});
|
|
200
|
+
it("should find all the async translation keys", () => {
|
|
201
|
+
const keys = (0, nextIntlSrcParser_1.extract)([asyncExampleFile]);
|
|
202
|
+
expect(keys).toEqual([
|
|
203
|
+
{
|
|
204
|
+
key: "async.two.title",
|
|
205
|
+
meta: {
|
|
206
|
+
file: "translations/codeExamples/next-intl/src/AsyncExample.tsx",
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
key: "Async.title",
|
|
211
|
+
meta: {
|
|
212
|
+
file: "translations/codeExamples/next-intl/src/AsyncExample.tsx",
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
]);
|
|
216
|
+
});
|
|
217
|
+
it("should find all dynamic keys defined as comments", () => {
|
|
218
|
+
const keys = (0, nextIntlSrcParser_1.extract)([dynamicKeysExamplFile]);
|
|
219
|
+
expect(keys).toEqual([
|
|
220
|
+
{
|
|
221
|
+
key: "dynamic.three.value",
|
|
222
|
+
meta: {
|
|
223
|
+
file: "translations/codeExamples/next-intl/src/DynamicKeysExample.tsx",
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
key: "dynamic.three.title",
|
|
228
|
+
meta: {
|
|
229
|
+
file: "translations/codeExamples/next-intl/src/DynamicKeysExample.tsx",
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
key: "dynamic.two.value",
|
|
234
|
+
meta: {
|
|
235
|
+
file: "translations/codeExamples/next-intl/src/DynamicKeysExample.tsx",
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
key: "dynamic.two.title",
|
|
240
|
+
meta: {
|
|
241
|
+
file: "translations/codeExamples/next-intl/src/DynamicKeysExample.tsx",
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
key: "dynamic.one.value",
|
|
246
|
+
meta: {
|
|
247
|
+
file: "translations/codeExamples/next-intl/src/DynamicKeysExample.tsx",
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
key: "dynamic.one.title",
|
|
252
|
+
meta: {
|
|
253
|
+
file: "translations/codeExamples/next-intl/src/DynamicKeysExample.tsx",
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
key: "dynamic.four.four",
|
|
258
|
+
meta: {
|
|
259
|
+
file: "translations/codeExamples/next-intl/src/DynamicKeysExample.tsx",
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
key: "dynamic.four.three",
|
|
264
|
+
meta: {
|
|
265
|
+
file: "translations/codeExamples/next-intl/src/DynamicKeysExample.tsx",
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
key: "dynamic.four.two",
|
|
270
|
+
meta: {
|
|
271
|
+
file: "translations/codeExamples/next-intl/src/DynamicKeysExample.tsx",
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
key: "dynamic.four.one",
|
|
276
|
+
meta: {
|
|
277
|
+
file: "translations/codeExamples/next-intl/src/DynamicKeysExample.tsx",
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
key: "dynamic.four.nameFour",
|
|
282
|
+
meta: {
|
|
283
|
+
file: "translations/codeExamples/next-intl/src/DynamicKeysExample.tsx",
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
key: "dynamic.four.nameThree",
|
|
288
|
+
meta: {
|
|
289
|
+
file: "translations/codeExamples/next-intl/src/DynamicKeysExample.tsx",
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
key: "dynamic.four.nameTwo",
|
|
294
|
+
meta: {
|
|
295
|
+
file: "translations/codeExamples/next-intl/src/DynamicKeysExample.tsx",
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
key: "dynamic.four.nameOne",
|
|
300
|
+
meta: {
|
|
301
|
+
file: "translations/codeExamples/next-intl/src/DynamicKeysExample.tsx",
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
]);
|
|
305
|
+
});
|
|
306
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lingual/i18n-check",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "i18n translation messages check",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
"commander": "^12.1.0",
|
|
25
25
|
"glob": "^11.0.1",
|
|
26
26
|
"i18next-parser": "^9.3.0",
|
|
27
|
-
"js-yaml": "^4.1.0"
|
|
27
|
+
"js-yaml": "^4.1.0",
|
|
28
|
+
"typescript": "^5.8.3"
|
|
28
29
|
},
|
|
29
30
|
"devDependencies": {
|
|
30
31
|
"@types/jest": "^29.5.14",
|
|
@@ -33,8 +34,7 @@
|
|
|
33
34
|
"@types/vinyl": "^2.0.12",
|
|
34
35
|
"braces": "^3.0.3",
|
|
35
36
|
"jest": "^29.7.0",
|
|
36
|
-
"ts-jest": "^29.2.6"
|
|
37
|
-
"typescript": "^5.7.3"
|
|
37
|
+
"ts-jest": "^29.2.6"
|
|
38
38
|
},
|
|
39
39
|
"repository": {
|
|
40
40
|
"type": "git",
|