@lingual/i18n-check 0.6.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -114,35 +114,58 @@ yarn i18n:check --locales translations/i18NextMessageExamples -s en-US -f i18nex
114
114
 
115
115
  ### --only, -o
116
116
 
117
- By default i18n-check will perform a validation against any **missing** and/or **invalid** keys. There are situations where only a specific check should run. By using the `-o` or `--only` option you can specify a specific check to run.
117
+ By default i18n-check will perform a validation against any **missing** and/or **invalid** keys, additionally **unused** and **undefined** checks if the `--unused` option is set. There are situations where only a specific check should run. By using the `-o` or `--only` option you can specify a specific check to run.
118
118
 
119
- The available options are `missingKeys`, which will check against any missing keys in the target files and `invalidKeys` will check for invalid keys, where the target translations has a different type then the one defined in the source file.
119
+ The available options are:
120
120
 
121
- Check for missing keys:
121
+ - `missingKeys`: will check against any missing keys in the target files.
122
+ - `invalidKeys`: will check for invalid keys, where the target translations has a different type then the one defined in the source file.
123
+ - `unused`: will check for any locale keys that do not exist in the codebase.
124
+ - `undefined`: will check for any keys that exist in the codebase but not in the source locale files.
125
+
126
+ Check for missing keys only:
122
127
 
123
128
  ```bash
124
129
  yarn i18n:check --locales translations/messageExamples -s en-US -o missingKeys
125
130
  ```
126
131
 
127
- Check for invalid keys:
132
+ Check for invalid keys only:
128
133
 
129
134
  ```bash
130
135
  yarn i18n:check --locales translations/messageExamples -s en-US -o invalidKeys
131
136
  ```
132
137
 
138
+ Check for unused key only:
139
+
140
+ ```bash
141
+ yarn i18n:check --locales translations/messageExamples -s en-US -o unused
142
+ ```
143
+
144
+ Check for undefined keys only:
145
+
146
+ ```bash
147
+ yarn i18n:check --locales translations/messageExamples -s en-US -o undefined
148
+ ```
149
+
133
150
  Check for missing and invalid keys (which is the default):
134
151
 
135
152
  ```bash
136
153
  yarn i18n:check --locales translations/messageExamples -s en-US -o missingKeys invalidKeys
137
154
  ```
138
155
 
156
+ Check for unused and undefined keys only:
157
+
158
+ ```bash
159
+ yarn i18n:check --locales translations/messageExamples -s en-US -o unused undefined
160
+ ```
161
+
139
162
  ### --unused, -u
140
163
 
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.
164
+ 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
165
 
143
166
  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
167
 
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.
168
+ 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.
146
169
 
147
170
  ```bash
148
171
  yarn i18n:check --locales translations/messageExamples -s en-US -u client/ -f react-intl
@@ -154,6 +177,12 @@ or
154
177
  yarn i18n:check --locales translations/messageExamples -s en-US -u client/ -f i18next
155
178
  ```
156
179
 
180
+ or
181
+
182
+ ```bash
183
+ yarn i18n:check --locales translations/messageExamples -s en-US -u client/ -f next-intl
184
+ ```
185
+
157
186
  ### --reporter, -r
158
187
 
