@lingual/i18n-check 0.8.3 → 0.8.5

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
@@ -4,9 +4,9 @@
4
4
  It compares the defined source language with all target translation files and finds inconsistencies between source and target files.
5
5
  You can run these checks as a pre-commit hook or on the CI depending on your use-case and setup.
6
6
 
7
- ![example 1](./assets/i18n-check-screenshot-full.png)
7
+ ![example 1](./assets/i18n_check_multipleFilesFolderExample_basic_v2.png)
8
8
 
9
- ![example 2](./assets/i18n-check-screenshot-summary.png)
9
+ ![example 2](./assets/i18n_check_multipleFilesFolderExample_react_intl_unused_v2.png)
10
10
 
11
11
  ## Table of Contents
12
12
 
@@ -106,7 +106,7 @@ Additionally the `i18next` format is supported and can be set via the `-f` or `-
106
106
 
107
107
  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.
108
108
 
109
- Hint: If you want to use the `--unused` flag, you should provide `react-intl` or `i18-next` as the format. Also see the [`unused` section](#--unused) for more details.
109
+ Hint: If you want to use the `--unused` flag, you should provide `react-intl` or `i18-next` as the format. Also see the [`unused` section](#--unused--u) for more details.
110
110
 
111
111
  ```bash
112
112
  yarn i18n:check --locales translations/i18NextMessageExamples -s en-US -f i18next
@@ -165,7 +165,7 @@ This feature is currently only supported for `react-intl` and `i18next` as well
165
165
 
166
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.
167
167
 
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.
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--f) for more information.
169
169
 
