@lingual/i18n-check 0.8.1 → 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 +73 -99
- package/dist/bin/index.test.js +560 -472
- package/dist/errorReporters.d.ts +5 -7
- package/dist/errorReporters.js +67 -81
- package/dist/errorReporters.test.d.ts +1 -0
- package/dist/errorReporters.test.js +165 -0
- package/dist/index.d.ts +7 -7
- package/dist/index.js +78 -54
- package/dist/types.d.ts +5 -0
- package/dist/utils/findInvalidTranslations.d.ts +4 -7
- package/dist/utils/findInvalidTranslations.js +23 -22
- package/dist/utils/findInvalidTranslations.test.js +30 -30
- package/dist/utils/findInvalidi18nTranslations.d.ts +4 -4
- package/dist/utils/findInvalidi18nTranslations.js +36 -36
- package/dist/utils/findInvalidi18nTranslations.test.js +72 -72
- package/dist/utils/findMissingKeys.d.ts +2 -2
- package/dist/utils/findMissingKeys.js +3 -3
- package/dist/utils/findMissingKeys.test.js +20 -20
- package/dist/utils/flattenTranslations.d.ts +1 -1
- package/dist/utils/flattenTranslations.js +4 -4
- 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 +206 -206
- package/package.json +14 -4
package/dist/index.js
CHANGED
|
@@ -10,9 +10,10 @@ const findInvalidi18nTranslations_1 = require("./utils/findInvalidi18nTranslatio
|
|
|
10
10
|
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
|
-
const
|
|
14
|
-
const
|
|
15
|
-
|
|
13
|
+
const path_1 = __importDefault(require("path"));
|
|
14
|
+
const ParseFormats = ['react-intl', 'i18next', 'next-intl'];
|
|
15
|
+
const checkInvalidTranslations = (source, targets, options = { format: 'icu' }) => {
|
|
16
|
+
return options.format === 'i18next'
|
|
16
17
|
? (0, findInvalidi18nTranslations_1.findInvalid18nTranslations)(source, targets)
|
|
17
18
|
: (0, findInvalidTranslations_1.findInvalidTranslations)(source, targets);
|
|
18
19
|
};
|
|
@@ -21,23 +22,21 @@ const checkMissingTranslations = (source, targets) => {
|
|
|
21
22
|
return (0, findMissingKeys_1.findMissingKeys)(source, targets);
|
|
22
23
|
};
|
|
23
24
|
exports.checkMissingTranslations = checkMissingTranslations;
|
|
24
|
-
const checkTranslations = (source, targets, options = { format:
|
|
25
|
-
const { checks = [
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const hasMissingKeys = checks.includes(
|
|
29
|
-
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');
|
|
30
31
|
source.forEach(({ name, content }) => {
|
|
31
|
-
const files = targets
|
|
32
|
+
const files = Object.fromEntries(targets
|
|
32
33
|
.filter(({ reference }) => reference === name)
|
|
33
|
-
.
|
|
34
|
-
return Object.assign(obj, { [key]: content });
|
|
35
|
-
}, {});
|
|
34
|
+
.map(({ name, content }) => [name, content]));
|
|
36
35
|
if (hasMissingKeys) {
|
|
37
|
-
|
|
36
|
+
merge(missingKeys, (0, exports.checkMissingTranslations)(content, files));
|
|
38
37
|
}
|
|
39
38
|
if (hasInvalidKeys) {
|
|
40
|
-
|
|
39
|
+
merge(invalidKeys, (0, exports.checkInvalidTranslations)(content, files, options));
|
|
41
40
|
}
|
|
42
41
|
});
|
|
43
42
|
return {
|
|
@@ -46,29 +45,34 @@ const checkTranslations = (source, targets, options = { format: "icu", checks: [
|
|
|
46
45
|
};
|
|
47
46
|
};
|
|
48
47
|
exports.checkTranslations = checkTranslations;
|
|
48
|
+
function merge(left, right) {
|
|
49
|
+
for (const [k, v] of Object.entries(right)) {
|
|
50
|
+
left[k] = (left?.[k] ?? []).concat(v);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
49
53
|
const checkUnusedKeys = async (translationFiles, filesToParse, options = {
|
|
50
|
-
format:
|
|
54
|
+
format: 'react-intl',
|
|
51
55
|
checks: [],
|
|
52
56
|
}, componentFunctions = []) => {
|
|
53
57
|
if (!options.format || !ParseFormats.includes(options.format)) {
|
|
54
58
|
return undefined;
|
|
55
59
|
}
|
|
56
|
-
if (!options.checks || !options.checks.includes(
|
|
60
|
+
if (!options.checks || !options.checks.includes('unused')) {
|
|
57
61
|
return undefined;
|
|
58
62
|
}
|
|
59
|
-
if (options.format ===
|
|
63
|
+
if (options.format === 'react-intl') {
|
|
60
64
|
return findUnusedReactIntlTranslations(translationFiles, filesToParse);
|
|
61
65
|
}
|
|
62
|
-
else if (options.format ===
|
|
66
|
+
else if (options.format === 'i18next') {
|
|
63
67
|
return findUnusedI18NextTranslations(translationFiles, filesToParse, componentFunctions);
|
|
64
68
|
}
|
|
65
|
-
else if (options.format ===
|
|
69
|
+
else if (options.format === 'next-intl') {
|
|
66
70
|
return findUnusedNextIntlTranslations(translationFiles, filesToParse);
|
|
67
71
|
}
|
|
68
72
|
};
|
|
69
73
|
exports.checkUnusedKeys = checkUnusedKeys;
|
|
70
74
|
const findUnusedReactIntlTranslations = async (translationFiles, filesToParse) => {
|
|
71
|
-
|
|
75
|
+
const unusedKeys = {};
|
|
72
76
|
const extracted = await (0, cli_lib_1.extract)(filesToParse, {});
|
|
73
77
|
const extractedResultSet = new Set(Object.keys(JSON.parse(extracted)));
|
|
74
78
|
translationFiles.forEach(({ name, content }) => {
|
|
@@ -79,14 +83,14 @@ const findUnusedReactIntlTranslations = async (translationFiles, filesToParse) =
|
|
|
79
83
|
found.push(keyInSource);
|
|
80
84
|
}
|
|
81
85
|
}
|
|
82
|
-
|
|
86
|
+
unusedKeys[name] = found;
|
|
83
87
|
});
|
|
84
88
|
return unusedKeys;
|
|
85
89
|
};
|
|
86
90
|
const findUnusedI18NextTranslations = async (source, filesToParse, componentFunctions = []) => {
|
|
87
|
-
|
|
91
|
+
const unusedKeys = {};
|
|
88
92
|
const { extractedResult, skippableKeys } = await getI18NextKeysInCode(filesToParse, componentFunctions);
|
|
89
|
-
const extractedResultSet = new Set(extractedResult.map(({ key }) => key));
|
|
93
|
+
const extractedResultSet = new Set(extractedResult.map(({ key, namespace }) => namespace ? `${namespace}.${key}` : key));
|
|
90
94
|
source.forEach(({ name, content }) => {
|
|
91
95
|
const keysInSource = Object.keys(content);
|
|
92
96
|
const found = [];
|
|
@@ -97,16 +101,19 @@ const findUnusedI18NextTranslations = async (source, filesToParse, componentFunc
|
|
|
97
101
|
if (isSkippable !== undefined) {
|
|
98
102
|
continue;
|
|
99
103
|
}
|
|
100
|
-
|
|
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)) {
|
|
101
108
|
found.push(keyInSource);
|
|
102
109
|
}
|
|
103
110
|
}
|
|
104
|
-
|
|
111
|
+
unusedKeys[name] = found;
|
|
105
112
|
});
|
|
106
113
|
return unusedKeys;
|
|
107
114
|
};
|
|
108
115
|
const findUnusedNextIntlTranslations = async (translationFiles, filesToParse) => {
|
|
109
|
-
|
|
116
|
+
const unusedKeys = {};
|
|
110
117
|
const extracted = (0, nextIntlSrcParser_1.extract)(filesToParse);
|
|
111
118
|
const dynamicNamespaces = extracted.flatMap((namespace) => {
|
|
112
119
|
if (namespace.meta.dynamic) {
|
|
@@ -127,8 +134,8 @@ const findUnusedNextIntlTranslations = async (translationFiles, filesToParse) =>
|
|
|
127
134
|
// Check if key is part of a dynamic namespace
|
|
128
135
|
// Skip the key if it is part of the dynamic namespace
|
|
129
136
|
const isDynamicNamespace = dynamicNamespaces.find((dynamicNamespace) => {
|
|
130
|
-
const keyInSourceNamespaces = keyInSource.split(
|
|
131
|
-
return dynamicNamespace.split(
|
|
137
|
+
const keyInSourceNamespaces = keyInSource.split('.');
|
|
138
|
+
return dynamicNamespace.split('.').every((namePart, index) => {
|
|
132
139
|
return namePart === keyInSourceNamespaces[index];
|
|
133
140
|
});
|
|
134
141
|
});
|
|
@@ -139,27 +146,27 @@ const findUnusedNextIntlTranslations = async (translationFiles, filesToParse) =>
|
|
|
139
146
|
found.push(keyInSource);
|
|
140
147
|
}
|
|
141
148
|
}
|
|
142
|
-
|
|
149
|
+
unusedKeys[name] = found;
|
|
143
150
|
});
|
|
144
151
|
return unusedKeys;
|
|
145
152
|
};
|
|
146
153
|
const checkUndefinedKeys = async (source, filesToParse, options = {
|
|
147
|
-
format:
|
|
154
|
+
format: 'react-intl',
|
|
148
155
|
checks: [],
|
|
149
156
|
}, componentFunctions = []) => {
|
|
150
157
|
if (!options.format || !ParseFormats.includes(options.format)) {
|
|
151
158
|
return undefined;
|
|
152
159
|
}
|
|
153
|
-
if (!options.checks || !options.checks.includes(
|
|
160
|
+
if (!options.checks || !options.checks.includes('undefined')) {
|
|
154
161
|
return undefined;
|
|
155
162
|
}
|
|
156
|
-
if (options.format ===
|
|
163
|
+
if (options.format === 'react-intl') {
|
|
157
164
|
return findUndefinedReactIntlKeys(source, filesToParse);
|
|
158
165
|
}
|
|
159
|
-
else if (options.format ===
|
|
166
|
+
else if (options.format === 'i18next') {
|
|
160
167
|
return findUndefinedI18NextKeys(source, filesToParse, componentFunctions);
|
|
161
168
|
}
|
|
162
|
-
else if (options.format ===
|
|
169
|
+
else if (options.format === 'next-intl') {
|
|
163
170
|
return findUndefinedNextIntlKeys(source, filesToParse);
|
|
164
171
|
}
|
|
165
172
|
};
|
|
@@ -171,11 +178,14 @@ const findUndefinedReactIntlKeys = async (translationFiles, filesToParse) => {
|
|
|
171
178
|
const extractedResult = await (0, cli_lib_1.extract)(filesToParse, {
|
|
172
179
|
extractSourceLocation: true,
|
|
173
180
|
});
|
|
174
|
-
|
|
181
|
+
const undefinedKeys = {};
|
|
175
182
|
Object.entries(JSON.parse(extractedResult)).forEach(([key, meta]) => {
|
|
176
183
|
if (!sourceKeys.has(key)) {
|
|
177
|
-
|
|
178
|
-
|
|
184
|
+
const data = meta;
|
|
185
|
+
if (!('file' in data) || typeof data.file !== 'string') {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const file = path_1.default.normalize(data.file);
|
|
179
189
|
if (!undefinedKeys[file]) {
|
|
180
190
|
undefinedKeys[file] = [];
|
|
181
191
|
}
|
|
@@ -189,7 +199,7 @@ const findUndefinedI18NextKeys = async (source, filesToParse, componentFunctions
|
|
|
189
199
|
const sourceKeys = new Set(source.flatMap(({ content }) => {
|
|
190
200
|
return Object.keys(content);
|
|
191
201
|
}));
|
|
192
|
-
|
|
202
|
+
const undefinedKeys = {};
|
|
193
203
|
extractedResult.forEach(({ file, key }) => {
|
|
194
204
|
const isSkippable = skippableKeys.find((skippableKey) => {
|
|
195
205
|
return key.includes(skippableKey);
|
|
@@ -208,10 +218,9 @@ const findUndefinedNextIntlKeys = async (translationFiles, filesToParse) => {
|
|
|
208
218
|
return Object.keys(content);
|
|
209
219
|
}));
|
|
210
220
|
const extractedResult = (0, nextIntlSrcParser_1.extract)(filesToParse);
|
|
211
|
-
|
|
221
|
+
const undefinedKeys = {};
|
|
212
222
|
extractedResult.forEach(({ key, meta }) => {
|
|
213
223
|
if (!meta.dynamic && !sourceKeys.has(key)) {
|
|
214
|
-
// @ts-ignore
|
|
215
224
|
const file = meta.file;
|
|
216
225
|
if (!undefinedKeys[file]) {
|
|
217
226
|
undefinedKeys[file] = [];
|
|
@@ -222,26 +231,27 @@ const findUndefinedNextIntlKeys = async (translationFiles, filesToParse) => {
|
|
|
222
231
|
return undefinedKeys;
|
|
223
232
|
};
|
|
224
233
|
const isRecord = (data) => {
|
|
225
|
-
return (typeof data ===
|
|
234
|
+
return (typeof data === 'object' &&
|
|
226
235
|
!Array.isArray(data) &&
|
|
227
236
|
data !== null &&
|
|
228
237
|
data !== undefined);
|
|
229
238
|
};
|
|
230
239
|
const getI18NextKeysInCode = async (filesToParse, componentFunctions = []) => {
|
|
240
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
231
241
|
// @ts-ignore
|
|
232
|
-
const { transform } = await import(
|
|
242
|
+
const { transform } = await import('i18next-parser');
|
|
233
243
|
const i18nextParser = new transform({
|
|
234
244
|
lexers: {
|
|
235
245
|
jsx: [
|
|
236
246
|
{
|
|
237
|
-
lexer:
|
|
238
|
-
componentFunctions: componentFunctions.concat([
|
|
247
|
+
lexer: 'JsxLexer',
|
|
248
|
+
componentFunctions: componentFunctions.concat(['Trans']),
|
|
239
249
|
},
|
|
240
250
|
],
|
|
241
251
|
tsx: [
|
|
242
252
|
{
|
|
243
|
-
lexer:
|
|
244
|
-
componentFunctions: componentFunctions.concat([
|
|
253
|
+
lexer: 'JsxLexer',
|
|
254
|
+
componentFunctions: componentFunctions.concat(['Trans']),
|
|
245
255
|
},
|
|
246
256
|
],
|
|
247
257
|
},
|
|
@@ -249,32 +259,46 @@ const getI18NextKeysInCode = async (filesToParse, componentFunctions = []) => {
|
|
|
249
259
|
// Skip any parsed keys that have the `returnObjects` property set to true
|
|
250
260
|
// As these are used dynamically, they will be skipped to prevent
|
|
251
261
|
// these keys from being marked as unused.
|
|
252
|
-
|
|
262
|
+
const extractedResult = [];
|
|
253
263
|
const skippableKeys = [];
|
|
254
264
|
filesToParse.forEach((file) => {
|
|
255
|
-
const rawContent = fs_1.default.readFileSync(file,
|
|
265
|
+
const rawContent = fs_1.default.readFileSync(file, 'utf-8');
|
|
256
266
|
const entries = i18nextParser.parser.parse(rawContent, file);
|
|
257
267
|
// Intermediate solution to retrieve all keys from the parser.
|
|
258
268
|
// This will be built out to also include the namespace and check
|
|
259
269
|
// the key against the namespace corresponding file.
|
|
260
270
|
// The current implementation considers the key as used no matter the namespace.
|
|
261
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
|
+
}
|
|
262
282
|
if (entry.returnObjects) {
|
|
263
283
|
skippableKeys.push(entry.key);
|
|
264
284
|
}
|
|
265
285
|
else {
|
|
266
|
-
extractedResult.push({
|
|
286
|
+
extractedResult.push({
|
|
287
|
+
file,
|
|
288
|
+
key: entry.key,
|
|
289
|
+
namespace: entry.namespace,
|
|
290
|
+
});
|
|
267
291
|
}
|
|
268
292
|
}
|
|
269
293
|
});
|
|
270
294
|
return { extractedResult, skippableKeys };
|
|
271
295
|
};
|
|
272
|
-
function
|
|
273
|
-
for (
|
|
274
|
-
|
|
296
|
+
function _flatten(object, prefix = null, result = {}) {
|
|
297
|
+
for (const key in object) {
|
|
298
|
+
const propName = prefix ? `${prefix}.${key}` : key;
|
|
275
299
|
const data = object[key];
|
|
276
300
|
if (isRecord(data)) {
|
|
277
|
-
|
|
301
|
+
_flatten(data, propName, result);
|
|
278
302
|
}
|
|
279
303
|
else {
|
|
280
304
|
result[propName] = data;
|
package/dist/types.d.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
export type Translation = Record<string, unknown>;
|
|
2
2
|
export type CheckResult = Record<string, string[]>;
|
|
3
|
+
export type InvalidTranslationEntry = {
|
|
4
|
+
key: string;
|
|
5
|
+
msg: string;
|
|
6
|
+
};
|
|
7
|
+
export type InvalidTranslationsResult = Record<string, InvalidTranslationEntry[]>;
|
|
3
8
|
export type TranslationFile = {
|
|
4
9
|
reference: string | null;
|
|
5
10
|
name: string;
|
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
import { MessageFormatElement } from
|
|
2
|
-
import { Translation } from
|
|
3
|
-
export declare const findInvalidTranslations: (source: Translation, files: Record<string, Translation>) =>
|
|
4
|
-
export declare const compareTranslationFiles: (a: Translation, b: Translation) =>
|
|
5
|
-
key: string;
|
|
6
|
-
msg: string;
|
|
7
|
-
}[];
|
|
1
|
+
import { MessageFormatElement } from '@formatjs/icu-messageformat-parser';
|
|
2
|
+
import { InvalidTranslationEntry, InvalidTranslationsResult, Translation } from '../types';
|
|
3
|
+
export declare const findInvalidTranslations: (source: Translation, files: Record<string, Translation>) => InvalidTranslationsResult;
|
|
4
|
+
export declare const compareTranslationFiles: (a: Translation, b: Translation) => InvalidTranslationEntry[];
|
|
8
5
|
export declare const hasDiff: (a: MessageFormatElement[], b: MessageFormatElement[]) => boolean;
|
|
@@ -3,14 +3,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.hasDiff = exports.compareTranslationFiles = exports.findInvalidTranslations = void 0;
|
|
4
4
|
const icu_messageformat_parser_1 = require("@formatjs/icu-messageformat-parser");
|
|
5
5
|
const findInvalidTranslations = (source, files) => {
|
|
6
|
-
|
|
6
|
+
const differences = {};
|
|
7
7
|
if (Object.keys(files).length === 0) {
|
|
8
8
|
return differences;
|
|
9
9
|
}
|
|
10
10
|
for (const [lang, file] of Object.entries(files)) {
|
|
11
11
|
const result = (0, exports.compareTranslationFiles)(source, file);
|
|
12
12
|
if (result.length > 0) {
|
|
13
|
-
differences
|
|
13
|
+
differences[lang] = result;
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
return differences;
|
|
@@ -27,7 +27,7 @@ const sortParsedKeys = (a, b) => {
|
|
|
27
27
|
return a.type - b.type;
|
|
28
28
|
};
|
|
29
29
|
const compareTranslationFiles = (a, b) => {
|
|
30
|
-
|
|
30
|
+
const diffs = [];
|
|
31
31
|
for (const key in a) {
|
|
32
32
|
if (b[key] === undefined) {
|
|
33
33
|
continue;
|
|
@@ -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
|
};
|
|
@@ -2,81 +2,81 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const findInvalidTranslations_1 = require("./findInvalidTranslations");
|
|
4
4
|
const flattenTranslations_1 = require("./flattenTranslations");
|
|
5
|
-
const sourceFile = require(
|
|
6
|
-
const secondaryFile = require(
|
|
7
|
-
describe(
|
|
8
|
-
it(
|
|
5
|
+
const sourceFile = require('../../translations/messageExamples/en-us.json');
|
|
6
|
+
const secondaryFile = require('../../translations/messageExamples/de-de.json');
|
|
7
|
+
describe('findInvalidTranslations:compareTranslationFiles', () => {
|
|
8
|
+
it('should return empty array if files are identical', () => {
|
|
9
9
|
expect((0, findInvalidTranslations_1.compareTranslationFiles)((0, flattenTranslations_1.flattenTranslations)(sourceFile), (0, flattenTranslations_1.flattenTranslations)(sourceFile))).toEqual([]);
|
|
10
10
|
});
|
|
11
|
-
it(
|
|
11
|
+
it('should return the invalid keys in the target file', () => {
|
|
12
12
|
expect((0, findInvalidTranslations_1.compareTranslationFiles)((0, flattenTranslations_1.flattenTranslations)({
|
|
13
13
|
...sourceFile,
|
|
14
|
-
|
|
15
|
-
}), (0, flattenTranslations_1.flattenTranslations)(secondaryFile))).toEqual([{ key:
|
|
14
|
+
'ten.eleven.twelve': 'ten eleven twelve',
|
|
15
|
+
}), (0, flattenTranslations_1.flattenTranslations)(secondaryFile))).toEqual([{ key: 'multipleVariables', msg: 'Unexpected date element' }]);
|
|
16
16
|
});
|
|
17
|
-
it(
|
|
17
|
+
it('should return empty array if placeholders are identical but in different positions', () => {
|
|
18
18
|
expect((0, findInvalidTranslations_1.compareTranslationFiles)({
|
|
19
|
-
basic:
|
|
19
|
+
basic: 'added {this} and {that} should work.',
|
|
20
20
|
}, {
|
|
21
|
-
basic:
|
|
21
|
+
basic: 'It is {this} with different position {that}',
|
|
22
22
|
})).toEqual([]);
|
|
23
23
|
});
|
|
24
24
|
});
|
|
25
|
-
describe(
|
|
26
|
-
it(
|
|
25
|
+
describe('findInvalidTranslations', () => {
|
|
26
|
+
it('should return an empty object if all files have no invalid keys', () => {
|
|
27
27
|
expect((0, findInvalidTranslations_1.findInvalidTranslations)(sourceFile, { de: sourceFile })).toEqual({});
|
|
28
28
|
});
|
|
29
|
-
it(
|
|
30
|
-
expect((0, findInvalidTranslations_1.findInvalidTranslations)({ ...sourceFile,
|
|
31
|
-
de: [{ key:
|
|
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({
|
|
31
|
+
de: [{ key: 'multipleVariables', msg: 'Unexpected date element' }],
|
|
32
32
|
});
|
|
33
33
|
});
|
|
34
|
-
it(
|
|
35
|
-
expect((0, findInvalidTranslations_1.findInvalidTranslations)({ ...sourceFile,
|
|
34
|
+
it('should return an object containing the keys for every language with missing key', () => {
|
|
35
|
+
expect((0, findInvalidTranslations_1.findInvalidTranslations)({ ...sourceFile, 'ten.eleven.twelve': 'ten eleven twelve' }, {
|
|
36
36
|
de: secondaryFile,
|
|
37
37
|
fr: {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
'four.five.six': 'four five six',
|
|
39
|
+
'seven.eight.nine': 'seven eight nine',
|
|
40
|
+
'message.text-format': 'yo,<p><b>John</b></p>!',
|
|
41
41
|
},
|
|
42
42
|
})).toEqual({
|
|
43
|
-
de: [{ key:
|
|
43
|
+
de: [{ key: 'multipleVariables', msg: 'Unexpected date element' }],
|
|
44
44
|
fr: [
|
|
45
45
|
{
|
|
46
|
-
key:
|
|
46
|
+
key: 'message.text-format',
|
|
47
47
|
msg: 'Expected tag to contain "b" but received "p"',
|
|
48
48
|
},
|
|
49
49
|
],
|
|
50
50
|
});
|
|
51
51
|
});
|
|
52
|
-
it(
|
|
52
|
+
it('should allow for different types of keys per locale', () => {
|
|
53
53
|
expect((0, findInvalidTranslations_1.findInvalidTranslations)(sourceFile, {
|
|
54
54
|
de: {
|
|
55
55
|
...secondaryFile,
|
|
56
|
-
|
|
56
|
+
'message.plural': '{count, plural, other {# of {total} items}}',
|
|
57
57
|
},
|
|
58
58
|
})).toEqual({
|
|
59
59
|
de: [
|
|
60
60
|
{
|
|
61
|
-
key:
|
|
62
|
-
msg:
|
|
61
|
+
key: 'multipleVariables',
|
|
62
|
+
msg: 'Unexpected date element',
|
|
63
63
|
},
|
|
64
64
|
],
|
|
65
65
|
});
|
|
66
66
|
});
|
|
67
|
-
it(
|
|
67
|
+
it('should fail if a variable is changed in one of the translations', () => {
|
|
68
68
|
expect((0, findInvalidTranslations_1.findInvalidTranslations)(sourceFile, {
|
|
69
69
|
de: {
|
|
70
70
|
...secondaryFile,
|
|
71
|
-
|
|
71
|
+
'message.plural': '{count, plural, other {# of {cargado} items}}',
|
|
72
72
|
},
|
|
73
73
|
})).toEqual({
|
|
74
74
|
de: [
|
|
75
75
|
{
|
|
76
|
-
key:
|
|
76
|
+
key: 'message.plural',
|
|
77
77
|
msg: 'Error in plural: Expected argument to contain "total" but received "cargado"',
|
|
78
78
|
},
|
|
79
|
-
{ key:
|
|
79
|
+
{ key: 'multipleVariables', msg: 'Unexpected date element' },
|
|
80
80
|
],
|
|
81
81
|
});
|
|
82
82
|
});
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
*
|
|
5
5
|
*
|
|
6
6
|
*/
|
|
7
|
-
import { MessageFormatElement } from
|
|
8
|
-
import { Translation } from
|
|
9
|
-
export declare const findInvalid18nTranslations: (source: Translation, targets: Record<string, Translation>) =>
|
|
10
|
-
export declare const compareTranslationFiles: (a: Translation, b: Translation) =>
|
|
7
|
+
import { MessageFormatElement } from './i18NextParser';
|
|
8
|
+
import { InvalidTranslationEntry, InvalidTranslationsResult, Translation } from '../types';
|
|
9
|
+
export declare const findInvalid18nTranslations: (source: Translation, targets: Record<string, Translation>) => InvalidTranslationsResult;
|
|
10
|
+
export declare const compareTranslationFiles: (a: Translation, b: Translation) => InvalidTranslationEntry[];
|
|
11
11
|
export declare const hasDiff: (a: MessageFormatElement[], b: MessageFormatElement[]) => boolean;
|