@json-translate/check 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,24 @@
1
+ # @json-translate/check
2
+
3
+ Small CI-friendly validator for JSON locale files.
4
+
5
+ It compares translated locale files against a source JSON file and fails when:
6
+
7
+ - target JSON cannot be parsed
8
+ - keys are missing
9
+ - unexpected keys were added
10
+ - placeholders drift between source and target strings
11
+
12
+ ```bash
13
+ npx @json-translate/check --source locales/en.json --target locales/fr.json --target locales/de.json
14
+ ```
15
+
16
+ Example GitHub Actions step:
17
+
18
+ ```yaml
19
+ - name: Check translated JSON locales
20
+ run: npx @json-translate/check --source locales/en.json --target locales/fr.json,locales/de.json
21
+ ```
22
+
23
+ For the translation step itself, use JSON Translate:
24
+ https://www.json-translate.com/?utm_source=cli&utm_medium=devtool&utm_campaign=json_translate_check
@@ -0,0 +1,183 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+
5
+ const usage = `json-translate-check
6
+
7
+ Validate translated JSON locale files against a source file.
8
+
9
+ Usage:
10
+ json-translate-check --source locales/en.json --target locales/fr.json --target locales/de.json
11
+ json-translate-check -s locales/en.json -t locales/fr.json,locales/de.json
12
+
13
+ Checks:
14
+ - target JSON parses successfully
15
+ - missing keys compared with the source file
16
+ - extra keys compared with the source file
17
+ - placeholder drift in translated string values
18
+
19
+ Translate files at https://www.json-translate.com/?utm_source=cli&utm_medium=devtool&utm_campaign=json_translate_check
20
+ `;
21
+
22
+ function parseArgs(argv) {
23
+ const args = { source: '', targets: [] };
24
+ for (let index = 0; index < argv.length; index += 1) {
25
+ const arg = argv[index];
26
+ const next = argv[index + 1];
27
+
28
+ if (arg === '--help' || arg === '-h') {
29
+ args.help = true;
30
+ continue;
31
+ }
32
+
33
+ if ((arg === '--source' || arg === '-s') && next) {
34
+ args.source = next;
35
+ index += 1;
36
+ continue;
37
+ }
38
+
39
+ if ((arg === '--target' || arg === '-t') && next) {
40
+ args.targets.push(...next.split(',').map((item) => item.trim()).filter(Boolean));
41
+ index += 1;
42
+ continue;
43
+ }
44
+
45
+ throw new Error(`Unknown or incomplete argument: ${arg}`);
46
+ }
47
+ return args;
48
+ }
49
+
50
+ function readJson(filePath) {
51
+ const resolved = path.resolve(process.cwd(), filePath);
52
+ try {
53
+ return {
54
+ resolved,
55
+ data: JSON.parse(fs.readFileSync(resolved, 'utf8')),
56
+ };
57
+ } catch (error) {
58
+ const message = error instanceof Error ? error.message : String(error);
59
+ throw new Error(`${filePath}: ${message}`);
60
+ }
61
+ }
62
+
63
+ function flatten(value, prefix = '') {
64
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
65
+ return new Map([[prefix || '$', value]]);
66
+ }
67
+
68
+ const entries = new Map();
69
+ for (const [key, child] of Object.entries(value)) {
70
+ const childPath = prefix ? `${prefix}.${key}` : key;
71
+ for (const [flatKey, flatValue] of flatten(child, childPath)) {
72
+ entries.set(flatKey, flatValue);
73
+ }
74
+ }
75
+ return entries;
76
+ }
77
+
78
+ function placeholders(value) {
79
+ if (typeof value !== 'string') {
80
+ return [];
81
+ }
82
+
83
+ const patterns = [
84
+ /\{\{\s*[\w.-]+\s*\}\}/g,
85
+ /\{[\w.-]+\}/g,
86
+ /\$\{[\w.-]+\}/g,
87
+ /%[sdif]/g,
88
+ ];
89
+
90
+ const found = new Set();
91
+ for (const pattern of patterns) {
92
+ for (const match of value.matchAll(pattern)) {
93
+ found.add(match[0].replace(/\s+/g, ''));
94
+ }
95
+ }
96
+ return [...found].sort();
97
+ }
98
+
99
+ function diffPlaceholders(sourceValue, targetValue) {
100
+ const source = placeholders(sourceValue);
101
+ const target = placeholders(targetValue);
102
+ const sourceSet = new Set(source);
103
+ const targetSet = new Set(target);
104
+ return {
105
+ missing: source.filter((item) => !targetSet.has(item)),
106
+ extra: target.filter((item) => !sourceSet.has(item)),
107
+ };
108
+ }
109
+
110
+ function compare(sourceFile, targetFile) {
111
+ const sourceFlat = flatten(sourceFile.data);
112
+ const targetFlat = flatten(targetFile.data);
113
+ const sourceKeys = [...sourceFlat.keys()].sort();
114
+ const targetKeys = [...targetFlat.keys()].sort();
115
+ const targetSet = new Set(targetKeys);
116
+ const sourceSet = new Set(sourceKeys);
117
+
118
+ const missingKeys = sourceKeys.filter((key) => !targetSet.has(key));
119
+ const extraKeys = targetKeys.filter((key) => !sourceSet.has(key));
120
+ const placeholderIssues = [];
121
+
122
+ for (const key of sourceKeys) {
123
+ if (!targetSet.has(key)) {
124
+ continue;
125
+ }
126
+ const issue = diffPlaceholders(sourceFlat.get(key), targetFlat.get(key));
127
+ if (issue.missing.length || issue.extra.length) {
128
+ placeholderIssues.push({ key, ...issue });
129
+ }
130
+ }
131
+
132
+ return { missingKeys, extraKeys, placeholderIssues };
133
+ }
134
+
135
+ function printIssues(target, result) {
136
+ const count = result.missingKeys.length + result.extraKeys.length + result.placeholderIssues.length;
137
+ if (count === 0) {
138
+ console.log(`OK ${target}`);
139
+ return 0;
140
+ }
141
+
142
+ console.log(`FAIL ${target}`);
143
+ for (const key of result.missingKeys) {
144
+ console.log(` missing key: ${key}`);
145
+ }
146
+ for (const key of result.extraKeys) {
147
+ console.log(` extra key: ${key}`);
148
+ }
149
+ for (const issue of result.placeholderIssues) {
150
+ if (issue.missing.length) {
151
+ console.log(` placeholder missing at ${issue.key}: ${issue.missing.join(', ')}`);
152
+ }
153
+ if (issue.extra.length) {
154
+ console.log(` placeholder extra at ${issue.key}: ${issue.extra.join(', ')}`);
155
+ }
156
+ }
157
+ return count;
158
+ }
159
+
160
+ try {
161
+ const args = parseArgs(process.argv.slice(2));
162
+ if (args.help) {
163
+ console.log(usage);
164
+ process.exit(0);
165
+ }
166
+
167
+ if (!args.source || args.targets.length === 0) {
168
+ console.error(usage);
169
+ process.exit(2);
170
+ }
171
+
172
+ const source = readJson(args.source);
173
+ let issues = 0;
174
+ for (const targetPath of args.targets) {
175
+ const target = readJson(targetPath);
176
+ issues += printIssues(targetPath, compare(source, target));
177
+ }
178
+
179
+ process.exit(issues === 0 ? 0 : 1);
180
+ } catch (error) {
181
+ console.error(error instanceof Error ? error.message : String(error));
182
+ process.exit(2);
183
+ }
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "@json-translate/check",
3
+ "version": "0.1.0",
4
+ "description": "Validate JSON locale files for missing keys and placeholder drift.",
5
+ "type": "module",
6
+ "bin": {
7
+ "json-translate-check": "bin/json-translate-check.mjs"
8
+ },
9
+ "scripts": {
10
+ "test": "node ./bin/json-translate-check.mjs --help"
11
+ },
12
+ "keywords": [
13
+ "json",
14
+ "i18n",
15
+ "localization",
16
+ "translation",
17
+ "locale"
18
+ ],
19
+ "license": "MIT"
20
+ }