170
170
  ```bash
171
171
  yarn i18n:check --locales translations/messageExamples -s en-US -u client/ -f react-intl
@@ -223,6 +223,28 @@ yarn i18n:check --locales translations/folderExamples -s en-US -e translations/f
223
223
  The `--exclude` option also accepts a mix of files and folders, which follows the same pattern as above, i.e.
224
224
  `-e translations/folderExamples/fr/* translations/messageExamples/it.json`
225
225
 
226
+ ### --ignore, -i
227
+
228
+ There can be situations where we only want to translate a feature for a specific region and therefore need to ignore any missing key checks against non supported locales. Another scenario is that we know of the missing keys and want to be able to skip these missing keys when running checks. For these aforementioned scenarios, by using the `--ignore` or `-i` option you can specify which keys to ignore, additionally also being able to define ignoring all keys inside a defined path, i.e. `some.keys.path.*`.
229
+
230
+ To ignore regular keys:
231
+
232
+ ```bash
233
+ yarn i18n:check --locales translations/folderExamples -s en-US -i some.key.to.ignore other.key.to.ignore
234
+ ```
235
+
236
+ To ignore all keys within a provided path:
237
+
238
+ ```bash
239
+ yarn i18n:check --locales translations/folderExamples -s en-US -i "some.path.to.keys.*"
240
+ ```
241
+
242
+ A mix of regular keys and paths:
243
+
244
+ ```bash
245
+ yarn i18n:check --locales translations/folderExamples -s en-US -i "some.path.to.keys.*" some.key.to.ignore other.key.to.ignore
246
+ ```
247
+
226
248
  ### --parser-component-functions
227
249
 
228
250
  When using the `--unused` option, there will be situations where the i18next-parser will not be able to find components that wrap a `Trans` component.The component names for i18next-parser to match should be provided via the `--parser-component-functions` option. This option should onlybe used to define additional names for matching, a by default `Trans` will always be matched.
package/dist/bin/index.js CHANGED
@@ -24,6 +24,7 @@ commander_1.program
24
24
  .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')
25
25
  .option('-r, --reporter <style>', 'define the reporting style: standard or summary')
26
26
  .option('-e, --exclude <exclude...>', 'define the file(s) and/or folders(s) that should be excluded from the check')
27
+ .option('-i, --ignore <ignore...>', 'define the key(s) or group of keys (i.e. `some.namespace.*`) that should be excluded from the check')
27
28
  .option('-u, --unused <paths...>', 'define the source path(s) to find all unused and undefined keys')
28
29
  .option('--parser-component-functions <components...>', 'a list of component names to parse when using the --unused option')
29
30
  .parse();
@@ -47,6 +48,7 @@ const main = async () => {
47
48
  const localePath = commander_1.program.getOptionValue('locales');
48
49
  const format = commander_1.program.getOptionValue('format');
49
50
  const exclude = commander_1.program.getOptionValue('exclude');
51
+ const ignore = commander_1.program.getOptionValue('ignore');
50
52
  const unusedSrcPath = commander_1.program.getOptionValue('unused');
51
53
  const componentFunctions = commander_1.program.getOptionValue('parserComponentFunctions');
52
54
  if (!srcPath) {
@@ -77,6 +79,7 @@ const main = async () => {
77
79
  const options = {
78
80
  checks: getCheckOptions(),
79
81
  format: format ?? undefined,
82
+ ignore,
80
83
  };
81
84
  const fileInfos = [];
82
85
  files.sort().forEach((file) => {
@@ -157,6 +160,7 @@ const main = async () => {
157
160
  }
158
161
  try {
159
162
  const result = (0, __1.checkTranslations)(srcFiles, targetFiles, options);
163
+ let undefinedKeyResult = undefined;
160
164
  printTranslationResult(result);
161
165
  if (unusedSrcPath) {
162
166
  const isMultiUnusedFolders = unusedSrcPath.length > 1;
@@ -173,11 +177,13 @@ const main = async () => {
173
177
  printUndefinedKeysResult({
174
178
  undefinedKeys,
175
179
  });
180
+ undefinedKeyResult = undefinedKeys;
176
181
  }
177
182
  const end = performance.now();
178
183
  console.log(chalk_1.default.green(`\nDone in ${Math.round(((end - start) * 100) / 1000) / 100}s.`));
179
184
  if ((result.missingKeys && Object.keys(result.missingKeys).length > 0) ||
180
- (result.invalidKeys && Object.keys(result.invalidKeys).length > 0)) {
185
+ (result.invalidKeys && Object.keys(result.invalidKeys).length > 0) ||
186
+ (undefinedKeyResult && Object.keys(undefinedKeyResult).length > 0)) {
181
187
  (0, node_process_1.exit)(1);
182
188
  }
183
189
  else {
package/dist/index.d.ts CHANGED
@@ -3,6 +3,7 @@ import { Context } from './errorReporters';
3
3
  export type Options = {
4
4
  format?: 'icu' | 'i18next' | 'react-intl' | 'next-intl';
5
5
  checks?: Context[];
6
+ ignore?: string[];
6
7
  };
7
8
  export declare const checkInvalidTranslations: (source: Translation, targets: Record<string, Translation>, options?: Options) => InvalidTranslationsResult;
8
9
  export declare const checkMissingTranslations: (source: Translation, targets: Record<string, Translation>) => CheckResult;
package/dist/index.js CHANGED
@@ -26,22 +26,23 @@ const checkTranslations = (source, targets, options = { format: 'icu', checks: [
26
26
  const { checks = ['invalidKeys', 'missingKeys'] } = options;
27
27
  const missingKeys = {};
28
28
  const invalidKeys = {};
29
- const hasMissingKeys = checks.includes('missingKeys');
30
- const hasInvalidKeys = checks.includes('invalidKeys');
29
+ const hasMissingKeysCheck = checks.includes('missingKeys');
30
+ const hasInvalidKeysCheck = checks.includes('invalidKeys');
31
31
  source.forEach(({ name, content }) => {
32
32
  const files = Object.fromEntries(targets
33
33
  .filter(({ reference }) => reference === name)
34
34
  .map(({ name, content }) => [name, content]));
35
- if (hasMissingKeys) {
36
- merge(missingKeys, (0, exports.checkMissingTranslations)(content, files));
35
+ if (hasMissingKeysCheck) {
36
+ const filteredContent = filterKeys(content, options.ignore ?? []);
37
+ merge(missingKeys, (0, exports.checkMissingTranslations)(filteredContent, files));
37
38
  }
38
- if (hasInvalidKeys) {
39
+ if (hasInvalidKeysCheck) {
39
40
  merge(invalidKeys, (0, exports.checkInvalidTranslations)(content, files, options));
40
41
  }
41
42
  });
42
43
  return {
43
- missingKeys: hasMissingKeys ? missingKeys : undefined,
44
- invalidKeys: hasInvalidKeys ? invalidKeys : undefined,
44
+ missingKeys: hasMissingKeysCheck ? missingKeys : undefined,
45
+ invalidKeys: hasInvalidKeysCheck ? invalidKeys : undefined,
45
46
  };
46
47
  };
47
48
  exports.checkTranslations = checkTranslations;
@@ -102,7 +103,7 @@ const findUnusedI18NextTranslations = async (source, filesToParse, componentFunc
102
103
  continue;
103
104
  }
104
105
  // find the file name
105
- const [fileName] = (name.split(path_1.default.sep).pop() ?? "").split(".");
106
+ const [fileName] = (name.split(path_1.default.sep).pop() ?? '').split('.');
106
107
  if (!extractedResultSet.has(`${fileName}.${keyInSource}`) &&
107
108
  !extractedResultSet.has(keyInSource)) {
108
109
  found.push(keyInSource);
@@ -270,14 +271,14 @@ const getI18NextKeysInCode = async (filesToParse, componentFunctions = []) => {
270
271
  // The current implementation considers the key as used no matter the namespace.
271
272
  for (const entry of entries) {
272
273
  // check for namespace, i.e. `namespace:some.key`
273
- const [namespace, ...keyParts] = entry.key.split(":");
274
+ const [namespace, ...keyParts] = entry.key.split(':');
274
275
  // If there is a namespace make sure to assign the namespace
275
276
  // and update the key name
276
277
  // Ensure that the assumed key is not the default value
277
278
  if (keyParts.length > 0 && entry.key !== entry.defaultValue) {
278
279
  entry.namespace = namespace;
279
280
  // rebuild the key without the namespace
280
- entry.key = keyParts.join(":");
281
+ entry.key = keyParts.join(':');
281
282
  }
282
283
  if (entry.returnObjects) {
283
284
  skippableKeys.push(entry.key);
@@ -293,6 +294,23 @@ const getI18NextKeysInCode = async (filesToParse, componentFunctions = []) => {
293
294
  });
294
295
  return { extractedResult, skippableKeys };
295
296
  };
297
+ const filterKeys = (content, keysToIgnore = []) => {
298
+ if (keysToIgnore.length > 0) {
299
+ return Object.entries(content).reduce((acc, [key, value]) => {
300
+ if (keysToIgnore.find((ignoreKey) => {
301
+ if (ignoreKey.endsWith('*')) {
302
+ return key.includes(ignoreKey.slice(0, ignoreKey.length - 1));
303
+ }
304
+ return ignoreKey === key;
305
+ })) {
306
+ return acc;
307
+ }
308
+ acc[key] = value;
309
+ return acc;
310
+ }, {});
311
+ }
312
+ return content;
313
+ };
296
314
  function _flatten(object, prefix = null, result = {}) {
297
315
  for (const key in object) {
298
316
  const propName = prefix ? `${prefix}.${key}` : key;
@@ -140,6 +140,49 @@ const getKeys = (path) => {
140
140
  }
141
141
  }
142
142
  }
143
+ // Check if getTranslations is called inside a promise.all
144
+ // Example:
145
+ // const [data, t] = await Promise.all([
146
+ // loadData(id),
147
+ // getTranslations('asyncPromiseAll'),
148
+ // ]);
149
+ if (ts.isCallExpression(node.initializer.expression) &&
150
+ node.initializer.expression.arguments.length > 0 &&
151
+ ts.isArrayLiteralExpression(node.initializer.expression.arguments[0])) {
152
+ const functionNameIndex = node.initializer.expression.arguments[0].elements.findIndex((argument) => {
153
+ return (ts.isCallExpression(argument) &&
154
+ ts.isIdentifier(argument.expression) &&
155
+ argument.expression.text === GET_TRANSLATIONS);
156
+ });
157
+ // Try to find the correct function name via the position in the variable declaration
158
+ if (functionNameIndex !== -1 &&
159
+ ts.isArrayBindingPattern(node.name) &&
160
+ ts.isBindingElement(node.name.elements[functionNameIndex]) &&
161
+ ts.isIdentifier(node.name.elements[functionNameIndex].name)) {
162
+ const variable = node.name.elements[functionNameIndex].name.text;
163
+ const [argument] = ts.isCallExpression(node.initializer.expression.arguments[0].elements[functionNameIndex])
164
+ ? node.initializer.expression.arguments[0].elements[functionNameIndex].arguments
165
+ : [];
166
+ if (argument && ts.isObjectLiteralExpression(argument)) {
167
+ argument.properties.forEach((property) => {
168
+ if (property &&
169
+ ts.isPropertyAssignment(property) &&
170
+ property.name &&
171
+ ts.isIdentifier(property.name) &&
172
+ property.name.text === 'namespace' &&
173
+ ts.isStringLiteral(property.initializer)) {
174
+ pushNamespace({ name: property.initializer.text, variable });
175
+ }
176
+ });
177
+ }
178
+ else if (argument && ts.isStringLiteral(argument)) {
179
+ pushNamespace({ name: argument.text, variable });
180
+ }
181
+ else if (argument === undefined) {
182
+ pushNamespace({ name: '', variable });
183
+ }
184
+ }
185
+ }
143
186
  }
144
187
  }
145
188
  // Search for direct inline calls and extract namespace and key
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lingual/i18n-check",
3
- "version": "0.8.3",
3
+ "version": "0.8.5",
4
4
  "description": "i18n translation messages check",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -10,7 +10,7 @@
10
10
  },
11
11
  "types": "dist/index.d.ts",
12
12
  "scripts": {
13
- "build": "tsc",
13
+ "build": "tsc -p tsconfig.production.json",
14
14
  "format": "prettier --write .",
15
15
  "lint": "eslint src",
16
16
  "lint:fix": "eslint src --fix ",
@@ -19,31 +19,32 @@
19
19
  "test:cli": "tsc && jest src/bin/index.test.ts"
20
20
  },
21
21
  "files": [
22
- "dist/"
22
+ "dist/",
23
+ "!dist/**/*.test.*"
23
24
  ],
