@lingual/i18n-check 0.1.0 → 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.
Files changed (64) hide show
  1. package/dist/bin/index.d.ts +2 -0
  2. package/dist/bin/index.js +170 -0
  3. package/dist/errorReporters.d.ts +5 -0
  4. package/dist/errorReporters.js +16 -0
  5. package/dist/index.d.ts +12 -0
  6. package/dist/index.js +41 -0
  7. package/{src/types.ts → dist/types.d.ts} +3 -5
  8. package/dist/types.js +2 -0
  9. package/dist/utils/findInvalidTranslations.d.ts +5 -0
  10. package/dist/utils/findInvalidTranslations.js +82 -0
  11. package/dist/utils/findInvalidTranslations.test.d.ts +1 -0
  12. package/dist/utils/findInvalidTranslations.test.js +42 -0
  13. package/dist/utils/findInvalidi18nTranslations.d.ts +11 -0
  14. package/dist/utils/findInvalidi18nTranslations.js +108 -0
  15. package/dist/utils/findInvalidi18nTranslations.test.d.ts +1 -0
  16. package/dist/utils/findInvalidi18nTranslations.test.js +120 -0
  17. package/dist/utils/findMissingKeys.d.ts +3 -0
  18. package/dist/utils/findMissingKeys.js +25 -0
  19. package/dist/utils/findMissingKeys.test.d.ts +1 -0
  20. package/dist/utils/findMissingKeys.test.js +41 -0
  21. package/dist/utils/flattenTranslations.d.ts +3 -0
  22. package/dist/utils/flattenTranslations.js +33 -0
  23. package/dist/utils/flattenTranslations.test.d.ts +1 -0
  24. package/dist/utils/flattenTranslations.test.js +28 -0
  25. package/dist/utils/i18NextParser.d.ts +37 -0
  26. package/dist/utils/i18NextParser.js +104 -0
  27. package/dist/utils/i18NextParser.test.d.ts +1 -0
  28. package/dist/utils/i18NextParser.test.js +150 -0
  29. package/package.json +8 -5
  30. package/.github/workflows/tests.yaml +0 -31
  31. package/jest.config.js +0 -6
  32. package/src/bin/index.ts +0 -244
  33. package/src/errorReporters.ts +0 -18
  34. package/src/index.ts +0 -68
  35. package/src/utils/findInvalidTranslations.test.ts +0 -78
  36. package/src/utils/findInvalidTranslations.ts +0 -120
  37. package/src/utils/findInvalidi18nTranslations.test.ts +0 -213
  38. package/src/utils/findInvalidi18nTranslations.ts +0 -137
  39. package/src/utils/findMissingKeys.test.ts +0 -60
  40. package/src/utils/findMissingKeys.ts +0 -27
  41. package/src/utils/flattenTranslations.test.ts +0 -32
  42. package/src/utils/flattenTranslations.ts +0 -42
  43. package/src/utils/i18NextParser.test.ts +0 -169
  44. package/src/utils/i18NextParser.ts +0 -149
  45. package/translations/de-de.json +0 -6
  46. package/translations/en-us.json +0 -6
  47. package/translations/flattenExamples/de-de.json +0 -6
  48. package/translations/flattenExamples/en-us.json +0 -18
  49. package/translations/folderExample/de-DE/index.json +0 -7
  50. package/translations/folderExample/en-US/index.json +0 -8
  51. package/translations/i18NextMessageExamples/de-de.json +0 -73
  52. package/translations/i18NextMessageExamples/en-us.json +0 -90
  53. package/translations/largeFileExamples/de-de.json +0 -5272
  54. package/translations/largeFileExamples/en-us.json +0 -5278
  55. package/translations/largeFileExamples/fr-fr.json +0 -871
  56. package/translations/messageExamples/de-de.json +0 -24
  57. package/translations/messageExamples/en-us.json +0 -30
  58. package/translations/multipleFilesFolderExample/de-DE/one.json +0 -7
  59. package/translations/multipleFilesFolderExample/de-DE/three.json +0 -8
  60. package/translations/multipleFilesFolderExample/de-DE/two.json +0 -5
  61. package/translations/multipleFilesFolderExample/en-US/one.json +0 -8
  62. package/translations/multipleFilesFolderExample/en-US/three.json +0 -8
  63. package/translations/multipleFilesFolderExample/en-US/two.json +0 -6
  64. package/tsconfig.json +0 -113
@@ -0,0 +1,2 @@
1
+ #! /usr/bin/env node
2
+ export {};
@@ -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;
@@ -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;
@@ -1,9 +1,7 @@
1
1
  export type Translation = Record<string, unknown>;
2
-
3
2
  export type CheckResult = Record<string, string[]>;
4
-
5
3
  export type TranslationFile = {
6
- reference: string | null;
7
- name: string;
8
- content: Translation;
4
+ reference: string | null;
5
+ name: string;
6
+ content: Translation;
9
7
  };
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -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,3 @@
1
+ import { Translation } from "../types";
2
+ export declare const findMissingKeys: (source: Translation, targets: Record<string, Translation>) => {};
3
+ export declare const compareTranslationFiles: (a: Translation, b: Translation) => string[];