@lingual/i18n-check 0.1.1 → 0.1.5
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.d.ts +2 -0
- package/dist/bin/index.js +170 -0
- package/dist/errorReporters.d.ts +5 -0
- package/dist/errorReporters.js +16 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +41 -0
- package/dist/types.d.ts +7 -0
- package/dist/types.js +2 -0
- package/dist/utils/findInvalidTranslations.d.ts +5 -0
- package/dist/utils/findInvalidTranslations.js +82 -0
- package/dist/utils/findInvalidTranslations.test.d.ts +1 -0
- package/dist/utils/findInvalidTranslations.test.js +42 -0
- package/dist/utils/findInvalidi18nTranslations.d.ts +11 -0
- package/dist/utils/findInvalidi18nTranslations.js +108 -0
- package/dist/utils/findInvalidi18nTranslations.test.d.ts +1 -0
- package/dist/utils/findInvalidi18nTranslations.test.js +120 -0
- package/dist/utils/findMissingKeys.d.ts +3 -0
- package/dist/utils/findMissingKeys.js +25 -0
- package/dist/utils/findMissingKeys.test.d.ts +1 -0
- package/dist/utils/findMissingKeys.test.js +41 -0
- package/dist/utils/flattenTranslations.d.ts +3 -0
- package/dist/utils/flattenTranslations.js +33 -0
- package/dist/utils/flattenTranslations.test.d.ts +1 -0
- package/dist/utils/flattenTranslations.test.js +28 -0
- package/dist/utils/i18NextParser.d.ts +37 -0
- package/dist/utils/i18NextParser.js +104 -0
- package/dist/utils/i18NextParser.test.d.ts +1 -0
- package/dist/utils/i18NextParser.test.js +150 -0
- package/package.json +10 -10
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
#! /usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
4
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
5
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
6
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
7
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
8
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
9
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
10
|
+
});
|
|
11
|
+
};
|
|
12
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
13
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
14
|
+
};
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
const glob_1 = require("glob");
|
|
17
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
18
|
+
const fs_1 = __importDefault(require("fs"));
|
|
19
|
+
const process_1 = require("process");
|
|
20
|
+
const commander_1 = require("commander");
|
|
21
|
+
const __1 = require("..");
|
|
22
|
+
const errorReporters_1 = require("../errorReporters");
|
|
23
|
+
const flattenTranslations_1 = require("../utils/flattenTranslations");
|
|
24
|
+
commander_1.program
|
|
25
|
+
.version("0.1.0")
|
|
26
|
+
.option("-t, --target [directory]", "name of the directory containing the JSON files to validate")
|
|
27
|
+
.option("-s, --source [source file(s)]", "path to the reference file(s)")
|
|
28
|
+
.option("-f, --format [format type]", "define the specific format, i.e. i18next")
|
|
29
|
+
.option("-c, --check [checks]", "define the specific checks you want to run: invalid, missing. By default the check will validate against missing and invalid keys, i.e. --check invalidKeys,missingKeys")
|
|
30
|
+
.option("-r, --reporter [error reporting style]", "define the reporting style: standard or summary")
|
|
31
|
+
.option("-e, --exclude [file(s), folder(s)]", "define the file(s) and/or folders(s) that should be excluded from the check")
|
|
32
|
+
.parse();
|
|
33
|
+
const getCheckOptions = () => {
|
|
34
|
+
const checkOption = commander_1.program.getOptionValue("check");
|
|
35
|
+
if (!checkOption) {
|
|
36
|
+
return ["invalidKeys", "missingKeys"];
|
|
37
|
+
}
|
|
38
|
+
const checks = checkOption
|
|
39
|
+
.split(",")
|
|
40
|
+
.filter((check) => ["invalidKeys", "missingKeys"].includes(check.trim()));
|
|
41
|
+
return checks.length > 0 ? checks : ["invalidKeys", "missingKeys"];
|
|
42
|
+
};
|
|
43
|
+
const getSourcePath = (sourcePaths, fileName) => {
|
|
44
|
+
return sourcePaths.find((basePathName) => {
|
|
45
|
+
return fileName.includes(basePathName);
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
const getTargetPath = (paths, sourcePaths, fileName) => {
|
|
49
|
+
const basePath = paths.find((path) => {
|
|
50
|
+
return fileName.includes(path);
|
|
51
|
+
});
|
|
52
|
+
if (!basePath) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
return sourcePaths.find((path) => path.includes(basePath));
|
|
56
|
+
};
|
|
57
|
+
const toArray = (input) => {
|
|
58
|
+
return input
|
|
59
|
+
.trim()
|
|
60
|
+
.split(",")
|
|
61
|
+
.filter((a) => a);
|
|
62
|
+
};
|
|
63
|
+
const main = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
64
|
+
const start = performance.now();
|
|
65
|
+
const srcPath = commander_1.program.getOptionValue("source");
|
|
66
|
+
const targetPath = commander_1.program.getOptionValue("target");
|
|
67
|
+
const format = commander_1.program.getOptionValue("format");
|
|
68
|
+
const exclude = commander_1.program.getOptionValue("exclude");
|
|
69
|
+
if (!srcPath) {
|
|
70
|
+
console.log(chalk_1.default.red("Source file(s) not found. Please provide valid source file(s), i.e. -s translations/en-us.json"));
|
|
71
|
+
(0, process_1.exit)(1);
|
|
72
|
+
}
|
|
73
|
+
if (!targetPath) {
|
|
74
|
+
console.log(chalk_1.default.red("Target file(s) not found. Please provide valid target file(s), i.e. -t translations/"));
|
|
75
|
+
(0, process_1.exit)(1);
|
|
76
|
+
}
|
|
77
|
+
const excludedPaths = exclude ? toArray(exclude) : [];
|
|
78
|
+
const targetPathFolders = toArray(targetPath);
|
|
79
|
+
const srcPaths = toArray(srcPath);
|
|
80
|
+
const isMultiFolders = targetPathFolders.length > 1;
|
|
81
|
+
let srcFiles = [];
|
|
82
|
+
let targetFiles = [];
|
|
83
|
+
const pattern = isMultiFolders
|
|
84
|
+
? `{${targetPath.trim()}}/**/*.json`
|
|
85
|
+
: `${targetPath.trim()}/**/*.json`;
|
|
86
|
+
const files = yield (0, glob_1.glob)(pattern, {
|
|
87
|
+
ignore: ["node_modules/**"].concat(excludedPaths),
|
|
88
|
+
});
|
|
89
|
+
console.log(chalk_1.default.blue("i18n translations checker"));
|
|
90
|
+
console.log(chalk_1.default.blackBright(`Source file(s): ${srcPath}`));
|
|
91
|
+
if (format) {
|
|
92
|
+
console.log(chalk_1.default.blackBright(`Selected format is: ${format}`));
|
|
93
|
+
}
|
|
94
|
+
const options = {
|
|
95
|
+
checks: getCheckOptions(),
|
|
96
|
+
format: format !== null && format !== void 0 ? format : undefined,
|
|
97
|
+
};
|
|
98
|
+
files.forEach((file) => {
|
|
99
|
+
const content = JSON.parse(fs_1.default.readFileSync(file, "utf-8"));
|
|
100
|
+
const sourcePath = getSourcePath(srcPaths, file);
|
|
101
|
+
if (sourcePath) {
|
|
102
|
+
srcFiles.push({
|
|
103
|
+
reference: null,
|
|
104
|
+
name: file,
|
|
105
|
+
content: (0, flattenTranslations_1.flattenTranslations)(content),
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
const targetPath = getTargetPath(targetPathFolders, srcPaths, file);
|
|
110
|
+
const reference = (targetPath === null || targetPath === void 0 ? void 0 : targetPath.includes(".json"))
|
|
111
|
+
? targetPath
|
|
112
|
+
: `${targetPath}${file.split("/").pop()}`;
|
|
113
|
+
targetFiles.push({
|
|
114
|
+
reference,
|
|
115
|
+
name: file,
|
|
116
|
+
content: (0, flattenTranslations_1.flattenTranslations)(content),
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
if (srcFiles.length === 0) {
|
|
121
|
+
console.log(chalk_1.default.red("Source file(s) not found. Please provide valid source file(s), i.e. -s translations/en-us.json"));
|
|
122
|
+
(0, process_1.exit)(1);
|
|
123
|
+
}
|
|
124
|
+
if (targetFiles.length === 0) {
|
|
125
|
+
console.log(chalk_1.default.red("Target file(s) not found. Please provide valid target file(s), i.e. -t translations/"));
|
|
126
|
+
(0, process_1.exit)(1);
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
const result = (0, __1.checkTranslations)(srcFiles, targetFiles, options);
|
|
130
|
+
print(result);
|
|
131
|
+
const end = performance.now();
|
|
132
|
+
console.log(chalk_1.default.green(`\nDone in ${Math.round(((end - start) * 100) / 1000) / 100}s.`));
|
|
133
|
+
if ((result.missingKeys && Object.keys(result.missingKeys).length > 0) ||
|
|
134
|
+
(result.invalidKeys && Object.keys(result.invalidKeys).length > 0)) {
|
|
135
|
+
(0, process_1.exit)(1);
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
(0, process_1.exit)(0);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch (e) {
|
|
142
|
+
console.log(chalk_1.default.red("\nError: Can't validate translations. Check if the format is supported or specify the translation format i.e. -f i18next"));
|
|
143
|
+
(0, process_1.exit)(1);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
const print = ({ missingKeys, invalidKeys, }) => {
|
|
147
|
+
const reporter = commander_1.program.getOptionValue("reporter");
|
|
148
|
+
const errorReporter = reporter === "summary" ? errorReporters_1.summaryReporter : errorReporters_1.standardReporter;
|
|
149
|
+
if (missingKeys && Object.keys(missingKeys).length > 0) {
|
|
150
|
+
console.log(chalk_1.default.bgRed(chalk_1.default.white("\nFound missing keys!")));
|
|
151
|
+
for (const [lang, missingMessageKeys] of Object.entries(missingKeys)) {
|
|
152
|
+
console.log(chalk_1.default.red(`\nIn ${lang}:\n`));
|
|
153
|
+
console.log(chalk_1.default.red(errorReporter(missingMessageKeys, "missingKeys")));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
else if (missingKeys) {
|
|
157
|
+
console.log(chalk_1.default.green("\nNo missing keys found!"));
|
|
158
|
+
}
|
|
159
|
+
if (invalidKeys && Object.keys(invalidKeys).length > 0) {
|
|
160
|
+
console.log(chalk_1.default.bgRed(chalk_1.default.white("\nFound invalid keys!")));
|
|
161
|
+
for (const [lang, invalidMessageKeys] of Object.entries(invalidKeys)) {
|
|
162
|
+
console.log(chalk_1.default.red(`\nIn ${lang}:\n`));
|
|
163
|
+
console.log(chalk_1.default.red(errorReporter(invalidMessageKeys, "invalidKeys")));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
else if (invalidKeys) {
|
|
167
|
+
console.log(chalk_1.default.green("\nNo invalid translations found!"));
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
main();
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export type Context = "missingKeys" | "invalidKeys";
|
|
2
|
+
export type ErrorReporter = (result: string[], context: Context) => void;
|
|
3
|
+
export declare const contextMapping: Record<Context, string>;
|
|
4
|
+
export declare const standardReporter: ErrorReporter;
|
|
5
|
+
export declare const summaryReporter: ErrorReporter;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.summaryReporter = exports.standardReporter = exports.contextMapping = void 0;
|
|
4
|
+
exports.contextMapping = {
|
|
5
|
+
invalidKeys: "invalid",
|
|
6
|
+
missingKeys: "missing",
|
|
7
|
+
};
|
|
8
|
+
const standardReporter = (result) => {
|
|
9
|
+
return result.map((key) => `◯ ${key}`).join("\n");
|
|
10
|
+
};
|
|
11
|
+
exports.standardReporter = standardReporter;
|
|
12
|
+
const summaryReporter = (result, context) => {
|
|
13
|
+
const count = result.length;
|
|
14
|
+
return `Found ${count} ${exports.contextMapping[context]} ${count === 1 ? "key" : "keys"}.`;
|
|
15
|
+
};
|
|
16
|
+
exports.summaryReporter = summaryReporter;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { CheckResult, Translation, TranslationFile } from "./types";
|
|
2
|
+
import { Context } from "./errorReporters";
|
|
3
|
+
export type Options = {
|
|
4
|
+
format?: "icu" | "i18next";
|
|
5
|
+
checks?: Context[];
|
|
6
|
+
};
|
|
7
|
+
export declare const checkInvalidTranslations: (source: Translation, targets: Record<string, Translation>, options?: Options) => CheckResult;
|
|
8
|
+
export declare const checkMissingTranslations: (source: Translation, targets: Record<string, Translation>) => CheckResult;
|
|
9
|
+
export declare const checkTranslations: (source: TranslationFile[], targets: TranslationFile[], options?: Options) => {
|
|
10
|
+
missingKeys: CheckResult | undefined;
|
|
11
|
+
invalidKeys: CheckResult | undefined;
|
|
12
|
+
};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.checkTranslations = exports.checkMissingTranslations = exports.checkInvalidTranslations = void 0;
|
|
4
|
+
const findMissingKeys_1 = require("./utils/findMissingKeys");
|
|
5
|
+
const findInvalidTranslations_1 = require("./utils/findInvalidTranslations");
|
|
6
|
+
const findInvalidi18nTranslations_1 = require("./utils/findInvalidi18nTranslations");
|
|
7
|
+
const checkInvalidTranslations = (source, targets, options = { format: "icu" }) => {
|
|
8
|
+
return options.format === "i18next"
|
|
9
|
+
? (0, findInvalidi18nTranslations_1.findInvalid18nTranslations)(source, targets)
|
|
10
|
+
: (0, findInvalidTranslations_1.findInvalidTranslations)(source, targets);
|
|
11
|
+
};
|
|
12
|
+
exports.checkInvalidTranslations = checkInvalidTranslations;
|
|
13
|
+
const checkMissingTranslations = (source, targets) => {
|
|
14
|
+
return (0, findMissingKeys_1.findMissingKeys)(source, targets);
|
|
15
|
+
};
|
|
16
|
+
exports.checkMissingTranslations = checkMissingTranslations;
|
|
17
|
+
const checkTranslations = (source, targets, options = { format: "icu", checks: ["invalidKeys", "missingKeys"] }) => {
|
|
18
|
+
const { checks = ["invalidKeys", "missingKeys"] } = options;
|
|
19
|
+
let missingKeys = {};
|
|
20
|
+
let invalidKeys = {};
|
|
21
|
+
const hasMissingKeys = checks.includes("missingKeys");
|
|
22
|
+
const hasInvalidKeys = checks.includes("invalidKeys");
|
|
23
|
+
source.forEach(({ name, content }) => {
|
|
24
|
+
const files = targets
|
|
25
|
+
.filter(({ reference }) => reference === name)
|
|
26
|
+
.reduce((obj, { name: key, content }) => {
|
|
27
|
+
return Object.assign(obj, { [key]: content });
|
|
28
|
+
}, {});
|
|
29
|
+
if (hasMissingKeys) {
|
|
30
|
+
Object.assign(missingKeys, (0, exports.checkMissingTranslations)(content, files));
|
|
31
|
+
}
|
|
32
|
+
if (hasInvalidKeys) {
|
|
33
|
+
Object.assign(invalidKeys, (0, exports.checkInvalidTranslations)(content, files, options));
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
return {
|
|
37
|
+
missingKeys: hasMissingKeys ? missingKeys : undefined,
|
|
38
|
+
invalidKeys: hasInvalidKeys ? invalidKeys : undefined,
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
exports.checkTranslations = checkTranslations;
|
package/dist/types.d.ts
ADDED
package/dist/types.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { MessageFormatElement } from "@formatjs/icu-messageformat-parser";
|
|
2
|
+
import { Translation } from "../types";
|
|
3
|
+
export declare const findInvalidTranslations: (source: Translation, files: Record<string, Translation>) => {};
|
|
4
|
+
export declare const compareTranslationFiles: (a: Translation, b: Translation) => string[];
|
|
5
|
+
export declare const hasDiff: (a: MessageFormatElement[], b: MessageFormatElement[]) => boolean;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.hasDiff = exports.compareTranslationFiles = exports.findInvalidTranslations = void 0;
|
|
4
|
+
const icu_messageformat_parser_1 = require("@formatjs/icu-messageformat-parser");
|
|
5
|
+
const findInvalidTranslations = (source, files) => {
|
|
6
|
+
let differences = {};
|
|
7
|
+
if (Object.keys(files).length === 0) {
|
|
8
|
+
return differences;
|
|
9
|
+
}
|
|
10
|
+
for (const [lang, file] of Object.entries(files)) {
|
|
11
|
+
const result = (0, exports.compareTranslationFiles)(source, file);
|
|
12
|
+
if (result.length > 0) {
|
|
13
|
+
differences = Object.assign(differences, { [lang]: result });
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return differences;
|
|
17
|
+
};
|
|
18
|
+
exports.findInvalidTranslations = findInvalidTranslations;
|
|
19
|
+
const sortParsedKeys = (a, b) => {
|
|
20
|
+
if (a.type === b.type) {
|
|
21
|
+
return !(0, icu_messageformat_parser_1.isPoundElement)(a) && !(0, icu_messageformat_parser_1.isPoundElement)(b)
|
|
22
|
+
? a.value < b.value
|
|
23
|
+
? -1
|
|
24
|
+
: 1
|
|
25
|
+
: -1;
|
|
26
|
+
}
|
|
27
|
+
return a.type - b.type;
|
|
28
|
+
};
|
|
29
|
+
const compareTranslationFiles = (a, b) => {
|
|
30
|
+
let diffs = [];
|
|
31
|
+
for (const key in a) {
|
|
32
|
+
if (b[key] !== undefined &&
|
|
33
|
+
(0, exports.hasDiff)((0, icu_messageformat_parser_1.parse)(String(a[key])), (0, icu_messageformat_parser_1.parse)(String(b[key])))) {
|
|
34
|
+
diffs.push(key);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return diffs;
|
|
38
|
+
};
|
|
39
|
+
exports.compareTranslationFiles = compareTranslationFiles;
|
|
40
|
+
const hasDiff = (a, b) => {
|
|
41
|
+
const compA = a
|
|
42
|
+
.filter((element) => !(0, icu_messageformat_parser_1.isLiteralElement)(element))
|
|
43
|
+
.sort(sortParsedKeys);
|
|
44
|
+
const compB = b
|
|
45
|
+
.filter((element) => !(0, icu_messageformat_parser_1.isLiteralElement)(element))
|
|
46
|
+
.sort(sortParsedKeys);
|
|
47
|
+
if (compA.length !== compB.length) {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
const hasErrors = compA.some((formatElementA, index) => {
|
|
51
|
+
const formatElementB = compB[index];
|
|
52
|
+
if (formatElementA.type !== formatElementB.type ||
|
|
53
|
+
formatElementA.location !== formatElementB.location) {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
if (((0, icu_messageformat_parser_1.isLiteralElement)(formatElementA) && (0, icu_messageformat_parser_1.isLiteralElement)(formatElementB)) ||
|
|
57
|
+
((0, icu_messageformat_parser_1.isPoundElement)(formatElementA) && (0, icu_messageformat_parser_1.isPoundElement)(formatElementB))) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
// @ts-ignore
|
|
61
|
+
if (formatElementA.value !== formatElementB.value) {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
if ((0, icu_messageformat_parser_1.isTagElement)(formatElementA) && (0, icu_messageformat_parser_1.isTagElement)(formatElementB)) {
|
|
65
|
+
return (0, exports.hasDiff)(formatElementA.children, formatElementB.children);
|
|
66
|
+
}
|
|
67
|
+
if (((0, icu_messageformat_parser_1.isSelectElement)(formatElementA) && (0, icu_messageformat_parser_1.isSelectElement)(formatElementB)) ||
|
|
68
|
+
((0, icu_messageformat_parser_1.isPluralElement)(formatElementA) && (0, icu_messageformat_parser_1.isPluralElement)(formatElementB))) {
|
|
69
|
+
const optionsA = Object.keys(formatElementA.options).sort();
|
|
70
|
+
const optionsB = Object.keys(formatElementA.options).sort();
|
|
71
|
+
if (optionsA.join("-") !== optionsB.join("-")) {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
return optionsA.some((key) => {
|
|
75
|
+
return (0, exports.hasDiff)(formatElementA.options[key].value, formatElementB.options[key].value);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
});
|
|
80
|
+
return hasErrors;
|
|
81
|
+
};
|
|
82
|
+
exports.hasDiff = hasDiff;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const findInvalidTranslations_1 = require("./findInvalidTranslations");
|
|
4
|
+
const flattenTranslations_1 = require("./flattenTranslations");
|
|
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
|
+
expect((0, findInvalidTranslations_1.compareTranslationFiles)((0, flattenTranslations_1.flattenTranslations)(sourceFile), (0, flattenTranslations_1.flattenTranslations)(sourceFile))).toEqual([]);
|
|
10
|
+
});
|
|
11
|
+
it("should return the invalid keys in the target file", () => {
|
|
12
|
+
expect((0, findInvalidTranslations_1.compareTranslationFiles)((0, flattenTranslations_1.flattenTranslations)(Object.assign(Object.assign({}, sourceFile), { "ten.eleven.twelve": "ten eleven twelve" })), (0, flattenTranslations_1.flattenTranslations)(secondaryFile))).toEqual(["multipleVariables"]);
|
|
13
|
+
});
|
|
14
|
+
it("should return empty array if placeholders are identical but in different positions", () => {
|
|
15
|
+
expect((0, findInvalidTranslations_1.compareTranslationFiles)({
|
|
16
|
+
basic: "added {this} and {that} should work.",
|
|
17
|
+
}, {
|
|
18
|
+
basic: "It is {this} with different position {that}",
|
|
19
|
+
})).toEqual([]);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
describe("findInvalidTranslations", () => {
|
|
23
|
+
it("should return an empty object if all files have no invalid keys", () => {
|
|
24
|
+
expect((0, findInvalidTranslations_1.findInvalidTranslations)(sourceFile, { de: sourceFile })).toEqual({});
|
|
25
|
+
});
|
|
26
|
+
it("should return an object containing the keys for the missing language", () => {
|
|
27
|
+
expect((0, findInvalidTranslations_1.findInvalidTranslations)(Object.assign(Object.assign({}, sourceFile), { "ten.eleven.twelve": "ten eleven twelve" }), { de: secondaryFile })).toEqual({ de: ["multipleVariables"] });
|
|
28
|
+
});
|
|
29
|
+
it("should return an object containing the keys for every language with missing key", () => {
|
|
30
|
+
expect((0, findInvalidTranslations_1.findInvalidTranslations)(Object.assign(Object.assign({}, sourceFile), { "ten.eleven.twelve": "ten eleven twelve" }), {
|
|
31
|
+
de: secondaryFile,
|
|
32
|
+
fr: {
|
|
33
|
+
"four.five.six": "four five six",
|
|
34
|
+
"seven.eight.nine": "seven eight nine",
|
|
35
|
+
"message.text-format": "yo,<p><b>John</b></p>!",
|
|
36
|
+
},
|
|
37
|
+
})).toEqual({
|
|
38
|
+
de: ["multipleVariables"],
|
|
39
|
+
fr: ["message.text-format"],
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* i18next specific invalid translations check
|
|
4
|
+
*
|
|
5
|
+
*
|
|
6
|
+
*/
|
|
7
|
+
import { MessageFormatElement } from "./i18NextParser";
|
|
8
|
+
import { Translation } from "../types";
|
|
9
|
+
export declare const findInvalid18nTranslations: (source: Translation, targets: Record<string, Translation>) => {};
|
|
10
|
+
export declare const compareTranslationFiles: (a: Translation, b: Translation) => unknown[];
|
|
11
|
+
export declare const hasDiff: (a: MessageFormatElement[], b: MessageFormatElement[]) => boolean;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
*
|
|
4
|
+
* i18next specific invalid translations check
|
|
5
|
+
*
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.hasDiff = exports.compareTranslationFiles = exports.findInvalid18nTranslations = void 0;
|
|
10
|
+
const i18NextParser_1 = require("./i18NextParser");
|
|
11
|
+
const findInvalid18nTranslations = (source, targets) => {
|
|
12
|
+
let differences = {};
|
|
13
|
+
if (Object.keys(targets).length === 0) {
|
|
14
|
+
return differences;
|
|
15
|
+
}
|
|
16
|
+
for (const [lang, file] of Object.entries(targets)) {
|
|
17
|
+
const result = (0, exports.compareTranslationFiles)(source, file);
|
|
18
|
+
if (result.length > 0) {
|
|
19
|
+
differences = Object.assign(differences, { [lang]: result });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return differences;
|
|
23
|
+
};
|
|
24
|
+
exports.findInvalid18nTranslations = findInvalid18nTranslations;
|
|
25
|
+
const compareTranslationFiles = (a, b) => {
|
|
26
|
+
let diffs = [];
|
|
27
|
+
for (const key in a) {
|
|
28
|
+
if (b[key] !== undefined &&
|
|
29
|
+
(0, exports.hasDiff)((0, i18NextParser_1.parse)(String(a[key])), (0, i18NextParser_1.parse)(String(b[key])))) {
|
|
30
|
+
diffs.push(key);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return diffs;
|
|
34
|
+
};
|
|
35
|
+
exports.compareTranslationFiles = compareTranslationFiles;
|
|
36
|
+
const lookUp = {
|
|
37
|
+
text: 0,
|
|
38
|
+
interpolation: 1,
|
|
39
|
+
interpolation_unescaped: 2,
|
|
40
|
+
nesting: 3,
|
|
41
|
+
plural: 4,
|
|
42
|
+
tag: 5,
|
|
43
|
+
};
|
|
44
|
+
const sortParsedKeys = (a, b) => {
|
|
45
|
+
if (a.type === b.type && a.type !== "tag" && b.type !== "tag") {
|
|
46
|
+
return a.content < b.content ? -1 : 1;
|
|
47
|
+
}
|
|
48
|
+
if (a.type === "tag" && b.type === "tag") {
|
|
49
|
+
return a.raw < b.raw ? -1 : 1;
|
|
50
|
+
}
|
|
51
|
+
return lookUp[a.type] - lookUp[b.type];
|
|
52
|
+
};
|
|
53
|
+
const hasDiff = (a, b) => {
|
|
54
|
+
const compA = a
|
|
55
|
+
.filter((element) => element.type !== "text")
|
|
56
|
+
.sort(sortParsedKeys);
|
|
57
|
+
const compB = b
|
|
58
|
+
.filter((element) => element.type !== "text")
|
|
59
|
+
.sort(sortParsedKeys);
|
|
60
|
+
if (compA.length !== compB.length) {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
const hasErrors = compA.some((formatElementA, index) => {
|
|
64
|
+
const formatElementB = compB[index];
|
|
65
|
+
if (formatElementA.type !== formatElementB.type) {
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
if (formatElementA.type === "text" && formatElementB.type === "text") {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
if (formatElementA.type === "tag" && formatElementB.type === "tag") {
|
|
72
|
+
return (formatElementA.raw !== formatElementB.raw ||
|
|
73
|
+
formatElementA.voidElement !== formatElementB.voidElement);
|
|
74
|
+
}
|
|
75
|
+
if ((formatElementA.type === "interpolation" &&
|
|
76
|
+
formatElementB.type === "interpolation") ||
|
|
77
|
+
(formatElementA.type === "interpolation_unescaped" &&
|
|
78
|
+
formatElementB.type === "interpolation_unescaped") ||
|
|
79
|
+
(formatElementA.type === "nesting" &&
|
|
80
|
+
formatElementB.type === "nesting") ||
|
|
81
|
+
(formatElementA.type === "plural" && formatElementB.type === "plural")) {
|
|
82
|
+
const optionsA = formatElementA.variable
|
|
83
|
+
.split(",")
|
|
84
|
+
.map((value) => value.trim())
|
|
85
|
+
.sort()
|
|
86
|
+
.join("-")
|
|
87
|
+
.trim();
|
|
88
|
+
const optionsB = formatElementB.variable
|
|
89
|
+
.split(",")
|
|
90
|
+
.map((value) => value.trim())
|
|
91
|
+
.sort()
|
|
92
|
+
.join("-")
|
|
93
|
+
.trim();
|
|
94
|
+
if (optionsA !== optionsB) {
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
if (formatElementA.prefix !== formatElementA.prefix) {
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
if (formatElementA.suffix !== formatElementA.suffix) {
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return false;
|
|
105
|
+
});
|
|
106
|
+
return hasErrors;
|
|
107
|
+
};
|
|
108
|
+
exports.hasDiff = hasDiff;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const findInvalidi18nTranslations_1 = require("./findInvalidi18nTranslations");
|
|
4
|
+
const flattenTranslations_1 = require("./flattenTranslations");
|
|
5
|
+
const sourceFile = require("../../translations/i18NextMessageExamples/en-us.json");
|
|
6
|
+
const targetFile = require("../../translations/i18NextMessageExamples/de-de.json");
|
|
7
|
+
describe("findInvalid18nTranslations:compareTranslationFiles", () => {
|
|
8
|
+
it("should return empty array if files are identical", () => {
|
|
9
|
+
expect((0, findInvalidi18nTranslations_1.compareTranslationFiles)((0, flattenTranslations_1.flattenTranslations)(sourceFile), (0, flattenTranslations_1.flattenTranslations)(sourceFile))).toEqual([]);
|
|
10
|
+
});
|
|
11
|
+
it("should return the invalid keys in the target file", () => {
|
|
12
|
+
expect((0, findInvalidi18nTranslations_1.compareTranslationFiles)((0, flattenTranslations_1.flattenTranslations)(Object.assign(Object.assign({}, sourceFile), { "ten.eleven.twelve": "ten eleven twelve" })), (0, flattenTranslations_1.flattenTranslations)(targetFile))).toEqual(["key_with_broken_de", "intlNumber_broken_de"]);
|
|
13
|
+
});
|
|
14
|
+
it("should return empty array if placeholders are identical but in different positions", () => {
|
|
15
|
+
expect((0, findInvalidi18nTranslations_1.compareTranslationFiles)({
|
|
16
|
+
basic: "added {{this}} and {{that}} should work.",
|
|
17
|
+
}, {
|
|
18
|
+
basic: "It is {{this}} with different position {{that}}",
|
|
19
|
+
})).toEqual([]);
|
|
20
|
+
});
|
|
21
|
+
it("should return the invalid key if tags are not identical", () => {
|
|
22
|
+
expect((0, findInvalidi18nTranslations_1.compareTranslationFiles)({
|
|
23
|
+
tag: "This is some <b>bold text</b> and some <i>italic</i> text.",
|
|
24
|
+
}, {
|
|
25
|
+
tag: "There is some <b>bold text</b> and some other <span>italic</span> text.",
|
|
26
|
+
})).toEqual(["tag"]);
|
|
27
|
+
});
|
|
28
|
+
it("should return empty array if tags are identical", () => {
|
|
29
|
+
expect((0, findInvalidi18nTranslations_1.compareTranslationFiles)({
|
|
30
|
+
tag: "This is some <b>bold text</b> and some <i>italic</i> text.",
|
|
31
|
+
}, {
|
|
32
|
+
tag: "There is some <b>bold text</b> and some other <i>italic</i> text.",
|
|
33
|
+
})).toEqual([]);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
describe("findInvalidTranslations", () => {
|
|
37
|
+
it("should return an empty object if all files have no invalid keys", () => {
|
|
38
|
+
expect((0, findInvalidi18nTranslations_1.findInvalid18nTranslations)(sourceFile, { de: sourceFile })).toEqual({});
|
|
39
|
+
});
|
|
40
|
+
it("should return an object containing the keys for the missing language", () => {
|
|
41
|
+
expect((0, findInvalidi18nTranslations_1.findInvalid18nTranslations)(Object.assign(Object.assign({}, sourceFile), { "ten.eleven.twelve": "ten eleven twelve" }), { de: targetFile })).toEqual({ de: ["key_with_broken_de", "intlNumber_broken_de"] });
|
|
42
|
+
});
|
|
43
|
+
it("should return an object containing the keys for every language with missing key", () => {
|
|
44
|
+
expect((0, findInvalidi18nTranslations_1.findInvalid18nTranslations)(Object.assign(Object.assign({}, sourceFile), { "ten.eleven.twelve": "ten eleven twelve" }), {
|
|
45
|
+
de: targetFile,
|
|
46
|
+
fr: {
|
|
47
|
+
key_with_broken_de: "Some format {{value, formatname}} and some other format {{value, formatname}}",
|
|
48
|
+
},
|
|
49
|
+
})).toEqual({
|
|
50
|
+
de: ["key_with_broken_de", "intlNumber_broken_de"],
|
|
51
|
+
fr: ["key_with_broken_de"],
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
it("should find invalid interval", () => {
|
|
55
|
+
expect((0, findInvalidi18nTranslations_1.findInvalid18nTranslations)({
|
|
56
|
+
key1_interval: "(1)[one item];(2-7)[a few items];(7-inf)[a lot of items];",
|
|
57
|
+
}, {
|
|
58
|
+
de: {
|
|
59
|
+
key1_interval: "(1-2)[one or two items];(3-7)[a few items];(7-inf)[a lot of items];",
|
|
60
|
+
},
|
|
61
|
+
})).toEqual({
|
|
62
|
+
de: ["key1_interval"],
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
it("should find invalid nested interpolation", () => {
|
|
66
|
+
expect((0, findInvalidi18nTranslations_1.findInvalid18nTranslations)({
|
|
67
|
+
"tree.one": "added {{something}}",
|
|
68
|
+
}, {
|
|
69
|
+
de: {
|
|
70
|
+
"tree.one": "added {{somethings}}",
|
|
71
|
+
},
|
|
72
|
+
})).toEqual({
|
|
73
|
+
de: ["tree.one"],
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
it("should find invalid relative time formatting", () => {
|
|
77
|
+
expect((0, findInvalidi18nTranslations_1.findInvalid18nTranslations)({
|
|
78
|
+
intlRelativeTimeWithOptionsExplicit: "Lorem {{val, relativetime(range: quarter; style: narrow;)}}",
|
|
79
|
+
}, {
|
|
80
|
+
de: {
|
|
81
|
+
intlRelativeTimeWithOptionsExplicit: "Lorem {{val, relativetime(range: quarter; style: long;)}}",
|
|
82
|
+
},
|
|
83
|
+
})).toEqual({
|
|
84
|
+
de: ["intlRelativeTimeWithOptionsExplicit"],
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
it("should find invalid key with options", () => {
|
|
88
|
+
expect((0, findInvalidi18nTranslations_1.findInvalid18nTranslations)({
|
|
89
|
+
keyWithOptions: "Some format {{value, formatname(option1Name: option1Value; option2Name: option2Value)}}",
|
|
90
|
+
}, {
|
|
91
|
+
de: {
|
|
92
|
+
keyWithOptions: "Some format {{value, formatname(option3Name: option3Value; option4Name: option4Value)}}",
|
|
93
|
+
},
|
|
94
|
+
})).toEqual({
|
|
95
|
+
de: ["keyWithOptions"],
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
it("should find invalid nesting", () => {
|
|
99
|
+
expect((0, findInvalidi18nTranslations_1.findInvalid18nTranslations)({
|
|
100
|
+
nesting1: "1 $t(nesting2)",
|
|
101
|
+
}, {
|
|
102
|
+
de: {
|
|
103
|
+
nesting1: "1 $t(nesting3)",
|
|
104
|
+
},
|
|
105
|
+
})).toEqual({
|
|
106
|
+
de: ["nesting1"],
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
it("should find invalid tags", () => {
|
|
110
|
+
expect((0, findInvalidi18nTranslations_1.findInvalid18nTranslations)({
|
|
111
|
+
tag: "This is some <b>bold text</b> and some <i>italic</i> text.",
|
|
112
|
+
}, {
|
|
113
|
+
de: {
|
|
114
|
+
tag: "There is some <b>bold text</b> and some other <span>text inside a span</span>!",
|
|
115
|
+
},
|
|
116
|
+
})).toEqual({
|
|
117
|
+
de: ["tag"],
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.compareTranslationFiles = exports.findMissingKeys = void 0;
|
|
4
|
+
const findMissingKeys = (source, targets) => {
|
|
5
|
+
let differences = {};
|
|
6
|
+
for (const [lang, file] of Object.entries(targets)) {
|
|
7
|
+
const result = (0, exports.compareTranslationFiles)(source, file);
|
|
8
|
+
if (result.length > 0) {
|
|
9
|
+
differences = Object.assign(differences, { [lang]: result });
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
return differences;
|
|
13
|
+
};
|
|
14
|
+
exports.findMissingKeys = findMissingKeys;
|
|
15
|
+
const compareTranslationFiles = (a, b) => {
|
|
16
|
+
let diffs = [];
|
|
17
|
+
for (const key in a) {
|
|
18
|
+
const counterKey = b[key];
|
|
19
|
+
if (!counterKey) {
|
|
20
|
+
diffs.push(key);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return diffs;
|
|
24
|
+
};
|
|
25
|
+
exports.compareTranslationFiles = compareTranslationFiles;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const findMissingKeys_1 = require("./findMissingKeys");
|
|
4
|
+
const sourceFile = {
|
|
5
|
+
"one.two.three": "one two three",
|
|
6
|
+
"four.five.six": "four five six",
|
|
7
|
+
"seven.eight.nine": "seven eight nine",
|
|
8
|
+
};
|
|
9
|
+
const secondaryFile = {
|
|
10
|
+
"one.two.three": "one two three",
|
|
11
|
+
"four.five.six": "four five six",
|
|
12
|
+
"seven.eight.nine": "seven eight nine",
|
|
13
|
+
};
|
|
14
|
+
describe("findMissingKeys:compareTranslationFiles", () => {
|
|
15
|
+
it("should return empty array if files are identical", () => {
|
|
16
|
+
expect((0, findMissingKeys_1.compareTranslationFiles)(sourceFile, secondaryFile)).toEqual([]);
|
|
17
|
+
});
|
|
18
|
+
it("should return the missing keys in the secondary file", () => {
|
|
19
|
+
expect((0, findMissingKeys_1.compareTranslationFiles)(Object.assign(Object.assign({}, sourceFile), { "ten.eleven.twelve": "ten eleven twelve" }), secondaryFile)).toEqual(["ten.eleven.twelve"]);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
describe("findMissingKeys", () => {
|
|
23
|
+
it("should return an empty object if all files have no missing keys", () => {
|
|
24
|
+
expect((0, findMissingKeys_1.findMissingKeys)(sourceFile, { de: secondaryFile })).toEqual({});
|
|
25
|
+
});
|
|
26
|
+
it("should return an object containing the keys for the missing language", () => {
|
|
27
|
+
expect((0, findMissingKeys_1.findMissingKeys)(Object.assign(Object.assign({}, sourceFile), { "ten.eleven.twelve": "ten eleven twelve" }), { de: secondaryFile })).toEqual({ de: ["ten.eleven.twelve"] });
|
|
28
|
+
});
|
|
29
|
+
it("should return an object containing the keys for every language with missing key", () => {
|
|
30
|
+
expect((0, findMissingKeys_1.findMissingKeys)(Object.assign(Object.assign({}, sourceFile), { "ten.eleven.twelve": "ten eleven twelve" }), {
|
|
31
|
+
de: secondaryFile,
|
|
32
|
+
fr: {
|
|
33
|
+
"four.five.six": "four five six",
|
|
34
|
+
"seven.eight.nine": "seven eight nine",
|
|
35
|
+
},
|
|
36
|
+
})).toEqual({
|
|
37
|
+
de: ["ten.eleven.twelve"],
|
|
38
|
+
fr: ["one.two.three", "ten.eleven.twelve"],
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.flattenEntry = exports.flattenTranslations = void 0;
|
|
4
|
+
const flattenTranslations = (translations) => {
|
|
5
|
+
if (!hasNestedDefinitions(translations)) {
|
|
6
|
+
return translations;
|
|
7
|
+
}
|
|
8
|
+
return (0, exports.flattenEntry)(translations);
|
|
9
|
+
};
|
|
10
|
+
exports.flattenTranslations = flattenTranslations;
|
|
11
|
+
/**
|
|
12
|
+
* Top level search for any objects
|
|
13
|
+
*/
|
|
14
|
+
const hasNestedDefinitions = (translations) => {
|
|
15
|
+
return Object.values(translations).find((translation) => typeof translation === "object");
|
|
16
|
+
};
|
|
17
|
+
const isTranslationObject = (entry) => {
|
|
18
|
+
return typeof entry === "object";
|
|
19
|
+
};
|
|
20
|
+
const flattenEntry = (entry, keys = []) => {
|
|
21
|
+
let result = {};
|
|
22
|
+
if (!entry) {
|
|
23
|
+
return result;
|
|
24
|
+
}
|
|
25
|
+
const entries = Object.entries(entry);
|
|
26
|
+
for (const [k, v] of entries) {
|
|
27
|
+
Object.assign(result, isTranslationObject(v)
|
|
28
|
+
? (0, exports.flattenEntry)(v, [...keys, String(k)])
|
|
29
|
+
: { [[...keys, String(k)].join(".")]: v });
|
|
30
|
+
}
|
|
31
|
+
return result;
|
|
32
|
+
};
|
|
33
|
+
exports.flattenEntry = flattenEntry;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const flattenTranslations_1 = require("./flattenTranslations");
|
|
4
|
+
const flatStructure = require("../../translations/en-us.json");
|
|
5
|
+
const nestedStructure = require("../../translations/flattenExamples/en-us.json");
|
|
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
|
+
describe("flattenTranslations", () => {
|
|
13
|
+
it("should do nothing if the file structure is flat", () => {
|
|
14
|
+
expect((0, flattenTranslations_1.flattenTranslations)(flatStructure)).toEqual(flatStructure);
|
|
15
|
+
});
|
|
16
|
+
describe("flattenEntry", () => {
|
|
17
|
+
it("should flatten a nested object", () => {
|
|
18
|
+
expect((0, flattenTranslations_1.flattenEntry)({
|
|
19
|
+
a: {
|
|
20
|
+
b: { c: "one" },
|
|
21
|
+
},
|
|
22
|
+
})).toEqual({ "a.b.c": "one" });
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
it("should do nothing if the file structure is flat", () => {
|
|
26
|
+
expect((0, flattenTranslations_1.flattenTranslations)(nestedStructure)).toEqual(expectedFlatStructure);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export type MessageFormatElement = {
|
|
2
|
+
type: "text";
|
|
3
|
+
content: string;
|
|
4
|
+
} | {
|
|
5
|
+
type: "interpolation";
|
|
6
|
+
raw: string;
|
|
7
|
+
prefix: string;
|
|
8
|
+
suffix: string;
|
|
9
|
+
content: string;
|
|
10
|
+
variable: string;
|
|
11
|
+
} | {
|
|
12
|
+
type: "interpolation_unescaped";
|
|
13
|
+
raw: string;
|
|
14
|
+
prefix: string;
|
|
15
|
+
suffix: string;
|
|
16
|
+
content: string;
|
|
17
|
+
variable: string;
|
|
18
|
+
} | {
|
|
19
|
+
type: "nesting";
|
|
20
|
+
raw: string;
|
|
21
|
+
prefix: string;
|
|
22
|
+
suffix: string;
|
|
23
|
+
content: string;
|
|
24
|
+
variable: string;
|
|
25
|
+
} | {
|
|
26
|
+
type: "plural";
|
|
27
|
+
raw: string;
|
|
28
|
+
prefix: string;
|
|
29
|
+
suffix: string;
|
|
30
|
+
content: string;
|
|
31
|
+
variable: string;
|
|
32
|
+
} | {
|
|
33
|
+
type: "tag";
|
|
34
|
+
raw: string;
|
|
35
|
+
voidElement: boolean;
|
|
36
|
+
};
|
|
37
|
+
export declare const parse: (input: string) => MessageFormatElement[];
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Based on https://github.com/i18next/i18next-translation-parser/blob/v1.0.0/src/parse.js
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.parse = void 0;
|
|
5
|
+
const REGEXP = new RegExp("({{[^}]+}}|\\$t{[^}]+}|\\$t\\([^\\)]+\\)|\\([0-9\\-inf]+\\)|<[^>]+>)", "g");
|
|
6
|
+
const DOUBLE_BRACE = "{{";
|
|
7
|
+
const $_T_BRACE = "$t{";
|
|
8
|
+
const $_T_PARENTHESIS = "$t(";
|
|
9
|
+
const OPEN_PARENTHESIS = "(";
|
|
10
|
+
const OPEN_TAG = "<";
|
|
11
|
+
const CLOSE_TAG = "</";
|
|
12
|
+
const parse = (input) => {
|
|
13
|
+
let ast = [];
|
|
14
|
+
ast = parseInput([input]);
|
|
15
|
+
return ast;
|
|
16
|
+
};
|
|
17
|
+
exports.parse = parse;
|
|
18
|
+
const parseInput = (input) => {
|
|
19
|
+
if (input.length === 0) {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
let ast = [];
|
|
23
|
+
input.forEach((element) => {
|
|
24
|
+
const elements = element.split(REGEXP).filter((element) => element !== "");
|
|
25
|
+
const result = elements.reduce((acc, match) => {
|
|
26
|
+
if (match.indexOf("{{-") === 0) {
|
|
27
|
+
const content = match.substring(3, match.length - 2);
|
|
28
|
+
acc.push({
|
|
29
|
+
type: "interpolation_unescaped",
|
|
30
|
+
raw: match,
|
|
31
|
+
prefix: "{{-",
|
|
32
|
+
suffix: "}}",
|
|
33
|
+
content,
|
|
34
|
+
variable: content.trim(),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
else if (match.indexOf(DOUBLE_BRACE) === 0) {
|
|
38
|
+
const content = match.substring(2, match.length - 2);
|
|
39
|
+
acc.push({
|
|
40
|
+
type: "interpolation",
|
|
41
|
+
raw: match,
|
|
42
|
+
prefix: "{{",
|
|
43
|
+
suffix: "}}",
|
|
44
|
+
content,
|
|
45
|
+
variable: content.trim(),
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
else if (match.indexOf($_T_BRACE) === 0) {
|
|
49
|
+
const content = match.substring(3, match.length - 1);
|
|
50
|
+
acc.push({
|
|
51
|
+
type: "nesting",
|
|
52
|
+
raw: match,
|
|
53
|
+
prefix: "$t{",
|
|
54
|
+
suffix: "}",
|
|
55
|
+
content,
|
|
56
|
+
variable: content.trim(),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
else if (match.indexOf($_T_PARENTHESIS) === 0) {
|
|
60
|
+
const content = match.substring(3, match.length - 1);
|
|
61
|
+
acc.push({
|
|
62
|
+
type: "nesting",
|
|
63
|
+
raw: match,
|
|
64
|
+
prefix: "$t(",
|
|
65
|
+
suffix: ")",
|
|
66
|
+
content,
|
|
67
|
+
variable: content.trim(),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
else if (match.indexOf(OPEN_PARENTHESIS) === 0 &&
|
|
71
|
+
/\([0-9\-inf]+\)/.test(match)) {
|
|
72
|
+
const content = match.substring(1, match.length - 1);
|
|
73
|
+
acc.push({
|
|
74
|
+
type: "plural",
|
|
75
|
+
raw: match,
|
|
76
|
+
prefix: "(",
|
|
77
|
+
suffix: ")",
|
|
78
|
+
content,
|
|
79
|
+
variable: content.trim(),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
else if (match.indexOf(CLOSE_TAG) === 0) {
|
|
83
|
+
acc.push({
|
|
84
|
+
type: "tag",
|
|
85
|
+
raw: match,
|
|
86
|
+
voidElement: match.substring(match.length - 2) === "/>",
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
else if (match.indexOf(OPEN_TAG) === 0) {
|
|
90
|
+
acc.push({
|
|
91
|
+
type: "tag",
|
|
92
|
+
raw: match,
|
|
93
|
+
voidElement: match.substring(match.length - 2) === "/>",
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
acc.push({ type: "text", content: match });
|
|
98
|
+
}
|
|
99
|
+
return acc;
|
|
100
|
+
}, []);
|
|
101
|
+
ast = ast.concat(result);
|
|
102
|
+
});
|
|
103
|
+
return ast;
|
|
104
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const i18NextParser_1 = require("./i18NextParser");
|
|
4
|
+
describe("i18NextParser", () => {
|
|
5
|
+
it("should parse interpolation", () => {
|
|
6
|
+
expect((0, i18NextParser_1.parse)("test {{val}} text {{- encoded}} with {{val, format}} some $t{nesting} help")).toEqual([
|
|
7
|
+
{
|
|
8
|
+
type: "text",
|
|
9
|
+
content: "test ",
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
type: "interpolation",
|
|
13
|
+
raw: "{{val}}",
|
|
14
|
+
prefix: "{{",
|
|
15
|
+
suffix: "}}",
|
|
16
|
+
content: "val",
|
|
17
|
+
variable: "val",
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
type: "text",
|
|
21
|
+
content: " text ",
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
type: "interpolation_unescaped",
|
|
25
|
+
raw: "{{- encoded}}",
|
|
26
|
+
prefix: "{{-",
|
|
27
|
+
suffix: "}}",
|
|
28
|
+
content: " encoded",
|
|
29
|
+
variable: "encoded",
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
type: "text",
|
|
33
|
+
content: " with ",
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
type: "interpolation",
|
|
37
|
+
raw: "{{val, format}}",
|
|
38
|
+
prefix: "{{",
|
|
39
|
+
suffix: "}}",
|
|
40
|
+
content: "val, format",
|
|
41
|
+
variable: "val, format",
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
type: "text",
|
|
45
|
+
content: " some ",
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
type: "nesting",
|
|
49
|
+
raw: "$t{nesting}",
|
|
50
|
+
prefix: "$t{",
|
|
51
|
+
suffix: "}",
|
|
52
|
+
content: "nesting",
|
|
53
|
+
variable: "nesting",
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
type: "text",
|
|
57
|
+
content: " help",
|
|
58
|
+
},
|
|
59
|
+
]);
|
|
60
|
+
});
|
|
61
|
+
it("should parse plural translations", () => {
|
|
62
|
+
expect((0, i18NextParser_1.parse)("(1)[one item];(2-7)[a few items];(7-inf)[a lot of items];")).toEqual([
|
|
63
|
+
{
|
|
64
|
+
type: "plural",
|
|
65
|
+
raw: "(1)",
|
|
66
|
+
prefix: "(",
|
|
67
|
+
suffix: ")",
|
|
68
|
+
content: "1",
|
|
69
|
+
variable: "1",
|
|
70
|
+
},
|
|
71
|
+
{ type: "text", content: "[one item];" },
|
|
72
|
+
{
|
|
73
|
+
type: "plural",
|
|
74
|
+
raw: "(2-7)",
|
|
75
|
+
prefix: "(",
|
|
76
|
+
suffix: ")",
|
|
77
|
+
content: "2-7",
|
|
78
|
+
variable: "2-7",
|
|
79
|
+
},
|
|
80
|
+
{ type: "text", content: "[a few items];" },
|
|
81
|
+
{
|
|
82
|
+
type: "plural",
|
|
83
|
+
raw: "(7-inf)",
|
|
84
|
+
prefix: "(",
|
|
85
|
+
suffix: ")",
|
|
86
|
+
content: "7-inf",
|
|
87
|
+
variable: "7-inf",
|
|
88
|
+
},
|
|
89
|
+
{ type: "text", content: "[a lot of items];" },
|
|
90
|
+
]);
|
|
91
|
+
});
|
|
92
|
+
it("should not parse plural translations if regular text inside parenthesis", () => {
|
|
93
|
+
expect((0, i18NextParser_1.parse)("(This is a regular text inside parenthesis)")).toEqual([
|
|
94
|
+
{ type: "text", content: "(This is a regular text inside parenthesis)" },
|
|
95
|
+
]);
|
|
96
|
+
});
|
|
97
|
+
it("should parse translations with nesting", () => {
|
|
98
|
+
expect((0, i18NextParser_1.parse)("1 $t(nesting2)")).toEqual([
|
|
99
|
+
{
|
|
100
|
+
type: "text",
|
|
101
|
+
content: "1 ",
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
type: "nesting",
|
|
105
|
+
raw: "$t(nesting2)",
|
|
106
|
+
prefix: "$t(",
|
|
107
|
+
suffix: ")",
|
|
108
|
+
content: "nesting2",
|
|
109
|
+
variable: "nesting2",
|
|
110
|
+
},
|
|
111
|
+
]);
|
|
112
|
+
});
|
|
113
|
+
it("should parse translations with tags", () => {
|
|
114
|
+
expect((0, i18NextParser_1.parse)("This is some <b>bold text</b> and some <i>italic</i> text.")).toEqual([
|
|
115
|
+
{ type: "text", content: "This is some " },
|
|
116
|
+
{ type: "tag", raw: "<b>", voidElement: false },
|
|
117
|
+
{ type: "text", content: "bold text" },
|
|
118
|
+
{ type: "tag", raw: "</b>", voidElement: false },
|
|
119
|
+
{ type: "text", content: " and some " },
|
|
120
|
+
{ type: "tag", raw: "<i>", voidElement: false },
|
|
121
|
+
{ type: "text", content: "italic" },
|
|
122
|
+
{ type: "tag", raw: "</i>", voidElement: false },
|
|
123
|
+
{ type: "text", content: " text." },
|
|
124
|
+
]);
|
|
125
|
+
});
|
|
126
|
+
it("should parse translations with nested tags", () => {
|
|
127
|
+
expect((0, i18NextParser_1.parse)("This is some <b>bold text and some <i>nested italic</i> text</b>!")).toEqual([
|
|
128
|
+
{ type: "text", content: "This is some " },
|
|
129
|
+
{ type: "tag", raw: "<b>", voidElement: false },
|
|
130
|
+
{ type: "text", content: "bold text and some " },
|
|
131
|
+
{ type: "tag", raw: "<i>", voidElement: false },
|
|
132
|
+
{ type: "text", content: "nested italic" },
|
|
133
|
+
{ type: "tag", raw: "</i>", voidElement: false },
|
|
134
|
+
{ type: "text", content: " text" },
|
|
135
|
+
{ type: "tag", raw: "</b>", voidElement: false },
|
|
136
|
+
{ type: "text", content: "!" },
|
|
137
|
+
]);
|
|
138
|
+
});
|
|
139
|
+
it("should parse translations with self closing tags", () => {
|
|
140
|
+
expect((0, i18NextParser_1.parse)("This is some <b>bold text and some </b> and some random self closing tag <img /> as well.")).toEqual([
|
|
141
|
+
{ type: "text", content: "This is some " },
|
|
142
|
+
{ type: "tag", raw: "<b>", voidElement: false },
|
|
143
|
+
{ type: "text", content: "bold text and some " },
|
|
144
|
+
{ type: "tag", raw: "</b>", voidElement: false },
|
|
145
|
+
{ type: "text", content: " and some random self closing tag " },
|
|
146
|
+
{ type: "tag", raw: "<img />", voidElement: true },
|
|
147
|
+
{ type: "text", content: " as well." },
|
|
148
|
+
]);
|
|
149
|
+
});
|
|
150
|
+
});
|
package/package.json
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lingual/i18n-check",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"license": "MIT",
|
|
5
|
-
"main": "
|
|
6
|
-
"module": "
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.js",
|
|
7
7
|
"bin": {
|
|
8
|
-
"i18n-check": "
|
|
8
|
+
"i18n-check": "dist/bin/index.js"
|
|
9
|
+
},
|
|
10
|
+
"types": "dist/index.d.ts",
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsc",
|
|
13
|
+
"test": "jest"
|
|
9
14
|
},
|
|
10
|
-
"types": "./dist/index.d.ts",
|
|
11
15
|
"files": [
|
|
12
16
|
"dist/"
|
|
13
17
|
],
|
|
@@ -23,9 +27,5 @@
|
|
|
23
27
|
"@types/node": "^20.12.12",
|
|
24
28
|
"jest": "^29.7.0",
|
|
25
29
|
"ts-jest": "^29.1.2"
|
|
26
|
-
},
|
|
27
|
-
"scripts": {
|
|
28
|
-
"build": "tsc",
|
|
29
|
-
"test": "jest"
|
|
30
30
|
}
|
|
31
|
-
}
|
|
31
|
+
}
|