i18ntk 2.1.0 → 2.3.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.
Files changed (73) hide show
  1. package/README.md +87 -50
  2. package/main/i18ntk-analyze.js +63 -63
  3. package/main/i18ntk-backup-class.js +37 -41
  4. package/main/i18ntk-backup.js +28 -30
  5. package/main/i18ntk-complete.js +75 -74
  6. package/main/i18ntk-doctor.js +7 -6
  7. package/main/i18ntk-fixer.js +3 -3
  8. package/main/i18ntk-init.js +49 -13
  9. package/main/i18ntk-scanner.js +2 -2
  10. package/main/i18ntk-sizing.js +36 -37
  11. package/main/i18ntk-summary.js +4 -4
  12. package/main/i18ntk-ui.js +95 -96
  13. package/main/i18ntk-usage.js +31 -19
  14. package/main/i18ntk-validate.js +78 -27
  15. package/main/manage/commands/AnalyzeCommand.js +71 -73
  16. package/main/manage/commands/CommandRouter.js +15 -12
  17. package/main/manage/commands/FixerCommand.js +94 -38
  18. package/main/manage/commands/ScannerCommand.js +2 -2
  19. package/main/manage/commands/ValidateCommand.js +87 -36
  20. package/main/manage/index.js +165 -152
  21. package/main/manage/managers/DebugMenu.js +6 -6
  22. package/main/manage/managers/InteractiveMenu.js +6 -6
  23. package/main/manage/managers/LanguageMenu.js +12 -6
  24. package/main/manage/managers/SettingsMenu.js +6 -6
  25. package/main/manage/services/AuthenticationService.js +5 -6
  26. package/main/manage/services/ConfigurationService.js +22 -34
  27. package/main/manage/services/FileManagementService.js +6 -6
  28. package/main/manage/services/InitService.js +44 -8
  29. package/main/manage/services/UsageService.js +24 -12
  30. package/package.json +21 -42
  31. package/settings/settings-cli.js +5 -5
  32. package/settings/settings-manager.js +984 -968
  33. package/ui-locales/de.json +12 -11
  34. package/ui-locales/en.json +12 -11
  35. package/ui-locales/es.json +12 -11
  36. package/ui-locales/fr.json +12 -11
  37. package/ui-locales/ja.json +12 -11
  38. package/ui-locales/ru.json +12 -11
  39. package/ui-locales/zh.json +12 -11
  40. package/utils/config-helper.js +27 -16
  41. package/utils/config-manager.js +8 -7
  42. package/utils/i18n-helper.js +161 -166
  43. package/utils/init-helper.js +3 -2
  44. package/utils/json-output.js +11 -10
  45. package/{scripts → utils}/locale-optimizer.js +61 -60
  46. package/utils/logger.js +4 -4
  47. package/utils/safe-json.js +3 -3
  48. package/utils/secure-backup.js +8 -7
  49. package/utils/setup-enforcer.js +63 -98
  50. package/main/i18ntk-go.js +0 -283
  51. package/main/i18ntk-java.js +0 -380
  52. package/main/i18ntk-js.js +0 -512
  53. package/main/i18ntk-manage.js +0 -1694
  54. package/main/i18ntk-php.js +0 -462
  55. package/main/i18ntk-py.js +0 -379
  56. package/main/i18ntk-settings.js +0 -23
  57. package/main/manage/index-fixed.js +0 -1447
  58. package/scripts/build-lite.js +0 -279
  59. package/scripts/deprecate-versions.js +0 -317
  60. package/scripts/export-translations.js +0 -84
  61. package/scripts/fix-all-i18n.js +0 -236
  62. package/scripts/fix-and-purify-i18n.js +0 -233
  63. package/scripts/fix-locale-control-chars.js +0 -110
  64. package/scripts/lint-locales.js +0 -80
  65. package/scripts/prepublish-dev.js +0 -221
  66. package/scripts/prepublish.js +0 -362
  67. package/scripts/security-check.js +0 -117
  68. package/scripts/sync-translations.js +0 -151
  69. package/scripts/sync-ui-locales.js +0 -20
  70. package/scripts/validate-all-translations.js +0 -195
  71. package/scripts/verify-deprecations.js +0 -157
  72. package/scripts/verify-translations.js +0 -63
  73. package/utils/security-fixed.js +0 -609
