@lingual/i18n-check 0.9.3 → 0.9.4
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 +28 -0
- package/dist/bin/index.js +5 -2
- package/dist/index.js +43 -38
- package/dist/types.d.ts +1 -0
- package/dist/utils/i18NextParser.js +1 -3
- package/dist/utils/i18NextSrcParser.js +16 -0
- package/dist/utils/nextIntlSrcParser.d.ts +2 -1
- package/dist/utils/nextIntlSrcParser.js +82 -3
- package/package.json +13 -14
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
|
|
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 =
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
if (!(
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
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
|
@@ -10,9 +10,7 @@ const OPEN_PARENTHESIS = '(';
|
|
|
10
10
|
const OPEN_TAG = '<';
|
|
11
11
|
const CLOSE_TAG = '</';
|
|
12
12
|
const parse = (input) => {
|
|
13
|
-
|
|
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,25 @@ 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
|
+
}
|
|
368
377
|
else {
|
|
369
378
|
return null;
|
|
370
379
|
}
|
|
380
|
+
// check if the access is defined as an array.
|
|
381
|
+
// $["a.b.c"]
|
|
382
|
+
}
|
|
383
|
+
else if (ts.isElementAccessExpression(keyArgument.body)) {
|
|
384
|
+
entries[0].key = keyArgument.body.argumentExpression
|
|
385
|
+
.getFullText()
|
|
386
|
+
.replace(/['"]/g, '');
|
|
371
387
|
}
|
|
372
388
|
else {
|
|
373
389
|
const [_, ...keys] = keyArgument.body.getFullText().split('.');
|
|
@@ -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
|
|
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
|
+
"version": "0.9.4",
|
|
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.
|
|
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
|
-
"
|
|
33
|
-
"
|
|
31
|
+
"glob": "^13.0.6",
|
|
32
|
+
"typescript": "^5.9.3",
|
|
33
|
+
"yaml": "^2.8.3"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
|
-
"@eslint/js": "^
|
|
37
|
-
"@types/
|
|
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.
|
|
41
|
-
"globals": "^
|
|
42
|
-
"prettier": "^3.
|
|
43
|
-
"typescript-eslint": "^8.
|
|
44
|
-
"vitest": "^4.
|
|
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.
|
|
52
|
+
"packageManager": "pnpm@10.28.2+sha512.41872f037ad22f7348e3b1debbaf7e867cfd448f2726d9cf74c08f19507c31d2c8e7a11525b983febc2df640b5438dee6023ebb1f84ed43cc2d654d2bc326264"
|
|
54
53
|
}
|