@lingual/i18n-check 0.1.5 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,4 +1,13 @@
1
- # Lingual i18n Check
1
+ # Lingual i18n-check
2
+
3
+ **i18n-check** will help with validating your translation files, including
4
+ checking for missing and/or invalid/broken translations.
5
+ It will compare the defined source language with all target translation files and try to find inconsistencies between source and target files.
6
+ You can run these checks as a pre-commit hook or on the CI depending on your use-case and setup.
7
+
8
+ ![example 1](./assets/i18n-check-example-one.png)
9
+
10
+ ![example 2](./assets/i18n-check-example-two.png)
2
11
 
3
12
  ## Installation and Usage
4
13
 
@@ -66,6 +75,8 @@ Additionally the `i18next` format is supported and can be set via the `-f` or `-
66
75
 
67
76
  There are i18n libraries that have their own specific format, which might not be based on `icu` and therefore can not be validated against currently. On a side-note: there might be future support for more specific formats.
68
77
 
78
+ Hint: If you want to use the `--unused` flag, you should provide `react-intl` as the format. Also see the [`unused` section](#--unused) for more details.
79
+
69
80
  ```bash
70
81
  yarn i18n:check -t translations/i18NextMessageExamples -s translations/i18NextMessageExamples/en-us.json -f i18next
71
82
  ```
@@ -94,6 +105,16 @@ Check for missing an invalid keys (which is the default):
94
105
  yarn i18n:check -t translations/messageExamples -s translations/messageExamples/en-us.json -c missingKeys,invalidKeys
95
106
  ```
96
107
 
