@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 +24 -0
- package/bin/json-translate-check.mjs +183 -0
- package/package.json +20 -0
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
|
+
}
|