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,671 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const settingsManager = require('../settings/settings-manager');
6
+ const SecurityUtils = require('../utils/security');
7
+ const UIi18n = require('./ui-i18n');
8
+
9
+ // Get configuration from settings manager
10
+ function getConfig() {
11
+ const settings = settingsManager.getSettings();
12
+ return {
13
+ sourceLanguage: settings.directories?.sourceLanguage || 'en',
14
+ excludeFiles: settings.processing?.excludeFiles || ['index.js', 'index.ts', '.DS_Store'],
15
+ supportedExtensions: settings.processing?.supportedExtensions || ['.json', '.js', '.ts'],
16
+ sourceDir: settings.directories?.sourceDir || null,
17
+ uiLanguage: settings.language || 'en'
18
+ };
19
+ }
20
+
21
+ /**
22
+ * I18N SUMMARY REPORT GENERATOR
23
+ *
24
+ * Analyzes the i18n folder structure and generates a comprehensive summary report
25
+ * including key statistics, file structure analysis, and validation checks.
26
+ *
27
+ * This script is designed to be generic and work with any i18n project structure.
28
+ */
29
+ class I18nSummaryReporter {
30
+ constructor() {
31
+ this.config = getConfig();
32
+ this.ui = new UIi18n();
33
+ this.t = this.ui.t.bind(this.ui);
34
+ this.stats = {
35
+ languages: [],
36
+ totalFiles: 0,
37
+ totalKeys: 0,
38
+ keysByLanguage: {},
39
+ filesByLanguage: {},
40
+ missingFiles: [],
41
+ inconsistentKeys: [],
42
+ emptyFiles: [],
43
+ malformedFiles: [],
44
+ duplicateKeys: []
45
+ };
46
+ }
47
+
48
+ // Parse command line arguments
49
+ parseArgs() {
50
+ const args = process.argv.slice(2);
51
+ const parsed = {
52
+ sourceDir: null,
53
+ outputFile: null,
54
+ verbose: false,
55
+ help: false,
56
+ keepReports: false,
57
+ deleteReports: false
58
+ };
59
+
60
+ for (let i = 0; i < args.length; i++) {
61
+ const arg = args[i];
62
+
63
+ if (arg === '--help' || arg === '-h') {
64
+ parsed.help = true;
65
+ } else if (arg === '--verbose' || arg === '-v') {
66
+ parsed.verbose = true;
67
+ } else if (arg === '--source-dir' || arg === '-s') {
68
+ parsed.sourceDir = args[++i];
69
+ } else if (arg === '--output' || arg === '-o') {
70
+ parsed.outputFile = args[++i];
71
+ } else if (arg === '--keep-reports') {
72
+ parsed.keepReports = true;
73
+ } else if (arg === '--delete-reports') {
74
+ parsed.deleteReports = true;
75
+ }
76
+ }
77
+
78
+ return parsed;
79
+ }
80
+
81
+ // Show help information
82
+ showHelp() {
83
+ console.log(`\n${this.t('summary.helpTitle')}\n`);
84
+ console.log(this.t('summary.helpDescription') + '\n');
85
+ console.log(this.t('summary.helpUsage') + '\n');
86
+ console.log(this.t('summary.helpOptions'));
87
+ console.log(this.t('summary.helpSourceDir'));
88
+ console.log(this.t('summary.helpOutput'));
89
+ console.log(this.t('summary.helpVerbose'));
90
+ console.log(this.t('summary.helpKeepReports'));
91
+ console.log(this.t('summary.helpDeleteReports'));
92
+ console.log(this.t('summary.helpHelp') + '\n');
93
+ console.log(this.t('summary.helpExamples'));
94
+ console.log(this.t('summary.helpExample1'));
95
+ console.log(this.t('summary.helpExample2'));
96
+ console.log(this.t('summary.helpExample3'));
97
+ console.log(this.t('summary.helpExample4'));
98
+ console.log(this.t('summary.helpExample5') + '\n');
99
+ }
100
+
101
+ // Auto-detect i18n directory
102
+ detectI18nDirectory() {
103
+ const possiblePaths = [
104
+ 'locales',
105
+ 'i18ntk-reports',
106
+ 'src/i18n/locales',
107
+ 'src/locales',
108
+ 'src/i18n',
109
+ 'i18n/locales',
110
+ 'i18n',
111
+ 'public/locales',
112
+ 'assets/i18n',
113
+ 'translations'
114
+ ];
115
+
116
+ for (const possiblePath of possiblePaths) {
117
+ const fullPath = path.resolve(possiblePath);
118
+ if (fs.existsSync(fullPath)) {
119
+ const contents = fs.readdirSync(fullPath);
120
+ // Check if it contains language directories
121
+ const hasLanguageDirs = contents.some(item => {
122
+ const itemPath = path.join(fullPath, item);
123
+ return fs.statSync(itemPath).isDirectory() &&
124
+ item.length === 2 && // Language codes are typically 2 characters
125
+ /^[a-z]{2}$/.test(item);
126
+ });
127
+
128
+ if (hasLanguageDirs) {
129
+ return fullPath;
130
+ }
131
+ }
132
+ }
133
+
134
+ return null;
135
+ }
136
+
137
+ // Get all available languages
138
+ getAvailableLanguages() {
139
+ if (!fs.existsSync(this.config.sourceDir)) {
140
+ return [];
141
+ }
142
+
143
+ return fs.readdirSync(this.config.sourceDir)
144
+ .filter(item => {
145
+ const itemPath = path.join(this.config.sourceDir, item);
146
+ return fs.statSync(itemPath).isDirectory() &&
147
+ !item.startsWith('.') &&
148
+ item !== 'node_modules';
149
+ })
150
+ .sort();
151
+ }
152
+
153
+ // Get all translation files for a language
154
+ getLanguageFiles(language) {
155
+ const languageDir = path.join(this.config.sourceDir, language);
156
+
157
+ if (!fs.existsSync(languageDir)) {
158
+ return [];
159
+ }
160
+
161
+ return fs.readdirSync(languageDir)
162
+ .filter(file => {
163
+ return this.config.supportedExtensions.some(ext => file.endsWith(ext)) &&
164
+ !this.config.excludeFiles.includes(file);
165
+ })
166
+ .sort();
167
+ }
168
+
169
+ // Extract keys from a translation file
170
+ async extractKeysFromFile(filePath) {
171
+ try {
172
+ const content = await SecurityUtils.safeReadFile(filePath, this.config.sourceDir);
173
+ if (!content) {
174
+ console.warn(this.t('summary.couldNotReadFile', { filePath }));
175
+ return [];
176
+ }
177
+
178
+ if (filePath.endsWith('.json')) {
179
+ const data = JSON.parse(content);
180
+ return this.extractKeysFromObject(data);
181
+ } else if (filePath.endsWith('.js') || filePath.endsWith('.ts')) {
182
+ // Basic extraction for JS/TS files (assumes export default or module.exports)
183
+ const match = content.match(/(?:export\s+default|module\.exports\s*=)\s*({[\s\S]*})/);;
184
+ if (match) {
185
+ const objStr = match[1];
186
+ // This is a simplified approach - in production, you might want to use a proper JS parser
187
+ try {
188
+ const data = eval(`(${objStr})`);
189
+ return this.extractKeysFromObject(data);
190
+ } catch (e) {
191
+ console.warn(this.t('summary.couldNotParseJSFile', { filePath }));
192
+ return [];
193
+ }
194
+ }
195
+ }
196
+
197
+ return [];
198
+ } catch (error) {
199
+ console.warn(this.t('summary.errorReadingFile', { filePath, error: error.message }));
200
+ return [];
201
+ }
202
+ }
203
+
204
+ // Extract keys recursively from an object
205
+ extractKeysFromObject(obj, prefix = '') {
206
+ const keys = [];
207
+
208
+ for (const [key, value] of Object.entries(obj)) {
209
+ const fullKey = prefix ? `${prefix}.${key}` : key;
210
+
211
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
212
+ keys.push(...this.extractKeysFromObject(value, fullKey));
213
+ } else {
214
+ keys.push(fullKey);
215
+ }
216
+ }
217
+
218
+ return keys;
219
+ }
220
+
221
+ // Check if file is empty or has no meaningful content
222
+ async isFileEmpty(filePath) {
223
+ try {
224
+ const content = await SecurityUtils.safeReadFile(filePath, this.config.sourceDir);
225
+ if (!content) return true;
226
+ const trimmedContent = content.trim();
227
+ if (!trimmedContent) return true;
228
+
229
+ if (filePath.endsWith('.json')) {
230
+ const data = SecurityUtils.safeParseJSON(trimmedContent);
231
+ if (!data) return true;
232
+ return Object.keys(data).length === 0;
233
+ }
234
+
235
+ return false;
236
+ } catch (error) {
237
+ return false; // If we can't parse it, it's not empty, just malformed
238
+ }
239
+ }
240
+
241
+ // Check if file is malformed
242
+ async isFileMalformed(filePath) {
243
+ try {
244
+ const content = await SecurityUtils.safeReadFile(filePath, this.config.sourceDir);
245
+ if (!content) return true;
246
+
247
+ if (filePath.endsWith('.json')) {
248
+ const parsed = SecurityUtils.safeParseJSON(content);
249
+ if (!parsed) return true;
250
+ }
251
+
252
+ return false;
253
+ } catch (error) {
254
+ return true;
255
+ }
256
+ }
257
+
258
+ // Find duplicate keys within a file
259
+ async findDuplicateKeys(filePath) {
260
+ try {
261
+ const content = await SecurityUtils.safeReadFile(filePath, this.config.sourceDir);
262
+ if (!content) return [];
263
+
264
+ if (filePath.endsWith('.json')) {
265
+ // For JSON files, check for duplicate keys in the raw content
266
+ const keys = [];
267
+ const keyRegex = /"([^"]+)"\s*:/g;
268
+ let match;
269
+
270
+ while ((match = keyRegex.exec(content)) !== null) {
271
+ keys.push(match[1]);
272
+ }
273
+
274
+ const duplicates = keys.filter((key, index) => keys.indexOf(key) !== index);
275
+ return [...new Set(duplicates)];
276
+ }
277
+
278
+ return [];
279
+ } catch (error) {
280
+ return [];
281
+ }
282
+ }
283
+
284
+ // Analyze folder structure and collect statistics
285
+ async analyzeStructure() {
286
+ console.log(this.t('summary.analyzingFolder'));
287
+
288
+ this.stats.languages = this.getAvailableLanguages();
289
+
290
+ if (this.stats.languages.length === 0) {
291
+ console.log(this.t('summary.noLanguageDirectoriesFound'));
292
+ return;
293
+ }
294
+
295
+ console.log(this.t('summary.foundLanguages', {count: this.stats.languages.length, languages: this.stats.languages.join(', ')}));
296
+
297
+ // Use first language as reference for file structure
298
+ const referenceLanguage = this.stats.languages[0];
299
+ const referenceFiles = this.getLanguageFiles(referenceLanguage);
300
+
301
+ console.log(this.t('summary.referenceLanguageFiles', {language: referenceLanguage, count: referenceFiles.length}));
302
+
303
+ // Analyze each language
304
+ for (const language of this.stats.languages) {
305
+ console.log(this.t('summary.analyzingLanguage', {language}));
306
+
307
+ const files = this.getLanguageFiles(language);
308
+ this.stats.filesByLanguage[language] = files;
309
+ this.stats.keysByLanguage[language] = {};
310
+
311
+ let totalKeysForLanguage = 0;
312
+
313
+ // Check for missing files compared to reference
314
+ const missingFiles = referenceFiles.filter(file => !files.includes(file));
315
+ if (missingFiles.length > 0) {
316
+ this.stats.missingFiles.push({
317
+ language,
318
+ files: missingFiles
319
+ });
320
+ }
321
+
322
+ // Analyze each file
323
+ for (const file of files) {
324
+ const filePath = path.join(this.config.sourceDir, language, file);
325
+
326
+ // Check if file is empty
327
+ if (await this.isFileEmpty(filePath)) {
328
+ this.stats.emptyFiles.push({ language, file });
329
+ }
330
+
331
+ // Check if file is malformed
332
+ if (await this.isFileMalformed(filePath)) {
333
+ this.stats.malformedFiles.push({ language, file });
334
+ continue;
335
+ }
336
+
337
+ // Find duplicate keys
338
+ const duplicates = await this.findDuplicateKeys(filePath);
339
+ if (duplicates.length > 0) {
340
+ this.stats.duplicateKeys.push({ language, file, duplicates });
341
+ }
342
+
343
+ // Extract keys
344
+ const keys = await this.extractKeysFromFile(filePath);
345
+ this.stats.keysByLanguage[language][file] = keys;
346
+ totalKeysForLanguage += keys.length;
347
+
348
+ this.stats.totalFiles++;
349
+ }
350
+
351
+ this.stats.totalKeys += totalKeysForLanguage;
352
+ console.log(this.t('summary.keysInFiles', {keys: totalKeysForLanguage, files: files.length}));
353
+ }
354
+
355
+ // Find inconsistent keys across languages
356
+ this.findInconsistentKeys();
357
+ }
358
+
359
+ // Find keys that are inconsistent across languages
360
+ findInconsistentKeys() {
361
+ console.log(this.t('summary.checkingInconsistentKeys'));
362
+
363
+ const referenceLanguage = this.stats.languages[0];
364
+ const referenceKeys = this.stats.keysByLanguage[referenceLanguage];
365
+
366
+ for (const file of Object.keys(referenceKeys)) {
367
+ const refKeys = new Set(referenceKeys[file]);
368
+
369
+ for (const language of this.stats.languages.slice(1)) {
370
+ const langKeys = this.stats.keysByLanguage[language][file] || [];
371
+ const langKeySet = new Set(langKeys);
372
+
373
+ // Find missing keys in this language
374
+ const missingInLang = [...refKeys].filter(key => !langKeySet.has(key));
375
+ // Find extra keys in this language
376
+ const extraInLang = [...langKeySet].filter(key => !refKeys.has(key));
377
+
378
+ if (missingInLang.length > 0 || extraInLang.length > 0) {
379
+ this.stats.inconsistentKeys.push({
380
+ file,
381
+ language,
382
+ missing: missingInLang,
383
+ extra: extraInLang
384
+ });
385
+ }
386
+ }
387
+ }
388
+ }
389
+
390
+ // Generate summary report
391
+ generateReport() {
392
+ const report = [];
393
+ const timestamp = new Date().toISOString();
394
+
395
+ report.push(this.t('summary.reportTitle'));
396
+ report.push('='.repeat(50));
397
+ report.push(this.t('summary.generated', {timestamp}));
398
+ report.push(this.t('summary.sourceDirectory', {dir: this.config.sourceDir}));
399
+ report.push('');
400
+ // Overview
401
+ report.push(this.t('summary.overview'));
402
+ report.push('='.repeat(30));
403
+ report.push(this.t('summary.languagesCount', {count: this.stats.languages.length}));
404
+ report.push(this.t('summary.totalFiles', {count: this.stats.totalFiles}));
405
+ report.push(this.t('summary.totalKeys', {count: this.stats.totalKeys}));
406
+ report.push(this.t('summary.avgKeysPerLanguage', {count: Math.round(this.stats.totalKeys / this.stats.languages.length)}));
407
+ report.push('');
408
+ // Languages breakdown
409
+ report.push(this.t('summary.languagesBreakdown'));
410
+ report.push('='.repeat(30));
411
+ for (const language of this.stats.languages) {
412
+ const files = this.stats.filesByLanguage[language];
413
+ const totalKeys = Object.values(this.stats.keysByLanguage[language])
414
+ .reduce((sum, keys) => sum + keys.length, 0);
415
+ report.push(this.t('summary.languageBreakdown', {language, files: files.length, keys: totalKeys}));
416
+ }
417
+ report.push('');
418
+ // File structure
419
+ report.push(this.t('summary.fileStructure'));
420
+ report.push('='.repeat(30));
421
+ const referenceLanguage = this.stats.languages[0];
422
+ const referenceFiles = this.stats.filesByLanguage[referenceLanguage] || [];
423
+ for (const file of referenceFiles) {
424
+ const keysInFile = this.stats.keysByLanguage[referenceLanguage][file]?.length || 0;
425
+ report.push(this.t('summary.fileKeys', {file, keys: keysInFile}));
426
+ // Show which languages have this file
427
+ const languagesWithFile = this.stats.languages.filter(lang =>
428
+ this.stats.filesByLanguage[lang].includes(file)
429
+ );
430
+ if (languagesWithFile.length < this.stats.languages.length) {
431
+ const missingIn = this.stats.languages.filter(lang =>
432
+ !languagesWithFile.includes(lang)
433
+ );
434
+ report.push(this.t('summary.missingInLanguages', {languages: missingIn.join(', ')}));
435
+ }
436
+ }
437
+ report.push('');
438
+
439
+ // Issues
440
+ if (this.stats.missingFiles.length > 0 ||
441
+ this.stats.emptyFiles.length > 0 ||
442
+ this.stats.malformedFiles.length > 0 ||
443
+ this.stats.duplicateKeys.length > 0 ||
444
+ this.stats.inconsistentKeys.length > 0) {
445
+
446
+ report.push(this.t('summary.issuesFound'));
447
+ report.push('='.repeat(30));
448
+
449
+ if (this.stats.missingFiles.length > 0) {
450
+ report.push(this.t('summary.missingFiles'));
451
+ for (const item of this.stats.missingFiles) {
452
+ report.push(` ${item.language}: ${item.files.join(', ')}`);
453
+ }
454
+ report.push('');
455
+ }
456
+
457
+ if (this.stats.emptyFiles.length > 0) {
458
+ report.push(this.t('summary.emptyFiles'));
459
+ for (const item of this.stats.emptyFiles) {
460
+ report.push(` ${item.language}/${item.file}`);
461
+ }
462
+ report.push('');
463
+ }
464
+
465
+ if (this.stats.malformedFiles.length > 0) {
466
+ report.push(this.t('summary.malformedFiles'));
467
+ for (const item of this.stats.malformedFiles) {
468
+ report.push(` ${item.language}/${item.file}`);
469
+ }
470
+ report.push('');
471
+ }
472
+
473
+ if (this.stats.duplicateKeys.length > 0) {
474
+ report.push(this.t('summary.duplicateKeys'));
475
+ for (const item of this.stats.duplicateKeys) {
476
+ report.push(` ${item.language}/${item.file}: ${item.duplicates.join(', ')}`);
477
+ }
478
+ report.push('');
479
+ }
480
+
481
+ if (this.stats.inconsistentKeys.length > 0) {
482
+ report.push(this.t('summary.inconsistentKeys'));
483
+ for (const item of this.stats.inconsistentKeys) {
484
+ report.push(` ${item.language}/${item.file}:`);
485
+ if (item.missing.length > 0) {
486
+ report.push(this.t('summary.missingKeys', {keys: item.missing.slice(0, 5).join(', '), more: item.missing.length > 5 ? '...' : ''}));
487
+ }
488
+ if (item.extra.length > 0) {
489
+ report.push(this.t('summary.extraKeys', {keys: item.extra.slice(0, 5).join(', '), more: item.extra.length > 5 ? '...' : ''}));
490
+ }
491
+ }
492
+ report.push('');
493
+ }
494
+ } else {
495
+ report.push(this.t('summary.noIssuesFound'));
496
+ report.push('='.repeat(30));
497
+ report.push(this.t('summary.allFilesConsistent'));
498
+ report.push('');
499
+ }
500
+ // Recommendations
501
+ report.push(this.t('summary.recommendations'));
502
+ report.push('='.repeat(30));
503
+ if (this.stats.missingFiles.length > 0) {
504
+ report.push(this.t('summary.createMissingFiles'));
505
+ }
506
+ if (this.stats.emptyFiles.length > 0) {
507
+ report.push(this.t('summary.addContentToEmptyFiles'));
508
+ }
509
+ if (this.stats.malformedFiles.length > 0) {
510
+ report.push(this.t('summary.fixMalformedFiles'));
511
+ }
512
+ if (this.stats.duplicateKeys.length > 0) {
513
+ report.push(this.t('summary.removeDuplicateKeys'));
514
+ }
515
+ if (this.stats.inconsistentKeys.length > 0) {
516
+ report.push(this.t('summary.synchronizeKeys'));
517
+ }
518
+ if (this.stats.totalKeys > 10000) {
519
+ report.push(this.t('summary.splitLargeFiles'));
520
+ }
521
+ if (this.stats.languages.length === 1) {
522
+ report.push(this.t('summary.addMoreLanguages'));
523
+ }
524
+ report.push('');
525
+ report.push(this.t('summary.nextSteps'));
526
+ report.push(this.t('summary.nextStep1'));
527
+ report.push(this.t('summary.nextStep2'));
528
+ report.push(this.t('summary.nextStep3'));
529
+ report.push(this.t('summary.nextStep4'));
530
+ return report.join('\n');
531
+ }
532
+
533
+ // Run the analysis and generate report
534
+ async run() {
535
+ const args = this.parseArgs();
536
+
537
+ if (args.help) {
538
+ this.showHelp();
539
+ return;
540
+ }
541
+
542
+ // Determine source directory
543
+ if (args.sourceDir) {
544
+ this.config.sourceDir = path.resolve(args.sourceDir);
545
+ } else {
546
+ this.config.sourceDir = this.detectI18nDirectory();
547
+ }
548
+
549
+ if (!this.config.sourceDir) {
550
+ console.error(this.t('summary.couldNotFindI18nDirectory'));
551
+ process.exit(1);
552
+ }
553
+
554
+ if (!fs.existsSync(this.config.sourceDir)) {
555
+ console.error(this.t('summary.sourceDirectoryDoesNotExist', { sourceDir: this.config.sourceDir }));
556
+ process.exit(1);
557
+ }
558
+
559
+ console.log(this.t('summary.i18nSummaryReportGenerator'));
560
+ console.log('============================================================');
561
+ console.log(this.t('summary.sourceDirectory', {dir: this.config.sourceDir}));
562
+
563
+ if (args.verbose) {
564
+ console.log(`🔧 Configuration:`);
565
+ console.log(` Source language: ${this.config.sourceLanguage}`);
566
+ console.log(` Supported extensions: ${this.config.supportedExtensions.join(', ')}`);
567
+ console.log(` Excluded files: ${this.config.excludeFiles.join(', ')}`);
568
+ }
569
+
570
+ try {
571
+ // Analyze structure
572
+ await this.analyzeStructure();
573
+
574
+ // Generate report
575
+ console.log(this.t('summary.generatingSummaryReport'));
576
+ const report = this.generateReport();
577
+
578
+ // Output report
579
+ if (args.outputFile) {
580
+ // Always save summary reports to i18ntk-reports
581
+ const reportsDir = path.resolve(process.cwd(), 'i18ntk-reports');
582
+ if (!fs.existsSync(reportsDir)) {
583
+ fs.mkdirSync(reportsDir, { recursive: true });
584
+ }
585
+ const outputFileName = args.outputFile ? path.basename(args.outputFile) : `summary-report-${new Date().toISOString().slice(0,10)}.txt`;
586
+ const outputPath = path.join(reportsDir, outputFileName);
587
+ const success = await SecurityUtils.safeWriteFile(outputPath, report, reportsDir);
588
+ if (success) {
589
+ console.log(this.t('summary.reportSaved', { reportPath: outputPath }));
590
+ } else {
591
+ console.log(this.t('summary.reportSaveFailed', { reportPath: outputPath }));
592
+ }
593
+ } else {
594
+ console.log('\n' + report);
595
+ }
596
+
597
+ // Handle report file management
598
+ if (args.deleteReports && !args.keepReports) {
599
+ console.log(this.t('summary.cleaningUpReportFiles'));
600
+ try {
601
+ const reportsDir = path.join(this.config.sourceDir, 'scripts', 'i18n', 'reports');
602
+ if (fs.existsSync(reportsDir)) {
603
+ const files = fs.readdirSync(reportsDir);
604
+ const reportFiles = files.filter(file =>
605
+ (file.endsWith('.txt') || file.endsWith('.json') || file.endsWith('.log')) &&
606
+ file !== path.basename(args.outputFile || '')
607
+ );
608
+
609
+ let deletedCount = 0;
610
+ for (const file of reportFiles) {
611
+ try {
612
+ fs.unlinkSync(path.join(reportsDir, file));
613
+ deletedCount++;
614
+ } catch (error) {
615
+ console.log(this.t('summary.couldNotDelete', { file, error: error.message }));
616
+ }
617
+ }
618
+
619
+ if (deletedCount > 0) {
620
+ console.log(this.t('summary.deletedOldReportFiles', { count: deletedCount }));
621
+ } else {
622
+ console.log(this.t('summary.noOldReportFilesToDelete'));
623
+ }
624
+ }
625
+ } catch (error) {
626
+ console.log(this.t('summary.errorCleaningUpReports', { error: error.message }));
627
+ }
628
+ } else if (args.keepReports) {
629
+ console.log(this.t('summary.reportFilesPreserved'));
630
+ }
631
+
632
+ // Summary
633
+ console.log('\n============================================================');
634
+ console.log(this.t('summary.analysisComplete'));
635
+ console.log('============================================================');
636
+ console.log(this.t('summary.analyzedLanguages', { count: this.stats.languages.length }));
637
+ console.log(this.t('summary.processedFiles', { count: this.stats.totalFiles }));
638
+ console.log(this.t('summary.foundTranslationKeys', { count: this.stats.totalKeys }));
639
+
640
+ const totalIssues = this.stats.missingFiles.length +
641
+ this.stats.emptyFiles.length +
642
+ this.stats.malformedFiles.length +
643
+ this.stats.duplicateKeys.length +
644
+ this.stats.inconsistentKeys.length;
645
+
646
+ if (totalIssues > 0) {
647
+ console.log(this.t('summary.foundIssues', {count: totalIssues}));
648
+ } else {
649
+ console.log(this.t('summary.noIssuesConsole'));
650
+ }
651
+
652
+ } catch (error) {
653
+ console.error(this.t('summary.errorDuringAnalysis', { error: error.message }));
654
+ if (args.verbose) {
655
+ console.error(error.stack);
656
+ }
657
+ process.exit(1);
658
+ }
659
+ }
660
+ }
661
+
662
+ // Run if called directly
663
+ if (require.main === module) {
664
+ const reporter = new I18nSummaryReporter();
665
+ reporter.run().catch(error => {
666
+ console.error('❌ Fatal error:', error.message);
667
+ process.exit(1);
668
+ });
669
+ }
670
+
671
+ module.exports = I18nSummaryReporter;