@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,149 @@
|
|
|
1
|
+
// Based on https://github.com/i18next/i18next-translation-parser/blob/v1.0.0/src/parse.js
|
|
2
|
+
|
|
3
|
+
export type MessageFormatElement =
|
|
4
|
+
| {
|
|
5
|
+
type: "text";
|
|
6
|
+
content: string;
|
|
7
|
+
}
|
|
8
|
+
| {
|
|
9
|
+
type: "interpolation";
|
|
10
|
+
raw: string;
|
|
11
|
+
prefix: string;
|
|
12
|
+
suffix: string;
|
|
13
|
+
content: string;
|
|
14
|
+
variable: string;
|
|
15
|
+
}
|
|
16
|
+
| {
|
|
17
|
+
type: "interpolation_unescaped";
|
|
18
|
+
raw: string;
|
|
19
|
+
prefix: string;
|
|
20
|
+
suffix: string;
|
|
21
|
+
content: string;
|
|
22
|
+
variable: string;
|
|
23
|
+
}
|
|
24
|
+
| {
|
|
25
|
+
type: "nesting";
|
|
26
|
+
raw: string;
|
|
27
|
+
prefix: string;
|
|
28
|
+
suffix: string;
|
|
29
|
+
content: string;
|
|
30
|
+
variable: string;
|
|
31
|
+
}
|
|
32
|
+
| {
|
|
33
|
+
type: "plural";
|
|
34
|
+
raw: string;
|
|
35
|
+
prefix: string;
|
|
36
|
+
suffix: string;
|
|
37
|
+
content: string;
|
|
38
|
+
variable: string;
|
|
39
|
+
}
|
|
40
|
+
| {
|
|
41
|
+
type: "tag";
|
|
42
|
+
raw: string;
|
|
43
|
+
voidElement: boolean;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const REGEXP = new RegExp(
|
|
47
|
+
"({{[^}]+}}|\\$t{[^}]+}|\\$t\\([^\\)]+\\)|\\([0-9\\-inf]+\\)|<[^>]+>)",
|
|
48
|
+
"g"
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const DOUBLE_BRACE = "{{";
|
|
52
|
+
const $_T_BRACE = "$t{";
|
|
53
|
+
const $_T_PARENTHESIS = "$t(";
|
|
54
|
+
const OPEN_PARENTHESIS = "(";
|
|
55
|
+
const OPEN_TAG = "<";
|
|
56
|
+
const CLOSE_TAG = "</";
|
|
57
|
+
|
|
58
|
+
export const parse = (input: string) => {
|
|
59
|
+
let ast: MessageFormatElement[] = [];
|
|
60
|
+
|
|
61
|
+
ast = parseInput([input]);
|
|
62
|
+
|
|
63
|
+
return ast;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const parseInput = (input: string[]): MessageFormatElement[] => {
|
|
67
|
+
if (input.length === 0) {
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
let ast: MessageFormatElement[] = [];
|
|
71
|
+
input.forEach((element) => {
|
|
72
|
+
const elements = element.split(REGEXP).filter((element) => element !== "");
|
|
73
|
+
const result = elements.reduce((acc, match) => {
|
|
74
|
+
if (match.indexOf("{{-") === 0) {
|
|
75
|
+
const content = match.substring(3, match.length - 2);
|
|
76
|
+
acc.push({
|
|
77
|
+
type: "interpolation_unescaped",
|
|
78
|
+
raw: match,
|
|
79
|
+
prefix: "{{-",
|
|
80
|
+
suffix: "}}",
|
|
81
|
+
content,
|
|
82
|
+
variable: content.trim(),
|
|
83
|
+
});
|
|
84
|
+
} else if (match.indexOf(DOUBLE_BRACE) === 0) {
|
|
85
|
+
const content = match.substring(2, match.length - 2);
|
|
86
|
+
acc.push({
|
|
87
|
+
type: "interpolation",
|
|
88
|
+
raw: match,
|
|
89
|
+
prefix: "{{",
|
|
90
|
+
suffix: "}}",
|
|
91
|
+
content,
|
|
92
|
+
variable: content.trim(),
|
|
93
|
+
});
|
|
94
|
+
} else if (match.indexOf($_T_BRACE) === 0) {
|
|
95
|
+
const content = match.substring(3, match.length - 1);
|
|
96
|
+
acc.push({
|
|
97
|
+
type: "nesting",
|
|
98
|
+
raw: match,
|
|
99
|
+
prefix: "$t{",
|
|
100
|
+
suffix: "}",
|
|
101
|
+
content,
|
|
102
|
+
variable: content.trim(),
|
|
103
|
+
});
|
|
104
|
+
} else if (match.indexOf($_T_PARENTHESIS) === 0) {
|
|
105
|
+
const content = match.substring(3, match.length - 1);
|
|
106
|
+
acc.push({
|
|
107
|
+
type: "nesting",
|
|
108
|
+
raw: match,
|
|
109
|
+
prefix: "$t(",
|
|
110
|
+
suffix: ")",
|
|
111
|
+
content,
|
|
112
|
+
variable: content.trim(),
|
|
113
|
+
});
|
|
114
|
+
} else if (
|
|
115
|
+
match.indexOf(OPEN_PARENTHESIS) === 0 &&
|
|
116
|
+
/\([0-9\-inf]+\)/.test(match)
|
|
117
|
+
) {
|
|
118
|
+
const content = match.substring(1, match.length - 1);
|
|
119
|
+
acc.push({
|
|
120
|
+
type: "plural",
|
|
121
|
+
raw: match,
|
|
122
|
+
prefix: "(",
|
|
123
|
+
suffix: ")",
|
|
124
|
+
content,
|
|
125
|
+
variable: content.trim(),
|
|
126
|
+
});
|
|
127
|
+
} else if (match.indexOf(CLOSE_TAG) === 0) {
|
|
128
|
+
acc.push({
|
|
129
|
+
type: "tag",
|
|
130
|
+
raw: match,
|
|
131
|
+
voidElement: match.substring(match.length - 2) === "/>",
|
|
132
|
+
});
|
|
133
|
+
} else if (match.indexOf(OPEN_TAG) === 0) {
|
|
134
|
+
acc.push({
|
|
135
|
+
type: "tag",
|
|
136
|
+
raw: match,
|
|
137
|
+
voidElement: match.substring(match.length - 2) === "/>",
|
|
138
|
+
});
|
|
139
|
+
} else {
|
|
140
|
+
acc.push({ type: "text", content: match });
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return acc;
|
|
144
|
+
}, [] as MessageFormatElement[]);
|
|
145
|
+
ast = ast.concat(result);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
return ast;
|
|
149
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"message.simple": "A simple message.",
|
|
3
|
+
"message.argument": "Hi, {name}! 👋",
|
|
4
|
+
"message.plural": "{count, plural, one {# item} other {# items}}",
|
|
5
|
+
"message.select": "Hi {user}, it is {today, date, medium} and tomorrow it is {tomorrow, date, medium}.",
|
|
6
|
+
"message.number-format": "Formatted number: {num, number, ::K}"
|
|
7
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"message.simple": "A simple message.",
|
|
3
|
+
"message.argument": "Hi, {name}! 👋",
|
|
4
|
+
"message.plural": "{count, plural, one {# item} other {# items}}",
|
|
5
|
+
"message.select": "{gender, select, male {Mr} female {Mrs} other {User}}",
|
|
6
|
+
"message.text-format": "Hi, <b>John</b>!",
|
|
7
|
+
"message.number-format": "Formatted number: {num, number, ::K}"
|
|
8
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"key": "Some format {{value, formatname}}",
|
|
3
|
+
"key_with_broken_de": "Some format {{val, formatname}}",
|
|
4
|
+
"keyWithOptions": "Some format {{value, formatname(option1Name: option1Value; option2Name: option2Value)}}",
|
|
5
|
+
"keyWithTwoFormatters": "Some format {{value, formatter1, formatter2}}",
|
|
6
|
+
"intlNumber": "Some {{val, number}}",
|
|
7
|
+
"intlNumber_broken_de": "Some number",
|
|
8
|
+
"intlNumberWithOptions": "Some {{val, number(minimumFractionDigits: 2)}}",
|
|
9
|
+
"intlCurrencyWithOptionsSimplified": "The value is {{val, currency(USD)}}",
|
|
10
|
+
"intlCurrencyWithOptions": "The value is {{val, currency(currency: USD)}}",
|
|
11
|
+
"twoIntlCurrencyWithUniqueFormatOptions": "The value is {{localValue, currency}} or {{altValue, currency}}",
|
|
12
|
+
"intlDateTime": "On the {{val, datetime}}",
|
|
13
|
+
"intlRelativeTime": "Lorem {{val, relativetime}}",
|
|
14
|
+
"intlRelativeTimeWithOptions": "Lorem {{val, relativetime(quarter)}}",
|
|
15
|
+
"intlRelativeTimeWithOptionsExplicit": "Lorem {{val, relativetime(range: quarter; style: narrow;)}}",
|
|
16
|
+
"intlList": "A list of {{val, list}}",
|
|
17
|
+
"legacyDateFormat": "The current date is {{date, MM/DD/YYYY}}",
|
|
18
|
+
"legacyUppercase": "{{text, uppercase}} just uppercased",
|
|
19
|
+
"key_one": "item",
|
|
20
|
+
"key_other": "items",
|
|
21
|
+
"keyWithCount_one": "{{count}} item",
|
|
22
|
+
"keyWithCount_other": "{{count}} items",
|
|
23
|
+
"key_zero": "zero",
|
|
24
|
+
"key_multiple_translations_one": "singular",
|
|
25
|
+
"key_multiple_translations_two": "two",
|
|
26
|
+
"key_multiple_translations_few": "few",
|
|
27
|
+
"key_multiple_translations_many": "many",
|
|
28
|
+
"key_multiple_translations_other": "other",
|
|
29
|
+
"key_ordinal_one": "{{count}}st place",
|
|
30
|
+
"key_ordinal_two": "{{count}}nd place",
|
|
31
|
+
"key_ordinal_few": "{{count}}rd place",
|
|
32
|
+
"key_ordinal_other": "{{count}}th place",
|
|
33
|
+
"key1_one": "{{count}} item",
|
|
34
|
+
"key1_other": "{{count}} items",
|
|
35
|
+
"key1_interval": "(1)[one item];(2-7)[a few items];(7-inf)[a lot of items];",
|
|
36
|
+
"key2_one": "{{count}} item",
|
|
37
|
+
"key2_other": "{{count}} items",
|
|
38
|
+
"key2_interval": "(1)[one item];(2-7)[a few items];",
|
|
39
|
+
"look": {
|
|
40
|
+
"deep": "value of look deep"
|
|
41
|
+
},
|
|
42
|
+
"error": {
|
|
43
|
+
"unspecific": "Something went wrong.",
|
|
44
|
+
"404": "The page was not found."
|
|
45
|
+
},
|
|
46
|
+
"key_interpolation": "{{what}} is {{how}}",
|
|
47
|
+
"key_with_data_model": "I am {{author.name}}",
|
|
48
|
+
"keyEscaped": "no danger {{myVar}}",
|
|
49
|
+
"keyUnescaped": "dangerous {{- myVar}}",
|
|
50
|
+
"nesting1": "1 $t(nesting2)",
|
|
51
|
+
"nesting2": "2 $t(nesting3)",
|
|
52
|
+
"nesting3": "3",
|
|
53
|
+
"optionsToNesting": "They have $t(that, {\"count\": {{that}} }) and $t(this, {\"count\": {{this}} })",
|
|
54
|
+
"this": "{{count}} of this",
|
|
55
|
+
"this_other": "{{count}} of these",
|
|
56
|
+
"that": "{{count}} of that",
|
|
57
|
+
"that_other": "{{count}} of those",
|
|
58
|
+
"animal": "An animal",
|
|
59
|
+
"animal_dog": "A dog",
|
|
60
|
+
"animal_cat": "A cat",
|
|
61
|
+
"animal_dog_one": "A dog",
|
|
62
|
+
"animal_cat_one": "A cat",
|
|
63
|
+
"animal_dog_other": "{{count}} dogs",
|
|
64
|
+
"animal_cat_other": "{{count}} cats",
|
|
65
|
+
|
|
66
|
+
"tree": {
|
|
67
|
+
"res": "added {{something}}"
|
|
68
|
+
},
|
|
69
|
+
"array": ["a", "b", "c"],
|
|
70
|
+
"arrayJoin": ["line1", "line2", "line3"],
|
|
71
|
+
"arrayJoinWithInterpolation": ["you", "can", "{{myVar}}"],
|
|
72
|
+
"arrayOfObjects": [{ "name": "tom" }, { "name": "steve" }]
|
|
73
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
{
|
|
2
|
+
"basic": "basic",
|
|
3
|
+
"key": "Some format {{value, formatname}}",
|
|
4
|
+
"key_with_broken_de": "Some format {{value, formatname}}",
|
|
5
|
+
"keyWithOptions": "Some format {{value, formatname(option1Name: option1Value; option2Name: option2Value)}}",
|
|
6
|
+
"keyWithTwoFormatters": "Some format {{value, formatter1, formatter2}}",
|
|
7
|
+
"intlNumber": "Some {{val, number}}",
|
|
8
|
+
"intlNumber_broken_de": "Some {{val, number}}",
|
|
9
|
+
"intlNumberWithOptions": "Some {{val, number(minimumFractionDigits: 2)}}",
|
|
10
|
+
"intlCurrencyWithOptionsSimplified": "The value is {{val, currency(USD)}}",
|
|
11
|
+
"intlCurrencyWithOptions": "The value is {{val, currency(currency: USD)}}",
|
|
12
|
+
"twoIntlCurrencyWithUniqueFormatOptions": "The value is {{localValue, currency}} or {{altValue, currency}}",
|
|
13
|
+
"intlDateTime": "On the {{val, datetime}}",
|
|
14
|
+
"intlRelativeTime": "Lorem {{val, relativetime}}",
|
|
15
|
+
"intlRelativeTimeWithOptions": "Lorem {{val, relativetime(quarter)}}",
|
|
16
|
+
"intlRelativeTimeWithOptionsExplicit": "Lorem {{val, relativetime(range: quarter; style: narrow;)}}",
|
|
17
|
+
"intlList": "A list of {{val, list}}",
|
|
18
|
+
"legacyDateFormat": "The current date is {{date, MM/DD/YYYY}}",
|
|
19
|
+
"legacyUppercase": "{{text, uppercase}} just uppercased",
|
|
20
|
+
"key_one": "item",
|
|
21
|
+
"key_other": "items",
|
|
22
|
+
"keyWithCount_one": "{{count}} item",
|
|
23
|
+
"keyWithCount_other": "{{count}} items",
|
|
24
|
+
"key_zero": "zero",
|
|
25
|
+
"key_multiple_translations_one": "singular",
|
|
26
|
+
"key_multiple_translations_two": "two",
|
|
27
|
+
"key_multiple_translations_few": "few",
|
|
28
|
+
"key_multiple_translations_many": "many",
|
|
29
|
+
"key_multiple_translations_other": "other",
|
|
30
|
+
"key_ordinal_one": "{{count}}st place",
|
|
31
|
+
"key_ordinal_two": "{{count}}nd place",
|
|
32
|
+
"key_ordinal_few": "{{count}}rd place",
|
|
33
|
+
"key_ordinal_other": "{{count}}th place",
|
|
34
|
+
"key1_one": "{{count}} item",
|
|
35
|
+
"key1_other": "{{count}} items",
|
|
36
|
+
"key1_interval": "(1)[one item];(2-7)[a few items];(7-inf)[a lot of items];",
|
|
37
|
+
"key2_one": "{{count}} item",
|
|
38
|
+
"key2_other": "{{count}} items",
|
|
39
|
+
"key2_interval": "(1)[one item];(2-7)[a few items];",
|
|
40
|
+
"look": {
|
|
41
|
+
"deep": "value of look deep"
|
|
42
|
+
},
|
|
43
|
+
"error": {
|
|
44
|
+
"unspecific": "Something went wrong.",
|
|
45
|
+
"404": "The page was not found."
|
|
46
|
+
},
|
|
47
|
+
"key_interpolation": "{{what}} is {{how}}",
|
|
48
|
+
"key_with_data_model": "I am {{author.name}}",
|
|
49
|
+
"keyEscaped": "no danger {{myVar}}",
|
|
50
|
+
"keyUnescaped": "dangerous {{- myVar}}",
|
|
51
|
+
"nesting1": "1 $t(nesting2)",
|
|
52
|
+
"nesting2": "2 $t(nesting3)",
|
|
53
|
+
"nesting3": "3",
|
|
54
|
+
"optionsToNesting": "They have $t(that, {\"count\": {{that}} }) and $t(this, {\"count\": {{this}} })",
|
|
55
|
+
"this": "{{count}} of this",
|
|
56
|
+
"this_other": "{{count}} of these",
|
|
57
|
+
"that": "{{count}} of that",
|
|
58
|
+
"that_other": "{{count}} of those",
|
|
59
|
+
"animal": "An animal",
|
|
60
|
+
"animal_dog": "A dog",
|
|
61
|
+
"animal_cat": "A cat",
|
|
62
|
+
"animal_dog_one": "A dog",
|
|
63
|
+
"animal_cat_one": "A cat",
|
|
64
|
+
"animal_dog_other": "{{count}} dogs",
|
|
65
|
+
"animal_cat_other": "{{count}} cats",
|
|
66
|
+
"tree": {
|
|
67
|
+
"res": "added {{something}}"
|
|
68
|
+
},
|
|
69
|
+
"array": ["a", "b", "c"],
|
|
70
|
+
"arrayJoin": ["line1", "line2", "line3"],
|
|
71
|
+
"arrayJoinWithInterpolation": ["you", "can", "{{myVar}}"],
|
|
72
|
+
"arrayOfObjects": [{ "name": "tom" }, { "name": "steve" }],
|
|
73
|
+
"title": "Welcome to react using react-i18next",
|
|
74
|
+
"description.part1": "To get started, edit <1>src/App.js</1> and save to reload.",
|
|
75
|
+
"description.part2": "Switch language between english and german using buttons above.",
|
|
76
|
+
"icu": "{numPersons, plural, =0 {no persons} =1 {one person} other {# persons}}",
|
|
77
|
+
"icu_and_trans": "We invited <0>{numPersons, plural, =0 {no persons} =1 {one person} other {# persons}}</0>.",
|
|
78
|
+
"Welcome, {name}!": "Welcome, {name}!",
|
|
79
|
+
"Welcome, <0>{name}</0>!": "Welcome, <0>{name}</0>!",
|
|
80
|
+
"Trainers: {trainersCount, number}": "Trainers: {trainersCount, number}",
|
|
81
|
+
"Trainers: <0>{trainersCount, number}</0>!": "Trainers: <0>{trainersCount, number}</0>!",
|
|
82
|
+
"Caught on {catchDate, date, full}": "Caught on {catchDate, date, full}",
|
|
83
|
+
"Caught on <0>{catchDate, date, full}</0>!": "Caught on <0>{catchDate, date, full}</0>!",
|
|
84
|
+
"{gender, select, male {He avoids bugs.} female {She avoids bugs.} other {They avoid bugs.}}": "{gender, select, male {He avoids bugs.} female {She avoids bugs.} other {They avoid bugs.}}",
|
|
85
|
+
"{gender, select, male {<0>He</0> avoids bugs.} female {<1>She</1> avoids bugs.} other {<2>They</2> avoid bugs.}}": "{gender, select, male {<0>He</0> avoids bugs.} female {<1>She</1> avoids bugs.} other {<2>They</2> avoid bugs.}}",
|
|
86
|
+
"{itemsCount1, plural, =0 {There is no item.} one {There is # item.} other {There are # items.}}": "{itemsCount1, plural, =0 {There is no item.} one {There is # item.} other {There are # items.}}",
|
|
87
|
+
"{itemsCount2, plural, =0 {There is no item.} one {There is # item.} other {There are # items.}}": "{itemsCount2, plural, =0 {There is no item.} one {There is # item.} other {There are # items.}}",
|
|
88
|
+
"{itemsCount3, plural, =0 {There is no item.} one {There is # item.} other {There are # items.}}": "{itemsCount3, plural, =0 {There is no item.} one {There is # item.} other {There are # items.}}",
|
|
89
|
+
"testKey": "{itemsCount3, plural, =0 { There is <0>no</0> item. } one { There is <1>#</1> item. } other { There are <2>#</2> items. }}"
|
|
90
|
+
}
|