159
188
  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,9 +18,9 @@ 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 react-intl")
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
- .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")
23
+ .option("-o, --only <only...>", "define the specific checks you want to run: invalidKeys, missingKeys, unused, undefined. 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
26
  .option("-u, --unused <path>", "define the source path to find all unused and undefined keys")
@@ -41,15 +32,15 @@ const getCheckOptions = () => {
41
32
  console.log(chalk_1.default.yellow("The --check option has been deprecated, use the --only option instead."));
42
33
  }
43
34
  if (!checkOption) {
44
- return ["invalidKeys", "missingKeys"];
35
+ return errorReporters_1.CheckOptions;
45
36
  }
46
- const checks = checkOption.filter((check) => ["invalidKeys", "missingKeys"].includes(check.trim()));
47
- return checks.length > 0 ? checks : ["invalidKeys", "missingKeys"];
37
+ const checks = checkOption.filter((check) => errorReporters_1.CheckOptions.includes(check.trim()));
38
+ return checks.length > 0 ? checks : errorReporters_1.CheckOptions;
48
39
  };
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 = () => __awaiter(void 0, void 0, void 0, function* () {
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 !== null && exclude !== void 0 ? 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 = yield (0, glob_1.glob)(pattern, {
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 !== null && format !== void 0 ? format : undefined,
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 = (_a = path.pop()) !== null && _a !== void 0 ? _a : "";
93
- const extension = (_b = name.split(".").pop()) !== null && _b !== void 0 ? _b : "json";
82
+ const name = path.pop() ?? "";
83
+ const extension = name.split(".").pop() ?? "json";
94
84
  fileInfos.push({
95
85
  extension,
96
86
  file,
@@ -168,9 +158,9 @@ const main = () => __awaiter(void 0, void 0, void 0, function* () {
168
158
  const filesToParse = (0, glob_1.globSync)(`${unusedSrcPath}/**/*.{ts,tsx}`, {
169
159
  ignore: ["node_modules/**"],
170
160
  });
171
- const unusedKeys = yield (0, __1.checkUnusedKeys)(srcFiles, filesToParse, options, componentFunctions);
161
+ const unusedKeys = await (0, __1.checkUnusedKeys)(srcFiles, filesToParse, options, componentFunctions);
172
162
  printUnusedKeysResult({ unusedKeys });
173
- const undefinedKeys = yield (0, __1.checkUndefinedKeys)(srcFiles, filesToParse, options, componentFunctions);
163
+ const undefinedKeys = await (0, __1.checkUndefinedKeys)(srcFiles, filesToParse, options, componentFunctions);
174
164
  printUndefinedKeysResult({
175
165
  undefinedKeys,
176
166
  });
@@ -189,7 +179,7 @@ const main = () => __awaiter(void 0, void 0, void 0, function* () {
189
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"));
190
180
  (0, node_process_1.exit)(1);
191
181
  }
192
- });
182
+ };
193
183
  const printTranslationResult = ({ missingKeys, invalidKeys, }) => {
194
184
  const reporter = commander_1.program.getOptionValue("reporter");
195
185
  const isSummary = reporter === "summary";
@@ -295,7 +295,7 @@ Found undefined keys!
295
295
  });
296
296
  });
297
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) => {
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
299
  const result = stdout.split("Done")[0];
300
300
  expect(result).toEqual(`i18n translations checker
301
301
  Source: en-US
@@ -320,6 +320,36 @@ Found undefined keys!
320
320
  │ translations/codeExamples/react-intl/src/App.tsx │ some.key.that.is.not.defined │
321
321
  └────────────────────────────────────────────────────┴────────────────────────────────┘
322
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
+
323
353
  `);
324
354
  done();
325
355
  });
@@ -1,4 +1,5 @@
1
- export type Context = "missingKeys" | "invalidKeys";
1
+ export declare const CheckOptions: string[];
2
+ export type Context = (typeof CheckOptions)[number];
2
3
  export declare const contextMapping: Record<Context, string>;
3
4
  export declare const standardReporter: (result: {
4
5
  file: string;
@@ -1,11 +1,19 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.createTable = exports.summaryReporter = exports.standardReporter = exports.contextMapping = void 0;
3
+ exports.createTable = exports.summaryReporter = exports.standardReporter = exports.contextMapping = exports.CheckOptions = void 0;
4
4
  const console_1 = require("console");
5
5
  const stream_1 = require("stream");
6
+ exports.CheckOptions = [
7
+ "invalidKeys",
8
+ "missingKeys",
9
+ "unused",
10
+ "undefined",
11
+ ];
6
12
  exports.contextMapping = {
7
13
  invalidKeys: "invalid",
8
14
  missingKeys: "missing",
15
+ unused: "unused",
16
+ undefined: "undefined",
9
17
  };
10
18
  const standardReporter = (result) => {
11
19
  return (0, exports.createTable)(result.map(({ file, key }) => ({ file, key })));
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" | "react-i18next";
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;
package/dist/index.js CHANGED
@@ -1,13 +1,4 @@
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
  };
@@ -17,7 +8,9 @@ const findMissingKeys_1 = require("./utils/findMissingKeys");
17
8
  const findInvalidTranslations_1 = require("./utils/findInvalidTranslations");
18
9
  const findInvalidi18nTranslations_1 = require("./utils/findInvalidi18nTranslations");
19
10
  const cli_lib_1 = require("@formatjs/cli-lib");
11
+ const nextIntlSrcParser_1 = require("./utils/nextIntlSrcParser");
20
12
  const fs_1 = __importDefault(require("fs"));
13
+ const ParseFormats = ["react-intl", "i18next", "next-intl"];
21
14
  const checkInvalidTranslations = (source, targets, options = { format: "icu" }) => {
22
15
  return options.format === "i18next"
23
16
  ? (0, findInvalidi18nTranslations_1.findInvalid18nTranslations)(source, targets)
@@ -53,20 +46,30 @@ const checkTranslations = (source, targets, options = { format: "icu", checks: [
53
46
  };
54
47
  };
55
48
  exports.checkTranslations = checkTranslations;
56
- const checkUnusedKeys = (translationFiles_1, filesToParse_1, ...args_1) => __awaiter(void 0, [translationFiles_1, filesToParse_1, ...args_1], void 0, function* (translationFiles, filesToParse, options = {
49
+ const checkUnusedKeys = async (translationFiles, filesToParse, options = {
57
50
  format: "react-intl",
58
- }, componentFunctions = []) {
59
- if (!options.format || !["react-intl", "i18next"].includes(options.format)) {
51
+ checks: [],
52
+ }, componentFunctions = []) => {
53
+ if (!options.format || !ParseFormats.includes(options.format)) {
54
+ return undefined;
55
+ }
56
+ if (!options.checks || !options.checks.includes("unused")) {
60
57
  return undefined;
61
58
  }
62
- return options.format === "react-intl"
63
- ? findUnusedReactIntlTranslations(translationFiles, filesToParse)
64
- : findUnusedI18NextTranslations(translationFiles, filesToParse, componentFunctions);
65
- });
59
+ if (options.format === "react-intl") {
60
+ return findUnusedReactIntlTranslations(translationFiles, filesToParse);
61
+ }
62
+ else if (options.format === "i18next") {
63
+ return findUnusedI18NextTranslations(translationFiles, filesToParse, componentFunctions);
64
+ }
65
+ else if (options.format === "next-intl") {
66
+ return findUnusedNextIntlTranslations(translationFiles, filesToParse);
67
+ }
68
+ };
66
69
  exports.checkUnusedKeys = checkUnusedKeys;
67
- const findUnusedReactIntlTranslations = (translationFiles, keysInCode) => __awaiter(void 0, void 0, void 0, function* () {
70
+ const findUnusedReactIntlTranslations = async (translationFiles, filesToParse) => {
68
71
  let unusedKeys = {};
69
- const extracted = yield (0, cli_lib_1.extract)(keysInCode, {});
72
+ const extracted = await (0, cli_lib_1.extract)(filesToParse, {});
70
73
  const extractedResultSet = new Set(Object.keys(JSON.parse(extracted)));
71
74
  translationFiles.forEach(({ name, content }) => {
72
75
  const keysInSource = Object.keys(content);
@@ -79,10 +82,10 @@ const findUnusedReactIntlTranslations = (translationFiles, keysInCode) => __awai
79
82
  Object.assign(unusedKeys, { [name]: found });
80
83
  });
81
84
  return unusedKeys;
82
- });
83
- const findUnusedI18NextTranslations = (source_1, filesToParse_1, ...args_1) => __awaiter(void 0, [source_1, filesToParse_1, ...args_1], void 0, function* (source, filesToParse, componentFunctions = []) {
85
+ };
86
+ const findUnusedI18NextTranslations = async (source, filesToParse, componentFunctions = []) => {
84
87
  let unusedKeys = {};
85
- const { extractedResult, skippableKeys } = yield getI18NextKeysInCode(filesToParse, componentFunctions);
88
+ const { extractedResult, skippableKeys } = await getI18NextKeysInCode(filesToParse, componentFunctions);
86
89
  const extractedResultSet = new Set(extractedResult.map(({ key }) => key));
87
90
  source.forEach(({ name, content }) => {
88
91
  const keysInSource = Object.keys(content);
@@ -101,23 +104,49 @@ const findUnusedI18NextTranslations = (source_1, filesToParse_1, ...args_1) => _
101
104
  Object.assign(unusedKeys, { [name]: found });
102
105
  });
103
106
  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 = {
107
+ };
108
+ const findUnusedNextIntlTranslations = async (translationFiles, filesToParse) => {
109
+ let unusedKeys = {};
110
+ const extracted = (0, nextIntlSrcParser_1.extract)(filesToParse);
111
+ const extractedResultSet = new Set(extracted.map(({ key }) => key));
112
+ translationFiles.forEach(({ name, content }) => {
113
+ const keysInSource = Object.keys(content);
114
+ const found = [];
115
+ for (const keyInSource of keysInSource) {
116
+ if (!extractedResultSet.has(keyInSource)) {
117
+ found.push(keyInSource);
118
+ }
119
+ }
120
+ Object.assign(unusedKeys, { [name]: found });
121
+ });
122
+ return unusedKeys;
123
+ };
124
+ const checkUndefinedKeys = async (source, filesToParse, options = {
106
125
  format: "react-intl",
107
- }, componentFunctions = []) {
108
- if (!options.format || !["react-intl", "i18next"].includes(options.format)) {
126
+ checks: [],
127
+ }, componentFunctions = []) => {
128
+ if (!options.format || !ParseFormats.includes(options.format)) {
109
129
  return undefined;
110
130
  }
111
- return options.format === "react-intl"
112
- ? findUndefinedReactIntlKeys(source, filesToParse)
113
- : findUndefinedI18NextKeys(source, filesToParse, componentFunctions);
114
- });
131
+ if (!options.checks || !options.checks.includes("undefined")) {
132
+ return undefined;
133
+ }
134
+ if (options.format === "react-intl") {
135
+ return findUndefinedReactIntlKeys(source, filesToParse);
136
+ }
137
+ else if (options.format === "i18next") {
138
+ return findUndefinedI18NextKeys(source, filesToParse, componentFunctions);
139
+ }
140
+ else if (options.format === "next-intl") {
141
+ return findUndefinedNextIntlKeys(source, filesToParse);
142
+ }
143
+ };
115
144
  exports.checkUndefinedKeys = checkUndefinedKeys;
116
- const findUndefinedReactIntlKeys = (translationFiles, keysInCode) => __awaiter(void 0, void 0, void 0, function* () {
145
+ const findUndefinedReactIntlKeys = async (translationFiles, filesToParse) => {
117
146
  const sourceKeys = new Set(translationFiles.flatMap(({ content }) => {
118
147
  return Object.keys(content);
119
148
  }));
120
- const extractedResult = yield (0, cli_lib_1.extract)(keysInCode, {
149
+ const extractedResult = await (0, cli_lib_1.extract)(filesToParse, {
121
150
  extractSourceLocation: true,
122
151
  });
123
152
  let undefinedKeys = {};
@@ -132,9 +161,9 @@ const findUndefinedReactIntlKeys = (translationFiles, keysInCode) => __awaiter(v
132
161
  }
133
162
  });
134
163
  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);
164
+ };
165
+ const findUndefinedI18NextKeys = async (source, filesToParse, componentFunctions = []) => {
166
+ const { extractedResult, skippableKeys } = await getI18NextKeysInCode(filesToParse, componentFunctions);
138
167
  const sourceKeys = new Set(source.flatMap(({ content }) => {
139
168
  return Object.keys(content);
140
169
  }));
@@ -151,16 +180,34 @@ const findUndefinedI18NextKeys = (source_1, filesToParse_1, ...args_1) => __awai
151
180
  }
152
181
  });
153
182
  return undefinedKeys;
154
- });
183
+ };
184
+ const findUndefinedNextIntlKeys = async (translationFiles, filesToParse) => {
185
+ const sourceKeys = new Set(translationFiles.flatMap(({ content }) => {
186
+ return Object.keys(content);
187
+ }));
188
+ const extractedResult = (0, nextIntlSrcParser_1.extract)(filesToParse);
189
+ let undefinedKeys = {};
190
+ extractedResult.forEach(({ key, meta }) => {
191
+ if (!sourceKeys.has(key)) {
192
+ // @ts-ignore
193
+ const file = meta.file;
194
+ if (!undefinedKeys[file]) {
195
+ undefinedKeys[file] = [];
196
+ }
197
+ undefinedKeys[file].push(key);
198
+ }
199
+ });
200
+ return undefinedKeys;
201
+ };
155
202
  const isRecord = (data) => {
156
203
  return (typeof data === "object" &&
157
204
  !Array.isArray(data) &&
158
205
  data !== null &&
159
206
  data !== undefined);
160
207
  };
161
- const getI18NextKeysInCode = (filesToParse_1, ...args_1) => __awaiter(void 0, [filesToParse_1, ...args_1], void 0, function* (filesToParse, componentFunctions = []) {
208
+ const getI18NextKeysInCode = async (filesToParse, componentFunctions = []) => {
162
209
  // @ts-ignore
163
- const { transform } = yield import("i18next-parser");
210
+ const { transform } = await import("i18next-parser");
164
211
  const i18nextParser = new transform({
165
212
  lexers: {
166
213
  jsx: [
@@ -199,7 +246,7 @@ const getI18NextKeysInCode = (filesToParse_1, ...args_1) => __awaiter(void 0, [f
199
246
  }
200
247
  });
201
248
  return { extractedResult, skippableKeys };
202
- });
249
+ };
203
250
  function flatten(object, prefix = null, result = {}) {
204
251
  for (let key in object) {
205
252
  let propName = prefix ? `${prefix}.${key}` : key;
@@ -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)(Object.assign(Object.assign({}, sourceFile), { "ten.eleven.twelve": "ten eleven twelve" })), (0, flattenTranslations_1.flattenTranslations)(secondaryFile))).toEqual(["multipleVariables"]);
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)(Object.assign(Object.assign({}, sourceFile), { "ten.eleven.twelve": "ten eleven twelve" }), { de: secondaryFile })).toEqual({ de: ["multipleVariables"] });
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)(Object.assign(Object.assign({}, sourceFile), { "ten.eleven.twelve": "ten eleven twelve" }), {
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: Object.assign(Object.assign({}, secondaryFile), { "message.plural": "{count, plural, other {# of {total} items}}" })
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: Object.assign(Object.assign({}, secondaryFile), { "message.plural": "{count, plural, other {# of {cargado} items}}" })
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)(Object.assign(Object.assign({}, sourceFile), { "ten.eleven.twelve": "ten eleven twelve" })), (0, flattenTranslations_1.flattenTranslations)(targetFile))).toEqual(["key_with_broken_de", "intlNumber_broken_de"]);
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)(Object.assign(Object.assign({}, sourceFile), { "ten.eleven.twelve": "ten eleven twelve" }), { de: targetFile })).toEqual({ de: ["key_with_broken_de", "intlNumber_broken_de"] });
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)(Object.assign(Object.assign({}, sourceFile), { "ten.eleven.twelve": "ten eleven twelve" }), {
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)(Object.assign(Object.assign({}, sourceFile), { "ten.eleven.twelve": "ten eleven twelve" }), secondaryFile)).toEqual(["ten.eleven.twelve"]);
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)(Object.assign(Object.assign({}, sourceFile), { "ten.eleven.twelve": "ten eleven twelve" }), { de: secondaryFile })).toEqual({ de: ["ten.eleven.twelve"] });
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)(Object.assign(Object.assign({}, sourceFile), { "ten.eleven.twelve": "ten eleven twelve" }), {
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,6 @@
1
+ export declare const extract: (filesPaths: string[]) => {
2
+ key: string;
3
+ meta: {
4
+ file: string;
5
+ };
6
+ }[];
@@ -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.6.0",
3
+ "version": "0.7.1",
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",