108
+ ### --unused
109
+
110
+ This feature is currently only supported for `react-intl` and is useful if you need to know which keys exist in your translation files but not in your codebase. Via the `-u` or `--unused` option you provide a source path to the code, which will be parsed to find all unused keys in the primay target language.
111
+
112
+ It is important to note that you must also provide the `-f` or `--format` option with `react-intl` as value. See the [`format` section](#--format) for more information.
113
+
114
+ ```bash
115
+ yarn i18n:check -t translations/messageExamples -s translations/messageExamples/en-us.json -u client/ -f react-intl
116
+ ```
117
+
97
118
  ### --reporter
98
119
 
99
120
  The standard reporting prints out all the missing or invalid keys.
@@ -146,7 +167,7 @@ If all the locales are organized in a **single folder**:
146
167
  - en-en.json
147
168
  - de-de.json
148
169
 
149
- Use the `d` or `dir` option to define the directory that should be checked for target files. With the `s` or `source` option you can specify the base/reference file to compare the target files against.
170
+ Use the `t` or `target` option to define the directory that should be checked for target files. With the `s` or `source` option you can specify the base/reference file to compare the target files against.
150
171
 
151
172
  ```bash
152
173
  yarn i18n:check -t locales -s locales/en-us.json
@@ -216,7 +237,7 @@ If the locales are **organised as folders** containing multiple json files:
216
237
  Define the `locales` folder as the directory to look for target files and pass `locales/en-US/` as the `source` option. `i18n-check` will try to collect all the files in the provided source directory and compare each one against the corresponding files in the target locales.
217
238
 
218
239
  ```bash
219
- yarn i18n:check -t dirOne,dirTwo -s dirOne/en/,dirTwo/de
240
+ yarn i18n:check -t dirOne,dirTwo -s dirOne/en/,dirTwo/en
220
241
  ```
221
242
 
222
243
  ## As Github Action
@@ -400,3 +421,7 @@ To run the tests use the following command:
400
421
  ```bash
401
422
  pnpm run test // yarn test or npm test
402
423
  ```
424
+
425
+ ## Links
426
+
427
+ [Twitter](https://twitter.com/lingualdev)
package/dist/bin/index.js CHANGED
@@ -29,6 +29,7 @@ commander_1.program
29
29
  .option("-c, --check [checks]", "define the specific checks you want to run: invalid, missing. By default the check will validate against missing and invalid keys, i.e. --check invalidKeys,missingKeys")
30
30
  .option("-r, --reporter [error reporting style]", "define the reporting style: standard or summary")
31
31
  .option("-e, --exclude [file(s), folder(s)]", "define the file(s) and/or folders(s) that should be excluded from the check")
32
+ .option("-u, --unused [folder]", "define the source path to find all unused keys")
32
33
  .parse();
33
34
  const getCheckOptions = () => {
34
35
  const checkOption = commander_1.program.getOptionValue("check");
@@ -66,6 +67,7 @@ const main = () => __awaiter(void 0, void 0, void 0, function* () {
66
67
  const targetPath = commander_1.program.getOptionValue("target");
67
68
  const format = commander_1.program.getOptionValue("format");
68
69
  const exclude = commander_1.program.getOptionValue("exclude");
70
+ const unusedSrcPath = commander_1.program.getOptionValue("unused");
69
71
  if (!srcPath) {
70
72
  console.log(chalk_1.default.red("Source file(s) not found. Please provide valid source file(s), i.e. -s translations/en-us.json"));
71
73
  (0, process_1.exit)(1);
@@ -86,8 +88,8 @@ const main = () => __awaiter(void 0, void 0, void 0, function* () {
86
88
  const files = yield (0, glob_1.glob)(pattern, {
87
89
  ignore: ["node_modules/**"].concat(excludedPaths),
88
90
  });
89
- console.log(chalk_1.default.blue("i18n translations checker"));
90
- console.log(chalk_1.default.blackBright(`Source file(s): ${srcPath}`));
91
+ console.log("i18n translations checker");
92
+ console.log(chalk_1.default.gray(`Source file(s): ${srcPath}`));
91
93
  if (format) {
92
94
  console.log(chalk_1.default.blackBright(`Selected format is: ${format}`));
93
95
  }
@@ -127,7 +129,11 @@ const main = () => __awaiter(void 0, void 0, void 0, function* () {
127
129
  }
128
130
  try {
129
131
  const result = (0, __1.checkTranslations)(srcFiles, targetFiles, options);
130
- print(result);
132
+ printTranslationResult(result);
133
+ if (unusedSrcPath) {
134
+ const unusedKeys = yield (0, __1.checkUnusedKeys)(srcFiles, unusedSrcPath, options);
135
+ printUnusedKeysResult({ unusedKeys });
136
+ }
131
137
  const end = performance.now();
132
138
  console.log(chalk_1.default.green(`\nDone in ${Math.round(((end - start) * 100) / 1000) / 100}s.`));
133
139
  if ((result.missingKeys && Object.keys(result.missingKeys).length > 0) ||
@@ -143,28 +149,71 @@ const main = () => __awaiter(void 0, void 0, void 0, function* () {
143
149
  (0, process_1.exit)(1);
144
150
  }
145
151
  });
146
- const print = ({ missingKeys, invalidKeys, }) => {
152
+ const printTranslationResult = ({ missingKeys, invalidKeys, }) => {
147
153
  const reporter = commander_1.program.getOptionValue("reporter");
148
- const errorReporter = reporter === "summary" ? errorReporters_1.summaryReporter : errorReporters_1.standardReporter;
154
+ const isSummary = reporter === "summary";
149
155
  if (missingKeys && Object.keys(missingKeys).length > 0) {
150
- console.log(chalk_1.default.bgRed(chalk_1.default.white("\nFound missing keys!")));
151
- for (const [lang, missingMessageKeys] of Object.entries(missingKeys)) {
152
- console.log(chalk_1.default.red(`\nIn ${lang}:\n`));
153
- console.log(chalk_1.default.red(errorReporter(missingMessageKeys, "missingKeys")));
156
+ console.log(chalk_1.default.red("\nFound missing keys!"));
157
+ if (isSummary) {
158
+ console.log(chalk_1.default.red((0, errorReporters_1.summaryReporter)(getSummaryRows(missingKeys))));
159
+ }
160
+ else {
161
+ console.log(chalk_1.default.red((0, errorReporters_1.standardReporter)(getStandardRows(missingKeys))));
154
162
  }
155
163
  }
156
164
  else if (missingKeys) {
157
165
  console.log(chalk_1.default.green("\nNo missing keys found!"));
158
166
  }
159
167
  if (invalidKeys && Object.keys(invalidKeys).length > 0) {
160
- console.log(chalk_1.default.bgRed(chalk_1.default.white("\nFound invalid keys!")));
161
- for (const [lang, invalidMessageKeys] of Object.entries(invalidKeys)) {
162
- console.log(chalk_1.default.red(`\nIn ${lang}:\n`));
163
- console.log(chalk_1.default.red(errorReporter(invalidMessageKeys, "invalidKeys")));
168
+ console.log(chalk_1.default.red("\nFound invalid keys!"));
169
+ if (isSummary) {
170
+ console.log(chalk_1.default.red((0, errorReporters_1.summaryReporter)(getSummaryRows(invalidKeys))));
171
+ }
172
+ else {
173
+ console.log(chalk_1.default.red((0, errorReporters_1.standardReporter)(getStandardRows(invalidKeys))));
164
174
  }
165
175
  }
166
176
  else if (invalidKeys) {
167
177
  console.log(chalk_1.default.green("\nNo invalid translations found!"));
168
178
  }
169
179
  };
180
+ const printUnusedKeysResult = ({ unusedKeys, }) => {
181
+ const reporter = commander_1.program.getOptionValue("reporter");
182
+ const isSummary = reporter === "summary";
183
+ if (unusedKeys && Object.keys(unusedKeys).length > 0) {
184
+ console.log(chalk_1.default.red("\nFound unused keys!"));
185
+ if (isSummary) {
186
+ console.log(chalk_1.default.red((0, errorReporters_1.summaryReporter)(getSummaryRows(unusedKeys))));
187
+ }
188
+ else {
189
+ console.log(chalk_1.default.red((0, errorReporters_1.standardReporter)(getStandardRows(unusedKeys))));
190
+ }
191
+ }
192
+ else if (unusedKeys) {
193
+ console.log(chalk_1.default.green("\nNo unused found!"));
194
+ }
195
+ };
196
+ const truncate = (chars) => chars.length > 80 ? `${chars.substring(0, 80)}...` : chars;
197
+ const getSummaryRows = (checkResult) => {
198
+ const formattedRows = [];
199
+ for (const [file, keys] of Object.entries(checkResult)) {
200
+ formattedRows.push({
201
+ file: truncate(file),
202
+ total: keys.length,
203
+ });
204
+ }
205
+ return formattedRows;
206
+ };
207
+ const getStandardRows = (checkResult) => {
208
+ const formattedRows = [];
209
+ for (const [file, keys] of Object.entries(checkResult)) {
210
+ for (const key of keys) {
211
+ formattedRows.push({
212
+ file: truncate(file),
213
+ key: truncate(key),
214
+ });
215
+ }
216
+ }
217
+ return formattedRows;
218
+ };
170
219
  main();
@@ -1,5 +1,11 @@
1
1
  export type Context = "missingKeys" | "invalidKeys";
2
- export type ErrorReporter = (result: string[], context: Context) => void;
3
2
  export declare const contextMapping: Record<Context, string>;
4
- export declare const standardReporter: ErrorReporter;
5
- export declare const summaryReporter: ErrorReporter;
3
+ export declare const standardReporter: (result: {
4
+ file: string;
5
+ key: string;
6
+ }[]) => string;
7
+ export declare const summaryReporter: (result: {
8
+ file: string;
9
+ total: number;
10
+ }[]) => string;
11
+ export declare const createTable: (input: unknown[]) => string;
@@ -1,16 +1,40 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.summaryReporter = exports.standardReporter = exports.contextMapping = void 0;
3
+ exports.createTable = exports.summaryReporter = exports.standardReporter = exports.contextMapping = void 0;
4
+ const console_1 = require("console");
5
+ const stream_1 = require("stream");
4
6
  exports.contextMapping = {
5
7
  invalidKeys: "invalid",
6
8
  missingKeys: "missing",
7
9
  };
8
10
  const standardReporter = (result) => {
9
- return result.map((key) => `◯ ${key}`).join("\n");
11
+ return (0, exports.createTable)(result.map(({ file, key }) => ({ file, key })));
10
12
  };
11
13
  exports.standardReporter = standardReporter;
12
- const summaryReporter = (result, context) => {
13
- const count = result.length;
14
- return `Found ${count} ${exports.contextMapping[context]} ${count === 1 ? "key" : "keys"}.`;
14
+ const summaryReporter = (result) => {
15
+ return (0, exports.createTable)(result.map(({ file, total }) => ({ file, total })));
15
16
  };
16
17
  exports.summaryReporter = summaryReporter;
18
+ const createTable = (input) => {
19
+ // https://stackoverflow.com/a/67859384
20
+ const ts = new stream_1.Transform({
21
+ transform(chunk, enc, cb) {
22
+ cb(null, chunk);
23
+ },
24
+ });
25
+ const logger = new console_1.Console({ stdout: ts });
26
+ logger.table(input);
27
+ const table = (ts.read() || "").toString();
28
+ // https://stackoverflow.com/a/69874540
29
+ let output = "";
30
+ for (let line of table.split(/[\r\n]+/)) {
31
+ output += `${line
32
+ .replace(/[^┬]*┬/, "┌")
33
+ .replace(/^├─*┼/, "├")
34
+ .replace(/│[^│]*/, "")
35
+ .replace(/^└─*┴/, "└")
36
+ .replace(/'/g, " ")}\n`;
37
+ }
38
+ return output;
39
+ };
40
+ exports.createTable = createTable;
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { CheckResult, Translation, TranslationFile } from "./types";
2
2
  import { Context } from "./errorReporters";
3
3
  export type Options = {
4
- format?: "icu" | "i18next";
4
+ format?: "icu" | "i18next" | "react-intl" | "react-i18next";
5
5
  checks?: Context[];
6
6
  };
7
7
  export declare const checkInvalidTranslations: (source: Translation, targets: Record<string, Translation>, options?: Options) => CheckResult;
@@ -10,3 +10,4 @@ export declare const checkTranslations: (source: TranslationFile[], targets: Tra
10
10
  missingKeys: CheckResult | undefined;
11
11
  invalidKeys: CheckResult | undefined;
12
12
  };
13
+ export declare const checkUnusedKeys: (source: TranslationFile[], codebaseSrc: string, options?: Options) => Promise<CheckResult | undefined>;
package/dist/index.js CHANGED
@@ -1,9 +1,20 @@
1
1
  "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
2
11
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.checkTranslations = exports.checkMissingTranslations = exports.checkInvalidTranslations = void 0;
12
+ exports.checkUnusedKeys = exports.checkTranslations = exports.checkMissingTranslations = exports.checkInvalidTranslations = void 0;
4
13
  const findMissingKeys_1 = require("./utils/findMissingKeys");
5
14
  const findInvalidTranslations_1 = require("./utils/findInvalidTranslations");
6
15
  const findInvalidi18nTranslations_1 = require("./utils/findInvalidi18nTranslations");
16
+ const glob_1 = require("glob");
17
+ const cli_lib_1 = require("@formatjs/cli-lib");
7
18
  const checkInvalidTranslations = (source, targets, options = { format: "icu" }) => {
8
19
  return options.format === "i18next"
9
20
  ? (0, findInvalidi18nTranslations_1.findInvalid18nTranslations)(source, targets)
@@ -39,3 +50,34 @@ const checkTranslations = (source, targets, options = { format: "icu", checks: [
39
50
  };
40
51
  };
41
52
  exports.checkTranslations = checkTranslations;
53
+ const checkUnusedKeys = (source, codebaseSrc, options = {
54
+ format: "react-intl",
55
+ }) => __awaiter(void 0, void 0, void 0, function* () {
56
+ if (!options.format || !["react-intl", "i18next"].includes(options.format)) {
57
+ return undefined;
58
+ }
59
+ return options.format === "react-intl"
60
+ ? findUnusedReactIntlTranslations(source, codebaseSrc)
61
+ : undefined;
62
+ });
63
+ exports.checkUnusedKeys = checkUnusedKeys;
64
+ const findUnusedReactIntlTranslations = (source, codebaseSrc) => __awaiter(void 0, void 0, void 0, function* () {
65
+ let unusedKeys = {};
66
+ // find any unused keys in a react-intl code base
67
+ const unsuedKeysFiles = (0, glob_1.globSync)(codebaseSrc, {
68
+ ignore: ["node_modules/**"],
69
+ });
70
+ const extracted = yield (0, cli_lib_1.extract)(unsuedKeysFiles, {});
71
+ const extractedResultSet = new Set(Object.keys(JSON.parse(extracted)));
72
+ source.forEach(({ name, content }) => {
73
+ const keysInSource = Object.keys(content);
74
+ const found = [];
75
+ for (const keyInSource of keysInSource) {
76
+ if (!extractedResultSet.has(keyInSource)) {
77
+ found.push(keyInSource);
78
+ }
79
+ }
80
+ Object.assign(unusedKeys, { [name]: found });
81
+ });
82
+ return unusedKeys;
83
+ });
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@lingual/i18n-check",
3
- "version": "0.1.5",
3
+ "version": "0.2.0",
4
+ "description": "i18n translation messages check",
4
5
  "license": "MIT",
5
6
  "main": "dist/index.js",
6
7
  "module": "dist/index.js",
@@ -16,6 +17,7 @@
16
17
  "dist/"
17
18
  ],
18
19
  "dependencies": {
20
+ "@formatjs/cli-lib": "^6.4.2",
19
21
  "@formatjs/icu-messageformat-parser": "^2.7.6",
20
22
  "chalk": "^4.1.2",
21
23
  "commander": "^11.0.0",
@@ -25,7 +27,12 @@
25
27
  "devDependencies": {
26
28
  "@types/jest": "^29.5.12",
27
29
  "@types/node": "^20.12.12",
30
+ "braces": ">=3.0.3",
28
31
  "jest": "^29.7.0",
29
32
  "ts-jest": "^29.1.2"
33
+ },
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "https://github.com/lingualdev/i18n-check.git"
30
37
  }
31
38
  }