@@ -1,236 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Fix-all i18n script with auto-copy translations:
4
- * - Scans source for used i18n keys (console/UI/CLI)
5
- * - Ensures EN has all used keys (auto-generates readable EN values)
6
- * - Ensures all target locales have identical key structure as EN
7
- * - Fills missing values with country code + EN value (instead of generic marker)
8
- * - Optional: prune extra keys not present in EN
9
- */
10
-
11
- const fs = require('fs');
12
- const path = require('path');
13
- const SecurityUtils = require('../utils/security');
14
-
15
- const argv = Object.fromEntries(
16
- process.argv.slice(2).map(a => {
17
- const m = a.match(/^--([^=]+)(?:=(.*))?$/);
18
- return m ? [m[1], m[2] === undefined ? true : m[2]] : [a, true];
19
- })
20
- );
21
-
22
- const SOURCE_DIR = path.resolve(argv['source-dir'] || './');
23
- const I18N_DIR = path.resolve(argv['i18n-dir'] || './resources/i18n/ui-locales');
24
- const LANGS = (argv.languages || 'en,de,es,fr,ru,ja,zh').split(',').map(s => s.trim());
25
- const WRITE = !!argv.write;
26
- const PRUNE_EXTRAS = !!argv['prune-extras'];
27
-
28
- const KEY_PATTERNS = [
29
- /(?:^|[^.\w])t\(['"`]([^'"`]+)['"`]/g,
30
- /i18n\.t\(['"`]([^'"`]+)['"`]/g,
31
- /useTranslation\(\)\.t\(['"`]([^'"`]+)['"`]/g,
32
- /t\(`([^`]+)`\)/g,
33
- /i18nKey=['"`]([^'"`]+)['"`]/g,
34
- /\$t\(['"`]([^'"`]+)['"`]/g,
35
- /getTranslation\(['"`]([^'"`]+)['"`]/g
36
- ];
37
-
38
- const EXCLUDE_DIRS = new Set(['node_modules', '.git', path.basename(I18N_DIR)]);
39
-
40
- function readUTF8(p) { try { return SecurityUtils.safeReadFileSync(p, path.dirname(p), 'utf8'); } catch { return null; } }
41
- function writeJSON(p, obj) { fs.mkdirSync(path.dirname(p), { recursive: true }); SecurityUtils.safeWriteFileSync(p, JSON.stringify(obj, null, 2) + '\n', path.dirname(p), 'utf8'); }
42
- function isDir(p) { try { return fs.statSync(p).isDirectory(); } catch { return false; } }
43
- function isFile(p) { try { return fs.statSync(p).isFile(); } catch { return false; } }
44
-
45
- function listFilesRecursive(dir, exts = ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs']) {
46
- const out = [];
47
- (function walk(d) {
48
- for (const name of fs.readdirSync(d)) {
49
- const full = path.join(d, name);
50
- if (EXCLUDE_DIRS.has(name)) continue;
51
- try {
52
- const st = fs.statSync(full);
53
- if (st.isDirectory()) walk(full);
54
- else if (st.isFile() && exts.includes(path.extname(name))) out.push(full);
55
- } catch {}
56
- }
57
- })(dir);
58
- return out;
59
- }
60
-
61
- function normalizeKeyCandidate(rawKey) {
62
- if (rawKey === null || rawKey === undefined) return null;
63
- let key = String(rawKey).trim();
64
- if (!key) return null;
65
-
66
- key = key.replace(/\$\{[^}]+\}/g, '*');
67
-
68
- if (/[\r\n\t]/.test(key)) return null;
69
- if (/\s/.test(key)) return null;
70
- if (/(=>|\|\||&&|function\b|return\b|includes\()/i.test(key)) return null;
71
- if (!/^[A-Za-z0-9_.:*-]+$/.test(key)) return null;
72
- if (key.startsWith('.') || key.endsWith('.') || key.includes('..')) return null;
73
- if (key === '*' || key.includes('*')) return null;
74
-
75
- return key;
76
- }
77
-
78
- function extractKeysFromSource(file, patterns = KEY_PATTERNS) {
79
- const content = readUTF8(file);
80
- if (!content) return [];
81
- const keys = [];
82
- for (const re of patterns) {
83
- re.lastIndex = 0;
84
- let m; let guard = 0;
85
- while ((m = re.exec(content)) && guard++ < 10000) {
86
- if (m[1]) {
87
- const normalized = normalizeKeyCandidate(m[1]);
88
- if (normalized) keys.push(normalized);
89
- }
90
- }
91
- }
92
- return keys;
93
- }
94
-
95
- function flatten(obj, prefix = '') {
96
- const out = {};
97
- if (obj && typeof obj === 'object' && !Array.isArray(obj)) {
98
- for (const [k, v] of Object.entries(obj)) {
99
- const full = prefix ? `${prefix}.${k}` : k;
100
- Object.assign(out, flatten(v, full));
101
- }
102
- return out;
103
- }
104
- out[prefix] = obj;
105
- return out;
106
- }
107
-
108
- function unflatten(map) {
109
- const root = {};
110
- for (const [k, v] of Object.entries(map)) {
111
- const parts = k.split('.');
112
- let cur = root;
113
- for (let i = 0; i < parts.length - 1; i++) {
114
- cur[parts[i]] = cur[parts[i]] || {};
115
- cur = cur[parts[i]];
116
- }
117
- cur[parts[parts.length - 1]] = v;
118
- }
119
- return root;
120
- }
121
-
122
- function humanizeFromKey(keyPath) {
123
- const tail = keyPath.split('.').pop().split(':').pop();
124
- return tail.replace(/([A-Z])/g, ' $1').replace(/[_-]+/g, ' ').replace(/^./, c => c.toUpperCase()).trim();
125
- }
126
-
127
- function loadLanguage(lang) {
128
- const monolith = path.join(I18N_DIR, `${lang}.json`);
129
- if (isFile(monolith)) {
130
- const txt = readUTF8(monolith);
131
- try { return { type: 'monolith', path: monolith, data: JSON.parse(txt || '{}') }; } catch { return { type: 'monolith', path: monolith, data: {} }; }
132
- }
133
- const dir = path.join(I18N_DIR, lang);
134
- if (isDir(dir)) {
135
- const data = {};
136
- for (const f of fs.readdirSync(dir).filter(f => f.endsWith('.json'))) {
137
- const p = path.join(dir, f);
138
- const name = path.basename(f, '.json');
139
- const txt = readUTF8(p);
140
- try { data[name] = JSON.parse(txt || '{}'); } catch { data[name] = {}; }
141
- }
142
- return { type: 'dir', path: dir, data };
143
- }
144
- return { type: 'monolith', path: monolith, data: {} };
145
- }
146
-
147
- function saveLanguage(lang, langObj) {
148
- if (!WRITE) return;
149
- if (langObj.type === 'monolith') {
150
- writeJSON(langObj.path, langObj.data);
151
- } else {
152
- for (const [ns, obj] of Object.entries(langObj.data)) {
153
- const p = path.join(langObj.path, `${ns}.json`);
154
- writeJSON(p, obj);
155
- }
156
- }
157
- }
158
-
159
- (async function main() {
160
- console.log('🔎 Fix-all i18n starting...');
161
- console.log(`• sourceDir: ${SOURCE_DIR}`);
162
- console.log(`• i18nDir: ${I18N_DIR}`);
163
- console.log(`• languages: ${LANGS.join(', ')}`);
164
- console.log(`• write: ${WRITE ? 'yes' : 'no'}`);
165
- console.log(`• prune: ${PRUNE_EXTRAS ? 'yes' : 'no'}`);
166
-
167
- const srcFiles = listFilesRecursive(SOURCE_DIR);
168
- const usedKeys = new Set();
169
- for (const f of srcFiles) {
170
- extractKeysFromSource(f).forEach(k => usedKeys.add(k));
171
- }
172
- console.log(`📦 Found ${usedKeys.size} unique keys used in source.`);
173
-
174
- if (!LANGS.includes('en')) LANGS.unshift('en');
175
- const en = loadLanguage('en');
176
- const enFlat = flatten(en.data);
177
- let addedToEn = 0;
178
-
179
- for (const key of usedKeys) {
180
- if (!(key in enFlat)) {
181
- enFlat[key] = humanizeFromKey(key);
182
- addedToEn++;
183
- }
184
- }
185
-
186
- en.data = unflatten(enFlat);
187
- saveLanguage('en', en);
188
- console.log(`✅ EN normalized. Added ${addedToEn} missing keys from usage scan.`);
189
-
190
- const report = { added: {}, missingBefore: {}, extrasBefore: {} };
191
- for (const lang of LANGS.filter(l => l !== 'en')) {
192
- const langObj = loadLanguage(lang);
193
- const flat = flatten(langObj.data);
194
- const missing = [];
195
- const extras = [];
196
-
197
- for (const k of Object.keys(enFlat)) if (!(k in flat)) missing.push(k);
198
- for (const k of Object.keys(flat)) if (!(k in enFlat)) extras.push(k);
199
-
200
- for (const k of missing) {
201
- flat[k] = `[${lang.toUpperCase()}] ${enFlat[k]}`;
202
- }
203
-
204
- if (PRUNE_EXTRAS) {
205
- for (const k of extras) delete flat[k];
206
- }
207
-
208
- langObj.data = unflatten(flat);
209
- saveLanguage(lang, langObj);
210
-
211
- report.added[lang] = missing.length;
212
- report.missingBefore[lang] = missing;
213
- report.extrasBefore[lang] = extras;
214
- console.log(`🌐 ${lang.toUpperCase()}: +${missing.length} keys ${PRUNE_EXTRAS ? `, pruned ${extras.length}` : `(extras: ${extras.length})`}`);
215
- }
216
-
217
- const summaryPath = path.join(I18N_DIR, 'fix-all-report.json');
218
- const summary = {
219
- timestamp: new Date().toISOString(),
220
- sourceDir: SOURCE_DIR,
221
- i18nDir: I18N_DIR,
222
- languages: LANGS,
223
- write: !!WRITE,
224
- pruneExtras: !!PRUNE_EXTRAS,
225
- usageKeysFound: usedKeys.size,
226
- addedToEnglish: addedToEn,
227
- perLanguage: report
228
- };
229
- writeJSON(summaryPath, summary);
230
-
231
- const needsHuman = Object.values(report.added).reduce((a,b)=>a+b,0);
232
- console.log('\n📄 Report saved:', summaryPath);
233
- console.log(`\n🎯 Done. ${needsHuman ? `${needsHuman} new keys copied from EN with language code.` : 'All locales at parity with EN.'}`);
234
- if (!WRITE) console.log('\n(Preview mode) Re-run with --write to persist changes.');
235
- process.exit(needsHuman ? 2 : 0);
236
- })();
@@ -1,233 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * fix-and-purify-i18n.js
4
- *
5
- * - Scans source for all i18n keys (CLI/UI/console usage)
6
- * - Ensures EN has all keys, generating readable defaults
7
- * - For all other languages:
8
- * * Fill missing keys with `[LANGCODE] English text`
9
- * * Replace markers with `[LANGCODE] English text`
10
- * * Replace wrong country code leftovers with correct one
11
- * * Replace pure English leftovers with `[LANGCODE] English text`
12
- * - Optionally prune extras
13
- * - Outputs both a fix report and a purity report
14
- */
15
-
16
- const fs = require('fs');
17
- const path = require('path');
18
- const SecurityUtils = require('../utils/security');
19
-
20
- // ---------------- CLI ARGUMENTS ----------------
21
- const argv = Object.fromEntries(
22
- process.argv.slice(2).map(a => {
23
- const m = a.match(/^--([^=]+)(?:=(.*))?$/);
24
- return m ? [m[1], m[2] === undefined ? true : m[2]] : [a, true];
25
- })
26
- );
27
- const SOURCE_DIR = path.resolve(argv['source-dir'] || './');
28
- const I18N_DIR = path.resolve(argv['i18n-dir'] || './ui-locales');
29
- const LANGS = (argv.languages || 'en,de,es,fr,ru,ja,zh').split(',').map(s => s.trim());
30
- const WRITE = !!argv.write;
31
- const PRUNE_EXTRAS = !!argv['prune-extras'];
32
-
33
- // Regex patterns to detect keys (same as i18ntk-usage.js)
34
- const KEY_PATTERNS = [
35
- /(?:^|[^.\w])t\(['"`]([^'"`]+)['"`]/g,
36
- /i18n\.t\(['"`]([^'"`]+)['"`]/g,
37
- /useTranslation\(\)\.t\(['"`]([^'"`]+)['"`]/g,
38
- /t\(`([^`]+)`\)/g,
39
- /i18nKey=['"`]([^'"`]+)['"`]/g,
40
- /\$t\(['"`]([^'"`]+)['"`]/g,
41
- /getTranslation\(['"`]([^'"`]+)['"`]/g
42
- ];
43
-
44
- const EXCLUDE_DIRS = new Set(['node_modules', '.git', path.basename(I18N_DIR)]);
45
- const COUNTRY_CODES = { de: 'DE', es: 'ES', fr: 'FR', ru: 'RU', ja: 'JA', zh: 'ZH' };
46
-
47
- // ---------------- HELPERS ----------------
48
- function readUTF8(p) {
49
- try { return SecurityUtils.safeReadFileSync(p, path.dirname(p), 'utf8'); } catch { return null; }
50
- }
51
- function writeJSON(p, obj) {
52
- fs.mkdirSync(path.dirname(p), { recursive: true });
53
- SecurityUtils.safeWriteFileSync(p, JSON.stringify(obj, null, 2) + '\n', path.dirname(p), 'utf8');
54
- }
55
- function isDir(p) { try { return fs.statSync(p).isDirectory(); } catch { return false; } }
56
- function isFile(p) { try { return fs.statSync(p).isFile(); } catch { return false; } }
57
-
58
- function listFilesRecursive(dir, exts = ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs']) {
59
- const out = [];
60
- (function walk(d) {
61
- for (const name of fs.readdirSync(d)) {
62
- const full = path.join(d, name);
63
- if (EXCLUDE_DIRS.has(name)) continue;
64
- try {
65
- const st = fs.statSync(full);
66
- if (st.isDirectory()) walk(full);
67
- else if (st.isFile() && exts.includes(path.extname(name))) out.push(full);
68
- } catch {}
69
- }
70
- })(dir);
71
- return out;
72
- }
73
-
74
- function normalizeKeyCandidate(rawKey) {
75
- if (rawKey === null || rawKey === undefined) return null;
76
- let key = String(rawKey).trim();
77
- if (!key) return null;
78
-
79
- key = key.replace(/\$\{[^}]+\}/g, '*');
80
-
81
- if (/[\r\n\t]/.test(key)) return null;
82
- if (/\s/.test(key)) return null;
83
- if (/(=>|\|\||&&|function\b|return\b|includes\()/i.test(key)) return null;
84
- if (!/^[A-Za-z0-9_.:*-]+$/.test(key)) return null;
85
- if (key.startsWith('.') || key.endsWith('.') || key.includes('..')) return null;
86
- if (key === '*' || key.includes('*')) return null;
87
-
88
- return key;
89
- }
90
-
91
- function extractKeysFromSource(file, patterns = KEY_PATTERNS) {
92
- const content = readUTF8(file);
93
- if (!content) return [];
94
- const keys = [];
95
- for (const re of patterns) {
96
- re.lastIndex = 0;
97
- let m; let guard = 0;
98
- while ((m = re.exec(content)) && guard++ < 10000) {
99
- if (m[1]) {
100
- const normalized = normalizeKeyCandidate(m[1]);
101
- if (normalized) keys.push(normalized);
102
- }
103
- }
104
- }
105
- return keys;
106
- }
107
-
108
- function flatten(obj, prefix = '') {
109
- const out = {};
110
- if (obj && typeof obj === 'object' && !Array.isArray(obj)) {
111
- for (const [k, v] of Object.entries(obj)) {
112
- const full = prefix ? `${prefix}.${k}` : k;
113
- Object.assign(out, flatten(v, full));
114
- }
115
- return out;
116
- }
117
- out[prefix] = obj;
118
- return out;
119
- }
120
-
121
- function unflatten(map) {
122
- const root = {};
123
- for (const [k, v] of Object.entries(map)) {
124
- const parts = k.split('.');
125
- let cur = root;
126
- for (let i = 0; i < parts.length - 1; i++) {
127
- cur[parts[i]] = cur[parts[i]] || {};
128
- cur = cur[parts[i]];
129
- }
130
- cur[parts[parts.length - 1]] = v;
131
- }
132
- return root;
133
- }
134
-
135
- function humanizeFromKey(keyPath) {
136
- const tail = keyPath.split('.').pop().split(':').pop();
137
- return tail.replace(/([A-Z])/g, ' $1').replace(/[_-]+/g, ' ').replace(/^./, c => c.toUpperCase()).trim();
138
- }
139
-
140
- function loadLanguage(lang) {
141
- const monolith = path.join(I18N_DIR, `${lang}.json`);
142
- if (isFile(monolith)) {
143
- try { return { type: 'monolith', path: monolith, data: JSON.parse(readUTF8(monolith) || '{}') }; } catch { return { type: 'monolith', path: monolith, data: {} }; }
144
- }
145
- const dir = path.join(I18N_DIR, lang);
146
- if (isDir(dir)) {
147
- const data = {};
148
- for (const f of fs.readdirSync(dir).filter(f => f.endsWith('.json'))) {
149
- const p = path.join(dir, f);
150
- const name = path.basename(f, '.json');
151
- try { data[name] = JSON.parse(readUTF8(p) || '{}'); } catch { data[name] = {}; }
152
- }
153
- return { type: 'dir', path: dir, data };
154
- }
155
- return { type: 'monolith', path: monolith, data: {} };
156
- }
157
-
158
- function saveLanguage(lang, langObj) {
159
- if (!WRITE) return;
160
- if (langObj.type === 'monolith') writeJSON(langObj.path, langObj.data);
161
- else for (const [ns, obj] of Object.entries(langObj.data)) writeJSON(path.join(langObj.path, `${ns}.json`), obj);
162
- }
163
-
164
- // ---------------- MAIN ----------------
165
- (async function main() {
166
- console.log('🔎 Fix-and-Purify i18n starting...');
167
-
168
- // Scan source files for keys
169
- const srcFiles = listFilesRecursive(SOURCE_DIR);
170
- const usedKeys = new Set();
171
- for (const f of srcFiles) extractKeysFromSource(f).forEach(k => usedKeys.add(k));
172
- console.log(`📦 Found ${usedKeys.size} unique keys in source.`);
173
-
174
- // EN baseline
175
- if (!LANGS.includes('en')) LANGS.unshift('en');
176
- const en = loadLanguage('en');
177
- const enFlat = flatten(en.data);
178
- let addedToEn = 0;
179
- for (const key of usedKeys) {
180
- if (!(key in enFlat)) {
181
- enFlat[key] = humanizeFromKey(key);
182
- addedToEn++;
183
- }
184
- }
185
- en.data = unflatten(enFlat);
186
- saveLanguage('en', en);
187
-
188
- // Sync & Purify others
189
- const report = {};
190
- for (const lang of LANGS.filter(l => l !== 'en')) {
191
- const langObj = loadLanguage(lang);
192
- const flat = flatten(langObj.data);
193
- const missing = [], extras = [], replacedMarkers = [], replacedCountryCodes = [], replacedEnglish = [];
194
-
195
- for (const k of Object.keys(enFlat)) {
196
- if (!(k in flat)) {
197
- flat[k] = `[${lang.toUpperCase()}] ${enFlat[k]}`;
198
- missing.push(k);
199
- }
200
- }
201
-
202
- for (const k of Object.keys(flat)) {
203
- if (!(k in enFlat)) extras.push(k);
204
- const val = flat[k];
205
- if (typeof val === 'string') {
206
- if (/TRANSLATION NEEDED/i.test(val)) {
207
- flat[k] = `[${lang.toUpperCase()}] ${enFlat[k]}`;
208
- replacedMarkers.push(k);
209
- }
210
- if (/^\[[A-Z]{2}\]/.test(val) && !val.startsWith(`[${COUNTRY_CODES[lang]}]`)) {
211
- flat[k] = `[${COUNTRY_CODES[lang]}] ${enFlat[k]}`;
212
- replacedCountryCodes.push(k);
213
- }
214
- if (/^[A-Za-z0-9 ,.'!?:;-]+$/.test(val) && val === enFlat[k]) {
215
- flat[k] = `[${lang.toUpperCase()}] ${enFlat[k]}`;
216
- replacedEnglish.push(k);
217
- }
218
- }
219
- }
220
-
221
- if (PRUNE_EXTRAS) extras.forEach(k => delete flat[k]);
222
- langObj.data = unflatten(flat);
223
- saveLanguage(lang, langObj);
224
-
225
- report[lang] = { missing: missing.length, extras: extras.length, replacedMarkers: replacedMarkers.length, replacedCountryCodes: replacedCountryCodes.length, replacedEnglish: replacedEnglish.length };
226
- console.log(`🌐 ${lang.toUpperCase()}: +${missing.length}, markers→${replacedMarkers.length}, cc→${replacedCountryCodes.length}, en→${replacedEnglish.length}, extras: ${extras.length}`);
227
- }
228
-
229
- // Save reports
230
- const summary = { timestamp: new Date().toISOString(), addedToEnglish: addedToEn, perLanguage: report };
231
- writeJSON(path.join(I18N_DIR, 'fix-and-purify-report.json'), summary);
232
- console.log(`📄 Fix-and-purify report saved.`);
233
- })();
@@ -1,110 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Fix control characters in locale files
4
- * Removes problematic newlines and other control chars from JSON strings
5
- */
6
- const fs = require('fs');
7
- const path = require('path');
8
- const SecurityUtils = require('../utils/security');
9
-
10
- const uiLocalesDirs = [
11
- path.join(__dirname, '..', 'ui-locales'),
12
- path.join(__dirname, '..', 'resources', 'i18n', 'ui-locales')
13
- ];
14
-
15
- function sanitizeValue(value) {
16
- if (typeof value === 'string') {
17
- // Remove/escape control characters but keep the structure
18
- // Replace literal newlines/tabs with spaces, but preserve intended structure
19
- return value
20
- .replace(/[\u0000-\u0008\u000B-\u000C\u000E-\u001F]/g, '') // Remove other control chars
21
- .replace(/\u000A/g, ' ') // Replace newlines with space
22
- .replace(/\u000D/g, '') // Remove carriage returns
23
- .trim();
24
- }
25
- if (Array.isArray(value)) {
26
- return value.map(sanitizeValue);
27
- }
28
- if (value && typeof value === 'object') {
29
- const result = {};
30
- for (const [k, v] of Object.entries(value)) {
31
- result[k] = sanitizeValue(v);
32
- }
33
- return result;
34
- }
35
- return value;
36
- }
37
-
38
- function fixFile(filePath) {
39
- try {
40
- const baseDir = path.dirname(filePath);
41
- const raw = SecurityUtils.safeReadFileSync(filePath, baseDir, 'utf8');
42
- if (!raw) {
43
- console.error(`✗ Error reading ${path.basename(filePath)}`);
44
- return false;
45
- }
46
-
47
- const data = SecurityUtils.safeParseJSON(raw);
48
- if (!data) {
49
- console.error(`✗ Error parsing ${path.basename(filePath)}`);
50
- return false;
51
- }
52
-
53
- const locale = path.basename(filePath, '.json');
54
-
55
- // Sanitize all values
56
- const sanitized = sanitizeValue(data);
57
-
58
- // Write back with proper formatting using SecurityUtils
59
- const success = SecurityUtils.safeWriteFileSync(
60
- filePath,
61
- JSON.stringify(sanitized, null, 2) + '\n',
62
- baseDir,
63
- 'utf8'
64
- );
65
-
66
- if (success) {
67
- console.log(` ✓ Fixed ${locale}.json`);
68
- return true;
69
- } else {
70
- console.error(` ✗ Error writing ${path.basename(filePath)}`);
71
- return false;
72
- }
73
- } catch (error) {
74
- console.error(`✗ Error fixing ${path.basename(filePath)}: ${error.message}`);
75
- return false;
76
- }
77
- }
78
-
79
- function main() {
80
- console.log('Fixing control characters in locale files...\n');
81
-
82
- let fixed = 0;
83
- let failed = 0;
84
-
85
- for (const uiLocalesDir of uiLocalesDirs) {
86
- if (!SecurityUtils.safeExistsSync(uiLocalesDir)) {
87
- console.log(`ℹ️ Skipping ${uiLocalesDir} (does not exist)\n`);
88
- continue;
89
- }
90
-
91
- console.log(`Processing: ${uiLocalesDir}`);
92
- const files = SecurityUtils.safeReaddirSync(uiLocalesDir, path.dirname(uiLocalesDir))
93
- .filter(f => f.endsWith('.json'))
94
- .map(f => path.join(uiLocalesDir, f));
95
-
96
- for (const file of files) {
97
- if (fixFile(file)) {
98
- fixed++;
99
- } else {
100
- failed++;
101
- }
102
- }
103
- console.log();
104
- }
105
-
106
- console.log(`\nFixed ${fixed} file(s)${failed ? `, ${failed} failed` : ''}`);
107
- process.exit(failed > 0 ? 1 : 0);
108
- }
109
-
110
- main();
@@ -1,80 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Simple locale lint:
4
- * - flags control characters in any locale string
5
- * - en locale: ensures a small set of CLI strings stay plain ASCII (catches mojibake)
6
- */
7
- const fs = require('fs');
8
- const path = require('path');
9
- const SecurityUtils = require('../utils/security');
10
-
11
- const uiLocalesDir = path.join(__dirname, '..', 'ui-locales');
12
- const asciiOnlyKeys = new Set([
13
- 'ui.autoDetectedI18nDirectory',
14
- 'ui.executingCommand',
15
- 'ui.unknownCommand',
16
- 'ui.errorExecutingCommand',
17
- 'ui.errorLoadingTranslationFile',
18
- 'ui.errorSavingLanguagePreference',
19
- 'ui.noActiveReadlineInterface',
20
- 'ui.uiLanguageUpdated',
21
- 'menu.invalidChoice',
22
- 'menu.returning',
23
- 'menu.invalidOption',
24
- 'menu.nonInteractiveModeWarning',
25
- 'menu.useDirectExecution',
26
- 'menu.useHelpForCommands',
27
- 'menu.autoDetectedI18nDirectory'
28
- ]);
29
-
30
- function walk(value, pathParts, onString) {
31
- if (typeof value === 'string') {
32
- onString(value, pathParts.join('.'));
33
- return;
34
- }
35
- if (Array.isArray(value)) {
36
- value.forEach((item, idx) => walk(item, pathParts.concat(idx), onString));
37
- return;
38
- }
39
- if (value && typeof value === 'object') {
40
- for (const [k, v] of Object.entries(value)) {
41
- walk(v, pathParts.concat(k), onString);
42
- }
43
- }
44
- }
45
-
46
- function lintFile(filePath) {
47
- const raw = SecurityUtils.safeReadFileSync(filePath, path.dirname(filePath), 'utf8');
48
- if (!raw) {
49
- return [`${path.basename(filePath, '.json')}: unable to read locale file`];
50
- }
51
- const data = JSON.parse(raw);
52
- const locale = path.basename(filePath, '.json');
53
- const issues = [];
54
-
55
- walk(data, [], (str, keyPath) => {
56
- if (/[\u0000-\u001F]/.test(str)) {
57
- issues.push(`${locale}: control char in ${keyPath}`);
58
- }
59
- if (locale === 'en' && asciiOnlyKeys.has(keyPath) && /[^\x20-\x7E]/.test(str)) {
60
- issues.push(`${locale}: non-ASCII in ${keyPath} -> "${str}"`);
61
- }
62
- });
63
-
64
- return issues;
65
- }
66
-
67
- function main() {
68
- const files = fs.readdirSync(uiLocalesDir).filter(f => f.endsWith('.json'));
69
- const allIssues = files.flatMap(f => lintFile(path.join(uiLocalesDir, f)));
70
-
71
- if (allIssues.length) {
72
- console.error('Locale lint failed:');
73
- allIssues.forEach(msg => console.error(` - ${msg}`));
74
- process.exit(1);
75
- }
76
-
77
- console.log('Locale lint passed.');
78
- }
79
-
80
- main();