@lingual/i18n-check 0.8.2 → 0.8.3
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 +10 -18
- package/dist/bin/index.js +58 -57
- package/dist/bin/index.test.js +289 -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 +3 -3
- package/dist/index.js +59 -42
- 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,7 +1,7 @@
|
|
|
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
6
|
};
|
|
7
7
|
export declare const checkInvalidTranslations: (source: Translation, targets: Record<string, Translation>, options?: Options) => InvalidTranslationsResult;
|
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,12 +22,12 @@ 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 hasMissingKeys = checks.includes(
|
|
30
|
-
const hasInvalidKeys = checks.includes(
|
|
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 hasMissingKeys = checks.includes('missingKeys');
|
|
30
|
+
const hasInvalidKeys = checks.includes('invalidKeys');
|
|
31
31
|
source.forEach(({ name, content }) => {
|
|
32
32
|
const files = Object.fromEntries(targets
|
|
33
33
|
.filter(({ reference }) => reference === name)
|
|
@@ -46,27 +46,27 @@ const checkTranslations = (source, targets, options = { format: "icu", checks: [
|
|
|
46
46
|
};
|
|
47
47
|
exports.checkTranslations = checkTranslations;
|
|
48
48
|
function merge(left, right) {
|
|
49
|
-
for (
|
|
49
|
+
for (const [k, v] of Object.entries(right)) {
|
|
50
50
|
left[k] = (left?.[k] ?? []).concat(v);
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
const checkUnusedKeys = async (translationFiles, filesToParse, options = {
|
|
54
|
-
format:
|
|
54
|
+
format: 'react-intl',
|
|
55
55
|
checks: [],
|
|
56
56
|
}, componentFunctions = []) => {
|
|
57
57
|
if (!options.format || !ParseFormats.includes(options.format)) {
|
|
58
58
|
return undefined;
|
|
59
59
|
}
|
|
60
|
-
if (!options.checks || !options.checks.includes(
|
|
60
|
+
if (!options.checks || !options.checks.includes('unused')) {
|
|
61
61
|
return undefined;
|
|
62
62
|
}
|
|
63
|
-
if (options.format ===
|
|
63
|
+
if (options.format === 'react-intl') {
|
|
64
64
|
return findUnusedReactIntlTranslations(translationFiles, filesToParse);
|
|
65
65
|
}
|
|
66
|
-
else if (options.format ===
|
|
66
|
+
else if (options.format === 'i18next') {
|
|
67
67
|
return findUnusedI18NextTranslations(translationFiles, filesToParse, componentFunctions);
|
|
68
68
|
}
|
|
69
|
-
else if (options.format ===
|
|
69
|
+
else if (options.format === 'next-intl') {
|
|
70
70
|
return findUnusedNextIntlTranslations(translationFiles, filesToParse);
|
|
71
71
|
}
|
|
72
72
|
};
|
|
@@ -90,7 +90,7 @@ const findUnusedReactIntlTranslations = async (translationFiles, filesToParse) =
|
|
|
90
90
|
const findUnusedI18NextTranslations = async (source, filesToParse, componentFunctions = []) => {
|
|
91
91
|
const unusedKeys = {};
|
|
92
92
|
const { extractedResult, skippableKeys } = await getI18NextKeysInCode(filesToParse, componentFunctions);
|
|
93
|
-
const extractedResultSet = new Set(extractedResult.map(({ key }) => key));
|
|
93
|
+
const extractedResultSet = new Set(extractedResult.map(({ key, namespace }) => namespace ? `${namespace}.${key}` : key));
|
|
94
94
|
source.forEach(({ name, content }) => {
|
|
95
95
|
const keysInSource = Object.keys(content);
|
|
96
96
|
const found = [];
|
|
@@ -101,7 +101,10 @@ const findUnusedI18NextTranslations = async (source, filesToParse, componentFunc
|
|
|
101
101
|
if (isSkippable !== undefined) {
|
|
102
102
|
continue;
|
|
103
103
|
}
|
|
104
|
-
|
|
104
|
+
// find the file name
|
|
105
|
+
const [fileName] = (name.split(path_1.default.sep).pop() ?? "").split(".");
|
|
106
|
+
if (!extractedResultSet.has(`${fileName}.${keyInSource}`) &&
|
|
107
|
+
!extractedResultSet.has(keyInSource)) {
|
|
105
108
|
found.push(keyInSource);
|
|
106
109
|
}
|
|
107
110
|
}
|
|
@@ -131,8 +134,8 @@ const findUnusedNextIntlTranslations = async (translationFiles, filesToParse) =>
|
|
|
131
134
|
// Check if key is part of a dynamic namespace
|
|
132
135
|
// Skip the key if it is part of the dynamic namespace
|
|
133
136
|
const isDynamicNamespace = dynamicNamespaces.find((dynamicNamespace) => {
|
|
134
|
-
const keyInSourceNamespaces = keyInSource.split(
|
|
135
|
-
return dynamicNamespace.split(
|
|
137
|
+
const keyInSourceNamespaces = keyInSource.split('.');
|
|
138
|
+
return dynamicNamespace.split('.').every((namePart, index) => {
|
|
136
139
|
return namePart === keyInSourceNamespaces[index];
|
|
137
140
|
});
|
|
138
141
|
});
|
|
@@ -148,22 +151,22 @@ const findUnusedNextIntlTranslations = async (translationFiles, filesToParse) =>
|
|
|
148
151
|
return unusedKeys;
|
|
149
152
|
};
|
|
150
153
|
const checkUndefinedKeys = async (source, filesToParse, options = {
|
|
151
|
-
format:
|
|
154
|
+
format: 'react-intl',
|
|
152
155
|
checks: [],
|
|
153
156
|
}, componentFunctions = []) => {
|
|
154
157
|
if (!options.format || !ParseFormats.includes(options.format)) {
|
|
155
158
|
return undefined;
|
|
156
159
|
}
|
|
157
|
-
if (!options.checks || !options.checks.includes(
|
|
160
|
+
if (!options.checks || !options.checks.includes('undefined')) {
|
|
158
161
|
return undefined;
|
|
159
162
|
}
|
|
160
|
-
if (options.format ===
|
|
163
|
+
if (options.format === 'react-intl') {
|
|
161
164
|
return findUndefinedReactIntlKeys(source, filesToParse);
|
|
162
165
|
}
|
|
163
|
-
else if (options.format ===
|
|
166
|
+
else if (options.format === 'i18next') {
|
|
164
167
|
return findUndefinedI18NextKeys(source, filesToParse, componentFunctions);
|
|
165
168
|
}
|
|
166
|
-
else if (options.format ===
|
|
169
|
+
else if (options.format === 'next-intl') {
|
|
167
170
|
return findUndefinedNextIntlKeys(source, filesToParse);
|
|
168
171
|
}
|
|
169
172
|
};
|
|
@@ -175,11 +178,11 @@ const findUndefinedReactIntlKeys = async (translationFiles, filesToParse) => {
|
|
|
175
178
|
const extractedResult = await (0, cli_lib_1.extract)(filesToParse, {
|
|
176
179
|
extractSourceLocation: true,
|
|
177
180
|
});
|
|
178
|
-
|
|
181
|
+
const undefinedKeys = {};
|
|
179
182
|
Object.entries(JSON.parse(extractedResult)).forEach(([key, meta]) => {
|
|
180
183
|
if (!sourceKeys.has(key)) {
|
|
181
184
|
const data = meta;
|
|
182
|
-
if (!(
|
|
185
|
+
if (!('file' in data) || typeof data.file !== 'string') {
|
|
183
186
|
return;
|
|
184
187
|
}
|
|
185
188
|
const file = path_1.default.normalize(data.file);
|
|
@@ -196,7 +199,7 @@ const findUndefinedI18NextKeys = async (source, filesToParse, componentFunctions
|
|
|
196
199
|
const sourceKeys = new Set(source.flatMap(({ content }) => {
|
|
197
200
|
return Object.keys(content);
|
|
198
201
|
}));
|
|
199
|
-
|
|
202
|
+
const undefinedKeys = {};
|
|
200
203
|
extractedResult.forEach(({ file, key }) => {
|
|
201
204
|
const isSkippable = skippableKeys.find((skippableKey) => {
|
|
202
205
|
return key.includes(skippableKey);
|
|
@@ -215,10 +218,9 @@ const findUndefinedNextIntlKeys = async (translationFiles, filesToParse) => {
|
|
|
215
218
|
return Object.keys(content);
|
|
216
219
|
}));
|
|
217
220
|
const extractedResult = (0, nextIntlSrcParser_1.extract)(filesToParse);
|
|
218
|
-
|
|
221
|
+
const undefinedKeys = {};
|
|
219
222
|
extractedResult.forEach(({ key, meta }) => {
|
|
220
223
|
if (!meta.dynamic && !sourceKeys.has(key)) {
|
|
221
|
-
// @ts-ignore
|
|
222
224
|
const file = meta.file;
|
|
223
225
|
if (!undefinedKeys[file]) {
|
|
224
226
|
undefinedKeys[file] = [];
|
|
@@ -229,26 +231,27 @@ const findUndefinedNextIntlKeys = async (translationFiles, filesToParse) => {
|
|
|
229
231
|
return undefinedKeys;
|
|
230
232
|
};
|
|
231
233
|
const isRecord = (data) => {
|
|
232
|
-
return (typeof data ===
|
|
234
|
+
return (typeof data === 'object' &&
|
|
233
235
|
!Array.isArray(data) &&
|
|
234
236
|
data !== null &&
|
|
235
237
|
data !== undefined);
|
|
236
238
|
};
|
|
237
239
|
const getI18NextKeysInCode = async (filesToParse, componentFunctions = []) => {
|
|
240
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
238
241
|
// @ts-ignore
|
|
239
|
-
const { transform } = await import(
|
|
242
|
+
const { transform } = await import('i18next-parser');
|
|
240
243
|
const i18nextParser = new transform({
|
|
241
244
|
lexers: {
|
|
242
245
|
jsx: [
|
|
243
246
|
{
|
|
244
|
-
lexer:
|
|
245
|
-
componentFunctions: componentFunctions.concat([
|
|
247
|
+
lexer: 'JsxLexer',
|
|
248
|
+
componentFunctions: componentFunctions.concat(['Trans']),
|
|
246
249
|
},
|
|
247
250
|
],
|
|
248
251
|
tsx: [
|
|
249
252
|
{
|
|
250
|
-
lexer:
|
|
251
|
-
componentFunctions: componentFunctions.concat([
|
|
253
|
+
lexer: 'JsxLexer',
|
|
254
|
+
componentFunctions: componentFunctions.concat(['Trans']),
|
|
252
255
|
},
|
|
253
256
|
],
|
|
254
257
|
},
|
|
@@ -256,32 +259,46 @@ const getI18NextKeysInCode = async (filesToParse, componentFunctions = []) => {
|
|
|
256
259
|
// Skip any parsed keys that have the `returnObjects` property set to true
|
|
257
260
|
// As these are used dynamically, they will be skipped to prevent
|
|
258
261
|
// these keys from being marked as unused.
|
|
259
|
-
|
|
262
|
+
const extractedResult = [];
|
|
260
263
|
const skippableKeys = [];
|
|
261
264
|
filesToParse.forEach((file) => {
|
|
262
|
-
const rawContent = fs_1.default.readFileSync(file,
|
|
265
|
+
const rawContent = fs_1.default.readFileSync(file, 'utf-8');
|
|
263
266
|
const entries = i18nextParser.parser.parse(rawContent, file);
|
|
264
267
|
// Intermediate solution to retrieve all keys from the parser.
|
|
265
268
|
// This will be built out to also include the namespace and check
|
|
266
269
|
// the key against the namespace corresponding file.
|
|
267
270
|
// The current implementation considers the key as used no matter the namespace.
|
|
268
271
|
for (const entry of entries) {
|
|
272
|
+
// check for namespace, i.e. `namespace:some.key`
|
|
273
|
+
const [namespace, ...keyParts] = entry.key.split(":");
|
|
274
|
+
// If there is a namespace make sure to assign the namespace
|
|
275
|
+
// and update the key name
|
|
276
|
+
// Ensure that the assumed key is not the default value
|
|
277
|
+
if (keyParts.length > 0 && entry.key !== entry.defaultValue) {
|
|
278
|
+
entry.namespace = namespace;
|
|
279
|
+
// rebuild the key without the namespace
|
|
280
|
+
entry.key = keyParts.join(":");
|
|
281
|
+
}
|
|
269
282
|
if (entry.returnObjects) {
|
|
270
283
|
skippableKeys.push(entry.key);
|
|
271
284
|
}
|
|
272
285
|
else {
|
|
273
|
-
extractedResult.push({
|
|
286
|
+
extractedResult.push({
|
|
287
|
+
file,
|
|
288
|
+
key: entry.key,
|
|
289
|
+
namespace: entry.namespace,
|
|
290
|
+
});
|
|
274
291
|
}
|
|
275
292
|
}
|
|
276
293
|
});
|
|
277
294
|
return { extractedResult, skippableKeys };
|
|
278
295
|
};
|
|
279
|
-
function
|
|
280
|
-
for (
|
|
281
|
-
|
|
296
|
+
function _flatten(object, prefix = null, result = {}) {
|
|
297
|
+
for (const key in object) {
|
|
298
|
+
const propName = prefix ? `${prefix}.${key}` : key;
|
|
282
299
|
const data = object[key];
|
|
283
300
|
if (isRecord(data)) {
|
|
284
|
-
|
|
301
|
+
_flatten(data, propName, result);
|
|
285
302
|
}
|
|
286
303
|
else {
|
|
287
304
|
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;
|
|
@@ -62,8 +62,9 @@ const hasDiff = (a, b) => {
|
|
|
62
62
|
((0, icu_messageformat_parser_1.isPoundElement)(formatElementA) && (0, icu_messageformat_parser_1.isPoundElement)(formatElementB))) {
|
|
63
63
|
return false;
|
|
64
64
|
}
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
if (!(0, icu_messageformat_parser_1.isPoundElement)(formatElementA) &&
|
|
66
|
+
!(0, icu_messageformat_parser_1.isPoundElement)(formatElementB) &&
|
|
67
|
+
formatElementA.value !== formatElementB.value) {
|
|
67
68
|
return true;
|
|
68
69
|
}
|
|
69
70
|
if ((0, icu_messageformat_parser_1.isTagElement)(formatElementA) && (0, icu_messageformat_parser_1.isTagElement)(formatElementB)) {
|
|
@@ -72,7 +73,7 @@ const hasDiff = (a, b) => {
|
|
|
72
73
|
if ((0, icu_messageformat_parser_1.isSelectElement)(formatElementA) && (0, icu_messageformat_parser_1.isSelectElement)(formatElementB)) {
|
|
73
74
|
const optionsA = Object.keys(formatElementA.options).sort();
|
|
74
75
|
const optionsB = Object.keys(formatElementB.options).sort();
|
|
75
|
-
if (optionsA.join(
|
|
76
|
+
if (optionsA.join('-') !== optionsB.join('-')) {
|
|
76
77
|
return true;
|
|
77
78
|
}
|
|
78
79
|
return optionsA.some((key) => {
|
|
@@ -133,7 +134,7 @@ const getErrorMessage = (a, b) => {
|
|
|
133
134
|
}
|
|
134
135
|
if ((0, icu_messageformat_parser_1.isSelectElement)(formatElementA) && (0, icu_messageformat_parser_1.isSelectElement)(formatElementB)) {
|
|
135
136
|
const optionsA = Object.keys(formatElementA.options).sort();
|
|
136
|
-
|
|
137
|
+
const elementErrors = [];
|
|
137
138
|
optionsA.forEach((key) => {
|
|
138
139
|
if (formatElementB.options[key]) {
|
|
139
140
|
elementErrors.push(getErrorMessage(formatElementA.options[key].value, formatElementB.options[key].value));
|
|
@@ -141,12 +142,12 @@ const getErrorMessage = (a, b) => {
|
|
|
141
142
|
});
|
|
142
143
|
acc.push(`Error in select: ${elementErrors
|
|
143
144
|
.flatMap((elementError) => elementError)
|
|
144
|
-
.join(
|
|
145
|
+
.join(', ')}`);
|
|
145
146
|
return acc;
|
|
146
147
|
}
|
|
147
148
|
if ((0, icu_messageformat_parser_1.isPluralElement)(formatElementA) && (0, icu_messageformat_parser_1.isPluralElement)(formatElementB)) {
|
|
148
149
|
const optionsA = Object.keys(formatElementA.options).sort();
|
|
149
|
-
|
|
150
|
+
const elementErrors = [];
|
|
150
151
|
optionsA.forEach((key) => {
|
|
151
152
|
if (formatElementB.options[key]) {
|
|
152
153
|
elementErrors.push(getErrorMessage(formatElementA.options[key].value, formatElementB.options[key].value));
|
|
@@ -154,7 +155,7 @@ const getErrorMessage = (a, b) => {
|
|
|
154
155
|
});
|
|
155
156
|
acc.push(`Error in plural: ${elementErrors
|
|
156
157
|
.flatMap((elementError) => elementError)
|
|
157
|
-
.join(
|
|
158
|
+
.join(', ')}`);
|
|
158
159
|
return acc;
|
|
159
160
|
}
|
|
160
161
|
return acc;
|
|
@@ -166,19 +167,19 @@ const getErrorMessage = (a, b) => {
|
|
|
166
167
|
acc.push(`Unexpected ${typeLookup[formatElementB.type]} element`);
|
|
167
168
|
return acc;
|
|
168
169
|
}, [])
|
|
169
|
-
.join(
|
|
170
|
-
return [...errors, unexpectedElements].join(
|
|
170
|
+
.join(', ');
|
|
171
|
+
return [...errors, unexpectedElements].join(', ');
|
|
171
172
|
}
|
|
172
|
-
return errors.join(
|
|
173
|
+
return errors.join(', ');
|
|
173
174
|
};
|
|
174
175
|
const typeLookup = {
|
|
175
|
-
0:
|
|
176
|
-
1:
|
|
177
|
-
2:
|
|
178
|
-
3:
|
|
179
|
-
4:
|
|
180
|
-
5:
|
|
181
|
-
6:
|
|
182
|
-
7:
|
|
183
|
-
8:
|
|
176
|
+
0: 'literal',
|
|
177
|
+
1: 'argument',
|
|
178
|
+
2: 'number',
|
|
179
|
+
3: 'date',
|
|
180
|
+
4: 'time',
|
|
181
|
+
5: 'select',
|
|
182
|
+
6: 'plural',
|
|
183
|
+
7: 'pound',
|
|
184
|
+
8: 'tag',
|
|
184
185
|
};
|