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,557 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * I18n Sizing Analyzer
5
+ *
6
+ * Analyzes translation file sizes, character counts, and provides sizing statistics
7
+ * for different languages to help with UI layout planning and optimization.
8
+ *
9
+ * Features:
10
+ * - File size analysis for all translation files
11
+ * - Character count statistics per language
12
+ * - Key-level size comparison across languages
13
+ * - UI layout impact assessment
14
+ * - Size optimization recommendations
15
+ *
16
+ * Usage:
17
+ * i18ntk sizing [options]
18
+ *
19
+ * Options:
20
+ * --source-dir <dir> Source directory containing translation files (default: ./locales)
21
+ * --languages <langs> Comma-separated list of languages to analyze (default: all)
22
+ * --output-report Generate detailed sizing report
23
+ * --format <format> Output format: json, csv, table (default: table)
24
+ * --threshold <number> Size difference threshold for warnings (default: 50%)
25
+ * --detailed Generate detailed report with more information
26
+ * --help Show this help message
27
+ *
28
+ * Examples:
29
+ * i18ntk sizing --output-report
30
+ * i18ntk sizing --languages=en,de,fr --format=json
31
+ * i18ntk sizing --threshold=30 --output-report
32
+ */
33
+
34
+ const fs = require('fs');
35
+ const path = require('path');
36
+ const { performance } = require('perf_hooks');
37
+ const { loadTranslations, t } = require('../utils/i18n-helper');
38
+ const settingsManager = require('../settings/settings-manager');
39
+ const SecurityUtils = require('../utils/security');
40
+
41
+ // Get configuration from settings manager
42
+ function getConfig() {
43
+ const settings = settingsManager.getSettings();
44
+ return {
45
+ sourceDir: settings.sourceDir || './locales',
46
+ outputDir: settings.outputDir || './i18ntk-reports',
47
+ threshold: settings.processing?.sizingThreshold || 50,
48
+ uiLanguage: settings.language || 'en'
49
+ };
50
+ }
51
+
52
+ class I18nSizingAnalyzer {
53
+ constructor(options = {}) {
54
+ const config = getConfig();
55
+ this.sourceDir = options.sourceDir || config.sourceDir;
56
+ this.outputDir = options.outputDir || config.outputDir;
57
+ this.languages = options.languages || [];
58
+ this.threshold = options.threshold || config.threshold; // Size difference threshold in percentage
59
+ this.format = options.format || 'table';
60
+ this.outputReport = options.outputReport || false;
61
+
62
+ // Initialize i18n with UI language from config
63
+ const uiLanguage = options.uiLanguage || config.uiLanguage || 'en';
64
+ loadTranslations(uiLanguage);
65
+ this.t = t;
66
+
67
+ this.stats = {
68
+ files: {},
69
+ languages: {},
70
+ keys: {},
71
+ summary: {}
72
+ };
73
+ }
74
+
75
+ // Get available language files
76
+ getLanguageFiles() {
77
+ const validatedSourceDir = SecurityUtils.validatePath(this.sourceDir, process.cwd());
78
+ if (!validatedSourceDir) {
79
+ throw new Error(`Invalid source directory path: ${this.sourceDir}`);
80
+ }
81
+
82
+ if (!fs.existsSync(validatedSourceDir)) {
83
+ throw new Error(`Source directory not found: ${validatedSourceDir}`);
84
+ }
85
+
86
+ const files = [];
87
+ const items = fs.readdirSync(validatedSourceDir);
88
+
89
+ // Check for nested language directories
90
+ for (const item of items) {
91
+ const itemPath = SecurityUtils.validatePath(path.join(validatedSourceDir, item), process.cwd());
92
+ if (!itemPath) continue;
93
+
94
+ const stat = fs.statSync(itemPath);
95
+
96
+ if (stat.isDirectory()) {
97
+ // This is a language directory, combine all JSON files
98
+ const langFiles = fs.readdirSync(itemPath)
99
+ .filter(file => file.endsWith('.json'))
100
+ .map(file => SecurityUtils.validatePath(path.join(itemPath, file), process.cwd()))
101
+ .filter(file => file !== null);
102
+
103
+ if (langFiles.length > 0) {
104
+ files.push({
105
+ language: item,
106
+ file: `${item}/*.json`,
107
+ path: itemPath,
108
+ files: langFiles
109
+ });
110
+ }
111
+ } else if (item.endsWith('.json')) {
112
+ // Direct JSON file in root
113
+ const lang = path.basename(item, '.json');
114
+ files.push({
115
+ language: lang,
116
+ file: item,
117
+ path: itemPath
118
+ });
119
+ }
120
+ }
121
+
122
+ if (this.languages.length > 0) {
123
+ return files.filter(f => this.languages.includes(f.language));
124
+ }
125
+
126
+ return files;
127
+ }
128
+
129
+ // Analyze file sizes
130
+ analyzeFileSizes(files) {
131
+ console.log(this.t("sizing.analyzing_file_sizes"));
132
+
133
+ files.forEach(({ language, file, path: filePath, files: langFiles }) => {
134
+ if (langFiles) {
135
+ // Handle nested directory structure
136
+ let totalSize = 0;
137
+ let totalLines = 0;
138
+ let totalCharacters = 0;
139
+ let lastModified = new Date(0);
140
+
141
+ langFiles.forEach(langFile => {
142
+ const stats = fs.statSync(langFile);
143
+ let content = SecurityUtils.safeReadFileSync(langFile, process.cwd());
144
+ if (typeof content !== "string") content = "";
145
+ totalSize += stats.size;
146
+ totalLines += content.split('\n').length;
147
+ totalCharacters += content.length;
148
+ if (stats.mtime > lastModified) {
149
+ lastModified = stats.mtime;
150
+ }
151
+ });
152
+
153
+ this.stats.files[language] = {
154
+ file,
155
+ size: totalSize,
156
+ sizeKB: (totalSize / 1024).toFixed(2),
157
+ lines: totalLines,
158
+ characters: totalCharacters,
159
+ lastModified: lastModified,
160
+ fileCount: langFiles.length
161
+ };
162
+ } else {
163
+ // Handle single file structure
164
+ const stats = fs.statSync(filePath);
165
+ let content = SecurityUtils.safeReadFileSync(filePath, process.cwd());
166
+ if (typeof content !== "string") content = "";
167
+ this.stats.files[language] = {
168
+ file,
169
+ size: stats.size,
170
+ sizeKB: (stats.size / 1024).toFixed(2),
171
+ lines: content.split('\n').length,
172
+ characters: content.length,
173
+ lastModified: stats.mtime,
174
+ fileCount: 1
175
+ };
176
+ }
177
+ });
178
+ }
179
+
180
+ // Analyze translation content
181
+ analyzeTranslationContent(files) {
182
+ console.log(this.t("sizing.analyzing_translation_content"));
183
+
184
+ files.forEach(({ language, path: filePath, files: langFiles }) => {
185
+ try {
186
+ let combinedContent = {};
187
+
188
+ if (langFiles) {
189
+ // Handle nested directory structure - combine all JSON files
190
+ langFiles.forEach(langFile => {
191
+ const rawContent = SecurityUtils.safeReadFileSync(langFile, process.cwd());
192
+ const fileContent = SecurityUtils.safeParseJSON(rawContent);
193
+ if (fileContent) {
194
+ const fileName = path.basename(langFile, '.json');
195
+ combinedContent[fileName] = fileContent;
196
+ }
197
+ });
198
+ } else {
199
+ // Handle single file structure
200
+ const rawContent = SecurityUtils.safeReadFileSync(filePath, process.cwd());
201
+ combinedContent = SecurityUtils.safeParseJSON(rawContent) || {};
202
+ }
203
+
204
+ const analysis = this.analyzeTranslationObject(combinedContent, '');
205
+
206
+ this.stats.languages[language] = {
207
+ totalKeys: analysis.keyCount,
208
+ totalCharacters: analysis.charCount,
209
+ averageKeyLength: analysis.keyCount > 0 ? analysis.charCount / analysis.keyCount : 0,
210
+ maxKeyLength: analysis.maxLength,
211
+ minKeyLength: analysis.minLength,
212
+ emptyKeys: analysis.emptyKeys,
213
+ longKeys: analysis.longKeys
214
+ };
215
+
216
+ // Store individual key sizes for comparison
217
+ Object.entries(analysis.keys).forEach(([key, value]) => {
218
+ if (!this.stats.keys[key]) {
219
+ this.stats.keys[key] = {};
220
+ }
221
+ this.stats.keys[key][language] = {
222
+ length: value.length,
223
+ characters: value.length
224
+ };
225
+ });
226
+
227
+ } catch (error) {
228
+ console.error(this.t("sizing.failed_to_parse_language_error", { language, errorMessage: error.message }));
229
+ }
230
+ });
231
+ }
232
+
233
+ // Recursively analyze translation object
234
+ analyzeTranslationObject(obj, prefix = '') {
235
+ let keyCount = 0;
236
+ let charCount = 0;
237
+ let maxLength = 0;
238
+ let minLength = Infinity;
239
+ let emptyKeys = 0;
240
+ let longKeys = 0;
241
+ const keys = {};
242
+
243
+ const traverse = (current, currentPrefix) => {
244
+ Object.entries(current).forEach(([key, value]) => {
245
+ const fullKey = currentPrefix ? `${currentPrefix}.${key}` : key;
246
+
247
+ if (typeof value === 'string') {
248
+ keyCount++;
249
+ charCount += value.length;
250
+ maxLength = Math.max(maxLength, value.length);
251
+ minLength = Math.min(minLength, value.length);
252
+
253
+ if (value.length === 0) emptyKeys++;
254
+ if (value.length > 100) longKeys++;
255
+
256
+ keys[fullKey] = value;
257
+ } else if (typeof value === 'object' && value !== null) {
258
+ traverse(value, fullKey);
259
+ }
260
+ });
261
+ };
262
+
263
+ traverse(obj, prefix);
264
+
265
+ return {
266
+ keyCount,
267
+ charCount,
268
+ maxLength: maxLength === 0 ? 0 : maxLength,
269
+ minLength: minLength === Infinity ? 0 : minLength,
270
+ emptyKeys,
271
+ longKeys,
272
+ keys
273
+ };
274
+ }
275
+
276
+ // Generate size comparison analysis
277
+ generateSizeComparison() {
278
+ console.log(this.t("sizing.generating_size_comparisons"));
279
+
280
+ const languages = Object.keys(this.stats.languages);
281
+ const baseLanguage = languages[0]; // Use first language as baseline
282
+
283
+ if (!baseLanguage) {
284
+ console.warn(this.t("sizing.no_languages_found_for_comparison"));
285
+ return;
286
+ }
287
+
288
+ this.stats.summary = {
289
+ baseLanguage,
290
+ totalLanguages: languages.length,
291
+ sizeVariations: {},
292
+ problematicKeys: [],
293
+ recommendations: []
294
+ };
295
+
296
+ // Compare each language to base language
297
+ languages.forEach(lang => {
298
+ if (lang === baseLanguage) return;
299
+
300
+ const baseStats = this.stats.languages[baseLanguage];
301
+ const langStats = this.stats.languages[lang];
302
+
303
+ const sizeDiff = ((langStats.totalCharacters - baseStats.totalCharacters) / baseStats.totalCharacters) * 100;
304
+
305
+ this.stats.summary.sizeVariations[lang] = {
306
+ characterDifference: langStats.totalCharacters - baseStats.totalCharacters,
307
+ percentageDifference: sizeDiff.toFixed(2),
308
+ isProblematic: Math.abs(sizeDiff) > this.threshold
309
+ };
310
+ });
311
+
312
+ // Find problematic keys (significant size differences)
313
+ Object.entries(this.stats.keys).forEach(([key, langData]) => {
314
+ const baseLang = langData[baseLanguage];
315
+ if (!baseLang) return;
316
+
317
+ const variations = [];
318
+ Object.entries(langData).forEach(([lang, data]) => {
319
+ if (lang === baseLanguage) return;
320
+
321
+ const diff = ((data.length - baseLang.length) / baseLang.length) * 100;
322
+ if (Math.abs(diff) > this.threshold) {
323
+ variations.push({
324
+ language: lang,
325
+ difference: diff.toFixed(2),
326
+ baseLength: baseLang.length,
327
+ currentLength: data.length
328
+ });
329
+ }
330
+ });
331
+
332
+ if (variations.length > 0) {
333
+ this.stats.summary.problematicKeys.push({
334
+ key,
335
+ variations
336
+ });
337
+ }
338
+ });
339
+
340
+ // Generate recommendations
341
+ this.generateRecommendations();
342
+ }
343
+
344
+ // Generate optimization recommendations
345
+ generateRecommendations() {
346
+ const recommendations = [];
347
+
348
+ // Check for large size variations
349
+ Object.entries(this.stats.summary.sizeVariations).forEach(([lang, data]) => {
350
+ if (data.isProblematic) {
351
+ if (data.percentageDifference > 0) {
352
+ recommendations.push(`Consider reviewing ${lang} translations - they are ${data.percentageDifference}% longer than baseline`);
353
+ } else {
354
+ recommendations.push(`Consider reviewing ${lang} translations - they are ${Math.abs(data.percentageDifference)}% shorter than baseline`);
355
+ }
356
+ }
357
+ });
358
+
359
+ // Check for problematic keys
360
+ if (this.stats.summary.problematicKeys.length > 0) {
361
+ recommendations.push(`${this.stats.summary.problematicKeys.length} keys have significant size variations across languages`);
362
+ }
363
+
364
+ // Check for very long translations
365
+ Object.entries(this.stats.languages).forEach(([lang, data]) => {
366
+ if (data.longKeys > 0) {
367
+ recommendations.push(`${lang} has ${data.longKeys} translations longer than 100 characters - consider breaking them down`);
368
+ }
369
+ });
370
+
371
+ this.stats.summary.recommendations = recommendations;
372
+ }
373
+
374
+ // Display results in table format
375
+ displayTable() {
376
+ console.log(this.t("sizing.sizing_analysis_results"));
377
+ console.log("=".repeat(80));
378
+
379
+ // File sizes table
380
+ console.log("\n" + this.t("sizing.file_sizes_title"));
381
+ console.log("-".repeat(80));
382
+ console.log(this.t("sizing.file_sizes_header"));
383
+ console.log("-".repeat(80));
384
+
385
+ Object.entries(this.stats.files).forEach(([lang, data]) => {
386
+ console.log(this.t("sizing.file_size_row", { lang, sizeKB: data.sizeKB, lines: data.lines, characters: data.characters }));
387
+ });
388
+
389
+ // Language statistics
390
+ console.log("\n" + this.t("sizing.language_statistics_title"));
391
+ console.log("-".repeat(80));
392
+ console.log(this.t("sizing.language_stats_header"));
393
+ console.log("-".repeat(80));
394
+
395
+ Object.entries(this.stats.languages).forEach(([lang, data]) => {
396
+ console.log(this.t("sizing.language_stats_row", { lang, totalKeys: data.totalKeys, totalCharacters: data.totalCharacters, averageKeyLength: data.averageKeyLength.toFixed(1), maxKeyLength: data.maxKeyLength, emptyKeys: data.emptyKeys }));
397
+ });
398
+
399
+ // Size variations
400
+ if (this.stats.summary.sizeVariations) {
401
+ console.log("\n" + this.t("sizing.size_variations_title"));
402
+ console.log("-".repeat(80));
403
+ console.log(this.t("sizing.size_variations_header"));
404
+ console.log("-".repeat(80));
405
+
406
+ Object.entries(this.stats.summary.sizeVariations).forEach(([lang, data]) => {
407
+ const problematic = data.isProblematic ? this.t("sizing.problematic_yes") : this.t("sizing.problematic_no");
408
+ console.log(this.t("sizing.size_variation_row", { lang, characterDifference: data.characterDifference, percentageDifference: data.percentageDifference, problematic }));
409
+ });
410
+ }
411
+
412
+ // Recommendations
413
+ if (this.stats.summary.recommendations.length > 0) {
414
+ console.log("\n" + this.t("sizing.recommendations_title"));
415
+ console.log("-".repeat(80));
416
+ this.stats.summary.recommendations.forEach((rec, index) => {
417
+ console.log(`${index + 1}. ${rec}`);
418
+ });
419
+ }
420
+ }
421
+
422
+ // Generate detailed report
423
+ async generateReport() {
424
+ if (!this.outputReport) return;
425
+
426
+ console.log(this.t("sizing.generating_detailed_report"));
427
+
428
+ const validatedOutputDir = SecurityUtils.validatePath(this.outputDir, process.cwd());
429
+ if (!validatedOutputDir) {
430
+ throw new Error(`Invalid output directory path: ${this.outputDir}`);
431
+ }
432
+
433
+ // Ensure output directory exists
434
+ if (!fs.existsSync(validatedOutputDir)) {
435
+ fs.mkdirSync(validatedOutputDir, { recursive: true });
436
+ }
437
+
438
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
439
+ const reportPath = SecurityUtils.validatePath(path.join(validatedOutputDir, `sizing-analysis-${timestamp}.json`), process.cwd());
440
+
441
+ if (!reportPath) {
442
+ throw new Error('Invalid report file path');
443
+ }
444
+
445
+ const report = {
446
+ timestamp: new Date().toISOString(),
447
+ configuration: {
448
+ sourceDir: this.sourceDir,
449
+ languages: this.languages,
450
+ threshold: this.threshold
451
+ },
452
+ analysis: this.stats,
453
+ metadata: {
454
+ totalFiles: Object.keys(this.stats.files).length,
455
+ totalLanguages: Object.keys(this.stats.languages).length,
456
+ totalKeys: Object.keys(this.stats.keys).length
457
+ }
458
+ };
459
+
460
+ const success = SecurityUtils.safeWriteFileSync(reportPath, JSON.stringify(report, null, 2), process.cwd());
461
+ if (success) {
462
+ console.log(this.t("sizing.report_saved_to", { reportPath }));
463
+ SecurityUtils.logSecurityEvent('Sizing report saved', 'info', { reportPath });
464
+ } else {
465
+ throw new Error('Failed to save report securely');
466
+ }
467
+
468
+ // Generate CSV if requested
469
+ if (this.format === 'csv') {
470
+ await this.generateCSVReport(timestamp);
471
+ }
472
+ }
473
+
474
+ // Generate CSV report
475
+ async generateCSVReport(timestamp) {
476
+ const validatedOutputDir = SecurityUtils.validatePath(this.outputDir, process.cwd());
477
+ if (!validatedOutputDir) {
478
+ throw new Error(`Invalid output directory path: ${this.outputDir}`);
479
+ }
480
+
481
+ const csvPath = SecurityUtils.validatePath(path.join(validatedOutputDir, `sizing-analysis-${timestamp}.csv`), process.cwd());
482
+ if (!csvPath) {
483
+ throw new Error('Invalid CSV file path');
484
+ }
485
+
486
+ let csvContent = 'Language,File Size (KB),Lines,Characters,Total Keys,Avg Key Length,Max Key Length,Empty Keys,Long Keys\n';
487
+
488
+ Object.entries(this.stats.files).forEach(([lang]) => {
489
+ const fileData = this.stats.files[lang];
490
+ const langData = this.stats.languages[lang];
491
+
492
+ csvContent += `${lang},${fileData.sizeKB},${fileData.lines},${fileData.characters},${langData.totalKeys},${langData.averageKeyLength.toFixed(1)},${langData.maxKeyLength},${langData.emptyKeys},${langData.longKeys}\n`;
493
+ });
494
+
495
+ const success = SecurityUtils.safeWriteFileSync(csvPath, csvContent, process.cwd());
496
+ if (success) {
497
+ console.log(this.t("sizing.csv_report_saved_to", { csvPath }));
498
+ SecurityUtils.logSecurityEvent('CSV report saved', 'info', { csvPath });
499
+ } else {
500
+ throw new Error('Failed to save CSV report securely');
501
+ }
502
+ }
503
+
504
+ // Main analysis method
505
+ async analyze() {
506
+ const startTime = performance.now();
507
+
508
+ try {
509
+ console.log(this.t("sizing.starting_i18n_sizing_analysis"));
510
+ console.log(this.t("sizing.source_directory", { sourceDir: this.sourceDir }));
511
+
512
+ const files = this.getLanguageFiles();
513
+
514
+ if (files.length === 0) {
515
+ console.log(this.t("sizing.no_translation_files_found"));
516
+ return;
517
+ }
518
+
519
+ console.log(this.t("sizing.found_languages", { languages: files.map(f => f.language).join(', ') }));
520
+
521
+ this.analyzeFileSizes(files);
522
+ this.analyzeTranslationContent(files);
523
+ this.generateSizeComparison();
524
+
525
+ if (this.format === 'table') {
526
+ this.displayTable();
527
+ } else if (this.format === 'json') {
528
+ console.log(JSON.stringify(this.stats, null, 2));
529
+ }
530
+
531
+ await this.generateReport();
532
+
533
+ const endTime = performance.now();
534
+ console.log(this.t("sizing.analysis_completed", { duration: (endTime - startTime).toFixed(2) }));
535
+
536
+ } catch (error) {
537
+ console.error(this.t("sizing.analysis_failed", { errorMessage: error.message }));
538
+ process.exit(1);
539
+ }
540
+ }
541
+
542
+ // Add run method for compatibility with manager
543
+ async run() {
544
+ return await this.analyze();
545
+ }
546
+ }
547
+
548
+ // Update the execution block at the end
549
+ if (require.main === module) {
550
+ const analyzer = new I18nSizingAnalyzer();
551
+ analyzer.analyze().catch(error => {
552
+ console.error('❌ Sizing analysis failed:', error.message);
553
+ process.exit(1);
554
+ });
555
+ }
556
+
557
+ module.exports = I18nSizingAnalyzer;