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.
@@ -1,139 +1,195 @@
1
- #!/usr/bin/env node
2
- /**
3
- * validate-all-translations.js (Upgraded)
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. No untranslated English values in non-English locales
11
- *
12
- * Usage:
13
- * node scripts/validate-all-translations.js \
14
- * --i18n-dir=./ui-locales \
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 = (argv.languages || 'en,de,es,fr,ru,ja,zh').split(',').map(s => s.trim());
31
- const MARKER = argv.marker || '⚠️ TRANSLATION NEEDED ⚠️';
32
-
33
- // ------------ helpers ------------
34
- function readJSON(p) {
35
- try { return JSON.parse(SecurityUtils.safeReadFileSync(p, path.dirname(p), 'utf8')); }
36
- catch { return {}; }
37
- }
38
-
39
- function flatten(obj, prefix = '') {
40
- const out = {};
41
- if (obj && typeof obj === 'object' && !Array.isArray(obj)) {
42
- for (const [k, v] of Object.entries(obj)) {
43
- const full = prefix ? `${prefix}.${k}` : k;
44
- Object.assign(out, flatten(v, full));
45
- }
46
- return out;
47
- }
48
- out[prefix] = obj;
49
- return out;
50
- }
51
-
52
- function listLocaleFile(lang) {
53
- const file = path.join(I18N_DIR, `${lang}.json`);
54
- if (SecurityUtils.safeExistsSync(file)) return file;
55
- throw new Error(`Locale file not found: ${file}`);
56
- }
57
-
58
- // ------------ validation ------------
59
- function validate() {
60
- console.log(`🔍 Validating translations in: ${I18N_DIR}`);
61
- console.log(`🌐 Languages: ${LANGS.join(', ')}`);
62
- console.log('');
63
-
64
- // Load EN baseline
65
- const enFlat = flatten(readJSON(listLocaleFile('en')));
66
- const report = {};
67
-
68
- LANGS.forEach(lang => {
69
- const langFile = listLocaleFile(lang);
70
- const flat = flatten(readJSON(langFile));
71
-
72
- const missing = [];
73
- const extra = [];
74
- const markers = [];
75
- const countryCodeLeftovers = [];
76
- const englishLeftovers = [];
77
-
78
- // Compare keys
79
- for (const k of Object.keys(enFlat)) {
80
- if (!(k in flat)) {
81
- missing.push(k);
82
- } else {
83
- const val = flat[k];
84
- if (typeof val === 'string') {
85
- // Placeholder marker check
86
- if (val.includes(MARKER)) {
87
- markers.push(k);
88
- }
89
- // Country code leftover check
90
- if (lang !== 'en' && /^\[[A-Z]{2}\]/.test(val.trim())) {
91
- countryCodeLeftovers.push(k);
92
- }
93
- // English leftover check
94
- if (lang !== 'en' && val.trim() === enFlat[k]?.trim()) {
95
- englishLeftovers.push(k);
96
- }
97
- }
98
- }
99
- }
100
-
101
- // Extra keys not in EN
102
- for (const k of Object.keys(flat)) {
103
- if (!(k in enFlat)) {
104
- extra.push(k);
105
- }
106
- }
107
-
108
- report[lang] = {
109
- missing,
110
- extra,
111
- markers,
112
- countryCodeLeftovers,
113
- englishLeftovers
114
- };
115
-
116
- console.log(`📄 ${lang.toUpperCase()}:`);
117
- console.log(` Missing: ${missing.length}`);
118
- console.log(` Extra: ${extra.length}`);
119
- console.log(` Markers: ${markers.length}`);
120
- if (lang !== 'en') {
121
- console.log(` Country code leftovers: ${countryCodeLeftovers.length}`);
122
- console.log(` English leftovers: ${englishLeftovers.length}`);
123
- }
124
- console.log('');
125
- });
126
-
127
- const reportFile = path.join(I18N_DIR, 'validation-purity-report.json');
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(`✅ Validation report saved: ${reportFile}`);
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
+ }