24
25
  "dependencies": {
25
26
  "@formatjs/cli-lib": "^6.6.6",
26
27
  "@formatjs/icu-messageformat-parser": "^2.11.2",
27
28
  "chalk": "^4.1.2",
28
29
  "commander": "^12.1.0",
29
- "glob": "^11.0.2",
30
+ "glob": "11.0.2",
30
31
  "i18next-parser": "^9.3.0",
31
32
  "js-yaml": "^4.1.0",
32
33
  "typescript": "^5.8.3"
33
34
  },
34
35
  "devDependencies": {
35
- "@eslint/js": "^9.26.0",
36
+ "@eslint/js": "^9.30.1",
36
37
  "@types/jest": "^29.5.14",
37
38
  "@types/js-yaml": "^4.0.9",
38
- "@types/node": "^22.14.1",
39
+ "@types/node": "^22.16.0",
39
40
  "@types/vinyl": "^2.0.12",
40
41
  "braces": "^3.0.3",
41
- "eslint": "^9.26.0",
42
- "globals": "^16.0.0",
42
+ "eslint": "^9.30.1",
43
+ "globals": "^16.3.0",
43
44
  "jest": "^29.7.0",
44
- "prettier": "^3.5.3",
45
- "ts-jest": "^29.3.2",
46
- "typescript-eslint": "^8.31.1"
45
+ "prettier": "^3.6.2",
46
+ "ts-jest": "^29.4.0",
47
+ "typescript-eslint": "^8.35.1"
47
48
  },
48
49
  "repository": {
49
50
  "type": "git",
@@ -1 +0,0 @@
1
- export {};