i18ntk 1.4.1 → 1.4.2
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/CHANGELOG.md +9 -1
- package/README.md +8 -2
- package/main/i18ntk-manage.js +5 -5
- package/package.json +4 -4
- package/scripts/debug/README.md +34 -0
- package/scripts/debug/complete-console-translations.js +295 -0
- package/scripts/debug/console-key-checker.js +408 -0
- package/scripts/debug/console-translations.js +335 -0
- package/scripts/debug/debug-security.js +0 -0
- package/scripts/debug/debug-translation.js +56 -0
- package/scripts/debug/debugger.js +476 -0
- package/scripts/debug/export-missing-keys.js +432 -0
- package/scripts/debug/final-normalize.js +236 -0
- package/scripts/debug/find-extra-keys.js +68 -0
- package/scripts/debug/normalize-locales.js +153 -0
- package/scripts/debug/refactor-locales.js +240 -0
- package/scripts/debug/reorder-locales.js +85 -0
- package/scripts/debug/replace-hardcoded-console.js +378 -0
- package/scripts/prepublish.js +2 -2
- package/scripts/validate-all-translations.js +2 -2
- package/utils/test-complete-system.js +1 -1
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Console Key Checker v1.5.1
|
|
5
|
+
*
|
|
6
|
+
* This script finds missing translation keys by comparing the source language (en.json)
|
|
7
|
+
* with other language files and adds [NOT TRANSLATED] placeholders for missing keys.
|
|
8
|
+
* Improved to prevent duplicate key additions and handle nested structures properly.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* node console-key-checker.js [options]
|
|
12
|
+
*
|
|
13
|
+
* Options:
|
|
14
|
+
* --dry-run Show what would be changed without making changes
|
|
15
|
+
* --backup Create backup files (default: true)
|
|
16
|
+
* --languages=<list> Specific languages to check (default: all)
|
|
17
|
+
* --verbose Show detailed progress
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
|
|
23
|
+
class ConsoleKeyChecker {
|
|
24
|
+
constructor() {
|
|
25
|
+
this.uiLocalesDir = path.join(__dirname, '..', '..', 'ui-locales');
|
|
26
|
+
this.sourceLanguage = 'en';
|
|
27
|
+
this.supportedLanguages = ['de', 'es', 'fr', 'ja', 'ru', 'zh'];
|
|
28
|
+
this.dryRun = process.argv.includes('--dry-run');
|
|
29
|
+
this.createBackup = !process.argv.includes('--no-backup');
|
|
30
|
+
this.verbose = process.argv.includes('--verbose');
|
|
31
|
+
this.missingKeys = new Map();
|
|
32
|
+
|
|
33
|
+
// Parse specific languages if provided
|
|
34
|
+
const langArg = process.argv.find(arg => arg.startsWith('--languages='));
|
|
35
|
+
if (langArg) {
|
|
36
|
+
this.supportedLanguages = langArg.split('=')[1].split(',').map(l => l.trim());
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get all keys from an object with dot notation
|
|
42
|
+
*/
|
|
43
|
+
getKeysFromObject(obj, prefix = '') {
|
|
44
|
+
const keys = [];
|
|
45
|
+
|
|
46
|
+
for (const key in obj) {
|
|
47
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
48
|
+
|
|
49
|
+
if (typeof obj[key] === 'object' && obj[key] !== null && !Array.isArray(obj[key])) {
|
|
50
|
+
keys.push(...this.getKeysFromObject(obj[key], fullKey));
|
|
51
|
+
} else {
|
|
52
|
+
keys.push(fullKey);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return keys;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Check if a key exists in the object using dot notation
|
|
61
|
+
*/
|
|
62
|
+
keyExists(obj, keyPath) {
|
|
63
|
+
const keys = keyPath.split('.');
|
|
64
|
+
let current = obj;
|
|
65
|
+
|
|
66
|
+
for (const key of keys) {
|
|
67
|
+
if (current === null || current === undefined || !(key in current)) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
current = current[key];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get a value from an object using dot notation
|
|
78
|
+
*/
|
|
79
|
+
getValueByPath(obj, keyPath) {
|
|
80
|
+
const keys = keyPath.split('.');
|
|
81
|
+
let current = obj;
|
|
82
|
+
|
|
83
|
+
for (const key of keys) {
|
|
84
|
+
if (current === null || current === undefined || !(key in current)) {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
current = current[key];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return current;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Set a value in an object using dot notation, creating nested structure as needed
|
|
95
|
+
*/
|
|
96
|
+
setValueByPath(obj, keyPath, value) {
|
|
97
|
+
const keys = keyPath.split('.');
|
|
98
|
+
let current = obj;
|
|
99
|
+
|
|
100
|
+
// Navigate to the parent of the target key
|
|
101
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
102
|
+
const key = keys[i];
|
|
103
|
+
|
|
104
|
+
if (!(key in current) || typeof current[key] !== 'object' || current[key] === null) {
|
|
105
|
+
current[key] = {};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
current = current[key];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Set the final value
|
|
112
|
+
const finalKey = keys[keys.length - 1];
|
|
113
|
+
current[finalKey] = value;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Find similar keys in the object (for suggestions)
|
|
118
|
+
*/
|
|
119
|
+
findSimilarKeys(obj, targetKey, threshold = 0.6) {
|
|
120
|
+
const allKeys = this.getKeysFromObject(obj);
|
|
121
|
+
const similar = [];
|
|
122
|
+
|
|
123
|
+
for (const key of allKeys) {
|
|
124
|
+
const similarity = this.calculateSimilarity(targetKey, key);
|
|
125
|
+
if (similarity >= threshold) {
|
|
126
|
+
similar.push({ key, similarity });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return similar.sort((a, b) => b.similarity - a.similarity);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Calculate string similarity (simple Levenshtein-based)
|
|
135
|
+
*/
|
|
136
|
+
calculateSimilarity(str1, str2) {
|
|
137
|
+
const longer = str1.length > str2.length ? str1 : str2;
|
|
138
|
+
const shorter = str1.length > str2.length ? str2 : str1;
|
|
139
|
+
|
|
140
|
+
if (longer.length === 0) return 1.0;
|
|
141
|
+
|
|
142
|
+
const distance = this.levenshteinDistance(longer, shorter);
|
|
143
|
+
return (longer.length - distance) / longer.length;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Calculate Levenshtein distance
|
|
148
|
+
*/
|
|
149
|
+
levenshteinDistance(str1, str2) {
|
|
150
|
+
const matrix = [];
|
|
151
|
+
|
|
152
|
+
for (let i = 0; i <= str2.length; i++) {
|
|
153
|
+
matrix[i] = [i];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
for (let j = 0; j <= str1.length; j++) {
|
|
157
|
+
matrix[0][j] = j;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
for (let i = 1; i <= str2.length; i++) {
|
|
161
|
+
for (let j = 1; j <= str1.length; j++) {
|
|
162
|
+
if (str2.charAt(i - 1) === str1.charAt(j - 1)) {
|
|
163
|
+
matrix[i][j] = matrix[i - 1][j - 1];
|
|
164
|
+
} else {
|
|
165
|
+
matrix[i][j] = Math.min(
|
|
166
|
+
matrix[i - 1][j - 1] + 1,
|
|
167
|
+
matrix[i][j - 1] + 1,
|
|
168
|
+
matrix[i - 1][j] + 1
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return matrix[str2.length][str1.length];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Load source language keys
|
|
179
|
+
*/
|
|
180
|
+
loadSourceKeys() {
|
|
181
|
+
const sourceFile = path.join(this.uiLocalesDir, this.sourceLanguage, 'common.json');
|
|
182
|
+
|
|
183
|
+
if (!fs.existsSync(sourceFile)) {
|
|
184
|
+
throw new Error(`Source language file not found: ${sourceFile}`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const sourceData = JSON.parse(fs.readFileSync(sourceFile, 'utf8'));
|
|
188
|
+
return this.getKeysFromObject(sourceData);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Check a single language file for missing keys
|
|
193
|
+
*/
|
|
194
|
+
checkLanguageFile(languageCode, sourceKeys) {
|
|
195
|
+
const languageFile = path.join(this.uiLocalesDir, languageCode, 'common.json');
|
|
196
|
+
|
|
197
|
+
if (!fs.existsSync(languageFile)) {
|
|
198
|
+
console.log(`⚠️ Language file not found: ${languageFile}`);
|
|
199
|
+
return { missingKeys: [], addedKeys: 0 };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
console.log(`🔍 Checking ${languageCode.toUpperCase()} for missing keys...`);
|
|
203
|
+
|
|
204
|
+
// Load current translations
|
|
205
|
+
let currentTranslations;
|
|
206
|
+
try {
|
|
207
|
+
const fileContent = fs.readFileSync(languageFile, 'utf8');
|
|
208
|
+
currentTranslations = JSON.parse(fileContent);
|
|
209
|
+
} catch (error) {
|
|
210
|
+
console.log(`❌ Error parsing ${languageCode}.json: ${error.message}`);
|
|
211
|
+
return { missingKeys: [], addedKeys: 0 };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Get current keys using proper nested key extraction
|
|
215
|
+
const currentKeys = this.getKeysFromObject(currentTranslations);
|
|
216
|
+
|
|
217
|
+
// Find missing keys by checking if each source key exists in current translations
|
|
218
|
+
const missingKeys = sourceKeys.filter(key => !this.keyExists(currentTranslations, key));
|
|
219
|
+
|
|
220
|
+
if (missingKeys.length === 0) {
|
|
221
|
+
console.log(`✅ ${languageCode.toUpperCase()}: No missing keys found`);
|
|
222
|
+
return { missingKeys: [], addedKeys: 0 };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
console.log(`📋 ${languageCode.toUpperCase()}: Found ${missingKeys.length} missing keys`);
|
|
226
|
+
|
|
227
|
+
if (this.verbose) {
|
|
228
|
+
missingKeys.forEach(key => {
|
|
229
|
+
console.log(` ❌ Missing: ${key}`);
|
|
230
|
+
|
|
231
|
+
// Suggest similar keys if available
|
|
232
|
+
const similarKeys = this.findSimilarKeys(currentTranslations, key, 0.7);
|
|
233
|
+
if (similarKeys.length > 0) {
|
|
234
|
+
console.log(` 💡 Similar keys found:`);
|
|
235
|
+
similarKeys.slice(0, 2).forEach(similar => {
|
|
236
|
+
console.log(` - ${similar.key} (${(similar.similarity * 100).toFixed(1)}% match)`);
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Add missing keys with [NOT TRANSLATED] placeholder
|
|
243
|
+
let addedKeys = 0;
|
|
244
|
+
if (!this.dryRun) {
|
|
245
|
+
// Create backup if enabled
|
|
246
|
+
if (this.createBackup) {
|
|
247
|
+
const backupFile = languageFile.replace('.json', '.backup.json');
|
|
248
|
+
fs.copyFileSync(languageFile, backupFile);
|
|
249
|
+
console.log(`📋 Backup created: ${path.basename(backupFile)}`);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Add missing keys in their proper nested locations
|
|
253
|
+
missingKeys.forEach(key => {
|
|
254
|
+
// Double-check the key doesn't exist before adding
|
|
255
|
+
if (!this.keyExists(currentTranslations, key)) {
|
|
256
|
+
this.setValueByPath(currentTranslations, key, '[NOT TRANSLATED]');
|
|
257
|
+
addedKeys++;
|
|
258
|
+
|
|
259
|
+
if (this.verbose) {
|
|
260
|
+
console.log(` ✅ Added: ${key} = [NOT TRANSLATED]`);
|
|
261
|
+
}
|
|
262
|
+
} else if (this.verbose) {
|
|
263
|
+
console.log(` ⚠️ Key already exists, skipping: ${key}`);
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// Save updated translations with proper formatting
|
|
268
|
+
try {
|
|
269
|
+
fs.writeFileSync(languageFile, JSON.stringify(currentTranslations, null, 2), 'utf8');
|
|
270
|
+
console.log(`💾 ${languageCode.toUpperCase()}: Added ${addedKeys} missing keys`);
|
|
271
|
+
} catch (error) {
|
|
272
|
+
console.log(`❌ Error writing ${languageCode}.json: ${error.message}`);
|
|
273
|
+
return { missingKeys, addedKeys: 0 };
|
|
274
|
+
}
|
|
275
|
+
} else {
|
|
276
|
+
console.log(`🔍 ${languageCode.toUpperCase()}: Would add ${missingKeys.length} missing keys`);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return { missingKeys, addedKeys };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Generate a report of missing keys
|
|
284
|
+
*/
|
|
285
|
+
generateReport() {
|
|
286
|
+
if (this.missingKeys.size === 0) {
|
|
287
|
+
console.log('\n📊 No missing keys found across all languages.');
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
console.log('\n📊 MISSING KEYS REPORT');
|
|
292
|
+
console.log('========================\n');
|
|
293
|
+
|
|
294
|
+
let totalMissing = 0;
|
|
295
|
+
|
|
296
|
+
for (const [language, keys] of this.missingKeys) {
|
|
297
|
+
if (keys.length > 0) {
|
|
298
|
+
console.log(`🌍 ${language.toUpperCase()}: ${keys.length} missing keys`);
|
|
299
|
+
totalMissing += keys.length;
|
|
300
|
+
|
|
301
|
+
if (this.verbose) {
|
|
302
|
+
keys.forEach(key => {
|
|
303
|
+
console.log(` - ${key}`);
|
|
304
|
+
});
|
|
305
|
+
console.log('');
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
console.log(`\n📈 Total missing keys across all languages: ${totalMissing}`);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Export missing keys to a JSON report file
|
|
315
|
+
*/
|
|
316
|
+
exportMissingKeysReport() {
|
|
317
|
+
const reportsDir = path.join(__dirname, 'reports');
|
|
318
|
+
if (!fs.existsSync(reportsDir)) {
|
|
319
|
+
fs.mkdirSync(reportsDir, { recursive: true });
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
323
|
+
const reportFile = path.join(reportsDir, `missing-keys-${timestamp}.json`);
|
|
324
|
+
|
|
325
|
+
const report = {
|
|
326
|
+
timestamp: new Date().toISOString(),
|
|
327
|
+
sourceLanguage: this.sourceLanguage,
|
|
328
|
+
checkedLanguages: this.supportedLanguages,
|
|
329
|
+
summary: {
|
|
330
|
+
totalLanguages: this.supportedLanguages.length,
|
|
331
|
+
languagesWithMissingKeys: Array.from(this.missingKeys.keys()).filter(lang =>
|
|
332
|
+
this.missingKeys.get(lang).length > 0
|
|
333
|
+
).length,
|
|
334
|
+
totalMissingKeys: Array.from(this.missingKeys.values()).reduce((sum, keys) => sum + keys.length, 0)
|
|
335
|
+
},
|
|
336
|
+
missingKeysByLanguage: Object.fromEntries(this.missingKeys),
|
|
337
|
+
options: {
|
|
338
|
+
dryRun: this.dryRun,
|
|
339
|
+
createBackup: this.createBackup,
|
|
340
|
+
verbose: this.verbose
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
fs.writeFileSync(reportFile, JSON.stringify(report, null, 2), 'utf8');
|
|
345
|
+
console.log(`\n📄 Missing keys report exported: ${path.basename(reportFile)}`);
|
|
346
|
+
|
|
347
|
+
return reportFile;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Main execution function
|
|
352
|
+
*/
|
|
353
|
+
async run() {
|
|
354
|
+
console.log('🔍 Console Key Checker v1.5.1');
|
|
355
|
+
console.log('===============================\n');
|
|
356
|
+
|
|
357
|
+
if (this.dryRun) {
|
|
358
|
+
console.log('⚠️ Running in DRY RUN mode - no files will be modified\n');
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
try {
|
|
362
|
+
// Load source keys
|
|
363
|
+
console.log(`📖 Loading source keys from ${this.sourceLanguage}.json...`);
|
|
364
|
+
const sourceKeys = this.loadSourceKeys();
|
|
365
|
+
console.log(`📊 Found ${sourceKeys.length} keys in source language\n`);
|
|
366
|
+
|
|
367
|
+
let totalAdded = 0;
|
|
368
|
+
|
|
369
|
+
// Check each language file
|
|
370
|
+
for (const languageCode of this.supportedLanguages) {
|
|
371
|
+
const result = this.checkLanguageFile(languageCode, sourceKeys);
|
|
372
|
+
this.missingKeys.set(languageCode, result.missingKeys);
|
|
373
|
+
totalAdded += result.addedKeys;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Generate and export report
|
|
377
|
+
this.generateReport();
|
|
378
|
+
|
|
379
|
+
if (!this.dryRun) {
|
|
380
|
+
this.exportMissingKeysReport();
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
console.log('\n✅ Console key checking complete!');
|
|
384
|
+
|
|
385
|
+
if (!this.dryRun && totalAdded > 0) {
|
|
386
|
+
console.log(`📊 Total keys added: ${totalAdded}`);
|
|
387
|
+
console.log('💡 Run the native-translations.js script to replace [NOT TRANSLATED] placeholders with proper translations.');
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (this.dryRun) {
|
|
391
|
+
console.log('\n⚠️ DRY RUN MODE - No files were modified');
|
|
392
|
+
console.log('💡 Remove --dry-run flag to apply changes');
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
} catch (error) {
|
|
396
|
+
console.error('❌ Error during key checking:', error);
|
|
397
|
+
process.exit(1);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Run the script if called directly
|
|
403
|
+
if (require.main === module) {
|
|
404
|
+
const checker = new ConsoleKeyChecker();
|
|
405
|
+
checker.run();
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
module.exports = ConsoleKeyChecker;
|