i18ntk 1.0.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 (86) hide show
  1. package/CHANGELOG.md +401 -0
  2. package/LICENSE +21 -0
  3. package/README.md +507 -0
  4. package/dev/README.md +37 -0
  5. package/dev/debug/README.md +30 -0
  6. package/dev/debug/complete-console-translations.js +295 -0
  7. package/dev/debug/console-key-checker.js +408 -0
  8. package/dev/debug/console-translations.js +335 -0
  9. package/dev/debug/debugger.js +408 -0
  10. package/dev/debug/export-missing-keys.js +432 -0
  11. package/dev/debug/final-normalize.js +236 -0
  12. package/dev/debug/find-extra-keys.js +68 -0
  13. package/dev/debug/normalize-locales.js +153 -0
  14. package/dev/debug/refactor-locales.js +240 -0
  15. package/dev/debug/reorder-locales.js +85 -0
  16. package/dev/debug/replace-hardcoded-console.js +378 -0
  17. package/docs/INSTALLATION.md +449 -0
  18. package/docs/README.md +222 -0
  19. package/docs/TODO_ROADMAP.md +279 -0
  20. package/docs/api/API_REFERENCE.md +377 -0
  21. package/docs/api/COMPONENTS.md +492 -0
  22. package/docs/api/CONFIGURATION.md +651 -0
  23. package/docs/api/NPM_PUBLISHING_GUIDE.md +434 -0
  24. package/docs/debug/DEBUG_README.md +30 -0
  25. package/docs/debug/DEBUG_TOOLS.md +494 -0
  26. package/docs/development/AGENTS.md +351 -0
  27. package/docs/development/DEVELOPMENT_RULES.md +165 -0
  28. package/docs/development/DEV_README.md +37 -0
  29. package/docs/release-notes/RELEASE_NOTES_v1.0.0.md +173 -0
  30. package/docs/release-notes/RELEASE_NOTES_v1.6.0.md +141 -0
  31. package/docs/release-notes/RELEASE_NOTES_v1.6.1.md +185 -0
  32. package/docs/release-notes/RELEASE_NOTES_v1.6.3.md +199 -0
  33. package/docs/reports/ANALYSIS_README.md +17 -0
  34. package/docs/reports/CONSOLE_MISMATCH_BUG_REPORT_v1.5.0.md +181 -0
  35. package/docs/reports/SIZING_README.md +18 -0
  36. package/docs/reports/SUMMARY_README.md +18 -0
  37. package/docs/reports/TRANSLATION_BUG_REPORT_v1.5.0.md +129 -0
  38. package/docs/reports/USAGE_README.md +18 -0
  39. package/docs/reports/VALIDATION_README.md +18 -0
  40. package/locales/de/auth.json +3 -0
  41. package/locales/de/common.json +16 -0
  42. package/locales/de/pagination.json +6 -0
  43. package/locales/en/auth.json +3 -0
  44. package/locales/en/common.json +16 -0
  45. package/locales/en/pagination.json +6 -0
  46. package/locales/es/auth.json +3 -0
  47. package/locales/es/common.json +16 -0
  48. package/locales/es/pagination.json +6 -0
  49. package/locales/fr/auth.json +3 -0
  50. package/locales/fr/common.json +16 -0
  51. package/locales/fr/pagination.json +6 -0
  52. package/locales/ru/auth.json +3 -0
  53. package/locales/ru/common.json +16 -0
  54. package/locales/ru/pagination.json +6 -0
  55. package/main/i18ntk-analyze.js +625 -0
  56. package/main/i18ntk-autorun.js +461 -0
  57. package/main/i18ntk-complete.js +494 -0
  58. package/main/i18ntk-init.js +686 -0
  59. package/main/i18ntk-manage.js +848 -0
  60. package/main/i18ntk-sizing.js +557 -0
  61. package/main/i18ntk-summary.js +671 -0
  62. package/main/i18ntk-usage.js +1282 -0
  63. package/main/i18ntk-validate.js +762 -0
  64. package/main/ui-i18n.js +332 -0
  65. package/package.json +152 -0
  66. package/scripts/fix-missing-translation-keys.js +214 -0
  67. package/scripts/verify-package.js +168 -0
  68. package/ui-locales/de.json +637 -0
  69. package/ui-locales/en.json +688 -0
  70. package/ui-locales/es.json +637 -0
  71. package/ui-locales/fr.json +637 -0
  72. package/ui-locales/ja.json +637 -0
  73. package/ui-locales/ru.json +637 -0
  74. package/ui-locales/zh.json +637 -0
  75. package/utils/admin-auth.js +317 -0
  76. package/utils/admin-cli.js +353 -0
  77. package/utils/admin-pin.js +409 -0
  78. package/utils/detect-language-mismatches.js +454 -0
  79. package/utils/i18n-helper.js +128 -0
  80. package/utils/maintain-language-purity.js +433 -0
  81. package/utils/native-translations.js +478 -0
  82. package/utils/security.js +384 -0
  83. package/utils/test-complete-system.js +356 -0
  84. package/utils/test-console-i18n.js +402 -0
  85. package/utils/translate-mismatches.js +571 -0
  86. package/utils/validate-language-purity.js +531 -0
