@lingual/i18n-check 0.1.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/.github/workflows/tests.yaml +31 -0
- package/LICENSE +21 -0
- package/README.md +402 -0
- package/jest.config.js +6 -0
- package/package.json +28 -0
- package/src/bin/index.ts +244 -0
- package/src/errorReporters.ts +18 -0
- package/src/index.ts +68 -0
- package/src/types.ts +9 -0
- package/src/utils/findInvalidTranslations.test.ts +78 -0
- package/src/utils/findInvalidTranslations.ts +120 -0
- package/src/utils/findInvalidi18nTranslations.test.ts +213 -0
- package/src/utils/findInvalidi18nTranslations.ts +137 -0
- package/src/utils/findMissingKeys.test.ts +60 -0
- package/src/utils/findMissingKeys.ts +27 -0
- package/src/utils/flattenTranslations.test.ts +32 -0
- package/src/utils/flattenTranslations.ts +42 -0
- package/src/utils/i18NextParser.test.ts +169 -0
- package/src/utils/i18NextParser.ts +149 -0
- package/translations/de-de.json +6 -0
- package/translations/en-us.json +6 -0
- package/translations/flattenExamples/de-de.json +6 -0
- package/translations/flattenExamples/en-us.json +18 -0
- package/translations/folderExample/de-DE/index.json +7 -0
- package/translations/folderExample/en-US/index.json +8 -0
- package/translations/i18NextMessageExamples/de-de.json +73 -0
- package/translations/i18NextMessageExamples/en-us.json +90 -0
- package/translations/largeFileExamples/de-de.json +5272 -0
- package/translations/largeFileExamples/en-us.json +5278 -0
- package/translations/largeFileExamples/fr-fr.json +871 -0
- package/translations/messageExamples/de-de.json +24 -0
- package/translations/messageExamples/en-us.json +30 -0
- package/translations/multipleFilesFolderExample/de-DE/one.json +7 -0
- package/translations/multipleFilesFolderExample/de-DE/three.json +8 -0
- package/translations/multipleFilesFolderExample/de-DE/two.json +5 -0
- package/translations/multipleFilesFolderExample/en-US/one.json +8 -0
- package/translations/multipleFilesFolderExample/en-US/three.json +8 -0
- package/translations/multipleFilesFolderExample/en-US/two.json +6 -0
- package/tsconfig.json +113 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import {
|
|
2
|
+
compareTranslationFiles,
|
|
3
|
+
findInvalid18nTranslations,
|
|
4
|
+
} from "./findInvalidi18nTranslations";
|
|
5
|
+
import { flattenTranslations } from "./flattenTranslations";
|
|
6
|
+
|
|
7
|
+
const sourceFile = require("../../translations/i18NextMessageExamples/en-us.json");
|
|
8
|
+
const targetFile = require("../../translations/i18NextMessageExamples/de-de.json");
|
|
9
|
+
|
|
10
|
+
describe("findInvalid18nTranslations:compareTranslationFiles", () => {
|
|
11
|
+
it("should return empty array if files are identical", () => {
|
|
12
|
+
expect(
|
|
13
|
+
compareTranslationFiles(
|
|
14
|
+
flattenTranslations(sourceFile),
|
|
15
|
+
flattenTranslations(sourceFile)
|
|
16
|
+
)
|
|
17
|
+
).toEqual([]);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("should return the invalid keys in the target file", () => {
|
|
21
|
+
expect(
|
|
22
|
+
compareTranslationFiles(
|
|
23
|
+
flattenTranslations({
|
|
24
|
+
...sourceFile,
|
|
25
|
+
"ten.eleven.twelve": "ten eleven twelve",
|
|
26
|
+
}),
|
|
27
|
+
flattenTranslations(targetFile)
|
|
28
|
+
)
|
|
29
|
+
).toEqual(["key_with_broken_de", "intlNumber_broken_de"]);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("should return empty array if placeholders are identical but in different positions", () => {
|
|
33
|
+
expect(
|
|
34
|
+
compareTranslationFiles(
|
|
35
|
+
{
|
|
36
|
+
basic: "added {{this}} and {{that}} should work.",
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
basic: "It is {{this}} with different position {{that}}",
|
|
40
|
+
}
|
|
41
|
+
)
|
|
42
|
+
).toEqual([]);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("should return the invalid key if tags are not identical", () => {
|
|
46
|
+
expect(
|
|
47
|
+
compareTranslationFiles(
|
|
48
|
+
{
|
|
49
|
+
tag: "This is some <b>bold text</b> and some <i>italic</i> text.",
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
tag: "There is some <b>bold text</b> and some other <span>italic</span> text.",
|
|
53
|
+
}
|
|
54
|
+
)
|
|
55
|
+
).toEqual(["tag"]);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("should return empty array if tags are identical", () => {
|
|
59
|
+
expect(
|
|
60
|
+
compareTranslationFiles(
|
|
61
|
+
{
|
|
62
|
+
tag: "This is some <b>bold text</b> and some <i>italic</i> text.",
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
tag: "There is some <b>bold text</b> and some other <i>italic</i> text.",
|
|
66
|
+
}
|
|
67
|
+
)
|
|
68
|
+
).toEqual([]);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe("findInvalidTranslations", () => {
|
|
73
|
+
it("should return an empty object if all files have no invalid keys", () => {
|
|
74
|
+
expect(findInvalid18nTranslations(sourceFile, { de: sourceFile })).toEqual(
|
|
75
|
+
{}
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("should return an object containing the keys for the missing language", () => {
|
|
80
|
+
expect(
|
|
81
|
+
findInvalid18nTranslations(
|
|
82
|
+
{ ...sourceFile, "ten.eleven.twelve": "ten eleven twelve" },
|
|
83
|
+
{ de: targetFile }
|
|
84
|
+
)
|
|
85
|
+
).toEqual({ de: ["key_with_broken_de", "intlNumber_broken_de"] });
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("should return an object containing the keys for every language with missing key", () => {
|
|
89
|
+
expect(
|
|
90
|
+
findInvalid18nTranslations(
|
|
91
|
+
{ ...sourceFile, "ten.eleven.twelve": "ten eleven twelve" },
|
|
92
|
+
{
|
|
93
|
+
de: targetFile,
|
|
94
|
+
fr: {
|
|
95
|
+
key_with_broken_de:
|
|
96
|
+
"Some format {{value, formatname}} and some other format {{value, formatname}}",
|
|
97
|
+
},
|
|
98
|
+
}
|
|
99
|
+
)
|
|
100
|
+
).toEqual({
|
|
101
|
+
de: ["key_with_broken_de", "intlNumber_broken_de"],
|
|
102
|
+
fr: ["key_with_broken_de"],
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("should find invalid interval", () => {
|
|
107
|
+
expect(
|
|
108
|
+
findInvalid18nTranslations(
|
|
109
|
+
{
|
|
110
|
+
key1_interval:
|
|
111
|
+
"(1)[one item];(2-7)[a few items];(7-inf)[a lot of items];",
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
de: {
|
|
115
|
+
key1_interval:
|
|
116
|
+
"(1-2)[one or two items];(3-7)[a few items];(7-inf)[a lot of items];",
|
|
117
|
+
},
|
|
118
|
+
}
|
|
119
|
+
)
|
|
120
|
+
).toEqual({
|
|
121
|
+
de: ["key1_interval"],
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("should find invalid nested interpolation", () => {
|
|
126
|
+
expect(
|
|
127
|
+
findInvalid18nTranslations(
|
|
128
|
+
{
|
|
129
|
+
"tree.one": "added {{something}}",
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
de: {
|
|
133
|
+
"tree.one": "added {{somethings}}",
|
|
134
|
+
},
|
|
135
|
+
}
|
|
136
|
+
)
|
|
137
|
+
).toEqual({
|
|
138
|
+
de: ["tree.one"],
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("should find invalid relative time formatting", () => {
|
|
143
|
+
expect(
|
|
144
|
+
findInvalid18nTranslations(
|
|
145
|
+
{
|
|
146
|
+
intlRelativeTimeWithOptionsExplicit:
|
|
147
|
+
"Lorem {{val, relativetime(range: quarter; style: narrow;)}}",
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
de: {
|
|
151
|
+
intlRelativeTimeWithOptionsExplicit:
|
|
152
|
+
"Lorem {{val, relativetime(range: quarter; style: long;)}}",
|
|
153
|
+
},
|
|
154
|
+
}
|
|
155
|
+
)
|
|
156
|
+
).toEqual({
|
|
157
|
+
de: ["intlRelativeTimeWithOptionsExplicit"],
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("should find invalid key with options", () => {
|
|
162
|
+
expect(
|
|
163
|
+
findInvalid18nTranslations(
|
|
164
|
+
{
|
|
165
|
+
keyWithOptions:
|
|
166
|
+
"Some format {{value, formatname(option1Name: option1Value; option2Name: option2Value)}}",
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
de: {
|
|
170
|
+
keyWithOptions:
|
|
171
|
+
"Some format {{value, formatname(option3Name: option3Value; option4Name: option4Value)}}",
|
|
172
|
+
},
|
|
173
|
+
}
|
|
174
|
+
)
|
|
175
|
+
).toEqual({
|
|
176
|
+
de: ["keyWithOptions"],
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("should find invalid nesting", () => {
|
|
181
|
+
expect(
|
|
182
|
+
findInvalid18nTranslations(
|
|
183
|
+
{
|
|
184
|
+
nesting1: "1 $t(nesting2)",
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
de: {
|
|
188
|
+
nesting1: "1 $t(nesting3)",
|
|
189
|
+
},
|
|
190
|
+
}
|
|
191
|
+
)
|
|
192
|
+
).toEqual({
|
|
193
|
+
de: ["nesting1"],
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("should find invalid tags", () => {
|
|
198
|
+
expect(
|
|
199
|
+
findInvalid18nTranslations(
|
|
200
|
+
{
|
|
201
|
+
tag: "This is some <b>bold text</b> and some <i>italic</i> text.",
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
de: {
|
|
205
|
+
tag: "There is some <b>bold text</b> and some other <span>text inside a span</span>!",
|
|
206
|
+
},
|
|
207
|
+
}
|
|
208
|
+
)
|
|
209
|
+
).toEqual({
|
|
210
|
+
de: ["tag"],
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
});
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* i18next specific invalid translations check
|
|
4
|
+
*
|
|
5
|
+
*
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { parse, MessageFormatElement } from "./i18NextParser";
|
|
9
|
+
import { Translation } from "../types";
|
|
10
|
+
|
|
11
|
+
export const findInvalid18nTranslations = (
|
|
12
|
+
source: Translation,
|
|
13
|
+
targets: Record<string, Translation>
|
|
14
|
+
) => {
|
|
15
|
+
let differences = {};
|
|
16
|
+
if (Object.keys(targets).length === 0) {
|
|
17
|
+
return differences;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
for (const [lang, file] of Object.entries(targets)) {
|
|
21
|
+
const result = compareTranslationFiles(source, file);
|
|
22
|
+
|
|
23
|
+
if (result.length > 0) {
|
|
24
|
+
differences = Object.assign(differences, { [lang]: result });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return differences;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const compareTranslationFiles = (a: Translation, b: Translation) => {
|
|
32
|
+
let diffs: unknown[] = [];
|
|
33
|
+
for (const key in a) {
|
|
34
|
+
if (
|
|
35
|
+
b[key] !== undefined &&
|
|
36
|
+
hasDiff(parse(String(a[key])), parse(String(b[key])))
|
|
37
|
+
) {
|
|
38
|
+
diffs.push(key);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return diffs;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const lookUp: Record<MessageFormatElement["type"], number> = {
|
|
45
|
+
text: 0,
|
|
46
|
+
interpolation: 1,
|
|
47
|
+
interpolation_unescaped: 2,
|
|
48
|
+
nesting: 3,
|
|
49
|
+
plural: 4,
|
|
50
|
+
tag: 5,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const sortParsedKeys = (a: MessageFormatElement, b: MessageFormatElement) => {
|
|
54
|
+
if (a.type === b.type && a.type !== "tag" && b.type !== "tag") {
|
|
55
|
+
return a.content < b.content ? -1 : 1;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (a.type === "tag" && b.type === "tag") {
|
|
59
|
+
return a.raw < b.raw ? -1 : 1;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return lookUp[a.type] - lookUp[b.type];
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const hasDiff = (
|
|
66
|
+
a: MessageFormatElement[],
|
|
67
|
+
b: MessageFormatElement[]
|
|
68
|
+
) => {
|
|
69
|
+
const compA = a
|
|
70
|
+
.filter((element) => element.type !== "text")
|
|
71
|
+
.sort(sortParsedKeys);
|
|
72
|
+
const compB = b
|
|
73
|
+
.filter((element) => element.type !== "text")
|
|
74
|
+
.sort(sortParsedKeys);
|
|
75
|
+
|
|
76
|
+
if (compA.length !== compB.length) {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const hasErrors = compA.some((formatElementA, index) => {
|
|
81
|
+
const formatElementB = compB[index];
|
|
82
|
+
|
|
83
|
+
if (formatElementA.type !== formatElementB.type) {
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (formatElementA.type === "text" && formatElementB.type === "text") {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (formatElementA.type === "tag" && formatElementB.type === "tag") {
|
|
92
|
+
return (
|
|
93
|
+
formatElementA.raw !== formatElementB.raw ||
|
|
94
|
+
formatElementA.voidElement !== formatElementB.voidElement
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (
|
|
99
|
+
(formatElementA.type === "interpolation" &&
|
|
100
|
+
formatElementB.type === "interpolation") ||
|
|
101
|
+
(formatElementA.type === "interpolation_unescaped" &&
|
|
102
|
+
formatElementB.type === "interpolation_unescaped") ||
|
|
103
|
+
(formatElementA.type === "nesting" &&
|
|
104
|
+
formatElementB.type === "nesting") ||
|
|
105
|
+
(formatElementA.type === "plural" && formatElementB.type === "plural")
|
|
106
|
+
) {
|
|
107
|
+
const optionsA = formatElementA.variable
|
|
108
|
+
.split(",")
|
|
109
|
+
.map((value) => value.trim())
|
|
110
|
+
.sort()
|
|
111
|
+
.join("-")
|
|
112
|
+
.trim();
|
|
113
|
+
const optionsB = formatElementB.variable
|
|
114
|
+
.split(",")
|
|
115
|
+
.map((value) => value.trim())
|
|
116
|
+
.sort()
|
|
117
|
+
.join("-")
|
|
118
|
+
.trim();
|
|
119
|
+
|
|
120
|
+
if (optionsA !== optionsB) {
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (formatElementA.prefix !== formatElementA.prefix) {
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (formatElementA.suffix !== formatElementA.suffix) {
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return false;
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
return hasErrors;
|
|
137
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { compareTranslationFiles, findMissingKeys } from "./findMissingKeys";
|
|
2
|
+
|
|
3
|
+
const sourceFile = {
|
|
4
|
+
"one.two.three": "one two three",
|
|
5
|
+
"four.five.six": "four five six",
|
|
6
|
+
"seven.eight.nine": "seven eight nine",
|
|
7
|
+
};
|
|
8
|
+
const secondaryFile = {
|
|
9
|
+
"one.two.three": "one two three",
|
|
10
|
+
"four.five.six": "four five six",
|
|
11
|
+
"seven.eight.nine": "seven eight nine",
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
describe("findMissingKeys:compareTranslationFiles", () => {
|
|
15
|
+
it("should return empty array if files are identical", () => {
|
|
16
|
+
expect(compareTranslationFiles(sourceFile, secondaryFile)).toEqual([]);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("should return the missing keys in the secondary file", () => {
|
|
20
|
+
expect(
|
|
21
|
+
compareTranslationFiles(
|
|
22
|
+
{ ...sourceFile, "ten.eleven.twelve": "ten eleven twelve" },
|
|
23
|
+
secondaryFile
|
|
24
|
+
)
|
|
25
|
+
).toEqual(["ten.eleven.twelve"]);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe("findMissingKeys", () => {
|
|
30
|
+
it("should return an empty object if all files have no missing keys", () => {
|
|
31
|
+
expect(findMissingKeys(sourceFile, { de: secondaryFile })).toEqual({});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("should return an object containing the keys for the missing language", () => {
|
|
35
|
+
expect(
|
|
36
|
+
findMissingKeys(
|
|
37
|
+
{ ...sourceFile, "ten.eleven.twelve": "ten eleven twelve" },
|
|
38
|
+
{ de: secondaryFile }
|
|
39
|
+
)
|
|
40
|
+
).toEqual({ de: ["ten.eleven.twelve"] });
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("should return an object containing the keys for every language with missing key", () => {
|
|
44
|
+
expect(
|
|
45
|
+
findMissingKeys(
|
|
46
|
+
{ ...sourceFile, "ten.eleven.twelve": "ten eleven twelve" },
|
|
47
|
+
{
|
|
48
|
+
de: secondaryFile,
|
|
49
|
+
fr: {
|
|
50
|
+
"four.five.six": "four five six",
|
|
51
|
+
"seven.eight.nine": "seven eight nine",
|
|
52
|
+
},
|
|
53
|
+
}
|
|
54
|
+
)
|
|
55
|
+
).toEqual({
|
|
56
|
+
de: ["ten.eleven.twelve"],
|
|
57
|
+
fr: ["one.two.three", "ten.eleven.twelve"],
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Translation } from "../types";
|
|
2
|
+
|
|
3
|
+
export const findMissingKeys = (
|
|
4
|
+
source: Translation,
|
|
5
|
+
targets: Record<string, Translation>
|
|
6
|
+
) => {
|
|
7
|
+
let differences = {};
|
|
8
|
+
for (const [lang, file] of Object.entries(targets)) {
|
|
9
|
+
const result = compareTranslationFiles(source, file);
|
|
10
|
+
if (result.length > 0) {
|
|
11
|
+
differences = Object.assign(differences, { [lang]: result });
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return differences;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const compareTranslationFiles = (a: Translation, b: Translation) => {
|
|
19
|
+
let diffs = [];
|
|
20
|
+
for (const key in a) {
|
|
21
|
+
const counterKey = b[key];
|
|
22
|
+
if (!counterKey) {
|
|
23
|
+
diffs.push(key);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return diffs;
|
|
27
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { flattenEntry, flattenTranslations } from "./flattenTranslations";
|
|
2
|
+
|
|
3
|
+
const flatStructure = require("../../translations/en-us.json");
|
|
4
|
+
const nestedStructure = require("../../translations/flattenExamples/en-us.json");
|
|
5
|
+
|
|
6
|
+
const expectedFlatStructure = {
|
|
7
|
+
"test.drive.one": "testing one",
|
|
8
|
+
"test.drive.two": "testing two",
|
|
9
|
+
"other.nested.three": "testing three",
|
|
10
|
+
"other.nested.deep.more.final": "nested translation",
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
describe("flattenTranslations", () => {
|
|
14
|
+
it("should do nothing if the file structure is flat", () => {
|
|
15
|
+
expect(flattenTranslations(flatStructure)).toEqual(flatStructure);
|
|
16
|
+
});
|
|
17
|
+
describe("flattenEntry", () => {
|
|
18
|
+
it("should flatten a nested object", () => {
|
|
19
|
+
expect(
|
|
20
|
+
flattenEntry({
|
|
21
|
+
a: {
|
|
22
|
+
b: { c: "one" },
|
|
23
|
+
},
|
|
24
|
+
})
|
|
25
|
+
).toEqual({ "a.b.c": "one" });
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("should do nothing if the file structure is flat", () => {
|
|
30
|
+
expect(flattenTranslations(nestedStructure)).toEqual(expectedFlatStructure);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Translation } from "../types";
|
|
2
|
+
|
|
3
|
+
export const flattenTranslations = (translations: Translation) => {
|
|
4
|
+
if (!hasNestedDefinitions(translations)) {
|
|
5
|
+
return translations;
|
|
6
|
+
}
|
|
7
|
+
return flattenEntry(translations);
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Top level search for any objects
|
|
12
|
+
*/
|
|
13
|
+
const hasNestedDefinitions = (translations: Translation) => {
|
|
14
|
+
return Object.values(translations).find(
|
|
15
|
+
(translation) => typeof translation === "object"
|
|
16
|
+
);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const isTranslationObject = (entry: unknown): entry is Translation => {
|
|
20
|
+
return typeof entry === "object";
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const flattenEntry = (
|
|
24
|
+
entry: Translation,
|
|
25
|
+
keys: string[] = []
|
|
26
|
+
): Translation => {
|
|
27
|
+
let result = {};
|
|
28
|
+
if (!entry) {
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
const entries = Object.entries(entry);
|
|
32
|
+
for (const [k, v] of entries) {
|
|
33
|
+
Object.assign(
|
|
34
|
+
result,
|
|
35
|
+
isTranslationObject(v)
|
|
36
|
+
? flattenEntry(v, [...keys, String(k)])
|
|
37
|
+
: { [[...keys, String(k)].join(".")]: v }
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return result;
|
|
42
|
+
};
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { parse } from "./i18NextParser";
|
|
2
|
+
|
|
3
|
+
describe("i18NextParser", () => {
|
|
4
|
+
it("should parse interpolation", () => {
|
|
5
|
+
expect(
|
|
6
|
+
parse(
|
|
7
|
+
"test {{val}} text {{- encoded}} with {{val, format}} some $t{nesting} help"
|
|
8
|
+
)
|
|
9
|
+
).toEqual([
|
|
10
|
+
{
|
|
11
|
+
type: "text",
|
|
12
|
+
content: "test ",
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
type: "interpolation",
|
|
16
|
+
raw: "{{val}}",
|
|
17
|
+
prefix: "{{",
|
|
18
|
+
suffix: "}}",
|
|
19
|
+
content: "val",
|
|
20
|
+
variable: "val",
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
type: "text",
|
|
24
|
+
content: " text ",
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
type: "interpolation_unescaped",
|
|
28
|
+
raw: "{{- encoded}}",
|
|
29
|
+
prefix: "{{-",
|
|
30
|
+
suffix: "}}",
|
|
31
|
+
content: " encoded",
|
|
32
|
+
variable: "encoded",
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
type: "text",
|
|
36
|
+
content: " with ",
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
type: "interpolation",
|
|
40
|
+
raw: "{{val, format}}",
|
|
41
|
+
prefix: "{{",
|
|
42
|
+
suffix: "}}",
|
|
43
|
+
content: "val, format",
|
|
44
|
+
variable: "val, format",
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
type: "text",
|
|
48
|
+
content: " some ",
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
type: "nesting",
|
|
52
|
+
raw: "$t{nesting}",
|
|
53
|
+
prefix: "$t{",
|
|
54
|
+
suffix: "}",
|
|
55
|
+
content: "nesting",
|
|
56
|
+
variable: "nesting",
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
type: "text",
|
|
60
|
+
content: " help",
|
|
61
|
+
},
|
|
62
|
+
]);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("should parse plural translations", () => {
|
|
66
|
+
expect(
|
|
67
|
+
parse("(1)[one item];(2-7)[a few items];(7-inf)[a lot of items];")
|
|
68
|
+
).toEqual([
|
|
69
|
+
{
|
|
70
|
+
type: "plural",
|
|
71
|
+
raw: "(1)",
|
|
72
|
+
prefix: "(",
|
|
73
|
+
suffix: ")",
|
|
74
|
+
content: "1",
|
|
75
|
+
variable: "1",
|
|
76
|
+
},
|
|
77
|
+
{ type: "text", content: "[one item];" },
|
|
78
|
+
{
|
|
79
|
+
type: "plural",
|
|
80
|
+
raw: "(2-7)",
|
|
81
|
+
prefix: "(",
|
|
82
|
+
suffix: ")",
|
|
83
|
+
content: "2-7",
|
|
84
|
+
variable: "2-7",
|
|
85
|
+
},
|
|
86
|
+
{ type: "text", content: "[a few items];" },
|
|
87
|
+
{
|
|
88
|
+
type: "plural",
|
|
89
|
+
raw: "(7-inf)",
|
|
90
|
+
prefix: "(",
|
|
91
|
+
suffix: ")",
|
|
92
|
+
content: "7-inf",
|
|
93
|
+
variable: "7-inf",
|
|
94
|
+
},
|
|
95
|
+
{ type: "text", content: "[a lot of items];" },
|
|
96
|
+
]);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("should not parse plural translations if regular text inside parenthesis", () => {
|
|
100
|
+
expect(parse("(This is a regular text inside parenthesis)")).toEqual([
|
|
101
|
+
{ type: "text", content: "(This is a regular text inside parenthesis)" },
|
|
102
|
+
]);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("should parse translations with nesting", () => {
|
|
106
|
+
expect(parse("1 $t(nesting2)")).toEqual([
|
|
107
|
+
{
|
|
108
|
+
type: "text",
|
|
109
|
+
content: "1 ",
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
type: "nesting",
|
|
113
|
+
raw: "$t(nesting2)",
|
|
114
|
+
prefix: "$t(",
|
|
115
|
+
suffix: ")",
|
|
116
|
+
content: "nesting2",
|
|
117
|
+
variable: "nesting2",
|
|
118
|
+
},
|
|
119
|
+
]);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("should parse translations with tags", () => {
|
|
123
|
+
expect(
|
|
124
|
+
parse("This is some <b>bold text</b> and some <i>italic</i> text.")
|
|
125
|
+
).toEqual([
|
|
126
|
+
{ type: "text", content: "This is some " },
|
|
127
|
+
{ type: "tag", raw: "<b>", voidElement: false },
|
|
128
|
+
{ type: "text", content: "bold text" },
|
|
129
|
+
{ type: "tag", raw: "</b>", voidElement: false },
|
|
130
|
+
{ type: "text", content: " and some " },
|
|
131
|
+
{ type: "tag", raw: "<i>", voidElement: false },
|
|
132
|
+
{ type: "text", content: "italic" },
|
|
133
|
+
{ type: "tag", raw: "</i>", voidElement: false },
|
|
134
|
+
{ type: "text", content: " text." },
|
|
135
|
+
]);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("should parse translations with nested tags", () => {
|
|
139
|
+
expect(
|
|
140
|
+
parse("This is some <b>bold text and some <i>nested italic</i> text</b>!")
|
|
141
|
+
).toEqual([
|
|
142
|
+
{ type: "text", content: "This is some " },
|
|
143
|
+
{ type: "tag", raw: "<b>", voidElement: false },
|
|
144
|
+
{ type: "text", content: "bold text and some " },
|
|
145
|
+
{ type: "tag", raw: "<i>", voidElement: false },
|
|
146
|
+
{ type: "text", content: "nested italic" },
|
|
147
|
+
{ type: "tag", raw: "</i>", voidElement: false },
|
|
148
|
+
{ type: "text", content: " text" },
|
|
149
|
+
{ type: "tag", raw: "</b>", voidElement: false },
|
|
150
|
+
{ type: "text", content: "!" },
|
|
151
|
+
]);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("should parse translations with self closing tags", () => {
|
|
155
|
+
expect(
|
|
156
|
+
parse(
|
|
157
|
+
"This is some <b>bold text and some </b> and some random self closing tag <img /> as well."
|
|
158
|
+
)
|
|
159
|
+
).toEqual([
|
|
160
|
+
{ type: "text", content: "This is some " },
|
|
161
|
+
{ type: "tag", raw: "<b>", voidElement: false },
|
|
162
|
+
{ type: "text", content: "bold text and some " },
|
|
163
|
+
{ type: "tag", raw: "</b>", voidElement: false },
|
|
164
|
+
{ type: "text", content: " and some random self closing tag " },
|
|
165
|
+
{ type: "tag", raw: "<img />", voidElement: true },
|
|
166
|
+
{ type: "text", content: " as well." },
|
|
167
|
+
]);
|
|
168
|
+
});
|
|
169
|
+
});
|