@lingual/i18n-check 0.5.5 → 0.6.0

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