@@ -0,0 +1,432 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Export Missing Translation Keys Script
5
+ *
6
+ * This script identifies and exports missing translation keys across all language files
7
+ * for tracking, debugging, and translation management purposes.
8
+ *
9
+ * Features:
10
+ * - Identifies missing keys by comparing against English reference
11
+ * - Exports missing keys in multiple formats (JSON, CSV, TXT)
12
+ * - Generates detailed reports for translators
13
+ * - Tracks translation progress over time
14
+ * - Provides language-specific missing key lists
15
+ *
16
+ * Usage:
17
+ * node export-missing-keys.js [options]
18
+ *
19
+ * Options:
20
+ * --format=json,csv,txt Export formats (default: json)
21
+ * --output-dir=<path> Output directory (default: ./reports/missing-keys/)
22
+ * --languages=<list> Specific languages to check (default: all)
23
+ * --include-empty Include empty translations in export
24
+ * --verbose Show detailed progress
25
+ */
26
+
27
+ const fs = require('fs');
28
+ const path = require('path');
29
+
30
+ class MissingKeysExporter {
31
+ constructor() {
32
+ this.uiLocalesDir = path.join(__dirname, 'ui-locales');
33
+ this.outputDir = path.join(__dirname, 'reports', 'missing-keys');
34
+ this.referenceLanguage = 'en';
35
+ this.supportedLanguages = ['de', 'es', 'fr', 'ja', 'ru', 'zh'];
36
+ this.exportFormats = ['json'];
37
+ this.includeEmpty = false;
38
+ this.verbose = false;
39
+ }
40
+
41
+ /**
42
+ * Parse command line arguments
43
+ */
44
+ parseArgs() {
45
+ const args = process.argv.slice(2);
46
+
47
+ for (const arg of args) {
48
+ if (arg.startsWith('--format=')) {
49
+ this.exportFormats = arg.split('=')[1].split(',');
50
+ } else if (arg.startsWith('--output-dir=')) {
51
+ this.outputDir = arg.split('=')[1];
52
+ } else if (arg.startsWith('--languages=')) {
53
+ this.supportedLanguages = arg.split('=')[1].split(',');
54
+ } else if (arg === '--include-empty') {
55
+ this.includeEmpty = true;
56
+ } else if (arg === '--verbose') {
57
+ this.verbose = true;
58
+ } else if (arg === '--help') {
59
+ this.showHelp();
60
+ process.exit(0);
61
+ }
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Show help information
67
+ */
68
+ showHelp() {
69
+ console.log(`
70
+ šŸ” MISSING KEYS EXPORTER
71
+ ${'='.repeat(50)}`);
72
+ console.log('Export missing translation keys for analysis and tracking\n');
73
+ console.log('USAGE:');
74
+ console.log(' node export-missing-keys.js [options]\n');
75
+ console.log('OPTIONS:');
76
+ console.log(' --format=json,csv,txt Export formats (default: json)');
77
+ console.log(' --output-dir=<path> Output directory (default: ./reports/missing-keys/)');
78
+ console.log(' --languages=<list> Specific languages to check (default: all)');
79
+ console.log(' --include-empty Include empty translations in export');
80
+ console.log(' --verbose Show detailed progress');
81
+ console.log(' --help Show this help message\n');
82
+ console.log('EXAMPLES:');
83
+ console.log(' node export-missing-keys.js');
84
+ console.log(' node export-missing-keys.js --format=json,csv --languages=de,fr');
85
+ console.log(' node export-missing-keys.js --include-empty --verbose');
86
+ }
87
+
88
+ /**
89
+ * Load and parse JSON file
90
+ */
91
+ loadJsonFile(filePath) {
92
+ try {
93
+ const content = fs.readFileSync(filePath, 'utf8');
94
+ return JSON.parse(content);
95
+ } catch (error) {
96
+ if (this.verbose) {
97
+ console.log(`āš ļø Warning: Could not load ${filePath}: ${error.message}`);
98
+ }
99
+ return null;
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Get all keys from an object recursively
105
+ */
106
+ getAllKeys(obj, prefix = '') {
107
+ const keys = [];
108
+
109
+ for (const [key, value] of Object.entries(obj)) {
110
+ const fullKey = prefix ? `${prefix}.${key}` : key;
111
+
112
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
113
+ keys.push(...this.getAllKeys(value, fullKey));
114
+ } else {
115
+ keys.push(fullKey);
116
+ }
117
+ }
118
+
119
+ return keys;
120
+ }
121
+
122
+ /**
123
+ * Get value from nested object using dot notation
124
+ */
125
+ getNestedValue(obj, key) {
126
+ return key.split('.').reduce((current, part) => {
127
+ return current && current[part] !== undefined ? current[part] : undefined;
128
+ }, obj);
129
+ }
130
+
131
+ /**
132
+ * Check if a translation is empty or placeholder
133
+ */
134
+ isEmptyOrPlaceholder(value) {
135
+ if (!value || typeof value !== 'string') return true;
136
+
137
+ const trimmed = value.trim();
138
+ if (!trimmed) return true;
139
+
140
+ // Check for placeholder patterns
141
+ const placeholderPatterns = [
142
+ /^\[NOT TRANSLATED\]/,
143
+ /^\[TRANSLATE\]/,
144
+ /^\[[A-Z]{2}\]/,
145
+ /^\[TODO\]/
146
+ ];
147
+
148
+ return placeholderPatterns.some(pattern => pattern.test(trimmed));
149
+ }
150
+
151
+ /**
152
+ * Analyze missing keys for a specific language
153
+ */
154
+ analyzeMissingKeys(referenceData, targetData, language) {
155
+ const referenceKeys = this.getAllKeys(referenceData);
156
+ const missingKeys = [];
157
+ const emptyKeys = [];
158
+ const placeholderKeys = [];
159
+
160
+ for (const key of referenceKeys) {
161
+ const targetValue = this.getNestedValue(targetData, key);
162
+ const referenceValue = this.getNestedValue(referenceData, key);
163
+
164
+ if (targetValue === undefined) {
165
+ missingKeys.push({
166
+ key,
167
+ referenceValue,
168
+ status: 'missing'
169
+ });
170
+ } else if (this.isEmptyOrPlaceholder(targetValue)) {
171
+ if (!targetValue || (typeof targetValue === 'string' && !targetValue.trim())) {
172
+ emptyKeys.push({
173
+ key,
174
+ referenceValue,
175
+ currentValue: targetValue,
176
+ status: 'empty'
177
+ });
178
+ } else {
179
+ placeholderKeys.push({
180
+ key,
181
+ referenceValue,
182
+ currentValue: targetValue,
183
+ status: 'placeholder'
184
+ });
185
+ }
186
+ }
187
+ }
188
+
189
+ return {
190
+ language,
191
+ totalKeys: referenceKeys.length,
192
+ missingKeys,
193
+ emptyKeys,
194
+ placeholderKeys,
195
+ missingCount: missingKeys.length,
196
+ emptyCount: emptyKeys.length,
197
+ placeholderCount: placeholderKeys.length,
198
+ completeness: Math.round(((referenceKeys.length - missingKeys.length - emptyKeys.length - placeholderKeys.length) / referenceKeys.length) * 100)
199
+ };
200
+ }
201
+
202
+ /**
203
+ * Export data in JSON format
204
+ */
205
+ exportJson(data, filename) {
206
+ const filePath = path.join(this.outputDir, `${filename}.json`);
207
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8');
208
+ return filePath;
209
+ }
210
+
211
+ /**
212
+ * Export data in CSV format
213
+ */
214
+ exportCsv(data, filename) {
215
+ const filePath = path.join(this.outputDir, `${filename}.csv`);
216
+
217
+ let csvContent = 'Language,Key,Status,Reference Value,Current Value\n';
218
+
219
+ for (const langData of data.languages) {
220
+ const allIssues = [
221
+ ...langData.missingKeys,
222
+ ...langData.emptyKeys,
223
+ ...langData.placeholderKeys
224
+ ];
225
+
226
+ for (const issue of allIssues) {
227
+ const refValue = typeof issue.referenceValue === 'string' ? issue.referenceValue : String(issue.referenceValue || '');
228
+ const currValue = typeof issue.currentValue === 'string' ? issue.currentValue : String(issue.currentValue || '');
229
+
230
+ const row = [
231
+ langData.language,
232
+ issue.key,
233
+ issue.status,
234
+ `"${refValue.replace(/"/g, '""')}"`,
235
+ `"${currValue.replace(/"/g, '""')}"`
236
+ ].join(',');
237
+ csvContent += row + '\n';
238
+ }
239
+ }
240
+
241
+ fs.writeFileSync(filePath, csvContent, 'utf8');
242
+ return filePath;
243
+ }
244
+
245
+ /**
246
+ * Export data in TXT format
247
+ */
248
+ exportTxt(data, filename) {
249
+ const filePath = path.join(this.outputDir, `${filename}.txt`);
250
+
251
+ let txtContent = `MISSING TRANSLATION KEYS REPORT\n`;
252
+ txtContent += `Generated: ${data.timestamp}\n`;
253
+ txtContent += `${'='.repeat(60)}\n\n`;
254
+
255
+ txtContent += `SUMMARY:\n`;
256
+ txtContent += `- Languages analyzed: ${data.summary.totalLanguages}\n`;
257
+ txtContent += `- Total missing keys: ${data.summary.totalMissing}\n`;
258
+ txtContent += `- Total empty keys: ${data.summary.totalEmpty}\n`;
259
+ txtContent += `- Total placeholder keys: ${data.summary.totalPlaceholders}\n`;
260
+ txtContent += `- Average completeness: ${data.summary.averageCompleteness}%\n\n`;
261
+
262
+ for (const langData of data.languages) {
263
+ txtContent += `${langData.language.toUpperCase()} (${langData.completeness}% complete):\n`;
264
+ txtContent += `${'─'.repeat(30)}\n`;
265
+
266
+ if (langData.missingKeys.length > 0) {
267
+ txtContent += `Missing Keys (${langData.missingKeys.length}):\n`;
268
+ for (const key of langData.missingKeys) {
269
+ txtContent += ` - ${key.key}\n`;
270
+ }
271
+ txtContent += '\n';
272
+ }
273
+
274
+ if (langData.placeholderKeys.length > 0) {
275
+ txtContent += `Placeholder Keys (${langData.placeholderKeys.length}):\n`;
276
+ for (const key of langData.placeholderKeys) {
277
+ txtContent += ` - ${key.key}: ${key.currentValue}\n`;
278
+ }
279
+ txtContent += '\n';
280
+ }
281
+
282
+ if (this.includeEmpty && langData.emptyKeys.length > 0) {
283
+ txtContent += `Empty Keys (${langData.emptyKeys.length}):\n`;
284
+ for (const key of langData.emptyKeys) {
285
+ txtContent += ` - ${key.key}\n`;
286
+ }
287
+ txtContent += '\n';
288
+ }
289
+
290
+ txtContent += '\n';
291
+ }
292
+
293
+ fs.writeFileSync(filePath, txtContent, 'utf8');
294
+ return filePath;
295
+ }
296
+
297
+ /**
298
+ * Ensure output directory exists
299
+ */
300
+ ensureOutputDir() {
301
+ if (!fs.existsSync(this.outputDir)) {
302
+ fs.mkdirSync(this.outputDir, { recursive: true });
303
+ if (this.verbose) {
304
+ console.log(`šŸ“ Created output directory: ${this.outputDir}`);
305
+ }
306
+ }
307
+ }
308
+
309
+ /**
310
+ * Main export function
311
+ */
312
+ async export() {
313
+ console.log('šŸ” EXPORTING MISSING TRANSLATION KEYS');
314
+ console.log('='.repeat(50));
315
+
316
+ this.ensureOutputDir();
317
+
318
+ // Load reference language
319
+ const referencePath = path.join(this.uiLocalesDir, `${this.referenceLanguage}.json`);
320
+ const referenceData = this.loadJsonFile(referencePath);
321
+
322
+ if (!referenceData) {
323
+ console.error(`āŒ Could not load reference language file: ${referencePath}`);
324
+ process.exit(1);
325
+ }
326
+
327
+ if (this.verbose) {
328
+ console.log(`šŸ“– Loaded reference language: ${this.referenceLanguage}`);
329
+ }
330
+
331
+ const results = {
332
+ timestamp: new Date().toISOString(),
333
+ referenceLanguage: this.referenceLanguage,
334
+ exportFormats: this.exportFormats,
335
+ includeEmpty: this.includeEmpty,
336
+ languages: [],
337
+ summary: {
338
+ totalLanguages: 0,
339
+ totalMissing: 0,
340
+ totalEmpty: 0,
341
+ totalPlaceholders: 0,
342
+ averageCompleteness: 0
343
+ }
344
+ };
345
+
346
+ // Analyze each target language
347
+ for (const language of this.supportedLanguages) {
348
+ if (this.verbose) {
349
+ console.log(`šŸ” Analyzing ${language}...`);
350
+ }
351
+
352
+ const languagePath = path.join(this.uiLocalesDir, `${language}.json`);
353
+ const languageData = this.loadJsonFile(languagePath);
354
+
355
+ if (!languageData) {
356
+ console.log(`āš ļø Skipping ${language}: file not found`);
357
+ continue;
358
+ }
359
+
360
+ const analysis = this.analyzeMissingKeys(referenceData, languageData, language);
361
+ results.languages.push(analysis);
362
+
363
+ console.log(` ${language}: ${analysis.completeness}% complete (${analysis.missingCount + analysis.placeholderCount} issues)`);
364
+ }
365
+
366
+ // Calculate summary
367
+ results.summary.totalLanguages = results.languages.length;
368
+ results.summary.totalMissing = results.languages.reduce((sum, lang) => sum + lang.missingCount, 0);
369
+ results.summary.totalEmpty = results.languages.reduce((sum, lang) => sum + lang.emptyCount, 0);
370
+ results.summary.totalPlaceholders = results.languages.reduce((sum, lang) => sum + lang.placeholderCount, 0);
371
+ results.summary.averageCompleteness = Math.round(
372
+ results.languages.reduce((sum, lang) => sum + lang.completeness, 0) / results.languages.length
373
+ );
374
+
375
+ // Export in requested formats
376
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
377
+ const baseFilename = `missing-keys-${timestamp}`;
378
+ const exportedFiles = [];
379
+
380
+ for (const format of this.exportFormats) {
381
+ let filePath;
382
+
383
+ switch (format.toLowerCase()) {
384
+ case 'json':
385
+ filePath = this.exportJson(results, baseFilename);
386
+ break;
387
+ case 'csv':
388
+ filePath = this.exportCsv(results, baseFilename);
389
+ break;
390
+ case 'txt':
391
+ filePath = this.exportTxt(results, baseFilename);
392
+ break;
393
+ default:
394
+ console.log(`āš ļø Unknown format: ${format}`);
395
+ continue;
396
+ }
397
+
398
+ exportedFiles.push(filePath);
399
+ console.log(`āœ… Exported ${format.toUpperCase()}: ${filePath}`);
400
+ }
401
+
402
+ console.log('\nšŸ“Š EXPORT SUMMARY');
403
+ console.log('─'.repeat(30));
404
+ console.log(`šŸŒ Languages analyzed: ${results.summary.totalLanguages}`);
405
+ console.log(`āŒ Total missing keys: ${results.summary.totalMissing}`);
406
+ console.log(`šŸ”„ Total placeholder keys: ${results.summary.totalPlaceholders}`);
407
+ if (this.includeEmpty) {
408
+ console.log(`⚪ Total empty keys: ${results.summary.totalEmpty}`);
409
+ }
410
+ console.log(`šŸ“Š Average completeness: ${results.summary.averageCompleteness}%`);
411
+ console.log(`šŸ“ Files exported: ${exportedFiles.length}`);
412
+
413
+ console.log('\nšŸŽ‰ Export completed successfully!');
414
+
415
+ return results;
416
+ }
417
+ }
418
+
419
+ // Run the exporter if called directly
420
+ if (require.main === module) {
421
+ const exporter = new MissingKeysExporter();
422
+ exporter.parseArgs();
423
+ exporter.export().catch(error => {
424
+ console.error('āŒ Export failed:', error.message);
425
+ if (exporter.verbose) {
426
+ console.error(error.stack);
427
+ }
428
+ process.exit(1);
429
+ });
430
+ }
431
+
432
+ module.exports = MissingKeysExporter;
@@ -0,0 +1,236 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ // Configuration
5
+ const LOCALES_DIR = path.join(__dirname, '../../ui-locales');
6
+ const PLACEHOLDER = '[NOT TRANSLATED]';
7
+
8
+ /**
9
+ * Load and parse JSON file
10
+ */
11
+ function loadJson(filePath) {
12
+ try {
13
+ const content = fs.readFileSync(filePath, 'utf8');
14
+ return JSON.parse(content);
15
+ } catch (error) {
16
+ console.error(`Error loading ${filePath}:`, error.message);
17
+ return null;
18
+ }
19
+ }
20
+
21
+ /**
22
+ * Save JSON file with proper formatting
23
+ */
24
+ function saveJson(filePath, data) {
25
+ try {
26
+ const jsonString = JSON.stringify(data, null, 2);
27
+ fs.writeFileSync(filePath, jsonString, 'utf8');
28
+ return true;
29
+ } catch (error) {
30
+ console.error(`Error saving ${filePath}:`, error.message);
31
+ return false;
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Get all keys from all locale files to create a master structure
37
+ */
38
+ function getAllKeysFromAllFiles() {
39
+ const allKeys = new Set();
40
+
41
+ // Get all JSON files
42
+ const files = fs.readdirSync(LOCALES_DIR)
43
+ .filter(file => file.endsWith('.json'))
44
+ .sort();
45
+
46
+ console.log(`šŸ“ Scanning ${files.length} files for all possible keys...`);
47
+
48
+ for (const file of files) {
49
+ const filePath = path.join(LOCALES_DIR, file);
50
+ const data = loadJson(filePath);
51
+
52
+ if (data) {
53
+ const keys = extractAllKeys(data);
54
+ keys.forEach(key => allKeys.add(key));
55
+ console.log(` ${file}: ${keys.length} keys found`);
56
+ }
57
+ }
58
+
59
+ console.log(`šŸ” Total unique keys found: ${allKeys.size}`);
60
+ return Array.from(allKeys).sort();
61
+ }
62
+
63
+ /**
64
+ * Extract all keys from an object recursively
65
+ */
66
+ function extractAllKeys(obj, prefix = '') {
67
+ const keys = [];
68
+
69
+ for (const [key, value] of Object.entries(obj)) {
70
+ const fullKey = prefix ? `${prefix}.${key}` : key;
71
+
72
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
73
+ keys.push(...extractAllKeys(value, fullKey));
74
+ } else {
75
+ keys.push(fullKey);
76
+ }
77
+ }
78
+
79
+ return keys;
80
+ }
81
+
82
+ /**
83
+ * Get value from nested object using dot notation
84
+ */
85
+ function getValue(obj, path) {
86
+ return path.split('.').reduce((current, key) => {
87
+ return current && current[key] !== undefined ? current[key] : undefined;
88
+ }, obj);
89
+ }
90
+
91
+ /**
92
+ * Set value in nested object using dot notation
93
+ */
94
+ function setValue(obj, path, value) {
95
+ const keys = path.split('.');
96
+ const lastKey = keys.pop();
97
+
98
+ const target = keys.reduce((current, key) => {
99
+ if (!current[key] || typeof current[key] !== 'object') {
100
+ current[key] = {};
101
+ }
102
+ return current[key];
103
+ }, obj);
104
+
105
+ target[lastKey] = value;
106
+ }
107
+
108
+ /**
109
+ * Create a complete structure with all keys
110
+ */
111
+ function createCompleteStructure(allKeys, sourceData = {}) {
112
+ const result = {};
113
+
114
+ for (const key of allKeys) {
115
+ const existingValue = getValue(sourceData, key);
116
+
117
+ if (existingValue !== undefined &&
118
+ existingValue !== '' &&
119
+ existingValue !== PLACEHOLDER &&
120
+ typeof existingValue === 'string' &&
121
+ existingValue.trim() !== '') {
122
+ setValue(result, key, existingValue);
123
+ } else {
124
+ setValue(result, key, PLACEHOLDER);
125
+ }
126
+ }
127
+
128
+ return result;
129
+ }
130
+
131
+ /**
132
+ * Remove empty objects recursively
133
+ */
134
+ function removeEmptyObjects(obj) {
135
+ const result = {};
136
+
137
+ for (const [key, value] of Object.entries(obj)) {
138
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
139
+ const cleaned = removeEmptyObjects(value);
140
+ if (Object.keys(cleaned).length > 0) {
141
+ result[key] = cleaned;
142
+ }
143
+ } else if (value !== undefined) {
144
+ result[key] = value;
145
+ }
146
+ }
147
+
148
+ return result;
149
+ }
150
+
151
+ /**
152
+ * Main normalization function
153
+ */
154
+ function finalNormalize() {
155
+ console.log('šŸ”„ Starting final normalization of all locale files...');
156
+
157
+ // Get all possible keys from all files
158
+ const allKeys = getAllKeysFromAllFiles();
159
+
160
+ if (allKeys.length === 0) {
161
+ console.error('āŒ No keys found in any locale files');
162
+ return;
163
+ }
164
+
165
+ // Get all locale files
166
+ const localeFiles = fs.readdirSync(LOCALES_DIR)
167
+ .filter(file => file.endsWith('.json'))
168
+ .sort();
169
+
170
+ console.log(`\nšŸ”„ Normalizing ${localeFiles.length} locale files...`);
171
+
172
+ let processedCount = 0;
173
+
174
+ // Process each locale file
175
+ for (const file of localeFiles) {
176
+ console.log(`\nšŸ“ Processing ${file}...`);
177
+
178
+ const filePath = path.join(LOCALES_DIR, file);
179
+ const originalData = loadJson(filePath);
180
+
181
+ if (!originalData) {
182
+ console.error(`āŒ Skipping ${file} due to load error`);
183
+ continue;
184
+ }
185
+
186
+ // Create complete structure
187
+ const normalizedData = createCompleteStructure(allKeys, originalData);
188
+
189
+ // Remove any empty objects
190
+ const cleanedData = removeEmptyObjects(normalizedData);
191
+
192
+ // Count translations
193
+ const totalKeys = allKeys.length;
194
+ const translatedKeys = allKeys.filter(key => {
195
+ const value = getValue(cleanedData, key);
196
+ return value &&
197
+ typeof value === 'string' &&
198
+ value !== PLACEHOLDER &&
199
+ value.trim() !== '';
200
+ }).length;
201
+
202
+ console.log(` āœ… Structure normalized: ${translatedKeys}/${totalKeys} keys translated`);
203
+
204
+ // Save the normalized file
205
+ if (saveJson(filePath, cleanedData)) {
206
+ console.log(` šŸ’¾ ${file} normalized successfully`);
207
+ processedCount++;
208
+ } else {
209
+ console.error(` āŒ Failed to save ${file}`);
210
+ }
211
+ }
212
+
213
+ console.log(`\nšŸŽ‰ Final normalization completed! Processed ${processedCount}/${localeFiles.length} files`);
214
+
215
+ // Show final file sizes
216
+ console.log('\nšŸ“Š Final file sizes:');
217
+ for (const file of localeFiles) {
218
+ const filePath = path.join(LOCALES_DIR, file);
219
+ try {
220
+ const content = fs.readFileSync(filePath, 'utf8');
221
+ const lines = content.split('\n').length;
222
+ console.log(` ${file}: ${lines} lines`);
223
+ } catch (error) {
224
+ console.log(` ${file}: Error reading file`);
225
+ }
226
+ }
227
+
228
+ console.log(`\nāœ… All files now have consistent structure with ${allKeys.length} total keys`);
229
+ }
230
+
231
+ // Run the final normalization
232
+ if (require.main === module) {
233
+ finalNormalize();
234
+ }
235
+
236
+ module.exports = { finalNormalize };