@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 +26 -4
- package/dist/bin/index.js +7 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +28 -10
- package/dist/utils/nextIntlSrcParser.js +43 -0
- package/package.json +12 -11
- package/dist/bin/index.test.d.ts +0 -1
- package/dist/bin/index.test.js +0 -754
- package/dist/errorReporters.test.d.ts +0 -1
- package/dist/errorReporters.test.js +0 -165
- package/dist/utils/findInvalidTranslations.test.d.ts +0 -1
- package/dist/utils/findInvalidTranslations.test.js +0 -83
- package/dist/utils/findInvalidi18nTranslations.test.d.ts +0 -1
- package/dist/utils/findInvalidi18nTranslations.test.js +0 -206
- package/dist/utils/findMissingKeys.test.d.ts +0 -1
- package/dist/utils/findMissingKeys.test.js +0 -41
- package/dist/utils/flattenTranslations.test.d.ts +0 -1
- package/dist/utils/flattenTranslations.test.js +0 -28
- package/dist/utils/i18NextParser.test.d.ts +0 -1
- package/dist/utils/i18NextParser.test.js +0 -150
- package/dist/utils/nextIntlSrcParser.test.d.ts +0 -1
- package/dist/utils/nextIntlSrcParser.test.js +0 -568
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
|
-

|
|
8
8
|
|
|
9
|
-

|
|
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
|
|
30
|
-
const
|
|
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 (
|
|
36
|
-
|
|
35
|
+
if (hasMissingKeysCheck) {
|
|
36
|
+
const filteredContent = filterKeys(content, options.ignore ?? []);
|
|
37
|
+
merge(missingKeys, (0, exports.checkMissingTranslations)(filteredContent, files));
|
|
37
38
|
}
|
|
38
|
-
if (
|
|
39
|
+
if (hasInvalidKeysCheck) {
|
|
39
40
|
merge(invalidKeys, (0, exports.checkInvalidTranslations)(content, files, options));
|
|
40
41
|
}
|
|
41
42
|
});
|
|
42
43
|
return {
|
|
43
|
-
missingKeys:
|
|
44
|
-
invalidKeys:
|
|
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() ??
|
|
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
|
+
"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": "
|
|
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.
|
|
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.
|
|
39
|
+
"@types/node": "^22.16.0",
|
|
39
40
|
"@types/vinyl": "^2.0.12",
|
|
40
41
|
"braces": "^3.0.3",
|
|
41
|
-
"eslint": "^9.
|
|
42
|
-
"globals": "^16.
|
|
42
|
+
"eslint": "^9.30.1",
|
|
43
|
+
"globals": "^16.3.0",
|
|
43
44
|
"jest": "^29.7.0",
|
|
44
|
-
"prettier": "^3.
|
|
45
|
-
"ts-jest": "^29.
|
|
46
|
-
"typescript-eslint": "^8.
|
|
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",
|
package/dist/bin/index.test.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|