i18ntk 2.0.3 → 2.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 +36 -32
- package/main/i18ntk-analyze.js +59 -54
- package/main/i18ntk-ui.js +96 -49
- package/main/manage/commands/AnalyzeCommand.js +59 -54
- package/main/manage/index.js +140 -71
- package/package.json +290 -290
- package/scripts/fix-all-i18n.js +41 -20
- package/scripts/fix-and-purify-i18n.js +43 -23
- package/scripts/prepublish-dev.js +221 -0
- package/scripts/prepublish.js +155 -141
- package/scripts/validate-all-translations.js +190 -134
- package/ui-locales/de.json +149 -155
- package/ui-locales/en.json +1 -7
- package/ui-locales/es.json +159 -173
- package/ui-locales/fr.json +143 -150
- package/ui-locales/ja.json +181 -233
- package/ui-locales/ru.json +133 -185
- package/ui-locales/zh.json +168 -175
- package/utils/cli-helper.js +26 -98
- package/utils/extractors/regex.js +39 -12
- package/utils/i18n-helper.js +181 -128
- package/utils/security-check-improved.js +16 -13
- package/utils/security-fixed.js +6 -4
- package/utils/security.js +6 -4
- package/main/manage/services/ConfigurationService-fixed.js +0 -449
|
@@ -1,139 +1,195 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* validate-all-translations.js
|
|
4
|
-
*
|
|
5
|
-
* Validates i18n translation files:
|
|
6
|
-
* 1. All locales have same keys as English
|
|
7
|
-
* 2. No missing or extra keys
|
|
8
|
-
* 3. No placeholder markers
|
|
9
|
-
* 4. No leftover country code prefixes in non-English locales
|
|
10
|
-
* 5.
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
* --languages=en,de,es,fr,ru,ja,zh
|
|
16
|
-
*/
|
|
17
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* validate-all-translations.js
|
|
4
|
+
* ----------------------------
|
|
5
|
+
* Validates i18n translation files:
|
|
6
|
+
* 1. All locales have same keys as English
|
|
7
|
+
* 2. No missing or extra keys
|
|
8
|
+
* 3. No placeholder markers
|
|
9
|
+
* 4. No leftover country code prefixes in non-English locales
|
|
10
|
+
* 5. Reports English-equal values in two buckets:
|
|
11
|
+
* - raw: exact string equality with English
|
|
12
|
+
* - actionable: likely untranslated user-facing text
|
|
13
|
+
*/
|
|
14
|
+
|
|
18
15
|
const fs = require('fs');
|
|
19
16
|
const path = require('path');
|
|
20
17
|
const SecurityUtils = require('../utils/security');
|
|
21
|
-
|
|
22
|
-
const argv = Object.fromEntries(
|
|
23
|
-
process.argv.slice(2).map(a => {
|
|
24
|
-
const m = a.match(/^--([^=]+)(?:=(.*))?$/);
|
|
25
|
-
return m ? [m[1], m[2] === undefined ? true : m[2]] : [a, true];
|
|
26
|
-
})
|
|
27
|
-
);
|
|
28
|
-
|
|
29
|
-
const I18N_DIR = path.resolve(argv['i18n-dir'] || './resources/i18n/ui-locales');
|
|
30
|
-
const LANGS
|
|
31
|
-
const MARKER
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
function readJSON(p) {
|
|
35
|
-
try {
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
18
|
+
|
|
19
|
+
const argv = Object.fromEntries(
|
|
20
|
+
process.argv.slice(2).map((a) => {
|
|
21
|
+
const m = a.match(/^--([^=]+)(?:=(.*))?$/);
|
|
22
|
+
return m ? [m[1], m[2] === undefined ? true : m[2]] : [a, true];
|
|
23
|
+
})
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const I18N_DIR = path.resolve(argv['i18n-dir'] || './resources/i18n/ui-locales');
|
|
27
|
+
const LANGS = (argv.languages || 'en,de,es,fr,ru,ja,zh').split(',').map((s) => s.trim());
|
|
28
|
+
const MARKER = argv.marker || 'TRANSLATION NEEDED';
|
|
29
|
+
const SHARED_TERMS = new Set(['error', 'errors', 'no', 'yes', 'navigation', 'system', 'modular', 'long']);
|
|
30
|
+
|
|
31
|
+
function readJSON(p) {
|
|
32
|
+
try {
|
|
33
|
+
const raw = SecurityUtils.safeReadFileSync(p, path.dirname(p), 'utf8');
|
|
34
|
+
return raw ? JSON.parse(raw) : {};
|
|
35
|
+
} catch {
|
|
36
|
+
return {};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function flatten(obj, prefix = '') {
|
|
41
|
+
const out = {};
|
|
42
|
+
if (obj && typeof obj === 'object' && !Array.isArray(obj)) {
|
|
43
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
44
|
+
const full = prefix ? `${prefix}.${k}` : k;
|
|
45
|
+
Object.assign(out, flatten(v, full));
|
|
46
|
+
}
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
out[prefix] = obj;
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function listLocaleFile(lang) {
|
|
54
|
+
const file = path.join(I18N_DIR, `${lang}.json`);
|
|
55
|
+
if (SecurityUtils.safeExistsSync(file, path.dirname(file))) return file;
|
|
56
|
+
throw new Error(`Locale file not found: ${file}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isActionableEnglishLeftover(value) {
|
|
60
|
+
if (typeof value !== 'string') return false;
|
|
61
|
+
const v = value.trim();
|
|
62
|
+
if (!v) return false;
|
|
63
|
+
|
|
64
|
+
if (!/[A-Za-z]/.test(v)) return false;
|
|
65
|
+
if (/^[=\-_*#\s]+$/.test(v)) return false;
|
|
66
|
+
if (/^[•✅❌⚠️📄📝🔑\s]+$/.test(v)) return false;
|
|
67
|
+
|
|
68
|
+
if (/^[-•]?\s*\{[^}]+\}$/.test(v)) return false;
|
|
69
|
+
if (/^\{[^}]+\}[:%\s]/.test(v)) return false;
|
|
70
|
+
if (/^[a-z]+(?:_[a-z0-9]+)+$/i.test(v)) return false;
|
|
71
|
+
|
|
72
|
+
if (/^(?:node|npm|npx|pnpm|yarn|i18ntk)\b/i.test(v)) return false;
|
|
73
|
+
if (/^[A-Za-z]:\\/.test(v) || /^https?:\/\//i.test(v)) return false;
|
|
74
|
+
if (v === '\\n') return false;
|
|
75
|
+
if (/^[yn]$/i.test(v)) return false;
|
|
76
|
+
|
|
77
|
+
const compact = v.replace(/[^\w]/g, '').toLowerCase();
|
|
78
|
+
if (SHARED_TERMS.has(compact)) return false;
|
|
79
|
+
|
|
80
|
+
const scrubbed = v
|
|
81
|
+
.replace(/\{[^}]+\}/g, ' ')
|
|
82
|
+
.replace(/`[^`]*`/g, ' ')
|
|
83
|
+
.replace(/[•✅❌⚠️📄📝🔑→:()"'[\].,_/%-]/g, ' ')
|
|
84
|
+
.replace(/\s+/g, ' ')
|
|
85
|
+
.trim();
|
|
86
|
+
if (!/[A-Za-z]/.test(scrubbed)) return false;
|
|
87
|
+
|
|
88
|
+
const words = scrubbed.split(/\s+/).map((w) => w.toLowerCase()).filter(Boolean);
|
|
89
|
+
const technicalWords = new Set([
|
|
90
|
+
'react',
|
|
91
|
+
'vue',
|
|
92
|
+
'nuxt',
|
|
93
|
+
'svelte',
|
|
94
|
+
'i18n',
|
|
95
|
+
'i18next',
|
|
96
|
+
'nuxtjs',
|
|
97
|
+
'index',
|
|
98
|
+
'displayname',
|
|
99
|
+
'current',
|
|
100
|
+
'filename',
|
|
101
|
+
'language',
|
|
102
|
+
'file',
|
|
103
|
+
'path',
|
|
104
|
+
'keypath',
|
|
105
|
+
'value',
|
|
106
|
+
'stepname',
|
|
107
|
+
'status',
|
|
108
|
+
'number',
|
|
109
|
+
'recommendation',
|
|
110
|
+
'message'
|
|
111
|
+
]);
|
|
112
|
+
if (words.length > 0 && words.every((word) => technicalWords.has(word))) return false;
|
|
113
|
+
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function validate() {
|
|
118
|
+
console.log(`Validating translations in: ${I18N_DIR}`);
|
|
119
|
+
console.log(`Languages: ${LANGS.join(', ')}`);
|
|
120
|
+
console.log('');
|
|
121
|
+
|
|
122
|
+
const enFlat = flatten(readJSON(listLocaleFile('en')));
|
|
123
|
+
const report = {};
|
|
124
|
+
|
|
125
|
+
LANGS.forEach((lang) => {
|
|
126
|
+
const langFile = listLocaleFile(lang);
|
|
127
|
+
const flat = flatten(readJSON(langFile));
|
|
128
|
+
|
|
129
|
+
const missing = [];
|
|
130
|
+
const extra = [];
|
|
131
|
+
const markers = [];
|
|
132
|
+
const countryCodeLeftovers = [];
|
|
133
|
+
const englishLeftovers = [];
|
|
134
|
+
const actionableEnglishLeftovers = [];
|
|
135
|
+
|
|
136
|
+
for (const k of Object.keys(enFlat)) {
|
|
137
|
+
if (!(k in flat)) {
|
|
138
|
+
missing.push(k);
|
|
139
|
+
} else {
|
|
140
|
+
const val = flat[k];
|
|
141
|
+
if (typeof val === 'string') {
|
|
142
|
+
if (val.includes(MARKER)) {
|
|
143
|
+
markers.push(k);
|
|
144
|
+
}
|
|
145
|
+
if (lang !== 'en' && /^\[[A-Z]{2}\]/.test(val.trim())) {
|
|
146
|
+
countryCodeLeftovers.push(k);
|
|
147
|
+
}
|
|
148
|
+
if (lang !== 'en' && val.trim() === enFlat[k]?.trim()) {
|
|
149
|
+
englishLeftovers.push(k);
|
|
150
|
+
if (isActionableEnglishLeftover(val)) {
|
|
151
|
+
actionableEnglishLeftovers.push(k);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
for (const k of Object.keys(flat)) {
|
|
159
|
+
if (!(k in enFlat)) {
|
|
160
|
+
extra.push(k);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
report[lang] = {
|
|
165
|
+
missing,
|
|
166
|
+
extra,
|
|
167
|
+
markers,
|
|
168
|
+
countryCodeLeftovers,
|
|
169
|
+
englishLeftovers,
|
|
170
|
+
actionableEnglishLeftovers
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
console.log(`${lang.toUpperCase()}:`);
|
|
174
|
+
console.log(` Missing: ${missing.length}`);
|
|
175
|
+
console.log(` Extra: ${extra.length}`);
|
|
176
|
+
console.log(` Markers: ${markers.length}`);
|
|
177
|
+
if (lang !== 'en') {
|
|
178
|
+
console.log(` Country code leftovers: ${countryCodeLeftovers.length}`);
|
|
179
|
+
console.log(` English leftovers (raw): ${englishLeftovers.length}`);
|
|
180
|
+
console.log(` English leftovers (actionable): ${actionableEnglishLeftovers.length}`);
|
|
181
|
+
}
|
|
182
|
+
console.log('');
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const reportFile = path.join(I18N_DIR, 'validation-purity-report.json');
|
|
128
186
|
SecurityUtils.safeWriteFileSync(reportFile, JSON.stringify(report, null, 2), path.dirname(reportFile), 'utf8');
|
|
129
|
-
console.log(
|
|
130
|
-
console.log(` Review this file for full details of problematic keys.`);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Run
|
|
134
|
-
try {
|
|
135
|
-
validate();
|
|
136
|
-
} catch (err) {
|
|
137
|
-
console.error('❌ Validation failed:', err.message);
|
|
138
|
-
process.exit(1);
|
|
187
|
+
console.log(`Validation report saved: ${reportFile}`);
|
|
139
188
|
}
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
validate();
|
|
192
|
+
} catch (err) {
|
|
193
|
+
console.error('Validation failed:', err.message);
|
|
194
|
+
process.exit(1);
|
|
195
|
+
}
|