@lingual/i18n-check 0.1.0 → 0.1.1

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 (37) hide show
  1. package/package.json +9 -6
  2. package/.github/workflows/tests.yaml +0 -31
  3. package/jest.config.js +0 -6
  4. package/src/bin/index.ts +0 -244
  5. package/src/errorReporters.ts +0 -18
  6. package/src/index.ts +0 -68
  7. package/src/types.ts +0 -9
  8. package/src/utils/findInvalidTranslations.test.ts +0 -78
  9. package/src/utils/findInvalidTranslations.ts +0 -120
  10. package/src/utils/findInvalidi18nTranslations.test.ts +0 -213
  11. package/src/utils/findInvalidi18nTranslations.ts +0 -137
  12. package/src/utils/findMissingKeys.test.ts +0 -60
  13. package/src/utils/findMissingKeys.ts +0 -27
  14. package/src/utils/flattenTranslations.test.ts +0 -32
  15. package/src/utils/flattenTranslations.ts +0 -42
  16. package/src/utils/i18NextParser.test.ts +0 -169
  17. package/src/utils/i18NextParser.ts +0 -149
  18. package/translations/de-de.json +0 -6
  19. package/translations/en-us.json +0 -6
  20. package/translations/flattenExamples/de-de.json +0 -6
  21. package/translations/flattenExamples/en-us.json +0 -18
  22. package/translations/folderExample/de-DE/index.json +0 -7
  23. package/translations/folderExample/en-US/index.json +0 -8
  24. package/translations/i18NextMessageExamples/de-de.json +0 -73
  25. package/translations/i18NextMessageExamples/en-us.json +0 -90
  26. package/translations/largeFileExamples/de-de.json +0 -5272
  27. package/translations/largeFileExamples/en-us.json +0 -5278
  28. package/translations/largeFileExamples/fr-fr.json +0 -871
  29. package/translations/messageExamples/de-de.json +0 -24
  30. package/translations/messageExamples/en-us.json +0 -30
  31. package/translations/multipleFilesFolderExample/de-DE/one.json +0 -7
  32. package/translations/multipleFilesFolderExample/de-DE/three.json +0 -8
  33. package/translations/multipleFilesFolderExample/de-DE/two.json +0 -5
  34. package/translations/multipleFilesFolderExample/en-US/one.json +0 -8
  35. package/translations/multipleFilesFolderExample/en-US/three.json +0 -8
  36. package/translations/multipleFilesFolderExample/en-US/two.json +0 -6
  37. package/tsconfig.json +0 -113
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lingual/i18n-check",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "license": "MIT",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -8,10 +8,9 @@
8
8
  "i18n-check": "./dist/bin/index.js"
9
9
  },
10
10
  "types": "./dist/index.d.ts",
11
- "scripts": {
12
- "build": "tsc",
13
- "test": "jest"
14
- },
11
+ "files": [
12
+ "dist/"
13
+ ],
15
14
  "dependencies": {
16
15
  "@formatjs/icu-messageformat-parser": "^2.7.6",
17
16
  "chalk": "^4.1.2",
@@ -24,5 +23,9 @@
24
23
  "@types/node": "^20.12.12",
25
24
  "jest": "^29.7.0",
26
25
  "ts-jest": "^29.1.2"
26
+ },
27
+ "scripts": {
28
+ "build": "tsc",
29
+ "test": "jest"
27
30
  }
