@lingual/i18n-check 0.7.4 → 0.8.0
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/dist/bin/index.js +16 -7
- package/dist/bin/index.test.js +169 -93
- package/dist/errorReporters.d.ts +7 -4
- package/dist/errorReporters.js +48 -4
- package/dist/index.js +24 -2
- package/dist/utils/findInvalidTranslations.d.ts +4 -1
- package/dist/utils/findInvalidTranslations.js +94 -4
- package/dist/utils/findInvalidTranslations.test.js +26 -8
- package/dist/utils/findInvalidi18nTranslations.js +90 -3
- package/dist/utils/findInvalidi18nTranslations.test.js +80 -11
- package/dist/utils/nextIntlSrcParser.d.ts +2 -0
- package/dist/utils/nextIntlSrcParser.js +72 -35
- package/dist/utils/nextIntlSrcParser.test.js +182 -94
- package/package.json +1 -1
|
@@ -29,9 +29,14 @@ const sortParsedKeys = (a, b) => {
|
|
|
29
29
|
const compareTranslationFiles = (a, b) => {
|
|
30
30
|
let diffs = [];
|
|
31
31
|
for (const key in a) {
|
|
32
|
-
if (b[key]
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
if (b[key] === undefined) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
const parsedTranslationA = (0, icu_messageformat_parser_1.parse)(String(a[key]));
|
|
36
|
+
const parsedTranslationB = (0, icu_messageformat_parser_1.parse)(String(b[key]));
|
|
37
|
+
if ((0, exports.hasDiff)(parsedTranslationA, parsedTranslationB)) {
|
|
38
|
+
const msg = getErrorMessage(parsedTranslationA, parsedTranslationB);
|
|
39
|
+
diffs.push({ key, msg });
|
|
35
40
|
}
|
|
36
41
|
}
|
|
37
42
|
return diffs;
|
|
@@ -64,7 +69,7 @@ const hasDiff = (a, b) => {
|
|
|
64
69
|
if ((0, icu_messageformat_parser_1.isTagElement)(formatElementA) && (0, icu_messageformat_parser_1.isTagElement)(formatElementB)) {
|
|
65
70
|
return (0, exports.hasDiff)(formatElementA.children, formatElementB.children);
|
|
66
71
|
}
|
|
67
|
-
if ((
|
|
72
|
+
if ((0, icu_messageformat_parser_1.isSelectElement)(formatElementA) && (0, icu_messageformat_parser_1.isSelectElement)(formatElementB)) {
|
|
68
73
|
const optionsA = Object.keys(formatElementA.options).sort();
|
|
69
74
|
const optionsB = Object.keys(formatElementB.options).sort();
|
|
70
75
|
if (optionsA.join("-") !== optionsB.join("-")) {
|
|
@@ -92,3 +97,88 @@ const hasDiff = (a, b) => {
|
|
|
92
97
|
return hasErrors;
|
|
93
98
|
};
|
|
94
99
|
exports.hasDiff = hasDiff;
|
|
100
|
+
const getErrorMessage = (a, b) => {
|
|
101
|
+
const compA = a
|
|
102
|
+
.filter((element) => !(0, icu_messageformat_parser_1.isLiteralElement)(element))
|
|
103
|
+
.sort(sortParsedKeys);
|
|
104
|
+
const compB = b
|
|
105
|
+
.filter((element) => !(0, icu_messageformat_parser_1.isLiteralElement)(element))
|
|
106
|
+
.sort(sortParsedKeys);
|
|
107
|
+
const errors = compA.reduce((acc, formatElementA, index) => {
|
|
108
|
+
const formatElementB = compB[index];
|
|
109
|
+
if (!formatElementB) {
|
|
110
|
+
acc.push(`Missing element ${typeLookup[formatElementA.type]}`);
|
|
111
|
+
return acc;
|
|
112
|
+
}
|
|
113
|
+
if (formatElementA.type !== formatElementB.type) {
|
|
114
|
+
acc.push(`Expected element of type "${typeLookup[formatElementA.type]}" but received "${typeLookup[formatElementB.type]}"`);
|
|
115
|
+
return acc;
|
|
116
|
+
}
|
|
117
|
+
if (formatElementA.location !== formatElementB.location) {
|
|
118
|
+
acc.push(`Expected location to be ${formatElementA.location?.start?.line}:${formatElementA.location?.start?.column}`);
|
|
119
|
+
return acc;
|
|
120
|
+
}
|
|
121
|
+
if ((0, icu_messageformat_parser_1.isPoundElement)(formatElementA) && (0, icu_messageformat_parser_1.isPoundElement)(formatElementB)) {
|
|
122
|
+
return acc;
|
|
123
|
+
}
|
|
124
|
+
if (!(0, icu_messageformat_parser_1.isPoundElement)(formatElementA) &&
|
|
125
|
+
!(0, icu_messageformat_parser_1.isPoundElement)(formatElementB) &&
|
|
126
|
+
formatElementA.value !== formatElementB.value) {
|
|
127
|
+
acc.push(`Expected ${typeLookup[formatElementA.type]} to contain "${formatElementA.value}" but received "${formatElementB.value}"`);
|
|
128
|
+
return acc;
|
|
129
|
+
}
|
|
130
|
+
if ((0, icu_messageformat_parser_1.isTagElement)(formatElementA) && (0, icu_messageformat_parser_1.isTagElement)(formatElementB)) {
|
|
131
|
+
acc.push(`Error in pound element: ${getErrorMessage(formatElementA.children, formatElementB.children)}`);
|
|
132
|
+
return acc;
|
|
133
|
+
}
|
|
134
|
+
if ((0, icu_messageformat_parser_1.isSelectElement)(formatElementA) && (0, icu_messageformat_parser_1.isSelectElement)(formatElementB)) {
|
|
135
|
+
const optionsA = Object.keys(formatElementA.options).sort();
|
|
136
|
+
let elementErrors = [];
|
|
137
|
+
optionsA.forEach((key) => {
|
|
138
|
+
if (formatElementB.options[key]) {
|
|
139
|
+
elementErrors.push(getErrorMessage(formatElementA.options[key].value, formatElementB.options[key].value));
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
acc.push(`Error in select: ${elementErrors
|
|
143
|
+
.flatMap((elementError) => elementError)
|
|
144
|
+
.join(", ")}`);
|
|
145
|
+
return acc;
|
|
146
|
+
}
|
|
147
|
+
if ((0, icu_messageformat_parser_1.isPluralElement)(formatElementA) && (0, icu_messageformat_parser_1.isPluralElement)(formatElementB)) {
|
|
148
|
+
const optionsA = Object.keys(formatElementA.options).sort();
|
|
149
|
+
let elementErrors = [];
|
|
150
|
+
optionsA.forEach((key) => {
|
|
151
|
+
if (formatElementB.options[key]) {
|
|
152
|
+
elementErrors.push(getErrorMessage(formatElementA.options[key].value, formatElementB.options[key].value));
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
acc.push(`Error in plural: ${elementErrors
|
|
156
|
+
.flatMap((elementError) => elementError)
|
|
157
|
+
.join(", ")}`);
|
|
158
|
+
return acc;
|
|
159
|
+
}
|
|
160
|
+
return acc;
|
|
161
|
+
}, []);
|
|
162
|
+
if (compA.length < compB.length) {
|
|
163
|
+
const unexpectedElements = compB
|
|
164
|
+
.slice(compA.length)
|
|
165
|
+
.reduce((acc, formatElementB) => {
|
|
166
|
+
acc.push(`Unexpected ${typeLookup[formatElementB.type]} element`);
|
|
167
|
+
return acc;
|
|
168
|
+
}, [])
|
|
169
|
+
.join(", ");
|
|
170
|
+
return [...errors, unexpectedElements].join(", ");
|
|
171
|
+
}
|
|
172
|
+
return errors.join(", ");
|
|
173
|
+
};
|
|
174
|
+
const typeLookup = {
|
|
175
|
+
0: "literal",
|
|
176
|
+
1: "argument",
|
|
177
|
+
2: "number",
|
|
178
|
+
3: "date",
|
|
179
|
+
4: "time",
|
|
180
|
+
5: "select",
|
|
181
|
+
6: "plural",
|
|
182
|
+
7: "pound",
|
|
183
|
+
8: "tag",
|
|
184
|
+
};
|
|
@@ -12,7 +12,7 @@ describe("findInvalidTranslations:compareTranslationFiles", () => {
|
|
|
12
12
|
expect((0, findInvalidTranslations_1.compareTranslationFiles)((0, flattenTranslations_1.flattenTranslations)({
|
|
13
13
|
...sourceFile,
|
|
14
14
|
"ten.eleven.twelve": "ten eleven twelve",
|
|
15
|
-
}), (0, flattenTranslations_1.flattenTranslations)(secondaryFile))).toEqual(["multipleVariables"]);
|
|
15
|
+
}), (0, flattenTranslations_1.flattenTranslations)(secondaryFile))).toEqual([{ key: "multipleVariables", msg: "Unexpected date element" }]);
|
|
16
16
|
});
|
|
17
17
|
it("should return empty array if placeholders are identical but in different positions", () => {
|
|
18
18
|
expect((0, findInvalidTranslations_1.compareTranslationFiles)({
|
|
@@ -27,7 +27,9 @@ describe("findInvalidTranslations", () => {
|
|
|
27
27
|
expect((0, findInvalidTranslations_1.findInvalidTranslations)(sourceFile, { de: sourceFile })).toEqual({});
|
|
28
28
|
});
|
|
29
29
|
it("should return an object containing the keys for the missing language", () => {
|
|
30
|
-
expect((0, findInvalidTranslations_1.findInvalidTranslations)({ ...sourceFile, "ten.eleven.twelve": "ten eleven twelve" }, { de: secondaryFile })).toEqual({
|
|
30
|
+
expect((0, findInvalidTranslations_1.findInvalidTranslations)({ ...sourceFile, "ten.eleven.twelve": "ten eleven twelve" }, { de: secondaryFile })).toEqual({
|
|
31
|
+
de: [{ key: "multipleVariables", msg: "Unexpected date element" }],
|
|
32
|
+
});
|
|
31
33
|
});
|
|
32
34
|
it("should return an object containing the keys for every language with missing key", () => {
|
|
33
35
|
expect((0, findInvalidTranslations_1.findInvalidTranslations)({ ...sourceFile, "ten.eleven.twelve": "ten eleven twelve" }, {
|
|
@@ -38,8 +40,13 @@ describe("findInvalidTranslations", () => {
|
|
|
38
40
|
"message.text-format": "yo,<p><b>John</b></p>!",
|
|
39
41
|
},
|
|
40
42
|
})).toEqual({
|
|
41
|
-
de: ["multipleVariables"],
|
|
42
|
-
fr: [
|
|
43
|
+
de: [{ key: "multipleVariables", msg: "Unexpected date element" }],
|
|
44
|
+
fr: [
|
|
45
|
+
{
|
|
46
|
+
key: "message.text-format",
|
|
47
|
+
msg: 'Expected tag to contain "b" but received "p"',
|
|
48
|
+
},
|
|
49
|
+
],
|
|
43
50
|
});
|
|
44
51
|
});
|
|
45
52
|
it("should allow for different types of keys per locale", () => {
|
|
@@ -47,9 +54,14 @@ describe("findInvalidTranslations", () => {
|
|
|
47
54
|
de: {
|
|
48
55
|
...secondaryFile,
|
|
49
56
|
"message.plural": "{count, plural, other {# of {total} items}}",
|
|
50
|
-
}
|
|
57
|
+
},
|
|
51
58
|
})).toEqual({
|
|
52
|
-
de: [
|
|
59
|
+
de: [
|
|
60
|
+
{
|
|
61
|
+
key: "multipleVariables",
|
|
62
|
+
msg: "Unexpected date element",
|
|
63
|
+
},
|
|
64
|
+
],
|
|
53
65
|
});
|
|
54
66
|
});
|
|
55
67
|
it("should fail if a variable is changed in one of the translations", () => {
|
|
@@ -57,9 +69,15 @@ describe("findInvalidTranslations", () => {
|
|
|
57
69
|
de: {
|
|
58
70
|
...secondaryFile,
|
|
59
71
|
"message.plural": "{count, plural, other {# of {cargado} items}}",
|
|
60
|
-
}
|
|
72
|
+
},
|
|
61
73
|
})).toEqual({
|
|
62
|
-
de: [
|
|
74
|
+
de: [
|
|
75
|
+
{
|
|
76
|
+
key: "message.plural",
|
|
77
|
+
msg: 'Error in plural: Expected argument to contain "total" but received "cargado"',
|
|
78
|
+
},
|
|
79
|
+
{ key: "multipleVariables", msg: "Unexpected date element" },
|
|
80
|
+
],
|
|
63
81
|
});
|
|
64
82
|
});
|
|
65
83
|
});
|
|
@@ -25,9 +25,14 @@ exports.findInvalid18nTranslations = findInvalid18nTranslations;
|
|
|
25
25
|
const compareTranslationFiles = (a, b) => {
|
|
26
26
|
let diffs = [];
|
|
27
27
|
for (const key in a) {
|
|
28
|
-
if (b[key]
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
if (b[key] === undefined) {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
const parsedTranslationA = (0, i18NextParser_1.parse)(String(a[key]));
|
|
32
|
+
const parsedTranslationB = (0, i18NextParser_1.parse)(String(b[key]));
|
|
33
|
+
if ((0, exports.hasDiff)(parsedTranslationA, parsedTranslationB)) {
|
|
34
|
+
const msg = getErrorMessage(parsedTranslationA, parsedTranslationB);
|
|
35
|
+
diffs.push({ key, msg });
|
|
31
36
|
}
|
|
32
37
|
}
|
|
33
38
|
return diffs;
|
|
@@ -103,3 +108,85 @@ const hasDiff = (a, b) => {
|
|
|
103
108
|
return hasErrors;
|
|
104
109
|
};
|
|
105
110
|
exports.hasDiff = hasDiff;
|
|
111
|
+
const getErrorMessage = (a, b) => {
|
|
112
|
+
const compA = a
|
|
113
|
+
.filter((element) => element.type !== "text")
|
|
114
|
+
.sort(sortParsedKeys);
|
|
115
|
+
const compB = b
|
|
116
|
+
.filter((element) => element.type !== "text")
|
|
117
|
+
.sort(sortParsedKeys);
|
|
118
|
+
const errors = compA.reduce((acc, formatElementA, index) => {
|
|
119
|
+
const formatElementB = compB[index];
|
|
120
|
+
if (!formatElementB) {
|
|
121
|
+
acc.push(`Missing element ${formatElementA.type}`);
|
|
122
|
+
return acc;
|
|
123
|
+
}
|
|
124
|
+
if (formatElementA.type !== formatElementB.type) {
|
|
125
|
+
acc.push(`Expected element of type "${formatElementA.type}" but received "${formatElementB.type}"`);
|
|
126
|
+
return acc;
|
|
127
|
+
}
|
|
128
|
+
if (formatElementA.type === "tag" && formatElementB.type === "tag") {
|
|
129
|
+
if (formatElementA.raw !== formatElementB.raw) {
|
|
130
|
+
acc.push(`Expected tag "${formatElementA.raw}" but received "${formatElementB.raw}"`);
|
|
131
|
+
}
|
|
132
|
+
else if (formatElementA.voidElement !== formatElementB.voidElement &&
|
|
133
|
+
formatElementA.voidElement === true) {
|
|
134
|
+
acc.push(`Expected a self-closing "${formatElementB.raw}" tag`);
|
|
135
|
+
return acc;
|
|
136
|
+
}
|
|
137
|
+
else if (formatElementA.voidElement !== formatElementB.voidElement &&
|
|
138
|
+
formatElementA.voidElement === false) {
|
|
139
|
+
acc.push(`Non expected self-closing "${formatElementB.raw}" tag`);
|
|
140
|
+
return acc;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if ((formatElementA.type === "interpolation" &&
|
|
144
|
+
formatElementB.type === "interpolation") ||
|
|
145
|
+
(formatElementA.type === "interpolation_unescaped" &&
|
|
146
|
+
formatElementB.type === "interpolation_unescaped") ||
|
|
147
|
+
(formatElementA.type === "nesting" &&
|
|
148
|
+
formatElementB.type === "nesting") ||
|
|
149
|
+
(formatElementA.type === "plural" && formatElementB.type === "plural")) {
|
|
150
|
+
if (formatElementA.prefix !== formatElementA.prefix) {
|
|
151
|
+
acc.push(`Error in ${formatElementA.type}: Expected prefix "${formatElementA.prefix}" but received "${formatElementB.prefix}"`);
|
|
152
|
+
return acc;
|
|
153
|
+
}
|
|
154
|
+
if (formatElementA.suffix !== formatElementA.suffix) {
|
|
155
|
+
acc.push(`Error in ${formatElementA.type}: Expected suffix "${formatElementA.suffix}" but received "${formatElementB.suffix}"`);
|
|
156
|
+
return acc;
|
|
157
|
+
}
|
|
158
|
+
const optionsA = formatElementA.variable
|
|
159
|
+
.split(",")
|
|
160
|
+
.map((value) => value.trim())
|
|
161
|
+
.sort();
|
|
162
|
+
const optionsB = formatElementB.variable
|
|
163
|
+
.split(",")
|
|
164
|
+
.map((value) => value.trim())
|
|
165
|
+
.sort();
|
|
166
|
+
let elementErrors = [];
|
|
167
|
+
optionsA.forEach((key, index) => {
|
|
168
|
+
if (key !== optionsB[index]) {
|
|
169
|
+
elementErrors.push(`Expected ${key} but received ${optionsB[index]}`);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
if (elementErrors.length > 0) {
|
|
173
|
+
acc.push(`Error in ${formatElementA.type}: ${elementErrors
|
|
174
|
+
.flatMap((elementError) => elementError)
|
|
175
|
+
.join(", ")}`);
|
|
176
|
+
}
|
|
177
|
+
return acc;
|
|
178
|
+
}
|
|
179
|
+
return acc;
|
|
180
|
+
}, []);
|
|
181
|
+
if (compA.length < compB.length) {
|
|
182
|
+
const unexpectedElements = compB
|
|
183
|
+
.slice(compA.length)
|
|
184
|
+
.reduce((acc, formatElementB) => {
|
|
185
|
+
acc.push(`Unexpected ${formatElementB.type} element`);
|
|
186
|
+
return acc;
|
|
187
|
+
}, [])
|
|
188
|
+
.join(", ");
|
|
189
|
+
return [...errors, unexpectedElements].join(", ");
|
|
190
|
+
}
|
|
191
|
+
return errors.join(", ");
|
|
192
|
+
};
|
|
@@ -12,7 +12,16 @@ describe("findInvalid18nTranslations:compareTranslationFiles", () => {
|
|
|
12
12
|
expect((0, findInvalidi18nTranslations_1.compareTranslationFiles)((0, flattenTranslations_1.flattenTranslations)({
|
|
13
13
|
...sourceFile,
|
|
14
14
|
"ten.eleven.twelve": "ten eleven twelve",
|
|
15
|
-
}), (0, flattenTranslations_1.flattenTranslations)(targetFile))).toEqual([
|
|
15
|
+
}), (0, flattenTranslations_1.flattenTranslations)(targetFile))).toEqual([
|
|
16
|
+
{
|
|
17
|
+
key: "key_with_broken_de",
|
|
18
|
+
msg: "Error in interpolation: Expected value but received val",
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
key: "intlNumber_broken_de",
|
|
22
|
+
msg: "Missing element interpolation",
|
|
23
|
+
},
|
|
24
|
+
]);
|
|
16
25
|
});
|
|
17
26
|
it("should return an empty array if the strings contain paranthesis that have different content", () => {
|
|
18
27
|
expect((0, findInvalidi18nTranslations_1.compareTranslationFiles)((0, flattenTranslations_1.flattenTranslations)({
|
|
@@ -31,7 +40,12 @@ describe("findInvalid18nTranslations:compareTranslationFiles", () => {
|
|
|
31
40
|
tag: "This is some <b>bold text</b> and some <i>italic</i> text.",
|
|
32
41
|
}, {
|
|
33
42
|
tag: "There is some <b>bold text</b> and some other <span>italic</span> text.",
|
|
34
|
-
})).toEqual([
|
|
43
|
+
})).toEqual([
|
|
44
|
+
{
|
|
45
|
+
key: "tag",
|
|
46
|
+
msg: 'Expected tag "</i>" but received "</span>", Expected tag "<i>" but received "<span>"',
|
|
47
|
+
},
|
|
48
|
+
]);
|
|
35
49
|
});
|
|
36
50
|
it("should return empty array if tags are identical", () => {
|
|
37
51
|
expect((0, findInvalidi18nTranslations_1.compareTranslationFiles)({
|
|
@@ -46,7 +60,18 @@ describe("findInvalidTranslations", () => {
|
|
|
46
60
|
expect((0, findInvalidi18nTranslations_1.findInvalid18nTranslations)(sourceFile, { de: sourceFile })).toEqual({});
|
|
47
61
|
});
|
|
48
62
|
it("should return an object containing the keys for the missing language", () => {
|
|
49
|
-
expect((0, findInvalidi18nTranslations_1.findInvalid18nTranslations)({ ...sourceFile, "ten.eleven.twelve": "ten eleven twelve" }, { de: targetFile })).toEqual({
|
|
63
|
+
expect((0, findInvalidi18nTranslations_1.findInvalid18nTranslations)({ ...sourceFile, "ten.eleven.twelve": "ten eleven twelve" }, { de: targetFile })).toEqual({
|
|
64
|
+
de: [
|
|
65
|
+
{
|
|
66
|
+
key: "key_with_broken_de",
|
|
67
|
+
msg: "Error in interpolation: Expected value but received val",
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
key: "intlNumber_broken_de",
|
|
71
|
+
msg: "Missing element interpolation",
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
});
|
|
50
75
|
});
|
|
51
76
|
it("should return an object containing the keys for every language with missing key", () => {
|
|
52
77
|
expect((0, findInvalidi18nTranslations_1.findInvalid18nTranslations)({ ...sourceFile, "ten.eleven.twelve": "ten eleven twelve" }, {
|
|
@@ -55,8 +80,22 @@ describe("findInvalidTranslations", () => {
|
|
|
55
80
|
key_with_broken_de: "Some format {{value, formatname}} and some other format {{value, formatname}}",
|
|
56
81
|
},
|
|
57
82
|
})).toEqual({
|
|
58
|
-
de: [
|
|
59
|
-
|
|
83
|
+
de: [
|
|
84
|
+
{
|
|
85
|
+
key: "key_with_broken_de",
|
|
86
|
+
msg: "Error in interpolation: Expected value but received val",
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
key: "intlNumber_broken_de",
|
|
90
|
+
msg: "Missing element interpolation",
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
fr: [
|
|
94
|
+
{
|
|
95
|
+
key: "key_with_broken_de",
|
|
96
|
+
msg: "Unexpected interpolation element",
|
|
97
|
+
},
|
|
98
|
+
],
|
|
60
99
|
});
|
|
61
100
|
});
|
|
62
101
|
it("should find invalid interval", () => {
|
|
@@ -67,7 +106,12 @@ describe("findInvalidTranslations", () => {
|
|
|
67
106
|
key1_interval: "(1-2)[one or two items];(3-7)[a few items];(7-inf)[a lot of items];",
|
|
68
107
|
},
|
|
69
108
|
})).toEqual({
|
|
70
|
-
de: [
|
|
109
|
+
de: [
|
|
110
|
+
{
|
|
111
|
+
key: "key1_interval",
|
|
112
|
+
msg: "Error in plural: Expected 1 but received 1-2, Error in plural: Expected 2-7 but received 3-7",
|
|
113
|
+
},
|
|
114
|
+
],
|
|
71
115
|
});
|
|
72
116
|
});
|
|
73
117
|
it("should find invalid nested interpolation", () => {
|
|
@@ -78,7 +122,12 @@ describe("findInvalidTranslations", () => {
|
|
|
78
122
|
"tree.one": "added {{somethings}}",
|
|
79
123
|
},
|
|
80
124
|
})).toEqual({
|
|
81
|
-
de: [
|
|
125
|
+
de: [
|
|
126
|
+
{
|
|
127
|
+
key: "tree.one",
|
|
128
|
+
msg: "Error in interpolation: Expected something but received somethings",
|
|
129
|
+
},
|
|
130
|
+
],
|
|
82
131
|
});
|
|
83
132
|
});
|
|
84
133
|
it("should find invalid relative time formatting", () => {
|
|
@@ -89,7 +138,12 @@ describe("findInvalidTranslations", () => {
|
|
|
89
138
|
intlRelativeTimeWithOptionsExplicit: "Lorem {{val, relativetime(range: quarter; style: long;)}}",
|
|
90
139
|
},
|
|
91
140
|
})).toEqual({
|
|
92
|
-
de: [
|
|
141
|
+
de: [
|
|
142
|
+
{
|
|
143
|
+
key: "intlRelativeTimeWithOptionsExplicit",
|
|
144
|
+
msg: "Error in interpolation: Expected relativetime(range: quarter; style: narrow;) but received relativetime(range: quarter; style: long;)",
|
|
145
|
+
},
|
|
146
|
+
],
|
|
93
147
|
});
|
|
94
148
|
});
|
|
95
149
|
it("should find invalid key with options", () => {
|
|
@@ -100,7 +154,12 @@ describe("findInvalidTranslations", () => {
|
|
|
100
154
|
keyWithOptions: "Some format {{value, formatname(option3Name: option3Value; option4Name: option4Value)}}",
|
|
101
155
|
},
|
|
102
156
|
})).toEqual({
|
|
103
|
-
de: [
|
|
157
|
+
de: [
|
|
158
|
+
{
|
|
159
|
+
key: "keyWithOptions",
|
|
160
|
+
msg: "Error in interpolation: Expected formatname(option1Name: option1Value; option2Name: option2Value) but received formatname(option3Name: option3Value; option4Name: option4Value)",
|
|
161
|
+
},
|
|
162
|
+
],
|
|
104
163
|
});
|
|
105
164
|
});
|
|
106
165
|
it("should find invalid nesting", () => {
|
|
@@ -111,7 +170,12 @@ describe("findInvalidTranslations", () => {
|
|
|
111
170
|
nesting1: "1 $t(nesting3)",
|
|
112
171
|
},
|
|
113
172
|
})).toEqual({
|
|
114
|
-
de: [
|
|
173
|
+
de: [
|
|
174
|
+
{
|
|
175
|
+
key: "nesting1",
|
|
176
|
+
msg: "Error in nesting: Expected nesting2 but received nesting3",
|
|
177
|
+
},
|
|
178
|
+
],
|
|
115
179
|
});
|
|
116
180
|
});
|
|
117
181
|
it("should find invalid tags", () => {
|
|
@@ -122,7 +186,12 @@ describe("findInvalidTranslations", () => {
|
|
|
122
186
|
tag: "There is some <b>bold text</b> and some other <span>text inside a span</span>!",
|
|
123
187
|
},
|
|
124
188
|
})).toEqual({
|
|
125
|
-
de: [
|
|
189
|
+
de: [
|
|
190
|
+
{
|
|
191
|
+
key: "tag",
|
|
192
|
+
msg: 'Expected tag "</i>" but received "</span>", Expected tag "<i>" but received "<span>"',
|
|
193
|
+
},
|
|
194
|
+
],
|
|
126
195
|
});
|
|
127
196
|
});
|
|
128
197
|
it("should recognize special characters", () => {
|
|
@@ -44,7 +44,7 @@ const GET_TRANSLATIONS = "getTranslations";
|
|
|
44
44
|
const COMMENT_CONTAINS_STATIC_KEY_REGEX = /t\((["'])(.*?[^\\])(["'])\)/;
|
|
45
45
|
const extract = (filesPaths) => {
|
|
46
46
|
return filesPaths.flatMap(getKeys).sort((a, b) => {
|
|
47
|
-
return a > b ? 1 : -1;
|
|
47
|
+
return a.key > b.key ? 1 : -1;
|
|
48
48
|
});
|
|
49
49
|
};
|
|
50
50
|
exports.extract = extract;
|
|
@@ -53,24 +53,36 @@ const getKeys = (path) => {
|
|
|
53
53
|
const sourceFile = ts.createSourceFile(path, content, ts.ScriptTarget.Latest, true);
|
|
54
54
|
const foundKeys = [];
|
|
55
55
|
let namespaces = [];
|
|
56
|
-
|
|
57
|
-
const getCurrentNamespace = () => {
|
|
56
|
+
const getCurrentNamespaces = (range = 1) => {
|
|
58
57
|
if (namespaces.length > 0) {
|
|
59
|
-
return namespaces
|
|
58
|
+
return namespaces.slice(namespaces.length - range);
|
|
60
59
|
}
|
|
61
60
|
return null;
|
|
62
61
|
};
|
|
63
|
-
const
|
|
64
|
-
namespaces.
|
|
62
|
+
const getCurrentNamespaceForIdentifier = (name) => {
|
|
63
|
+
return [...namespaces].reverse().find((namespace) => {
|
|
64
|
+
return namespace.variable === name;
|
|
65
|
+
});
|
|
65
66
|
};
|
|
66
|
-
const
|
|
67
|
+
const pushNamespace = (namespace) => {
|
|
68
|
+
namespaces.push(namespace);
|
|
69
|
+
};
|
|
70
|
+
const setNamespaceAsDynamic = (name) => {
|
|
71
|
+
namespaces = namespaces.map((namespace) => {
|
|
72
|
+
if (namespace.name === name) {
|
|
73
|
+
return { ...namespace, dynamic: true };
|
|
74
|
+
}
|
|
75
|
+
return namespace;
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
const removeNamespaces = (range = 1) => {
|
|
67
79
|
if (namespaces.length > 0) {
|
|
68
|
-
namespaces.
|
|
80
|
+
namespaces = namespaces.slice(0, namespaces.length - range);
|
|
69
81
|
}
|
|
70
82
|
};
|
|
71
83
|
const visit = (node) => {
|
|
72
84
|
let key = null;
|
|
73
|
-
let
|
|
85
|
+
let initialNamespacesLength = namespaces.length;
|
|
74
86
|
if (node === undefined) {
|
|
75
87
|
return;
|
|
76
88
|
}
|
|
@@ -82,14 +94,12 @@ const getKeys = (path) => {
|
|
|
82
94
|
// from the default `t`, i.e.: const other = useTranslations("namespace1");
|
|
83
95
|
if (node.initializer.expression.text === USE_TRANSLATIONS) {
|
|
84
96
|
const [argument] = node.initializer.arguments;
|
|
97
|
+
const variable = ts.isIdentifier(node.name) ? node.name.text : "t";
|
|
85
98
|
if (argument && ts.isStringLiteral(argument)) {
|
|
86
|
-
pushNamespace(argument.text);
|
|
99
|
+
pushNamespace({ name: argument.text, variable });
|
|
87
100
|
}
|
|
88
101
|
else if (argument === undefined) {
|
|
89
|
-
pushNamespace("");
|
|
90
|
-
}
|
|
91
|
-
if (ts.isIdentifier(node.name)) {
|
|
92
|
-
variable = node.name.text;
|
|
102
|
+
pushNamespace({ name: "", variable });
|
|
93
103
|
}
|
|
94
104
|
}
|
|
95
105
|
}
|
|
@@ -109,6 +119,7 @@ const getKeys = (path) => {
|
|
|
109
119
|
ts.isIdentifier(node.initializer.expression.expression)) {
|
|
110
120
|
if (node.initializer.expression.expression.text === GET_TRANSLATIONS) {
|
|
111
121
|
const [argument] = node.initializer.expression.arguments;
|
|
122
|
+
const variable = ts.isIdentifier(node.name) ? node.name.text : "t";
|
|
112
123
|
if (argument && ts.isObjectLiteralExpression(argument)) {
|
|
113
124
|
argument.properties.forEach((property) => {
|
|
114
125
|
if (property &&
|
|
@@ -117,18 +128,15 @@ const getKeys = (path) => {
|
|
|
117
128
|
ts.isIdentifier(property.name) &&
|
|
118
129
|
property.name.text === "namespace" &&
|
|
119
130
|
ts.isStringLiteral(property.initializer)) {
|
|
120
|
-
pushNamespace(property.initializer.text);
|
|
131
|
+
pushNamespace({ name: property.initializer.text, variable });
|
|
121
132
|
}
|
|
122
133
|
});
|
|
123
134
|
}
|
|
124
135
|
else if (argument && ts.isStringLiteral(argument)) {
|
|
125
|
-
pushNamespace(argument.text);
|
|
136
|
+
pushNamespace({ name: argument.text, variable });
|
|
126
137
|
}
|
|
127
138
|
else if (argument === undefined) {
|
|
128
|
-
pushNamespace("");
|
|
129
|
-
}
|
|
130
|
-
if (ts.isIdentifier(node.name)) {
|
|
131
|
-
variable = node.name.text;
|
|
139
|
+
pushNamespace({ name: "", variable });
|
|
132
140
|
}
|
|
133
141
|
}
|
|
134
142
|
}
|
|
@@ -168,7 +176,7 @@ const getKeys = (path) => {
|
|
|
168
176
|
if (key) {
|
|
169
177
|
foundKeys.push({
|
|
170
178
|
key: inlineNamespace ? `${inlineNamespace}.${key}` : key,
|
|
171
|
-
meta: { file: path },
|
|
179
|
+
meta: { file: path, namespace: inlineNamespace ?? undefined },
|
|
172
180
|
});
|
|
173
181
|
}
|
|
174
182
|
}
|
|
@@ -177,35 +185,50 @@ const getKeys = (path) => {
|
|
|
177
185
|
}
|
|
178
186
|
}
|
|
179
187
|
// Search for `t()` calls
|
|
180
|
-
if (
|
|
188
|
+
if (getCurrentNamespaces() !== null &&
|
|
181
189
|
ts.isCallExpression(node) &&
|
|
182
190
|
ts.isIdentifier(node.expression)) {
|
|
183
191
|
const expressionName = node.expression.text;
|
|
184
|
-
|
|
192
|
+
const namespace = getCurrentNamespaceForIdentifier(expressionName);
|
|
193
|
+
if (namespace) {
|
|
185
194
|
const [argument] = node.arguments;
|
|
186
195
|
if (argument && ts.isStringLiteral(argument)) {
|
|
187
|
-
key = argument.text;
|
|
196
|
+
key = { name: argument.text, identifier: expressionName };
|
|
197
|
+
}
|
|
198
|
+
else if (argument && ts.isIdentifier(argument)) {
|
|
199
|
+
setNamespaceAsDynamic(namespace.name);
|
|
200
|
+
}
|
|
201
|
+
else if (argument && ts.isTemplateExpression(argument)) {
|
|
202
|
+
setNamespaceAsDynamic(namespace.name);
|
|
188
203
|
}
|
|
189
204
|
}
|
|
190
205
|
}
|
|
191
206
|
// Search for `t.*()` calls, i.e. t.html() or t.rich()
|
|
192
|
-
if (
|
|
207
|
+
if (getCurrentNamespaces() !== null &&
|
|
193
208
|
ts.isCallExpression(node) &&
|
|
194
209
|
ts.isPropertyAccessExpression(node.expression) &&
|
|
195
210
|
ts.isIdentifier(node.expression.expression)) {
|
|
196
211
|
const expressionName = node.expression.expression.text;
|
|
197
|
-
|
|
212
|
+
const namespace = getCurrentNamespaceForIdentifier(expressionName);
|
|
213
|
+
if (namespace) {
|
|
198
214
|
const [argument] = node.arguments;
|
|
199
215
|
if (argument && ts.isStringLiteral(argument)) {
|
|
200
|
-
key = argument.text;
|
|
216
|
+
key = { name: argument.text, identifier: expressionName };
|
|
217
|
+
}
|
|
218
|
+
else if (argument && ts.isIdentifier(argument)) {
|
|
219
|
+
setNamespaceAsDynamic(namespace.name);
|
|
220
|
+
}
|
|
221
|
+
else if (argument && ts.isTemplateExpression(argument)) {
|
|
222
|
+
setNamespaceAsDynamic(namespace.name);
|
|
201
223
|
}
|
|
202
224
|
}
|
|
203
225
|
}
|
|
204
226
|
if (key) {
|
|
205
|
-
const namespace =
|
|
227
|
+
const namespace = getCurrentNamespaceForIdentifier(key.identifier);
|
|
228
|
+
const namespaceName = namespace ? namespace.name : "";
|
|
206
229
|
foundKeys.push({
|
|
207
|
-
key:
|
|
208
|
-
meta: { file: path },
|
|
230
|
+
key: namespaceName ? `${namespaceName}.${key.name}` : key.name,
|
|
231
|
+
meta: { file: path, namespace: namespaceName },
|
|
209
232
|
});
|
|
210
233
|
}
|
|
211
234
|
// Search for single-line comments that contain the static values of a dynamic key
|
|
@@ -225,18 +248,32 @@ const getKeys = (path) => {
|
|
|
225
248
|
// capture the string comment
|
|
226
249
|
const commentKey = COMMENT_CONTAINS_STATIC_KEY_REGEX.exec(comment)?.[2];
|
|
227
250
|
if (commentKey) {
|
|
228
|
-
const namespace =
|
|
251
|
+
const namespace = getCurrentNamespaces();
|
|
252
|
+
const namespaceName = namespace ? namespace[0]?.name : "";
|
|
229
253
|
foundKeys.push({
|
|
230
|
-
key:
|
|
231
|
-
|
|
254
|
+
key: namespaceName
|
|
255
|
+
? `${namespaceName}.${commentKey}`
|
|
256
|
+
: commentKey,
|
|
257
|
+
meta: { file: path, namespace: namespaceName },
|
|
232
258
|
});
|
|
233
259
|
}
|
|
234
260
|
}
|
|
235
261
|
});
|
|
236
262
|
}
|
|
237
263
|
ts.forEachChild(node, visit);
|
|
238
|
-
if (ts.isFunctionLike(node) &&
|
|
239
|
-
|
|
264
|
+
if (ts.isFunctionLike(node) &&
|
|
265
|
+
namespaces.length > initialNamespacesLength) {
|
|
266
|
+
// check if the namespaces are dynamic and add a placeholder key
|
|
267
|
+
const currentNamespaces = getCurrentNamespaces(namespaces?.length - initialNamespacesLength);
|
|
268
|
+
currentNamespaces?.forEach((namespace) => {
|
|
269
|
+
if (namespace.dynamic) {
|
|
270
|
+
foundKeys.push({
|
|
271
|
+
key: namespace.name,
|
|
272
|
+
meta: { file: path, namespace: namespace.name, dynamic: true },
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
removeNamespaces(namespaces.length - initialNamespacesLength);
|
|
240
277
|
}
|
|
241
278
|
};
|
|
242
279
|
ts.forEachChild(sourceFile, visit);
|