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.
@@ -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;