28
- }
31
+ }
@@ -1,31 +0,0 @@
1
- name: Test
2
- on:
3
- pull_request:
4
- branches:
5
- - main
6
- push:
7
- branches:
8
- - main
9
-
10
- jobs:
11
- test:
12
- runs-on: ubuntu-latest
13
-
14
- steps:
15
- - uses: actions/checkout@master
16
-
17
- - name: yarn install & test
18
- run: |
19
- yarn install
20
- yarn test
21
-
22
- test-build:
23
- runs-on: ubuntu-latest
24
-
25
- steps:
26
- - uses: actions/checkout@master
27
-
28
- - name: yarn install & build
29
- run: |
30
- yarn install
31
- yarn build
package/jest.config.js DELETED
@@ -1,6 +0,0 @@
1
- /** @type {import('ts-jest').JestConfigWithTsJest} */
2
- module.exports = {
3
- preset: "ts-jest",
4
- testEnvironment: "node",
5
- testPathIgnorePatterns: ["dist"],
6
- };
package/src/bin/index.ts DELETED
@@ -1,244 +0,0 @@
1
- #! /usr/bin/env node
2
-
3
- import { glob } from "glob";
4
- import chalk from "chalk";
5
- import fs from "fs";
6
- import { exit } from "process";
7
- import { program } from "commander";
8
- import { CheckResult, TranslationFile } from "../types";
9
- import { checkTranslations } from "..";
10
- import { Context, standardReporter, summaryReporter } from "../errorReporters";
11
- import { flattenTranslations } from "../utils/flattenTranslations";
12
-
13
- program
14
- .version("0.1.0")
15
- .option(
16
- "-t, --target [directory]",
17
- "name of the directory containing the JSON files to validate"
18
- )
19
- .option("-s, --source [source file(s)]", "path to the reference file(s)")
20
- .option(
21
- "-f, --format [format type]",
22
- "define the specific format, i.e. i18next"
23
- )
24
- .option(
25
- "-c, --check [checks]",
26
- "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"
27
- )
28
- .option(
29
- "-r, --reporter [error reporting style]",
30
- "define the reporting style: standard or summary"
31
- )
32
- .option(
33
- "-e, --exclude [file(s), folder(s)]",
34
- "define the file(s) and/or folders(s) that should be excluded from the check"
35
- )
36
- .parse();
37
-
38
- const getCheckOptions = (): Context[] => {
39
- const checkOption = program.getOptionValue("check");
40
-
41
- if (!checkOption) {
42
- return ["invalidKeys", "missingKeys"];
43
- }
44
-
45
- const checks = checkOption
46
- .split(",")
47
- .filter((check: string) =>
48
- ["invalidKeys", "missingKeys"].includes(check.trim())
49
- );
50
-
51
- return checks.length > 0 ? checks : ["invalidKeys", "missingKeys"];
52
- };
53
-
54
- const getSourcePath = (sourcePaths: string[], fileName: string) => {
55
- return sourcePaths.find((basePathName: string) => {
56
- return fileName.includes(basePathName);
57
- });
58
- };
59
-
60
- const getTargetPath = (
61
- paths: string[],
62
- sourcePaths: string[],
63
- fileName: string
64
- ) => {
65
- const basePath = paths.find((path: string) => {
66
- return fileName.includes(path);
67
- });
68
- if (!basePath) {
69
- return null;
70
- }
71
-
72
- return sourcePaths.find((path) => path.includes(basePath));
73
- };
74
-
75
- const toArray = (input: string): string[] => {
76
- return input
77
- .trim()
78
- .split(",")
79
- .filter((a: string) => a);
80
- };
81
-
82
- const main = async () => {
83
- const start = performance.now();
84
- const srcPath = program.getOptionValue("source");
85
- const targetPath = program.getOptionValue("target");
86
- const format = program.getOptionValue("format");
87
- const exclude = program.getOptionValue("exclude");
88
-
89
- if (!srcPath) {
90
- console.log(
91
- chalk.red(
92
- "Source file(s) not found. Please provide valid source file(s), i.e. -s translations/en-us.json"
93
- )
94
- );
95
- exit(1);
96
- }
97
-
98
- if (!targetPath) {
99
- console.log(
100
- chalk.red(
101
- "Target file(s) not found. Please provide valid target file(s), i.e. -t translations/"
102
- )
103
- );
104
- exit(1);
105
- }
106
-
107
- const excludedPaths = exclude ? toArray(exclude) : [];
108
- const targetPathFolders: string[] = toArray(targetPath);
109
- const srcPaths: string[] = toArray(srcPath);
110
-
111
- const isMultiFolders = targetPathFolders.length > 1;
112
-
113
- let srcFiles: TranslationFile[] = [];
114
- let targetFiles: TranslationFile[] = [];
115
-
116
- const pattern = isMultiFolders
117
- ? `{${targetPath.trim()}}/**/*.json`
118
- : `${targetPath.trim()}/**/*.json`;
119
-
120
- const files = await glob(pattern, {
121
- ignore: ["node_modules/**"].concat(excludedPaths),
122
- });
123
-
124
- console.log(chalk.blue("i18n translations checker"));
125
- console.log(chalk.blackBright(`Source file(s): ${srcPath}`));
126
-
127
- if (format) {
128
- console.log(chalk.blackBright(`Selected format is: ${format}`));
129
- }
130
-
131
- const options = {
132
- checks: getCheckOptions(),
133
- format: format ?? undefined,
134
- };
135
-
136
- files.forEach((file) => {
137
- const content = JSON.parse(fs.readFileSync(file, "utf-8"));
138
- const sourcePath = getSourcePath(srcPaths, file);
139
- if (sourcePath) {
140
- srcFiles.push({
141
- reference: null,
142
- name: file,
143
- content: flattenTranslations(content),
144
- });
145
- } else {
146
- const targetPath = getTargetPath(targetPathFolders, srcPaths, file);
147
- const reference = targetPath?.includes(".json")
148
- ? targetPath
149
- : `${targetPath}${file.split("/").pop()}`;
150
-
151
- targetFiles.push({
152
- reference,
153
- name: file,
154
- content: flattenTranslations(content),
155
- });
156
- }
157
- });
158
-
159
- if (srcFiles.length === 0) {
160
- console.log(
161
- chalk.red(
162
- "Source file(s) not found. Please provide valid source file(s), i.e. -s translations/en-us.json"
163
- )
164
- );
165
- exit(1);
166
- }
167
-
168
- if (targetFiles.length === 0) {
169
- console.log(
170
- chalk.red(
171
- "Target file(s) not found. Please provide valid target file(s), i.e. -t translations/"
172
- )
173
- );
174
- exit(1);
175
- }
176
-
177
- try {
178
- const result = checkTranslations(srcFiles, targetFiles, options);
179
-
180
- print(result);
181
-
182
- const end = performance.now();
183
-
184
- console.log(
185
- chalk.green(
186
- `\nDone in ${Math.round(((end - start) * 100) / 1000) / 100}s.`
187
- )
188
- );
189
- if (
190
- (result.missingKeys && Object.keys(result.missingKeys).length > 0) ||
191
- (result.invalidKeys && Object.keys(result.invalidKeys).length > 0)
192
- ) {
193
- exit(1);
194
- } else {
195
- exit(0);
196
- }
197
- } catch (e) {
198
- console.log(
199
- chalk.red(
200
- "\nError: Can't validate translations. Check if the format is supported or specify the translation format i.e. -f i18next"
201
- )
202
- );
203
- exit(1);
204
- }
205
- };
206
-
207
- const print = ({
208
- missingKeys,
209
- invalidKeys,
210
- }: {
211
- missingKeys: CheckResult | undefined;
212
- invalidKeys: CheckResult | undefined;
213
- }) => {
214
- const reporter = program.getOptionValue("reporter");
215
-
216
- const errorReporter =
217
- reporter === "summary" ? summaryReporter : standardReporter;
218
-
219
- if (missingKeys && Object.keys(missingKeys).length > 0) {
220
- console.log(chalk.bgRed(chalk.white("\nFound missing keys!")));
221
- for (const [lang, missingMessageKeys] of Object.entries<string[]>(
222
- missingKeys
223
- )) {
224
- console.log(chalk.red(`\nIn ${lang}:\n`));
225
- console.log(chalk.red(errorReporter(missingMessageKeys, "missingKeys")));
226
- }
227
- } else if (missingKeys) {
228
- console.log(chalk.green("\nNo missing keys found!"));
229
- }
230
-
231
- if (invalidKeys && Object.keys(invalidKeys).length > 0) {
232
- console.log(chalk.bgRed(chalk.white("\nFound invalid keys!")));
233
- for (const [lang, invalidMessageKeys] of Object.entries<string[]>(
234
- invalidKeys
235
- )) {
236
- console.log(chalk.red(`\nIn ${lang}:\n`));
237
- console.log(chalk.red(errorReporter(invalidMessageKeys, "invalidKeys")));
238
- }
239
- } else if (invalidKeys) {
240
- console.log(chalk.green("\nNo invalid translations found!"));
241
- }
242
- };
243
-
244
- main();
@@ -1,18 +0,0 @@
1
- export type Context = "missingKeys" | "invalidKeys";
2
- export type ErrorReporter = (result: string[], context: Context) => void;
3
-
4
- export const contextMapping: Record<Context, string> = {
5
- invalidKeys: "invalid",
6
- missingKeys: "missing",
7
- };
8
-
9
- export const standardReporter: ErrorReporter = (result) => {
10
- return result.map((key) => `◯ ${key}`).join("\n");
11
- };
12
-
13
- export const summaryReporter: ErrorReporter = (result, context) => {
14
- const count = result.length;
15
- return `Found ${count} ${contextMapping[context]} ${
16
- count === 1 ? "key" : "keys"
17
- }.`;
18
- };
package/src/index.ts DELETED
@@ -1,68 +0,0 @@
1
- import { findMissingKeys } from "./utils/findMissingKeys";
2
- import { CheckResult, Translation, TranslationFile } from "./types";
3
- import { findInvalidTranslations } from "./utils/findInvalidTranslations";
4
- import { findInvalid18nTranslations } from "./utils/findInvalidi18nTranslations";
5
- import { Context } from "./errorReporters";
6
-
7
- export type Options = {
8
- format?: "icu" | "i18next";
9
- checks?: Context[];
10
- };
11
-
12
- export const checkInvalidTranslations = (
13
- source: Translation,
14
- targets: Record<string, Translation>,
15
- options: Options = { format: "icu" }
16
- ): CheckResult => {
17
- return options.format === "i18next"
18
- ? findInvalid18nTranslations(source, targets)
19
- : findInvalidTranslations(source, targets);
20
- };
21
-
22
- export const checkMissingTranslations = (
23
- source: Translation,
24
- targets: Record<string, Translation>
25
- ): CheckResult => {
26
- return findMissingKeys(source, targets);
27
- };
28
-
29
- export const checkTranslations = (
30
- source: TranslationFile[],
31
- targets: TranslationFile[],
32
- options: Options = { format: "icu", checks: ["invalidKeys", "missingKeys"] }
33
- ): {
34
- missingKeys: CheckResult | undefined;
35
- invalidKeys: CheckResult | undefined;
36
- } => {
37
- const { checks = ["invalidKeys", "missingKeys"] } = options;
38
-
39
- let missingKeys = {};
40
- let invalidKeys = {};
41
-
42
- const hasMissingKeys = checks.includes("missingKeys");
43
- const hasInvalidKeys = checks.includes("invalidKeys");
44
-
45
- source.forEach(({ name, content }) => {
46
- const files = targets
47
- .filter(({ reference }) => reference === name)
48
- .reduce((obj, { name: key, content }) => {
49
- return Object.assign(obj, { [key]: content });
50
- }, {});
51
-
52
- if (hasMissingKeys) {
53
- Object.assign(missingKeys, checkMissingTranslations(content, files));
54
- }
55
-
56
- if (hasInvalidKeys) {
57
- Object.assign(
58
- invalidKeys,
59
- checkInvalidTranslations(content, files, options)
60
- );
61
- }
62
- });
63
-
64
- return {
65
- missingKeys: hasMissingKeys ? missingKeys : undefined,
66
- invalidKeys: hasInvalidKeys ? invalidKeys : undefined,
67
- };
68
- };
package/src/types.ts DELETED
@@ -1,9 +0,0 @@
1
- export type Translation = Record<string, unknown>;
2
-
3
- export type CheckResult = Record<string, string[]>;
4
-
5
- export type TranslationFile = {
6
- reference: string | null;
7
- name: string;
8
- content: Translation;
9
- };
@@ -1,78 +0,0 @@
1
- import {
2
- compareTranslationFiles,
3
- findInvalidTranslations,
4
- } from "./findInvalidTranslations";
5
- import { flattenTranslations } from "./flattenTranslations";
6
-
7
- const sourceFile = require("../../translations/messageExamples/en-us.json");
8
- const secondaryFile = require("../../translations/messageExamples/de-de.json");
9
-
10
- describe("findInvalidTranslations:compareTranslationFiles", () => {
11
- it("should return empty array if files are identical", () => {
12
- expect(
13
- compareTranslationFiles(
14
- flattenTranslations(sourceFile),
15
- flattenTranslations(sourceFile)
16
- )
17
- ).toEqual([]);
18
- });
19
-
20
- it("should return the invalid keys in the target file", () => {
21
- expect(
22
- compareTranslationFiles(
23
- flattenTranslations({
24
- ...sourceFile,
25
- "ten.eleven.twelve": "ten eleven twelve",
26
- }),
27
- flattenTranslations(secondaryFile)
28
- )
29
- ).toEqual(["multipleVariables"]);
30
- });
31
-
32
- it("should return empty array if placeholders are identical but in different positions", () => {
33
- expect(
34
- compareTranslationFiles(
35
- {
36
- basic: "added {this} and {that} should work.",
37
- },
38
- {
39
- basic: "It is {this} with different position {that}",
40
- }
41
- )
42
- ).toEqual([]);
43
- });
44
- });
45
-
46
- describe("findInvalidTranslations", () => {
47
- it("should return an empty object if all files have no invalid keys", () => {
48
- expect(findInvalidTranslations(sourceFile, { de: sourceFile })).toEqual({});
49
- });
50
-
51
- it("should return an object containing the keys for the missing language", () => {
52
- expect(
53
- findInvalidTranslations(
54
- { ...sourceFile, "ten.eleven.twelve": "ten eleven twelve" },
55
- { de: secondaryFile }
56
- )
57
- ).toEqual({ de: ["multipleVariables"] });
58
- });
59
-
60
- it("should return an object containing the keys for every language with missing key", () => {
61
- expect(
62
- findInvalidTranslations(
63
- { ...sourceFile, "ten.eleven.twelve": "ten eleven twelve" },
64
- {
65
- de: secondaryFile,
66
- fr: {
67
- "four.five.six": "four five six",
68
- "seven.eight.nine": "seven eight nine",
69
- "message.text-format": "yo,<p><b>John</b></p>!",
70
- },
71
- }
72
- )
73
- ).toEqual({
74
- de: ["multipleVariables"],
75
- fr: ["message.text-format"],
76
- });
77
- });
78
- });
@@ -1,120 +0,0 @@
1
- import {
2
- MessageFormatElement,
3
- isLiteralElement,
4
- isPluralElement,
5
- isPoundElement,
6
- isSelectElement,
7
- isTagElement,
8
- parse,
9
- } from "@formatjs/icu-messageformat-parser";
10
- import { Translation } from "../types";
11
-
12
- export const findInvalidTranslations = (
13
- source: Translation,
14
- files: Record<string, Translation>
15
- ) => {
16
- let differences = {};
17
- if (Object.keys(files).length === 0) {
18
- return differences;
19
- }
20
-
21
- for (const [lang, file] of Object.entries(files)) {
22
- const result = compareTranslationFiles(source, file);
23
-
24
- if (result.length > 0) {
25
- differences = Object.assign(differences, { [lang]: result });
26
- }
27
- }
28
-
29
- return differences;
30
- };
31
-
32
- const sortParsedKeys = (a: MessageFormatElement, b: MessageFormatElement) => {
33
- if (a.type === b.type) {
34
- return !isPoundElement(a) && !isPoundElement(b)
35
- ? a.value < b.value
36
- ? -1
37
- : 1
38
- : -1;
39
- }
40
- return a.type - b.type;
41
- };
42
-
43
- export const compareTranslationFiles = (a: Translation, b: Translation) => {
44
- let diffs = [];
45
- for (const key in a) {
46
- if (
47
- b[key] !== undefined &&
48
- hasDiff(parse(String(a[key])), parse(String(b[key])))
49
- ) {
50
- diffs.push(key);
51
- }
52
- }
53
-
54
- return diffs;
55
- };
56
-
57
- export const hasDiff = (
58
- a: MessageFormatElement[],
59
- b: MessageFormatElement[]
60
- ) => {
61
- const compA = a
62
- .filter((element) => !isLiteralElement(element))
63
- .sort(sortParsedKeys);
64
-
65
- const compB = b
66
- .filter((element) => !isLiteralElement(element))
67
- .sort(sortParsedKeys);
68
- if (compA.length !== compB.length) {
69
- return true;
70
- }
71
-
72
- const hasErrors = compA.some((formatElementA, index) => {
73
- const formatElementB = compB[index];
74
-
75
- if (
76
- formatElementA.type !== formatElementB.type ||
77
- formatElementA.location !== formatElementB.location
78
- ) {
79
- return true;
80
- }
81
-
82
- if (
83
- (isLiteralElement(formatElementA) && isLiteralElement(formatElementB)) ||
84
- (isPoundElement(formatElementA) && isPoundElement(formatElementB))
85
- ) {
86
- return false;
87
- }
88
-
89
- // @ts-ignore
90
- if (formatElementA.value !== formatElementB.value) {
91
- return true;
92
- }
93
-
94
- if (isTagElement(formatElementA) && isTagElement(formatElementB)) {
95
- return hasDiff(formatElementA.children, formatElementB.children);
96
- }
97
-
98
- if (
99
- (isSelectElement(formatElementA) && isSelectElement(formatElementB)) ||
100
- (isPluralElement(formatElementA) && isPluralElement(formatElementB))
101
- ) {
102
- const optionsA = Object.keys(formatElementA.options).sort();
103
- const optionsB = Object.keys(formatElementA.options).sort();
104
-
105
- if (optionsA.join("-") !== optionsB.join("-")) {
106
- return true;
107
- }
108
- return optionsA.some((key) => {
109
- return hasDiff(
110
- formatElementA.options[key].value,
111
- formatElementB.options[key].value
112
- );
113
- });
114
- }
115
-
116
- return false;
117
- });
118
-
119
- return hasErrors;
120
- };