@lingual/i18n-check 0.9.3 → 0.9.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
@@ -259,6 +259,34 @@ yarn i18n:check --locales translations/i18NextMessageExamples -s en-US -f i18nex
259
259
  -u src --parser-component-functions WrappedTransComponent AnotherWrappedTransComponent
260
260
  ```
261
261
 
262
+ ### --next-intl-translation-fn-type-alias
263
+
264
+ To find any indirect function calls inside a `next-intl` codebase, the parser will try to find the type `ReturnType<typeof useTranslations>` in the function call arguments.
265
+
266
+ ```ts
267
+ const indirectFnCall = (
268
+ t: ReturnType<typeof useTranslations<'SomeNameSpace'>>
269
+ ) => {
270
+ const indirectTranslation = t('someKey');
271
+ // Do something with the indirectTranslation
272
+ };
273
+ ```
274
+
275
+ When a type alias is used instead of `ReturnType<typeof useTranslations>`, the `--next-intl-translation-fn-type-alias` option can be used to tell the parser the name of the type or types.
276
+
277
+ ```ts
278
+ import { NextIntlTranslateFnAlias } from './types';
279
+
280
+ const indirectFnCall = (t: NextIntlTranslateFnAlias<'SomeNameSpace'>) => {
281
+ const indirectTranslation = t('someKey');
282
+ // Do something with the indirectTranslation
283
+ };
284
+ ```
285
+
286
+ ```bash
287
+ yarn i18n:check --source en --locales translations/codeExamples/next-intl/locales/ -f next-intl -u translations/codeExamples/next-intl/src --next-intl-translation-fn-type-alias NextIntlTranslateFnAlias
288
+ ```
289
+
262
290
  ## Examples
263
291
 
264
292
  i18n-check is able to load and validate against different locale folder structures. Depending on how the locale files are organized, there are different configuration options.
package/dist/bin/index.js CHANGED
@@ -9,7 +9,7 @@ const node_process_1 = require("node:process");
9
9
  const chalk_1 = __importDefault(require("chalk"));
10
10
  const commander_1 = require("commander");
11
11
  const glob_1 = require("glob");
12
- const js_yaml_1 = __importDefault(require("js-yaml"));
12
+ const yaml_1 = require("yaml");
13
13
  const __1 = require("..");
14
14
  const errorReporters_1 = require("../errorReporters");
15
15
  const flattenTranslations_1 = require("../utils/flattenTranslations");
@@ -28,6 +28,7 @@ commander_1.program
28
28
  .option('-i, --ignore <ignore...>', 'define the key(s) or group of keys (i.e. `some.namespace.*`) that should be excluded from the check')
29
29
  .option('-u, --unused <paths...>', 'define the source path(s) to find all unused and undefined keys')
30
30
  .option('--parser-component-functions <components...>', 'a list of component names to parse when using the --unused option')
31
+ .option('--next-intl-translation-fn-type-alias <nextIntlTranslationFnTypeAliases...>', 'next-intl translation function type aliases')
31
32
  .parse();
32
33
  const getCheckOptions = () => {
33
34
  const checkOption = commander_1.program.getOptionValue('only') || commander_1.program.getOptionValue('check');
@@ -52,6 +53,7 @@ const main = async () => {
52
53
  const ignore = commander_1.program.getOptionValue('ignore');
53
54
  const unusedSrcPath = commander_1.program.getOptionValue('unused');
54
55
  const componentFunctions = commander_1.program.getOptionValue('parserComponentFunctions');
56
+ const nextIntlTranslationFnTypeAlias = commander_1.program.getOptionValue('nextIntlTranslationFnTypeAlias');
55
57
  if (!srcPath) {
56
58
  console.log(chalk_1.default.red('Source not found. Please provide a valid source locale, i.e. -s en-US'));
57
59
  (0, node_process_1.exit)(1);
@@ -81,6 +83,7 @@ const main = async () => {
81
83
  checks: getCheckOptions(),
82
84
  format: format ?? undefined,
83
85
  ignore,
86
+ nextIntlTranslationFnTypeAlias,
84
87
  };
85
88
  const fileInfos = [];
86
89
  files.sort().forEach((file) => {
@@ -97,7 +100,7 @@ const main = async () => {
97
100
  fileInfos.forEach(({ extension, file, name, path }) => {
98
101
  let rawContent;
99
102
  if (extension === 'yaml') {
100
- rawContent = js_yaml_1.default.load(node_fs_1.default.readFileSync(file, 'utf-8'));
103
+ rawContent = (0, yaml_1.parse)(node_fs_1.default.readFileSync(file, 'utf-8'));
101
104
  }
102
105
  else {
103
106
  rawContent = JSON.parse(node_fs_1.default.readFileSync(file, 'utf-8'));
package/dist/index.js CHANGED
@@ -7,7 +7,6 @@ exports.checkUndefinedKeys = exports.checkUnusedKeys = exports.checkTranslations
7
7
  const findMissingKeys_1 = require("./utils/findMissingKeys");
8
8
  const findInvalidTranslations_1 = require("./utils/findInvalidTranslations");
9
9
  const findInvalidI18NextTranslations_1 = require("./utils/findInvalidI18NextTranslations");
10
- const cli_lib_1 = require("@formatjs/cli-lib");
11
10
  const i18NextSrcParser_1 = require("./utils/i18NextSrcParser");
12
11
  const nextIntlSrcParser_1 = require("./utils/nextIntlSrcParser");
13
12
  const fs_1 = __importDefault(require("fs"));
@@ -75,25 +74,28 @@ const checkUnusedKeys = async (translationFiles, filesToParse, options = {
75
74
  return findUnusedI18NextTranslations(filteredTranslationFiles, filesToParse, componentFunctions);
76
75
  }
77
76
  else if (options.format === 'next-intl') {
78
- return findUnusedNextIntlTranslations(filteredTranslationFiles, filesToParse);
77
+ return findUnusedNextIntlTranslations(filteredTranslationFiles, filesToParse, options);
79
78
  }
80
79
  };
81
80
  exports.checkUnusedKeys = checkUnusedKeys;
82
81
  const findUnusedReactIntlTranslations = async (translationFiles, filesToParse) => {
83
- const unusedKeys = {};
84
- const extracted = await (0, cli_lib_1.extract)(filesToParse, {});
85
- const extractedResultSet = new Set(Object.keys(JSON.parse(extracted)));
86
- translationFiles.forEach(({ name, content }) => {
87
- const keysInSource = Object.keys(content);
88
- const found = [];
89
- for (const keyInSource of keysInSource) {
90
- if (!extractedResultSet.has(keyInSource)) {
91
- found.push(keyInSource);
82
+ return (async () => {
83
+ const { extract } = await import('@formatjs/cli-lib');
84
+ const unusedKeys = {};
85
+ const extracted = await extract(filesToParse, {});
86
+ const extractedResultSet = new Set(Object.keys(JSON.parse(extracted)));
87
+ translationFiles.forEach(({ name, content }) => {
88
+ const keysInSource = Object.keys(content);
89
+ const found = [];
90
+ for (const keyInSource of keysInSource) {
91
+ if (!extractedResultSet.has(keyInSource)) {
92
+ found.push(keyInSource);
93
+ }
92
94
  }
93
- }
94
- unusedKeys[name] = found;
95
- });
96
- return unusedKeys;
95
+ unusedKeys[name] = found;
96
+ });
97
+ return unusedKeys;
98
+ })();
97
99
  };
98
100
  const findUnusedI18NextTranslations = async (source, filesToParse, componentFunctions = []) => {
99
101
  const unusedKeys = {};
@@ -128,9 +130,9 @@ const findUnusedI18NextTranslations = async (source, filesToParse, componentFunc
128
130
  });
129
131
  return unusedKeys;
130
132
  };
131
- const findUnusedNextIntlTranslations = async (translationFiles, filesToParse) => {
133
+ const findUnusedNextIntlTranslations = async (translationFiles, filesToParse, options) => {
132
134
  const unusedKeys = {};
133
- const extracted = (0, nextIntlSrcParser_1.extract)(filesToParse);
135
+ const extracted = (0, nextIntlSrcParser_1.extract)(filesToParse, options);
134
136
  const dynamicNamespaces = extracted.flatMap((namespace) => {
135
137
  if (namespace.meta.dynamic) {
136
138
  return [namespace.key];
@@ -190,27 +192,30 @@ exports.checkUndefinedKeys = checkUndefinedKeys;
190
192
  const findUndefinedReactIntlKeys = async (translationFiles, filesToParse, options = {
191
193
  ignore: [],
192
194
  }) => {
193
- const sourceKeys = new Set(translationFiles.flatMap(({ content }) => {
194
- return Object.keys(content);
195
- }));
196
- const extractedResult = await (0, cli_lib_1.extract)(filesToParse, {
197
- extractSourceLocation: true,
198
- });
199
- const undefinedKeys = {};
200
- Object.entries(JSON.parse(extractedResult)).forEach(([key, meta]) => {
201
- if (!sourceKeys.has(key) && !isIgnoredKey(options.ignore ?? [], key)) {
202
- const data = meta;
203
- if (!('file' in data) || typeof data.file !== 'string') {
204
- return;
205
- }
206
- const file = path_1.default.normalize(data.file);
207
- if (!undefinedKeys[file]) {
208
- undefinedKeys[file] = [];
195
+ return (async () => {
196
+ const { extract } = await import('@formatjs/cli-lib');
197
+ const sourceKeys = new Set(translationFiles.flatMap(({ content }) => {
198
+ return Object.keys(content);
199
+ }));
200
+ const undefinedKeys = {};
201
+ const extractedResult = await extract(filesToParse, {
202
+ extractSourceLocation: true,
203
+ });
204
+ Object.entries(JSON.parse(extractedResult)).forEach(([key, meta]) => {
205
+ if (!sourceKeys.has(key) && !isIgnoredKey(options.ignore ?? [], key)) {
206
+ const data = meta;
207
+ if (!('file' in data) || typeof data.file !== 'string') {
208
+ return;
209
+ }
210
+ const file = path_1.default.normalize(data.file);
211
+ if (!undefinedKeys[file]) {
212
+ undefinedKeys[file] = [];
213
+ }
214
+ undefinedKeys[file].push(key);
209
215
  }
210
- undefinedKeys[file].push(key);
211
- }
212
- });
213
- return undefinedKeys;
216
+ });
217
+ return undefinedKeys;
218
+ })();
214
219
  };
215
220
  const findUndefinedI18NextKeys = async (source, filesToParse, options = {
216
221
  ignore: [],
@@ -250,7 +255,7 @@ const findUndefinedNextIntlKeys = async (translationFiles, filesToParse, options
250
255
  const sourceKeys = new Set(translationFiles.flatMap(({ content }) => {
251
256
  return Object.keys(content);
252
257
  }));
253
- const extractedResult = (0, nextIntlSrcParser_1.extract)(filesToParse);
258
+ const extractedResult = (0, nextIntlSrcParser_1.extract)(filesToParse, options);
254
259
  const undefinedKeys = {};
255
260
  extractedResult.forEach(({ key, meta }) => {
256
261
  if (!meta.dynamic &&
package/dist/types.d.ts CHANGED
@@ -20,4 +20,5 @@ export type Options = {
20
20
  format?: 'icu' | 'i18next' | 'react-intl' | 'next-intl';
21
21
  checks?: Context[];
22
22
  ignore?: string[];
23
+ nextIntlTranslationFnTypeAlias?: string[];
23
24
  };
@@ -10,9 +10,7 @@ const OPEN_PARENTHESIS = '(';
10
10
  const OPEN_TAG = '<';
11
11
  const CLOSE_TAG = '</';
12
12
  const parse = (input) => {
13
- let ast = [];
14
- ast = parseInput([input]);
15
- return ast;
13
+ return parseInput([input]);
16
14
  };
17
15
  exports.parse = parse;
18
16
  const parseInput = (input) => {
@@ -365,9 +365,27 @@ const extractFromExpression = (node, options) => {
365
365
  .split('.');
366
366
  entries[0].key = keys.join('.');
367
367
  }
368
+ // check if the access is defined as an array.
369
+ // $["a.b.c"]
370
+ else if (returnStatement &&
371
+ returnStatement.expression &&
372
+ ts.isElementAccessExpression(returnStatement.expression)) {
373
+ entries[0].key = returnStatement.expression.argumentExpression
374
+ .getFullText()
375
+ .replace(/['"]/g, '')
376
+ .trim();
377
+ }
368
378
  else {
369
379
  return null;
370
380
  }
381
+ // check if the access is defined as an array.
382
+ // $["a.b.c"]
383
+ }
384
+ else if (ts.isElementAccessExpression(keyArgument.body)) {
385
+ entries[0].key = keyArgument.body.argumentExpression
386
+ .getFullText()
387
+ .replace(/['"]/g, '')
388
+ .trim();
371
389
  }
372
390
  else {
373
391
  const [_, ...keys] = keyArgument.body.getFullText().split('.');
@@ -1,4 +1,5 @@
1
- export declare const extract: (filesPaths: string[]) => {
1
+ import { Options } from '../types';
2
+ export declare const extract: (filesPaths: string[], options?: Options) => {
2
3
  key: string;
3
4
  meta: {
4
5
  file: string;
@@ -42,13 +42,15 @@ const ts = __importStar(require("typescript"));
42
42
  const USE_TRANSLATIONS = 'useTranslations';
43
43
  const GET_TRANSLATIONS = 'getTranslations';
44
44
  const COMMENT_CONTAINS_STATIC_KEY_REGEX = /i18n-check t\((["'])(.*?[^\\])(["'])\)/;
45
- const extract = (filesPaths) => {
46
- return filesPaths.flatMap(getKeys).sort((a, b) => {
45
+ const extract = (filesPaths, options = {}) => {
46
+ return filesPaths
47
+ .flatMap((path) => getKeys(path, options))
48
+ .sort((a, b) => {
47
49
  return a.key > b.key ? 1 : -1;
48
50
  });
49
51
  };
50
52
  exports.extract = extract;
51
- const getKeys = (path) => {
53
+ const getKeys = (path, options) => {
52
54
  const content = node_fs_1.default.readFileSync(path, 'utf-8');
53
55
  const sourceFile = ts.createSourceFile(path, content, ts.ScriptTarget.Latest, true);
54
56
  const foundKeys = [];
@@ -328,6 +330,83 @@ const getKeys = (path) => {
328
330
  }
329
331
  }
330
332
  }
333
+ // Third scenaro is if t function the type is
334
+ // an alias for ReturnType<typeof useTranslations>
335
+ const tFunctionAliasParam = node.parameters &&
336
+ node.parameters.find((param) => param.type &&
337
+ ts.isTypeReferenceNode(param.type) &&
338
+ param.type.typeName &&
339
+ ts.isIdentifier(param.type.typeName) &&
340
+ options &&
341
+ options.nextIntlTranslationFnTypeAlias !== undefined &&
342
+ options?.nextIntlTranslationFnTypeAlias.includes(param.type.typeName.text));
343
+ if (tFunctionAliasParam !== undefined &&
344
+ tFunctionAliasParam.type &&
345
+ ts.isTypeReferenceNode(tFunctionAliasParam.type)) {
346
+ const [namespaceArgument] = tFunctionAliasParam.type.typeArguments &&
347
+ tFunctionAliasParam.type.typeArguments.length > 0
348
+ ? tFunctionAliasParam.type.typeArguments
349
+ : [];
350
+ if (ts.isIdentifier(tFunctionAliasParam.name)) {
351
+ const name = namespaceArgument &&
352
+ ts.isLiteralTypeNode(namespaceArgument) &&
353
+ ts.isStringLiteral(namespaceArgument.literal)
354
+ ? namespaceArgument.literal.text
355
+ : '';
356
+ pushNamespace({
357
+ name,
358
+ variable: tFunctionAliasParam.name.text,
359
+ });
360
+ }
361
+ }
362
+ // Fourth scenario is the t function is defined as an object property and uses an alias:
363
+ // someFn({t}: {t: AliasForTheOriginalNextIntlType}>
364
+ const tFunctionParamAliasAsProperty = node.parameters &&
365
+ node.parameters.find((param) => param.type &&
366
+ ts.isTypeLiteralNode(param.type) &&
367
+ param.type.members.find((member) => {
368
+ return (ts.isPropertySignature(member) &&
369
+ member.type &&
370
+ ts.isTypeReferenceNode(member.type) &&
371
+ member.type.typeName &&
372
+ ts.isIdentifier(member.type.typeName) &&
373
+ options &&
374
+ options.nextIntlTranslationFnTypeAlias !== undefined &&
375
+ options?.nextIntlTranslationFnTypeAlias.includes(member.type.typeName.text));
376
+ }));
377
+ if (tFunctionParamAliasAsProperty !== undefined &&
378
+ tFunctionParamAliasAsProperty.type &&
379
+ ts.isTypeLiteralNode(tFunctionParamAliasAsProperty.type)) {
380
+ const fnType = tFunctionParamAliasAsProperty.type.members.find((member) => {
381
+ return (ts.isPropertySignature(member) &&
382
+ member.type &&
383
+ ts.isTypeReferenceNode(member.type) &&
384
+ member.type.typeName &&
385
+ ts.isIdentifier(member.type.typeName) &&
386
+ options &&
387
+ options.nextIntlTranslationFnTypeAlias !== undefined &&
388
+ options?.nextIntlTranslationFnTypeAlias.includes(member.type.typeName.text));
389
+ });
390
+ if (fnType &&
391
+ ts.isPropertySignature(fnType) &&
392
+ fnType.type &&
393
+ ts.isTypeReferenceNode(fnType.type)) {
394
+ const [namespaceArgument] = fnType.type.typeArguments && fnType.type.typeArguments.length > 0
395
+ ? fnType.type.typeArguments
396
+ : [];
397
+ if (fnType.name && ts.isIdentifier(fnType.name)) {
398
+ const name = namespaceArgument &&
399
+ ts.isLiteralTypeNode(namespaceArgument) &&
400
+ ts.isStringLiteral(namespaceArgument.literal)
401
+ ? namespaceArgument.literal.text
402
+ : '';
403
+ pushNamespace({
404
+ name,
405
+ variable: fnType.name.text,
406
+ });
407
+ }
408
+ }
409
+ }
331
410
  }
332
411
  // Search for `t()` calls
333
412
  if (getCurrentNamespaces() !== null &&
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lingual/i18n-check",
3
- "version": "0.9.3",
3
+ "version": "0.9.5",
4
4
  "description": "i18n translation messages check",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -24,24 +24,23 @@
24
24
  "!dist/**/*.test.*"
25
25
  ],
26
26
  "dependencies": {
27
- "@formatjs/cli-lib": "^8.2.3",
27
+ "@formatjs/cli-lib": "^8.5.1",
28
28
  "@formatjs/icu-messageformat-parser": "^2.11.4",
29
29
  "chalk": "^4.1.2",
30
30
  "commander": "^12.1.0",
31
- "glob": "13.0.6",
32
- "js-yaml": "^4.1.1",
33
- "typescript": "^5.9.3"
31
+ "glob": "^13.0.6",
32
+ "typescript": "^5.9.3",
33
+ "yaml": "^2.8.3"
34
34
  },
35
35
  "devDependencies": {
36
- "@eslint/js": "^9.39.1",
37
- "@types/js-yaml": "^4.0.9",
38
- "@types/node": "^22.19.1",
36
+ "@eslint/js": "^10.0.1",
37
+ "@types/node": "^22.19.17",
39
38
  "braces": "^3.0.3",
40
- "eslint": "^10.0.3",
41
- "globals": "^16.5.0",
42
- "prettier": "^3.6.2",
43
- "typescript-eslint": "^8.56.1",
44
- "vitest": "^4.0.18"
39
+ "eslint": "^10.2.1",
40
+ "globals": "^17.5.0",
41
+ "prettier": "^3.8.3",
42
+ "typescript-eslint": "^8.59.0",
43
+ "vitest": "^4.1.5"
45
44
  },
46
45
  "repository": {
47
46
  "type": "git",
@@ -50,5 +49,5 @@
50
49
  "engines": {
51
50
  "node": ">=20"
52
51
  },
53
- "packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39"
52
+ "packageManager": "pnpm@10.28.2+sha512.41872f037ad22f7348e3b1debbaf7e867cfd448f2726d9cf74c08f19507c31d2c8e7a11525b983febc2df640b5438dee6023ebb1f84ed43cc2d654d2bc326264"
54
53
  }