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.
- package/CHANGELOG.md +401 -0
- package/LICENSE +21 -0
- package/README.md +507 -0
- package/dev/README.md +37 -0
- package/dev/debug/README.md +30 -0
- package/dev/debug/complete-console-translations.js +295 -0
- package/dev/debug/console-key-checker.js +408 -0
- package/dev/debug/console-translations.js +335 -0
- package/dev/debug/debugger.js +408 -0
- package/dev/debug/export-missing-keys.js +432 -0
- package/dev/debug/final-normalize.js +236 -0
- package/dev/debug/find-extra-keys.js +68 -0
- package/dev/debug/normalize-locales.js +153 -0
- package/dev/debug/refactor-locales.js +240 -0
- package/dev/debug/reorder-locales.js +85 -0
- package/dev/debug/replace-hardcoded-console.js +378 -0
- package/docs/INSTALLATION.md +449 -0
- package/docs/README.md +222 -0
- package/docs/TODO_ROADMAP.md +279 -0
- package/docs/api/API_REFERENCE.md +377 -0
- package/docs/api/COMPONENTS.md +492 -0
- package/docs/api/CONFIGURATION.md +651 -0
- package/docs/api/NPM_PUBLISHING_GUIDE.md +434 -0
- package/docs/debug/DEBUG_README.md +30 -0
- package/docs/debug/DEBUG_TOOLS.md +494 -0
- package/docs/development/AGENTS.md +351 -0
- package/docs/development/DEVELOPMENT_RULES.md +165 -0
- package/docs/development/DEV_README.md +37 -0
- package/docs/release-notes/RELEASE_NOTES_v1.0.0.md +173 -0
- package/docs/release-notes/RELEASE_NOTES_v1.6.0.md +141 -0
- package/docs/release-notes/RELEASE_NOTES_v1.6.1.md +185 -0
- package/docs/release-notes/RELEASE_NOTES_v1.6.3.md +199 -0
- package/docs/reports/ANALYSIS_README.md +17 -0
- package/docs/reports/CONSOLE_MISMATCH_BUG_REPORT_v1.5.0.md +181 -0
- package/docs/reports/SIZING_README.md +18 -0
- package/docs/reports/SUMMARY_README.md +18 -0
- package/docs/reports/TRANSLATION_BUG_REPORT_v1.5.0.md +129 -0
- package/docs/reports/USAGE_README.md +18 -0
- package/docs/reports/VALIDATION_README.md +18 -0
- package/locales/de/auth.json +3 -0
- package/locales/de/common.json +16 -0
- package/locales/de/pagination.json +6 -0
- package/locales/en/auth.json +3 -0
- package/locales/en/common.json +16 -0
- package/locales/en/pagination.json +6 -0
- package/locales/es/auth.json +3 -0
- package/locales/es/common.json +16 -0
- package/locales/es/pagination.json +6 -0
- package/locales/fr/auth.json +3 -0
- package/locales/fr/common.json +16 -0
- package/locales/fr/pagination.json +6 -0
- package/locales/ru/auth.json +3 -0
- package/locales/ru/common.json +16 -0
- package/locales/ru/pagination.json +6 -0
- package/main/i18ntk-analyze.js +625 -0
- package/main/i18ntk-autorun.js +461 -0
- package/main/i18ntk-complete.js +494 -0
- package/main/i18ntk-init.js +686 -0
- package/main/i18ntk-manage.js +848 -0
- package/main/i18ntk-sizing.js +557 -0
- package/main/i18ntk-summary.js +671 -0
- package/main/i18ntk-usage.js +1282 -0
- package/main/i18ntk-validate.js +762 -0
- package/main/ui-i18n.js +332 -0
- package/package.json +152 -0
- package/scripts/fix-missing-translation-keys.js +214 -0
- package/scripts/verify-package.js +168 -0
- package/ui-locales/de.json +637 -0
- package/ui-locales/en.json +688 -0
- package/ui-locales/es.json +637 -0
- package/ui-locales/fr.json +637 -0
- package/ui-locales/ja.json +637 -0
- package/ui-locales/ru.json +637 -0
- package/ui-locales/zh.json +637 -0
- package/utils/admin-auth.js +317 -0
- package/utils/admin-cli.js +353 -0
- package/utils/admin-pin.js +409 -0
- package/utils/detect-language-mismatches.js +454 -0
- package/utils/i18n-helper.js +128 -0
- package/utils/maintain-language-purity.js +433 -0
- package/utils/native-translations.js +478 -0
- package/utils/security.js +384 -0
- package/utils/test-complete-system.js +356 -0
- package/utils/test-console-i18n.js +402 -0
- package/utils/translate-mismatches.js +571 -0
- package/utils/validate-language-purity.js +531 -0
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Complete Console Translations
|
|
5
|
+
* Adds missing translation keys to all ui-locales language files
|
|
6
|
+
* to ensure 100% translation coverage for the i18n-management-toolkit package.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const ConsoleTranslationsChecker = require('./console-translations');
|
|
12
|
+
const UIi18n = require('../../main/ui-i18n');
|
|
13
|
+
|
|
14
|
+
class ConsoleTranslationsCompleter {
|
|
15
|
+
constructor() {
|
|
16
|
+
this.uiLocalesDir = path.join(__dirname, '..', '..', 'ui-locales');
|
|
17
|
+
this.referenceLanguage = 'en';
|
|
18
|
+
this.supportedLanguages = ['de', 'es', 'fr', 'ja', 'ru', 'zh'];
|
|
19
|
+
this.dryRun = process.argv.includes('--dry-run');
|
|
20
|
+
this.results = {
|
|
21
|
+
totalKeysAdded: 0,
|
|
22
|
+
languagesProcessed: 0,
|
|
23
|
+
changes: {}
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Load and parse a JSON translation file
|
|
29
|
+
*/
|
|
30
|
+
loadTranslationFile(language) {
|
|
31
|
+
const filePath = path.join(this.uiLocalesDir, `${language}.json`);
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
if (!fs.existsSync(filePath)) {
|
|
35
|
+
throw new Error(`Translation file not found: ${filePath}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
39
|
+
return JSON.parse(content);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
console.log(`ā Error loading ${language}.json: ${error.message}`);
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Save translation file with proper formatting
|
|
48
|
+
*/
|
|
49
|
+
saveTranslationFile(language, data) {
|
|
50
|
+
const filePath = path.join(this.uiLocalesDir, `${language}.json`);
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const content = JSON.stringify(data, null, 2);
|
|
54
|
+
|
|
55
|
+
if (this.dryRun) {
|
|
56
|
+
console.log(` š§Ŗ DRY RUN: Would save ${language}.json`);
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
61
|
+
return true;
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.log(`ā Error saving ${language}.json: ${error.message}`);
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Get nested object value by dot notation path
|
|
70
|
+
*/
|
|
71
|
+
getNestedValue(obj, path) {
|
|
72
|
+
return path.split('.').reduce((current, key) => {
|
|
73
|
+
return current && current[key] !== undefined ? current[key] : undefined;
|
|
74
|
+
}, obj);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Set nested object value by dot notation path
|
|
79
|
+
*/
|
|
80
|
+
setNestedValue(obj, path, value) {
|
|
81
|
+
const keys = path.split('.');
|
|
82
|
+
const lastKey = keys.pop();
|
|
83
|
+
|
|
84
|
+
const target = keys.reduce((current, key) => {
|
|
85
|
+
if (!current[key] || typeof current[key] !== 'object') {
|
|
86
|
+
current[key] = {};
|
|
87
|
+
}
|
|
88
|
+
return current[key];
|
|
89
|
+
}, obj);
|
|
90
|
+
|
|
91
|
+
target[lastKey] = value;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Generate placeholder translation for missing keys
|
|
96
|
+
*/
|
|
97
|
+
generatePlaceholderTranslation(key, referenceValue, targetLanguage) {
|
|
98
|
+
// Use consistent placeholder prefix for all languages
|
|
99
|
+
const prefix = '[NOT TRANSLATED]';
|
|
100
|
+
|
|
101
|
+
// For simple strings, add consistent prefix
|
|
102
|
+
if (typeof referenceValue === 'string') {
|
|
103
|
+
// Keep emojis and special characters, but mark as needing translation
|
|
104
|
+
if (referenceValue.length < 50) {
|
|
105
|
+
return `${prefix} ${referenceValue}`;
|
|
106
|
+
} else {
|
|
107
|
+
// For longer strings, just use the prefix
|
|
108
|
+
return `${prefix} ${referenceValue.substring(0, 30)}...`;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// For arrays, copy structure but mark items
|
|
113
|
+
if (Array.isArray(referenceValue)) {
|
|
114
|
+
return referenceValue.map(item =>
|
|
115
|
+
typeof item === 'string' ? `${prefix} ${item}` : item
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// For other types, return as-is
|
|
120
|
+
return referenceValue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Complete translations for a specific language
|
|
125
|
+
*/
|
|
126
|
+
completeLanguage(language, missingKeys, referenceData) {
|
|
127
|
+
console.log(`š Processing ${language.toUpperCase()}...`);
|
|
128
|
+
|
|
129
|
+
const languageData = this.loadTranslationFile(language);
|
|
130
|
+
if (!languageData) {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let keysAdded = 0;
|
|
135
|
+
const changes = [];
|
|
136
|
+
|
|
137
|
+
for (const key of missingKeys) {
|
|
138
|
+
const referenceValue = this.getNestedValue(referenceData, key);
|
|
139
|
+
if (referenceValue !== undefined) {
|
|
140
|
+
const placeholderValue = this.generatePlaceholderTranslation(key, referenceValue, language);
|
|
141
|
+
this.setNestedValue(languageData, key, placeholderValue);
|
|
142
|
+
keysAdded++;
|
|
143
|
+
changes.push(key);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (keysAdded > 0) {
|
|
148
|
+
const success = this.saveTranslationFile(language, languageData);
|
|
149
|
+
if (success) {
|
|
150
|
+
console.log(` ā
Added ${keysAdded} missing keys`);
|
|
151
|
+
this.results.changes[language] = changes;
|
|
152
|
+
this.results.totalKeysAdded += keysAdded;
|
|
153
|
+
this.results.languagesProcessed++;
|
|
154
|
+
|
|
155
|
+
// Show sample of added keys
|
|
156
|
+
const sampleKeys = changes.slice(0, 3);
|
|
157
|
+
console.log(` š Sample: ${sampleKeys.join(', ')}`);
|
|
158
|
+
if (changes.length > 3) {
|
|
159
|
+
console.log(` š ... and ${changes.length - 3} more`);
|
|
160
|
+
}
|
|
161
|
+
} else {
|
|
162
|
+
console.log(` ā Failed to save changes`);
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
} else {
|
|
166
|
+
console.log(` ā
No missing keys found`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Run the completion process
|
|
174
|
+
*/
|
|
175
|
+
async run() {
|
|
176
|
+
console.log('š§ CONSOLE TRANSLATIONS COMPLETION');
|
|
177
|
+
console.log('============================================================');
|
|
178
|
+
console.log(`š UI Locales directory: ${this.uiLocalesDir}`);
|
|
179
|
+
console.log(`š¤ Reference language: ${this.referenceLanguage}`);
|
|
180
|
+
|
|
181
|
+
if (this.dryRun) {
|
|
182
|
+
console.log('š§Ŗ DRY RUN MODE - No files will be modified');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
console.log('');
|
|
186
|
+
|
|
187
|
+
// First, run analysis to get missing keys
|
|
188
|
+
console.log('š Analyzing current translation status...');
|
|
189
|
+
const checker = new ConsoleTranslationsChecker();
|
|
190
|
+
const analysisSuccess = await checker.run();
|
|
191
|
+
|
|
192
|
+
if (!analysisSuccess) {
|
|
193
|
+
console.log('ā Failed to analyze translations');
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Load reference data
|
|
198
|
+
const referenceData = this.loadTranslationFile(this.referenceLanguage);
|
|
199
|
+
if (!referenceData) {
|
|
200
|
+
console.log(`ā Cannot load reference language: ${this.referenceLanguage}`);
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
console.log('');
|
|
205
|
+
console.log('š COMPLETING MISSING TRANSLATIONS');
|
|
206
|
+
console.log('============================================================');
|
|
207
|
+
|
|
208
|
+
// Get missing keys from the analysis
|
|
209
|
+
const missingKeysData = checker.results.missingKeys;
|
|
210
|
+
|
|
211
|
+
// Process each language
|
|
212
|
+
for (const language of this.supportedLanguages) {
|
|
213
|
+
const missingKeys = missingKeysData[language] || [];
|
|
214
|
+
|
|
215
|
+
if (missingKeys.length === 0) {
|
|
216
|
+
console.log(`ā
${language.toUpperCase()}: Already complete`);
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
console.log(`š ${language.toUpperCase()}: ${missingKeys.length} missing keys`);
|
|
221
|
+
const success = this.completeLanguage(language, missingKeys, referenceData);
|
|
222
|
+
|
|
223
|
+
if (!success) {
|
|
224
|
+
console.log(`ā Failed to complete ${language}`);
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Generate summary
|
|
230
|
+
this.generateSummary();
|
|
231
|
+
|
|
232
|
+
// Run final verification
|
|
233
|
+
if (!this.dryRun && this.results.totalKeysAdded > 0) {
|
|
234
|
+
console.log('');
|
|
235
|
+
console.log('š VERIFICATION - Running final analysis...');
|
|
236
|
+
console.log('============================================================');
|
|
237
|
+
|
|
238
|
+
const finalChecker = new ConsoleTranslationsChecker();
|
|
239
|
+
await finalChecker.run();
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return true;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Generate completion summary
|
|
247
|
+
*/
|
|
248
|
+
generateSummary() {
|
|
249
|
+
console.log('');
|
|
250
|
+
console.log('š COMPLETION SUMMARY');
|
|
251
|
+
console.log('============================================================');
|
|
252
|
+
console.log(`ā
Total keys added: ${this.results.totalKeysAdded}`);
|
|
253
|
+
console.log(`š Languages processed: ${this.results.languagesProcessed}/${this.supportedLanguages.length}`);
|
|
254
|
+
|
|
255
|
+
if (this.results.totalKeysAdded > 0) {
|
|
256
|
+
console.log('');
|
|
257
|
+
console.log('š CHANGES BY LANGUAGE:');
|
|
258
|
+
console.log('------------------------------------------------------------');
|
|
259
|
+
|
|
260
|
+
for (const [language, changes] of Object.entries(this.results.changes)) {
|
|
261
|
+
console.log(`š¤ ${language.toUpperCase()}: ${changes.length} keys added`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
console.log('');
|
|
266
|
+
console.log('š” NEXT STEPS:');
|
|
267
|
+
console.log('------------------------------------------------------------');
|
|
268
|
+
|
|
269
|
+
if (this.dryRun) {
|
|
270
|
+
console.log('š§ Run without --dry-run to apply changes:');
|
|
271
|
+
console.log(' node complete-console-translations.js');
|
|
272
|
+
} else if (this.results.totalKeysAdded > 0) {
|
|
273
|
+
console.log('š Translation placeholders have been added!');
|
|
274
|
+
console.log('š Next steps:');
|
|
275
|
+
console.log('1. Review and translate the placeholder values');
|
|
276
|
+
console.log('2. Replace [DE], [ES], [FR], [JA], [RU], [ZH] prefixes with actual translations');
|
|
277
|
+
console.log('3. Run console-translations.js to verify 100% completion');
|
|
278
|
+
} else {
|
|
279
|
+
console.log('š All translations are already complete!');
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Run the completer if called directly
|
|
285
|
+
if (require.main === module) {
|
|
286
|
+
const completer = new ConsoleTranslationsCompleter();
|
|
287
|
+
completer.run().then(success => {
|
|
288
|
+
process.exit(success ? 0 : 1);
|
|
289
|
+
}).catch(error => {
|
|
290
|
+
console.error('ā Fatal error:', error.message);
|
|
291
|
+
process.exit(1);
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
module.exports = ConsoleTranslationsCompleter;
|
|
@@ -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}.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}.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;
|