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,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 };
|