@lingual/i18n-check 0.8.2 → 0.8.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 +32 -18
- package/dist/bin/index.js +61 -57
- package/dist/bin/index.test.js +321 -289
- package/dist/errorReporters.d.ts +1 -1
- package/dist/errorReporters.js +21 -21
- package/dist/errorReporters.test.js +39 -39
- package/dist/index.d.ts +4 -3
- package/dist/index.js +82 -47
- package/dist/utils/findInvalidTranslations.d.ts +2 -2
- package/dist/utils/findInvalidTranslations.js +20 -19
- package/dist/utils/findInvalidTranslations.test.js +30 -30
- package/dist/utils/findInvalidi18nTranslations.d.ts +2 -2
- package/dist/utils/findInvalidi18nTranslations.js +35 -35
- package/dist/utils/findInvalidi18nTranslations.test.js +72 -72
- package/dist/utils/findMissingKeys.d.ts +1 -1
- package/dist/utils/findMissingKeys.js +2 -2
- package/dist/utils/findMissingKeys.test.js +20 -20
- package/dist/utils/flattenTranslations.d.ts +1 -1
- package/dist/utils/flattenTranslations.js +3 -3
- package/dist/utils/flattenTranslations.test.js +13 -13
- package/dist/utils/i18NextParser.d.ts +6 -6
- package/dist/utils/i18NextParser.js +29 -29
- package/dist/utils/i18NextParser.test.js +104 -104
- package/dist/utils/nextIntlSrcParser.js +11 -11
- package/dist/utils/nextIntlSrcParser.test.js +156 -156
- package/package.json +14 -4
package/dist/errorReporters.d.ts
CHANGED
package/dist/errorReporters.js
CHANGED
|
@@ -6,16 +6,16 @@ exports.formatTable = formatTable;
|
|
|
6
6
|
exports.formatCheckResultTable = formatCheckResultTable;
|
|
7
7
|
exports.formatInvalidTranslationsResultTable = formatInvalidTranslationsResultTable;
|
|
8
8
|
exports.CheckOptions = [
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
'invalidKeys',
|
|
10
|
+
'missingKeys',
|
|
11
|
+
'unused',
|
|
12
|
+
'undefined',
|
|
13
13
|
];
|
|
14
14
|
exports.contextMapping = {
|
|
15
|
-
invalidKeys:
|
|
16
|
-
missingKeys:
|
|
17
|
-
unused:
|
|
18
|
-
undefined:
|
|
15
|
+
invalidKeys: 'invalid',
|
|
16
|
+
missingKeys: 'missing',
|
|
17
|
+
unused: 'unused',
|
|
18
|
+
undefined: 'undefined',
|
|
19
19
|
};
|
|
20
20
|
function formatSummaryTable(result) {
|
|
21
21
|
return formatTable(getSummaryRows(result));
|
|
@@ -25,9 +25,9 @@ const getSummaryRows = (checkResult) => {
|
|
|
25
25
|
for (const [file, keys] of Object.entries(checkResult)) {
|
|
26
26
|
rows.push([truncate(file), String(keys.length)]);
|
|
27
27
|
}
|
|
28
|
-
return [[[
|
|
28
|
+
return [[['file', 'total']], rows];
|
|
29
29
|
};
|
|
30
|
-
function formatTable(rowGroups, lineSep =
|
|
30
|
+
function formatTable(rowGroups, lineSep = '\n') {
|
|
31
31
|
// +2 for whitespace padding left and right
|
|
32
32
|
const padding = 2;
|
|
33
33
|
const colWidths = [];
|
|
@@ -39,40 +39,40 @@ function formatTable(rowGroups, lineSep = "\n") {
|
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
const lines = [];
|
|
42
|
-
lines.push(formatSeparatorRow(colWidths,
|
|
42
|
+
lines.push(formatSeparatorRow(colWidths, '┌┬┐'));
|
|
43
43
|
for (const rows of rowGroups) {
|
|
44
44
|
for (const row of rows) {
|
|
45
45
|
lines.push(formatRow(row, colWidths));
|
|
46
46
|
}
|
|
47
|
-
lines.push(formatSeparatorRow(colWidths,
|
|
47
|
+
lines.push(formatSeparatorRow(colWidths, '├┼┤'));
|
|
48
48
|
}
|
|
49
|
-
lines[lines.length - 1] = formatSeparatorRow(colWidths,
|
|
49
|
+
lines[lines.length - 1] = formatSeparatorRow(colWidths, '└┴┘');
|
|
50
50
|
return lines.join(lineSep);
|
|
51
51
|
}
|
|
52
52
|
function formatSeparatorRow(widths, [left, middle, right]) {
|
|
53
|
-
return (left + widths.map((width) =>
|
|
53
|
+
return (left + widths.map((width) => ''.padEnd(width, '─')).join(middle) + right);
|
|
54
54
|
}
|
|
55
55
|
function formatRow(values, widths) {
|
|
56
56
|
return (`│` +
|
|
57
57
|
values
|
|
58
|
-
.map((val, index) => ` ${val} `.padEnd(widths[index],
|
|
59
|
-
.join(
|
|
58
|
+
.map((val, index) => ` ${val} `.padEnd(widths[index], ' '))
|
|
59
|
+
.join('│') +
|
|
60
60
|
`│`);
|
|
61
61
|
}
|
|
62
62
|
const truncate = (chars, len = 80) => chars.length > 80 ? `${chars.substring(0, len)}...` : chars;
|
|
63
63
|
function formatCheckResultTable(result) {
|
|
64
64
|
return formatTable([
|
|
65
|
-
[[
|
|
65
|
+
[['file', 'key']],
|
|
66
66
|
Object.entries(result).flatMap(([file, keys]) => keys.map((key) => [truncate(file), truncate(key)])),
|
|
67
67
|
]);
|
|
68
68
|
}
|
|
69
69
|
function formatInvalidTranslationsResultTable(result) {
|
|
70
70
|
return formatTable([
|
|
71
|
-
[[
|
|
71
|
+
[['info', 'result']],
|
|
72
72
|
...Object.entries(result).flatMap(([file, errors]) => errors.map(({ key, msg }) => [
|
|
73
|
-
[
|
|
74
|
-
[
|
|
75
|
-
[
|
|
73
|
+
['file', truncate(file)],
|
|
74
|
+
['key', truncate(key)],
|
|
75
|
+
['msg', truncate(msg, 120)],
|
|
76
76
|
])),
|
|
77
77
|
]);
|
|
78
78
|
}
|
|
@@ -1,28 +1,28 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const errorReporters_1 = require("./errorReporters");
|
|
4
|
-
describe(
|
|
5
|
-
test(
|
|
6
|
-
expect((0, errorReporters_1.formatTable)([[[
|
|
4
|
+
describe('formatTable', () => {
|
|
5
|
+
test('single col and row', () => {
|
|
6
|
+
expect((0, errorReporters_1.formatTable)([[['lorem ipsum']]])).toEqual(`
|
|
7
7
|
┌─────────────┐
|
|
8
8
|
│ lorem ipsum │
|
|
9
9
|
└─────────────┘
|
|
10
10
|
`.trim());
|
|
11
11
|
});
|
|
12
|
-
test(
|
|
13
|
-
expect((0, errorReporters_1.formatTable)([[[
|
|
12
|
+
test('single col and two rows', () => {
|
|
13
|
+
expect((0, errorReporters_1.formatTable)([[['lorem ipsum'], ['foo bar']]])).toEqual(`
|
|
14
14
|
┌─────────────┐
|
|
15
15
|
│ lorem ipsum │
|
|
16
16
|
│ foo bar │
|
|
17
17
|
└─────────────┘
|
|
18
18
|
`.trim());
|
|
19
19
|
});
|
|
20
|
-
test(
|
|
20
|
+
test('with two columns and two row groups', () => {
|
|
21
21
|
expect((0, errorReporters_1.formatTable)([
|
|
22
|
-
[[
|
|
22
|
+
[['col1', 'col2']],
|
|
23
23
|
[
|
|
24
|
-
[
|
|
25
|
-
[
|
|
24
|
+
['lorem ipsum dolor', 'foobar'],
|
|
25
|
+
['baz', 'more text'],
|
|
26
26
|
],
|
|
27
27
|
])).toEqual(`
|
|
28
28
|
┌───────────────────┬───────────┐
|
|
@@ -33,14 +33,14 @@ describe("formatTable", () => {
|
|
|
33
33
|
└───────────────────┴───────────┘
|
|
34
34
|
`.trim());
|
|
35
35
|
});
|
|
36
|
-
test(
|
|
36
|
+
test('with two columns and three row groups', () => {
|
|
37
37
|
expect((0, errorReporters_1.formatTable)([
|
|
38
|
-
[[
|
|
38
|
+
[['one', 'two']],
|
|
39
39
|
[
|
|
40
|
-
[
|
|
41
|
-
[
|
|
40
|
+
['lorem ipsum dolor', 'foobar'],
|
|
41
|
+
['baz', 'more text'],
|
|
42
42
|
],
|
|
43
|
-
[[
|
|
43
|
+
[['hello world', 'here is more text for testing']],
|
|
44
44
|
])).toEqual(`
|
|
45
45
|
┌───────────────────┬───────────────────────────────┐
|
|
46
46
|
│ one │ two │
|
|
@@ -53,10 +53,10 @@ describe("formatTable", () => {
|
|
|
53
53
|
`.trim());
|
|
54
54
|
});
|
|
55
55
|
});
|
|
56
|
-
describe(
|
|
57
|
-
test(
|
|
56
|
+
describe('formatCheckResultTable', () => {
|
|
57
|
+
test('with one file and two keys', () => {
|
|
58
58
|
expect((0, errorReporters_1.formatCheckResultTable)({
|
|
59
|
-
|
|
59
|
+
'some/file.json': ['key.one', 'key.two'],
|
|
60
60
|
})).toEqual(`
|
|
61
61
|
┌────────────────┬─────────┐
|
|
62
62
|
│ file │ key │
|
|
@@ -66,10 +66,10 @@ describe("formatCheckResultTable", () => {
|
|
|
66
66
|
└────────────────┴─────────┘
|
|
67
67
|
`.trim());
|
|
68
68
|
});
|
|
69
|
-
test(
|
|
69
|
+
test('with two files and three keys', () => {
|
|
70
70
|
expect((0, errorReporters_1.formatCheckResultTable)({
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
'some/de.json': ['key.one', 'key.two'],
|
|
72
|
+
'some/en.json': ['key.three'],
|
|
73
73
|
})).toEqual(`
|
|
74
74
|
┌──────────────┬───────────┐
|
|
75
75
|
│ file │ key │
|
|
@@ -81,10 +81,10 @@ describe("formatCheckResultTable", () => {
|
|
|
81
81
|
`.trim());
|
|
82
82
|
});
|
|
83
83
|
});
|
|
84
|
-
describe(
|
|
85
|
-
test(
|
|
84
|
+
describe('formatInvalidTranslationsResultTable', () => {
|
|
85
|
+
test('with one file and one key', () => {
|
|
86
86
|
expect((0, errorReporters_1.formatInvalidTranslationsResultTable)({
|
|
87
|
-
|
|
87
|
+
'some/en.json': [{ key: 'key.one', msg: 'key one error msg' }],
|
|
88
88
|
})).toEqual(`
|
|
89
89
|
┌──────┬───────────────────┐
|
|
90
90
|
│ info │ result │
|
|
@@ -95,13 +95,13 @@ describe("formatInvalidTranslationsResultTable", () => {
|
|
|
95
95
|
└──────┴───────────────────┘
|
|
96
96
|
`.trim());
|
|
97
97
|
});
|
|
98
|
-
test(
|
|
98
|
+
test('with two files and three keys', () => {
|
|
99
99
|
expect((0, errorReporters_1.formatInvalidTranslationsResultTable)({
|
|
100
|
-
|
|
101
|
-
{ key:
|
|
102
|
-
{ key:
|
|
100
|
+
'some/en-US.json': [
|
|
101
|
+
{ key: 'key.one', msg: 'key one error msg' },
|
|
102
|
+
{ key: 'key.two', msg: 'another msg' },
|
|
103
103
|
],
|
|
104
|
-
|
|
104
|
+
'some/de.json': [{ key: 'key.three', msg: 'key three msg' }],
|
|
105
105
|
})).toEqual(`
|
|
106
106
|
┌──────┬───────────────────┐
|
|
107
107
|
│ info │ result │
|
|
@@ -121,10 +121,10 @@ describe("formatInvalidTranslationsResultTable", () => {
|
|
|
121
121
|
`.trim());
|
|
122
122
|
});
|
|
123
123
|
});
|
|
124
|
-
describe(
|
|
125
|
-
test(
|
|
124
|
+
describe('formatSummaryTable', () => {
|
|
125
|
+
test('with CheckResult with single file and key', () => {
|
|
126
126
|
expect((0, errorReporters_1.formatSummaryTable)({
|
|
127
|
-
|
|
127
|
+
'some/file.json': ['key.one'],
|
|
128
128
|
})).toEqual(`
|
|
129
129
|
┌────────────────┬───────┐
|
|
130
130
|
│ file │ total │
|
|
@@ -133,10 +133,10 @@ describe("formatSummaryTable", () => {
|
|
|
133
133
|
└────────────────┴───────┘
|
|
134
134
|
`.trim());
|
|
135
135
|
});
|
|
136
|
-
test(
|
|
136
|
+
test('with CheckResult with two files and three keys', () => {
|
|
137
137
|
expect((0, errorReporters_1.formatSummaryTable)({
|
|
138
|
-
|
|
139
|
-
|
|
138
|
+
'some/de.json': ['key.one', 'key.two'],
|
|
139
|
+
'some/en.json': ['key.three'],
|
|
140
140
|
})).toEqual(`
|
|
141
141
|
┌──────────────┬───────┐
|
|
142
142
|
│ file │ total │
|
|
@@ -146,13 +146,13 @@ describe("formatSummaryTable", () => {
|
|
|
146
146
|
└──────────────┴───────┘
|
|
147
147
|
`.trim());
|
|
148
148
|
});
|
|
149
|
-
test(
|
|
149
|
+
test('with InvalidTranslationsResult with two files and three keys', () => {
|
|
150
150
|
expect((0, errorReporters_1.formatSummaryTable)({
|
|
151
|
-
|
|
152
|
-
{ key:
|
|
153
|
-
{ key:
|
|
151
|
+
'some/en-US.json': [
|
|
152
|
+
{ key: 'key.one', msg: 'key one error msg' },
|
|
153
|
+
{ key: 'key.two', msg: 'another msg' },
|
|
154
154
|
],
|
|
155
|
-
|
|
155
|
+
'some/de.json': [{ key: 'key.three', msg: 'key three msg' }],
|
|
156
156
|
})).toEqual(`
|
|
157
157
|
┌─────────────────┬───────┐
|
|
158
158
|
│ file │ total │
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { CheckResult, InvalidTranslationsResult, Translation, TranslationFile } from
|
|
2
|
-
import { Context } from
|
|
1
|
+
import { CheckResult, InvalidTranslationsResult, Translation, TranslationFile } from './types';
|
|
2
|
+
import { Context } from './errorReporters';
|
|
3
3
|
export type Options = {
|
|
4
|
-
format?:
|
|
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
|
@@ -11,9 +11,9 @@ const cli_lib_1 = require("@formatjs/cli-lib");
|
|
|
11
11
|
const nextIntlSrcParser_1 = require("./utils/nextIntlSrcParser");
|
|
12
12
|
const fs_1 = __importDefault(require("fs"));
|
|
13
13
|
const path_1 = __importDefault(require("path"));
|
|
14
|
-
const ParseFormats = [
|
|
15
|
-
const checkInvalidTranslations = (source, targets, options = { format:
|
|
16
|
-
return options.format ===
|
|
14
|
+
const ParseFormats = ['react-intl', 'i18next', 'next-intl'];
|
|
15
|
+
const checkInvalidTranslations = (source, targets, options = { format: 'icu' }) => {
|
|
16
|
+
return options.format === 'i18next'
|
|
17
17
|
? (0, findInvalidi18nTranslations_1.findInvalid18nTranslations)(source, targets)
|
|
18
18
|
: (0, findInvalidTranslations_1.findInvalidTranslations)(source, targets);
|
|
19
19
|
};
|
|
@@ -22,51 +22,52 @@ const checkMissingTranslations = (source, targets) => {
|
|
|
22
22
|
return (0, findMissingKeys_1.findMissingKeys)(source, targets);
|
|
23
23
|
};
|
|
24
24
|
exports.checkMissingTranslations = checkMissingTranslations;
|
|
25
|
-
const checkTranslations = (source, targets, options = { format:
|
|
26
|
-
const { checks = [
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
const
|
|
25
|
+
const checkTranslations = (source, targets, options = { format: 'icu', checks: ['invalidKeys', 'missingKeys'] }) => {
|
|
26
|
+
const { checks = ['invalidKeys', 'missingKeys'] } = options;
|
|
27
|
+
const missingKeys = {};
|
|
28
|
+
const 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 (
|
|
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;
|
|
48
49
|
function merge(left, right) {
|
|
49
|
-
for (
|
|
50
|
+
for (const [k, v] of Object.entries(right)) {
|
|
50
51
|
left[k] = (left?.[k] ?? []).concat(v);
|
|
51
52
|
}
|
|
52
53
|
}
|
|
53
54
|
const checkUnusedKeys = async (translationFiles, filesToParse, options = {
|
|
54
|
-
format:
|
|
55
|
+
format: 'react-intl',
|
|
55
56
|
checks: [],
|
|
56
57
|
}, componentFunctions = []) => {
|
|
57
58
|
if (!options.format || !ParseFormats.includes(options.format)) {
|
|
58
59
|
return undefined;
|
|
59
60
|
}
|
|
60
|
-
if (!options.checks || !options.checks.includes(
|
|
61
|
+
if (!options.checks || !options.checks.includes('unused')) {
|
|
61
62
|
return undefined;
|
|
62
63
|
}
|
|
63
|
-
if (options.format ===
|
|
64
|
+
if (options.format === 'react-intl') {
|
|
64
65
|
return findUnusedReactIntlTranslations(translationFiles, filesToParse);
|
|
65
66
|
}
|
|
66
|
-
else if (options.format ===
|
|
67
|
+
else if (options.format === 'i18next') {
|
|
67
68
|
return findUnusedI18NextTranslations(translationFiles, filesToParse, componentFunctions);
|
|
68
69
|
}
|
|
69
|
-
else if (options.format ===
|
|
70
|
+
else if (options.format === 'next-intl') {
|
|
70
71
|
return findUnusedNextIntlTranslations(translationFiles, filesToParse);
|
|
71
72
|
}
|
|
72
73
|
};
|
|
@@ -90,7 +91,7 @@ const findUnusedReactIntlTranslations = async (translationFiles, filesToParse) =
|
|
|
90
91
|
const findUnusedI18NextTranslations = async (source, filesToParse, componentFunctions = []) => {
|
|
91
92
|
const unusedKeys = {};
|
|
92
93
|
const { extractedResult, skippableKeys } = await getI18NextKeysInCode(filesToParse, componentFunctions);
|
|
93
|
-
const extractedResultSet = new Set(extractedResult.map(({ key }) => key));
|
|
94
|
+
const extractedResultSet = new Set(extractedResult.map(({ key, namespace }) => namespace ? `${namespace}.${key}` : key));
|
|
94
95
|
source.forEach(({ name, content }) => {
|
|
95
96
|
const keysInSource = Object.keys(content);
|
|
96
97
|
const found = [];
|
|
@@ -101,7 +102,10 @@ const findUnusedI18NextTranslations = async (source, filesToParse, componentFunc
|
|
|
101
102
|
if (isSkippable !== undefined) {
|
|
102
103
|
continue;
|
|
103
104
|
}
|
|
104
|
-
|
|
105
|
+
// find the file name
|
|
106
|
+
const [fileName] = (name.split(path_1.default.sep).pop() ?? '').split('.');
|
|
107
|
+
if (!extractedResultSet.has(`${fileName}.${keyInSource}`) &&
|
|
108
|
+
!extractedResultSet.has(keyInSource)) {
|
|
105
109
|
found.push(keyInSource);
|
|
106
110
|
}
|
|
107
111
|
}
|
|
@@ -131,8 +135,8 @@ const findUnusedNextIntlTranslations = async (translationFiles, filesToParse) =>
|
|
|
131
135
|
// Check if key is part of a dynamic namespace
|
|
132
136
|
// Skip the key if it is part of the dynamic namespace
|
|
133
137
|
const isDynamicNamespace = dynamicNamespaces.find((dynamicNamespace) => {
|
|
134
|
-
const keyInSourceNamespaces = keyInSource.split(
|
|
135
|
-
return dynamicNamespace.split(
|
|
138
|
+
const keyInSourceNamespaces = keyInSource.split('.');
|
|
139
|
+
return dynamicNamespace.split('.').every((namePart, index) => {
|
|
136
140
|
return namePart === keyInSourceNamespaces[index];
|
|
137
141
|
});
|
|
138
142
|
});
|
|
@@ -148,22 +152,22 @@ const findUnusedNextIntlTranslations = async (translationFiles, filesToParse) =>
|
|
|
148
152
|
return unusedKeys;
|
|
149
153
|
};
|
|
150
154
|
const checkUndefinedKeys = async (source, filesToParse, options = {
|
|
151
|
-
format:
|
|
155
|
+
format: 'react-intl',
|
|
152
156
|
checks: [],
|
|
153
157
|
}, componentFunctions = []) => {
|
|
154
158
|
if (!options.format || !ParseFormats.includes(options.format)) {
|
|
155
159
|
return undefined;
|
|
156
160
|
}
|
|
157
|
-
if (!options.checks || !options.checks.includes(
|
|
161
|
+
if (!options.checks || !options.checks.includes('undefined')) {
|
|
158
162
|
return undefined;
|
|
159
163
|
}
|
|
160
|
-
if (options.format ===
|
|
164
|
+
if (options.format === 'react-intl') {
|
|
161
165
|
return findUndefinedReactIntlKeys(source, filesToParse);
|
|
162
166
|
}
|
|
163
|
-
else if (options.format ===
|
|
167
|
+
else if (options.format === 'i18next') {
|
|
164
168
|
return findUndefinedI18NextKeys(source, filesToParse, componentFunctions);
|
|
165
169
|
}
|
|
166
|
-
else if (options.format ===
|
|
170
|
+
else if (options.format === 'next-intl') {
|
|
167
171
|
return findUndefinedNextIntlKeys(source, filesToParse);
|
|
168
172
|
}
|
|
169
173
|
};
|
|
@@ -175,11 +179,11 @@ const findUndefinedReactIntlKeys = async (translationFiles, filesToParse) => {
|
|
|
175
179
|
const extractedResult = await (0, cli_lib_1.extract)(filesToParse, {
|
|
176
180
|
extractSourceLocation: true,
|
|
177
181
|
});
|
|
178
|
-
|
|
182
|
+
const undefinedKeys = {};
|
|
179
183
|
Object.entries(JSON.parse(extractedResult)).forEach(([key, meta]) => {
|
|
180
184
|
if (!sourceKeys.has(key)) {
|
|
181
185
|
const data = meta;
|
|
182
|
-
if (!(
|
|
186
|
+
if (!('file' in data) || typeof data.file !== 'string') {
|
|
183
187
|
return;
|
|
184
188
|
}
|
|
185
189
|
const file = path_1.default.normalize(data.file);
|
|
@@ -196,7 +200,7 @@ const findUndefinedI18NextKeys = async (source, filesToParse, componentFunctions
|
|
|
196
200
|
const sourceKeys = new Set(source.flatMap(({ content }) => {
|
|
197
201
|
return Object.keys(content);
|
|
198
202
|
}));
|
|
199
|
-
|
|
203
|
+
const undefinedKeys = {};
|
|
200
204
|
extractedResult.forEach(({ file, key }) => {
|
|
201
205
|
const isSkippable = skippableKeys.find((skippableKey) => {
|
|
202
206
|
return key.includes(skippableKey);
|
|
@@ -215,10 +219,9 @@ const findUndefinedNextIntlKeys = async (translationFiles, filesToParse) => {
|
|
|
215
219
|
return Object.keys(content);
|
|
216
220
|
}));
|
|
217
221
|
const extractedResult = (0, nextIntlSrcParser_1.extract)(filesToParse);
|
|
218
|
-
|
|
222
|
+
const undefinedKeys = {};
|
|
219
223
|
extractedResult.forEach(({ key, meta }) => {
|
|
220
224
|
if (!meta.dynamic && !sourceKeys.has(key)) {
|
|
221
|
-
// @ts-ignore
|
|
222
225
|
const file = meta.file;
|
|
223
226
|
if (!undefinedKeys[file]) {
|
|
224
227
|
undefinedKeys[file] = [];
|
|
@@ -229,26 +232,27 @@ const findUndefinedNextIntlKeys = async (translationFiles, filesToParse) => {
|
|
|
229
232
|
return undefinedKeys;
|
|
230
233
|
};
|
|
231
234
|
const isRecord = (data) => {
|
|
232
|
-
return (typeof data ===
|
|
235
|
+
return (typeof data === 'object' &&
|
|
233
236
|
!Array.isArray(data) &&
|
|
234
237
|
data !== null &&
|
|
235
238
|
data !== undefined);
|
|
236
239
|
};
|
|
237
240
|
const getI18NextKeysInCode = async (filesToParse, componentFunctions = []) => {
|
|
241
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
238
242
|
// @ts-ignore
|
|
239
|
-
const { transform } = await import(
|
|
243
|
+
const { transform } = await import('i18next-parser');
|
|
240
244
|
const i18nextParser = new transform({
|
|
241
245
|
lexers: {
|
|
242
246
|
jsx: [
|
|
243
247
|
{
|
|
244
|
-
lexer:
|
|
245
|
-
componentFunctions: componentFunctions.concat([
|
|
248
|
+
lexer: 'JsxLexer',
|
|
249
|
+
componentFunctions: componentFunctions.concat(['Trans']),
|
|
246
250
|
},
|
|
247
251
|
],
|
|
248
252
|
tsx: [
|
|
249
253
|
{
|
|
250
|
-
lexer:
|
|
251
|
-
componentFunctions: componentFunctions.concat([
|
|
254
|
+
lexer: 'JsxLexer',
|
|
255
|
+
componentFunctions: componentFunctions.concat(['Trans']),
|
|
252
256
|
},
|
|
253
257
|
],
|
|
254
258
|
},
|
|
@@ -256,32 +260,63 @@ const getI18NextKeysInCode = async (filesToParse, componentFunctions = []) => {
|
|
|
256
260
|
// Skip any parsed keys that have the `returnObjects` property set to true
|
|
257
261
|
// As these are used dynamically, they will be skipped to prevent
|
|
258
262
|
// these keys from being marked as unused.
|
|
259
|
-
|
|
263
|
+
const extractedResult = [];
|
|
260
264
|
const skippableKeys = [];
|
|
261
265
|
filesToParse.forEach((file) => {
|
|
262
|
-
const rawContent = fs_1.default.readFileSync(file,
|
|
266
|
+
const rawContent = fs_1.default.readFileSync(file, 'utf-8');
|
|
263
267
|
const entries = i18nextParser.parser.parse(rawContent, file);
|
|
264
268
|
// Intermediate solution to retrieve all keys from the parser.
|
|
265
269
|
// This will be built out to also include the namespace and check
|
|
266
270
|
// the key against the namespace corresponding file.
|
|
267
271
|
// The current implementation considers the key as used no matter the namespace.
|
|
268
272
|
for (const entry of entries) {
|
|
273
|
+
// check for namespace, i.e. `namespace:some.key`
|
|
274
|
+
const [namespace, ...keyParts] = entry.key.split(':');
|
|
275
|
+
// If there is a namespace make sure to assign the namespace
|
|
276
|
+
// and update the key name
|
|
277
|
+
// Ensure that the assumed key is not the default value
|
|
278
|
+
if (keyParts.length > 0 && entry.key !== entry.defaultValue) {
|
|
279
|
+
entry.namespace = namespace;
|
|
280
|
+
// rebuild the key without the namespace
|
|
281
|
+
entry.key = keyParts.join(':');
|
|
282
|
+
}
|
|
269
283
|
if (entry.returnObjects) {
|
|
270
284
|
skippableKeys.push(entry.key);
|
|
271
285
|
}
|
|
272
286
|
else {
|
|
273
|
-
extractedResult.push({
|
|
287
|
+
extractedResult.push({
|
|
288
|
+
file,
|
|
289
|
+
key: entry.key,
|
|
290
|
+
namespace: entry.namespace,
|
|
291
|
+
});
|
|
274
292
|
}
|
|
275
293
|
}
|
|
276
294
|
});
|
|
277
295
|
return { extractedResult, skippableKeys };
|
|
278
296
|
};
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
+
};
|
|
314
|
+
function _flatten(object, prefix = null, result = {}) {
|
|
315
|
+
for (const key in object) {
|
|
316
|
+
const propName = prefix ? `${prefix}.${key}` : key;
|
|
282
317
|
const data = object[key];
|
|
283
318
|
if (isRecord(data)) {
|
|
284
|
-
|
|
319
|
+
_flatten(data, propName, result);
|
|
285
320
|
}
|
|
286
321
|
else {
|
|
287
322
|
result[propName] = data;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { MessageFormatElement } from
|
|
2
|
-
import { InvalidTranslationEntry, InvalidTranslationsResult, Translation } from
|
|
1
|
+
import { MessageFormatElement } from '@formatjs/icu-messageformat-parser';
|
|
2
|
+
import { InvalidTranslationEntry, InvalidTranslationsResult, Translation } from '../types';
|
|
3
3
|
export declare const findInvalidTranslations: (source: Translation, files: Record<string, Translation>) => InvalidTranslationsResult;
|
|
4
4
|
export declare const compareTranslationFiles: (a: Translation, b: Translation) => InvalidTranslationEntry[];
|
|
5
5
|
export declare const hasDiff: (a: MessageFormatElement[], b: MessageFormatElement[]) => boolean;
|