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,625 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * I18N TRANSLATION ANALYSIS SCRIPT
4
+ *
5
+ * This script analyzes translation files to identify missing translations,
6
+ * inconsistencies, and provides detailed reports for each language.
7
+ *
8
+ * Usage:
9
+ * node scripts/i18n/02-analyze-translations.js
10
+ * node scripts/i18n/02-analyze-translations.js --language=de
11
+ * node scripts/i18n/02-analyze-translations.js --source-dir=./src/i18n/locales
12
+ * node scripts/i18n/02-analyze-translations.js --output-reports
13
+ */
14
+
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+ const readline = require('readline');
18
+ const { loadTranslations, t } = require('../utils/i18n-helper');
19
+ const settingsManager = require('../settings/settings-manager');
20
+ const SecurityUtils = require('../utils/security');
21
+
22
+ // Get configuration from settings manager
23
+ function getConfig() {
24
+ const settings = settingsManager.getSettings();
25
+ return {
26
+ sourceDir: settings.directories?.sourceDir || './locales',
27
+ sourceLanguage: settings.directories?.sourceLanguage || 'en',
28
+ notTranslatedMarker: settings.processing?.notTranslatedMarker || 'NOT_TRANSLATED',
29
+ outputDir: settings.directories?.outputDir || './i18ntk-reports',
30
+ excludeFiles: settings.processing?.excludeFiles || ['.DS_Store', 'Thumbs.db'],
31
+ uiLanguage: settings.language || 'en'
32
+ };
33
+ }
34
+
35
+ class I18nAnalyzer {
36
+ constructor(config = {}) {
37
+ this.config = { ...getConfig(), ...config };
38
+ this.sourceDir = path.resolve(this.config.sourceDir);
39
+ this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage);
40
+ this.outputDir = path.resolve(this.config.outputDir);
41
+
42
+ // Initialize i18n with UI language
43
+ const uiLanguage = this.config.uiLanguage || 'en';
44
+ loadTranslations(uiLanguage);
45
+ this.t = t;
46
+
47
+ // Initialize readline interface
48
+ this.rl = null;
49
+ }
50
+
51
+ // Initialize readline interface
52
+ initReadline() {
53
+ if (!this.rl) {
54
+ this.rl = readline.createInterface({
55
+ input: process.stdin,
56
+ output: process.stdout
57
+ });
58
+ }
59
+ return this.rl;
60
+ }
61
+
62
+ // Close readline interface
63
+ closeReadline() {
64
+ if (this.rl) {
65
+ this.rl.close();
66
+ this.rl = null;
67
+ }
68
+ }
69
+
70
+ // Prompt for user input
71
+ async prompt(question) {
72
+ const rl = this.initReadline();
73
+ return new Promise((resolve) => {
74
+ rl.question(question, resolve);
75
+ });
76
+ }
77
+
78
+ // Parse command line arguments
79
+ parseArgs() {
80
+ const args = process.argv.slice(2);
81
+ const parsed = {};
82
+
83
+ args.forEach(arg => {
84
+ if (arg.startsWith('--')) {
85
+ const [key, value] = arg.substring(2).split('=');
86
+ if (key === 'language') {
87
+ parsed.language = value;
88
+ } else if (key === 'source-dir') {
89
+ parsed.sourceDir = value;
90
+ } else if (key === 'output-reports') {
91
+ parsed.outputReports = true;
92
+ } else if (key === 'output-dir') {
93
+ parsed.outputDir = value;
94
+ } else if (key === 'ui-language') {
95
+ parsed.uiLanguage = value;
96
+ } else if (key === 'help') {
97
+ parsed.help = true;
98
+ } else if (key === 'no-prompt') {
99
+ parsed.noPrompt = true;
100
+ } else if (['en', 'de', 'es', 'fr', 'ru', 'ja', 'zh'].includes(key)) {
101
+ // Support shorthand language flags like --de, --fr, etc.
102
+ parsed.uiLanguage = key;
103
+ }
104
+ }
105
+ });
106
+
107
+ return parsed;
108
+ }
109
+
110
+ // Get all available languages
111
+ getAvailableLanguages() {
112
+ if (!fs.existsSync(this.sourceDir)) {
113
+ throw new Error(`Source directory not found: ${this.sourceDir}`);
114
+ }
115
+
116
+ return fs.readdirSync(this.sourceDir)
117
+ .filter(item => {
118
+ const itemPath = path.join(this.sourceDir, item);
119
+ return fs.statSync(itemPath).isDirectory() && item !== this.config.sourceLanguage;
120
+ });
121
+ }
122
+
123
+ // Get all JSON files from a language directory
124
+ getLanguageFiles(language) {
125
+ const languageDir = path.join(this.sourceDir, language);
126
+
127
+ const validatedPath = SecurityUtils.validatePath(languageDir, this.sourceDir);
128
+ if (!validatedPath || !fs.existsSync(validatedPath)) {
129
+ return [];
130
+ }
131
+
132
+ return fs.readdirSync(validatedPath)
133
+ .filter(file => {
134
+ return file.endsWith('.json') &&
135
+ !this.config.excludeFiles.includes(file);
136
+ });
137
+ }
138
+
139
+ // Get all keys recursively from an object
140
+ getAllKeys(obj, prefix = '') {
141
+ const keys = new Set();
142
+
143
+ for (const [key, value] of Object.entries(obj)) {
144
+ const fullKey = prefix ? `${prefix}.${key}` : key;
145
+ keys.add(fullKey);
146
+
147
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
148
+ const nestedKeys = this.getAllKeys(value, fullKey);
149
+ nestedKeys.forEach(k => keys.add(k));
150
+ }
151
+ }
152
+
153
+ return keys;
154
+ }
155
+
156
+ // Get value by key path
157
+ getValueByPath(obj, keyPath) {
158
+ const keys = keyPath.split('.');
159
+ let current = obj;
160
+
161
+ for (const key of keys) {
162
+ if (current && typeof current === 'object' && key in current) {
163
+ current = current[key];
164
+ } else {
165
+ return undefined;
166
+ }
167
+ }
168
+
169
+ return current;
170
+ }
171
+
172
+ // Analyze translation issues in an object
173
+ analyzeTranslationIssues(obj, sourceObj = null, prefix = '') {
174
+ const issues = [];
175
+
176
+ for (const [key, value] of Object.entries(obj)) {
177
+ const fullKey = prefix ? `${prefix}.${key}` : key;
178
+ const sourceValue = sourceObj ? this.getValueByPath(sourceObj, fullKey) : null;
179
+
180
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
181
+ issues.push(...this.analyzeTranslationIssues(value, sourceObj, fullKey));
182
+ } else if (typeof value === 'string') {
183
+ if (value === this.config.notTranslatedMarker) {
184
+ issues.push({
185
+ type: 'not_translated',
186
+ key: fullKey,
187
+ value,
188
+ sourceValue: sourceValue || 'N/A'
189
+ });
190
+ } else if (value === '') {
191
+ issues.push({
192
+ type: 'empty_value',
193
+ key: fullKey,
194
+ value,
195
+ sourceValue: sourceValue || 'N/A'
196
+ });
197
+ } else if (value.includes(this.config.notTranslatedMarker)) {
198
+ issues.push({
199
+ type: 'partial_translation',
200
+ key: fullKey,
201
+ value,
202
+ sourceValue: sourceValue || 'N/A'
203
+ });
204
+ } else if (sourceValue && value === sourceValue) {
205
+ issues.push({
206
+ type: 'same_as_source',
207
+ key: fullKey,
208
+ value,
209
+ sourceValue
210
+ });
211
+ }
212
+ }
213
+ }
214
+
215
+ return issues;
216
+ }
217
+
218
+ // Get translation statistics for an object
219
+ getTranslationStats(obj) {
220
+ let total = 0;
221
+ let translated = 0;
222
+ let notTranslated = 0;
223
+ let empty = 0;
224
+ let partial = 0;
225
+
226
+ const count = (item) => {
227
+ if (typeof item === 'string') {
228
+ total++;
229
+ if (item === this.config.notTranslatedMarker) {
230
+ notTranslated++;
231
+ } else if (item === '') {
232
+ empty++;
233
+ } else if (item.includes(this.config.notTranslatedMarker)) {
234
+ partial++;
235
+ } else {
236
+ translated++;
237
+ }
238
+ } else if (Array.isArray(item)) {
239
+ item.forEach(count);
240
+ } else if (item && typeof item === 'object') {
241
+ Object.values(item).forEach(count);
242
+ }
243
+ };
244
+
245
+ count(obj);
246
+
247
+ return {
248
+ total,
249
+ translated,
250
+ notTranslated,
251
+ empty,
252
+ partial,
253
+ percentage: total > 0 ? Math.round((translated / total) * 100) : 0,
254
+ missing: notTranslated + empty + partial
255
+ };
256
+ }
257
+
258
+ // Check structural consistency between source and target
259
+ checkStructuralConsistency(sourceObj, targetObj) {
260
+ const sourceKeys = this.getAllKeys(sourceObj);
261
+ const targetKeys = this.getAllKeys(targetObj);
262
+
263
+ const missingKeys = [...sourceKeys].filter(key => !targetKeys.has(key));
264
+ const extraKeys = [...targetKeys].filter(key => !sourceKeys.has(key));
265
+
266
+ return {
267
+ isConsistent: missingKeys.length === 0 && extraKeys.length === 0,
268
+ missingKeys,
269
+ extraKeys,
270
+ sourceKeyCount: sourceKeys.size,
271
+ targetKeyCount: targetKeys.size
272
+ };
273
+ }
274
+
275
+ // Analyze a single language
276
+ analyzeLanguage(language) {
277
+ const languageDir = path.join(this.sourceDir, language);
278
+ const sourceFiles = this.getLanguageFiles(this.config.sourceLanguage);
279
+ const targetFiles = this.getLanguageFiles(language);
280
+
281
+ const analysis = {
282
+ language,
283
+ files: {},
284
+ summary: {
285
+ totalFiles: sourceFiles.length,
286
+ analyzedFiles: 0,
287
+ totalKeys: 0,
288
+ translatedKeys: 0,
289
+ missingKeys: 0,
290
+ issues: []
291
+ }
292
+ };
293
+
294
+ for (const fileName of sourceFiles) {
295
+ const sourceFilePath = path.join(this.sourceLanguageDir, fileName);
296
+ const targetFilePath = path.join(languageDir, fileName);
297
+
298
+ if (!fs.existsSync(sourceFilePath)) {
299
+ continue;
300
+ }
301
+
302
+ let sourceContent, targetContent;
303
+
304
+ try {
305
+ const validatedSourcePath = SecurityUtils.validatePath(sourceFilePath, this.sourceDir);
306
+ if (!validatedSourcePath) {
307
+ analysis.files[fileName] = {
308
+ error: 'Invalid source file path'
309
+ };
310
+ continue;
311
+ }
312
+ const sourceFileContent = SecurityUtils.safeReadFile(validatedSourcePath, this.sourceDir);
313
+ if (!sourceFileContent) {
314
+ analysis.files[fileName] = {
315
+ error: 'Failed to read source file securely'
316
+ };
317
+ continue;
318
+ }
319
+ sourceContent = SecurityUtils.safeParseJSON(sourceFileContent);
320
+ if (!sourceContent) {
321
+ analysis.files[fileName] = {
322
+ error: 'Failed to parse source file JSON'
323
+ };
324
+ continue;
325
+ }
326
+ } catch (error) {
327
+ analysis.files[fileName] = {
328
+ error: `Failed to parse source file: ${error.message}`
329
+ };
330
+ continue;
331
+ }
332
+
333
+ if (!fs.existsSync(targetFilePath)) {
334
+ analysis.files[fileName] = {
335
+ status: 'missing',
336
+ sourceKeys: this.getAllKeys(sourceContent).size
337
+ };
338
+ continue;
339
+ }
340
+
341
+ try {
342
+ const validatedTargetPath = SecurityUtils.validatePath(targetFilePath, this.sourceDir);
343
+ if (!validatedTargetPath) {
344
+ analysis.files[fileName] = {
345
+ error: 'Invalid target file path'
346
+ };
347
+ continue;
348
+ }
349
+ const targetFileContent = SecurityUtils.safeReadFile(validatedTargetPath, this.sourceDir);
350
+ if (!targetFileContent) {
351
+ analysis.files[fileName] = {
352
+ error: 'Failed to read target file securely'
353
+ };
354
+ continue;
355
+ }
356
+ targetContent = SecurityUtils.safeParseJSON(targetFileContent);
357
+ if (!targetContent) {
358
+ analysis.files[fileName] = {
359
+ error: 'Failed to parse target file JSON'
360
+ };
361
+ continue;
362
+ }
363
+ } catch (error) {
364
+ analysis.files[fileName] = {
365
+ error: `Failed to parse target file: ${error.message}`
366
+ };
367
+ continue;
368
+ }
369
+
370
+ // Analyze this file
371
+ const stats = this.getTranslationStats(targetContent);
372
+ const structural = this.checkStructuralConsistency(sourceContent, targetContent);
373
+ const issues = this.analyzeTranslationIssues(targetContent, sourceContent);
374
+
375
+ analysis.files[fileName] = {
376
+ status: 'analyzed',
377
+ stats,
378
+ structural,
379
+ issues,
380
+ sourceFilePath,
381
+ targetFilePath
382
+ };
383
+
384
+ // Update summary
385
+ analysis.summary.analyzedFiles++;
386
+ analysis.summary.totalKeys += stats.total;
387
+ analysis.summary.translatedKeys += stats.translated;
388
+ analysis.summary.missingKeys += stats.missing;
389
+ analysis.summary.issues.push(...issues);
390
+ }
391
+
392
+ // Calculate overall percentage
393
+ analysis.summary.percentage = analysis.summary.totalKeys > 0
394
+ ? Math.round((analysis.summary.translatedKeys / analysis.summary.totalKeys) * 100)
395
+ : 0;
396
+
397
+ return analysis;
398
+ }
399
+
400
+ // Generate detailed report for a language
401
+ generateLanguageReport(analysis) {
402
+ const { language } = analysis;
403
+ const timestamp = new Date().toISOString();
404
+
405
+ let report = `${this.t('analyzeTranslations.reportTitle', { language: language.toUpperCase() })}\n`;
406
+ report += `${this.t('analyzeTranslations.generated', { timestamp })}\n`;
407
+ report += `${this.t('analyzeTranslations.status', { translated: analysis.summary.translatedKeys, total: analysis.summary.totalKeys, percentage: analysis.summary.percentage })}\n`;
408
+ report += `${this.t('analyzeTranslations.filesAnalyzed', { analyzed: analysis.summary.analyzedFiles, total: analysis.summary.totalFiles })}\n`;
409
+ report += `${this.t('analyzeTranslations.keysNeedingTranslation', { count: analysis.summary.missingKeys })}\n\n`;
410
+
411
+ report += `${this.t('analyzeTranslations.fileBreakdown')}\n`;
412
+ report += `${'='.repeat(50)}\n\n`;
413
+
414
+ Object.entries(analysis.files).forEach(([fileName, fileData]) => {
415
+ report += `\uD83D\uDCC4 ${fileName}\n`;
416
+
417
+ if (fileData.error) {
418
+ report += ` \u274C ${this.t('analyzeTranslations.error')}: ${fileData.error}\n\n`;
419
+ return;
420
+ }
421
+
422
+ if (fileData.status === 'missing') {
423
+ report += ` \u274C ${this.t('analyzeTranslations.statusFileMissing')}\n`;
424
+ report += ` \uD83D\uDCCA ${this.t('analyzeTranslations.sourceKeys', { count: fileData.sourceKeys })}\n\n`;
425
+ return;
426
+ }
427
+
428
+ const { stats, structural, issues } = fileData;
429
+
430
+ report += ` \uD83D\uDCCA ${this.t('analyzeTranslations.translation', { translated: stats.translated, total: stats.total, percentage: stats.percentage })}\n`;
431
+ report += ` \uD83C\uDFD7ļø ${this.t('analyzeTranslations.structure', { status: structural.isConsistent ? this.t('analyzeTranslations.consistent') : this.t('analyzeTranslations.inconsistent') })}\n`;
432
+
433
+ if (!structural.isConsistent) {
434
+ if (structural.missingKeys.length > 0) {
435
+ report += ` ${this.t('analyzeTranslations.missingKeys', { count: structural.missingKeys.length })}\n`;
436
+ }
437
+ if (structural.extraKeys.length > 0) {
438
+ report += ` ${this.t('analyzeTranslations.extraKeys', { count: structural.extraKeys.length })}\n`;
439
+ }
440
+ }
441
+
442
+ if (issues.length > 0) {
443
+ report += ` \u26A0ļø ${this.t('analyzeTranslations.issues', { count: issues.length })}\n`;
444
+
445
+ const issueTypes = {
446
+ not_translated: issues.filter(i => i.type === 'not_translated').length,
447
+ empty_value: issues.filter(i => i.type === 'empty_value').length,
448
+ partial_translation: issues.filter(i => i.type === 'partial_translation').length,
449
+ same_as_source: issues.filter(i => i.type === 'same_as_source').length
450
+ };
451
+
452
+ Object.entries(issueTypes).forEach(([type, count]) => {
453
+ if (count > 0) {
454
+ report += ` ${this.t('analyzeTranslations.issueType.' + type, { count })}\n`;
455
+ }
456
+ });
457
+ }
458
+
459
+ report += `\n`;
460
+ });
461
+
462
+ // Keys needing translation
463
+ const notTranslatedIssues = analysis.summary.issues.filter(issue =>
464
+ issue.type === 'not_translated' || issue.type === 'empty_value'
465
+ );
466
+
467
+ if (notTranslatedIssues.length > 0) {
468
+ report += `${this.t('analyzeTranslations.keysToTranslate')}\n`;
469
+ report += `${'='.repeat(50)}\n\n`;
470
+
471
+ notTranslatedIssues.slice(0, 50).forEach(issue => {
472
+ report += `${this.t('analyzeTranslations.key')}: ${issue.key}\n`;
473
+ report += `${this.t('analyzeTranslations.english')}: \"${issue.sourceValue}\"\n`;
474
+ report += `${language}: [${this.t('analyzeTranslations.needsTranslation')}]\n\n`;
475
+ });
476
+
477
+ if (notTranslatedIssues.length > 50) {
478
+ report += `${this.t('analyzeTranslations.andMoreKeys', { count: notTranslatedIssues.length - 50 })}\n\n`;
479
+ }
480
+ }
481
+
482
+ return report;
483
+ }
484
+
485
+ // Save report to file
486
+ async saveReport(language, report) {
487
+ const reportPath = path.join(this.outputDir, `analysis-${language}.txt`);
488
+ const validatedPath = SecurityUtils.validatePath(reportPath, this.outputDir);
489
+
490
+ if (!validatedPath) {
491
+ throw new Error('Invalid report file path');
492
+ }
493
+
494
+ const success = await SecurityUtils.safeWriteFile(validatedPath, report, this.outputDir);
495
+ if (!success) {
496
+ throw new Error('Failed to write report file securely');
497
+ }
498
+
499
+ return validatedPath;
500
+ }
501
+
502
+ // Show help message
503
+ showHelp() {
504
+ console.log(this.t('analyzeTranslations.help_message'));
505
+ }
506
+
507
+ // Main analyze method
508
+ async analyze() {
509
+ try {
510
+ const results = []; // Add this line to declare the results array
511
+
512
+ console.log(this.t('analyzeTranslations.starting') || 'šŸ” Starting translation analysis...');
513
+
514
+ // Ensure output directory exists
515
+ if (!fs.existsSync(this.outputDir)) {
516
+ fs.mkdirSync(this.outputDir, { recursive: true });
517
+ }
518
+
519
+ const languages = this.getAvailableLanguages();
520
+
521
+ if (languages.length === 0) {
522
+ console.log(this.t('analyzeTranslations.noLanguages') || 'āš ļø No target languages found.');
523
+ return;
524
+ }
525
+
526
+ console.log(this.t('analyzeTranslations.foundLanguages', { count: languages.length, languages: languages.join(', ') }) || `šŸ“‹ Found ${languages.length} languages to analyze: ${languages.join(', ')}`);
527
+
528
+ for (const language of languages) {
529
+ console.log(this.t('analyzeTranslations.analyzing', { language }) || `\nšŸ”„ Analyzing ${language}...`);
530
+
531
+ const analysis = this.analyzeLanguage(language);
532
+ const report = this.generateLanguageReport(analysis);
533
+
534
+ // Save report
535
+ const reportPath = await this.saveReport(language, report);
536
+
537
+ console.log(this.t('analyzeTranslations.completed', { language }) || `āœ… Analysis completed for ${language}`);
538
+ console.log(this.t('analyzeTranslations.progress', {
539
+ percentage: analysis.summary.percentage,
540
+ translatedKeys: analysis.summary.translatedKeys,
541
+ totalKeys: analysis.summary.totalKeys
542
+ }) || ` Progress: ${analysis.summary.percentage}% (${analysis.summary.translatedKeys}/${analysis.summary.totalKeys} keys)`);
543
+ console.log(this.t('analyzeTranslations.reportSaved', { reportPath }) || ` Report saved: ${reportPath}`);
544
+
545
+ results.push({
546
+ language,
547
+ analysis,
548
+ reportPath
549
+ });
550
+ }
551
+
552
+ // Summary
553
+ console.log(this.t('analyzeTranslations.summary') || '\nšŸ“Š ANALYSIS SUMMARY');
554
+ console.log('='.repeat(50));
555
+
556
+ results.forEach(({ language, analysis }) => {
557
+ console.log(`${language}: ${analysis.summary.percentage}% complete (${analysis.summary.translatedKeys}/${analysis.summary.totalKeys} keys)`);
558
+ });
559
+
560
+ console.log(this.t('analyzeTranslations.finished') || '\nāœ… Analysis completed successfully!');
561
+
562
+ // Only prompt for input if running standalone (not from menu or workflow)
563
+ if (require.main === module && !this.noPrompt) {
564
+ await this.prompt('\nPress Enter to continue...');
565
+ }
566
+ this.closeReadline();
567
+
568
+ return results;
569
+
570
+ } catch (error) {
571
+ console.error(this.t('analyzeTranslations.error') || 'āŒ Analysis failed:', error.message);
572
+ this.closeReadline();
573
+ throw error;
574
+ }
575
+ }
576
+
577
+ // Main analysis process
578
+ async run() {
579
+ try {
580
+ const args = this.parseArgs();
581
+
582
+ if (args.help) {
583
+ this.showHelp();
584
+ return;
585
+ }
586
+
587
+ // Set noPrompt flag
588
+ this.noPrompt = args.noPrompt;
589
+
590
+ // Handle UI language change
591
+ if (args.uiLanguage) {
592
+ loadTranslations(args.uiLanguage);
593
+ this.t = t;
594
+ }
595
+
596
+ // Update config if source directory is provided
597
+ if (args.sourceDir) {
598
+ this.config.sourceDir = args.sourceDir;
599
+ this.sourceDir = path.resolve(this.config.sourceDir);
600
+ this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage);
601
+ }
602
+
603
+ if (args.outputDir) {
604
+ this.config.outputDir = args.outputDir;
605
+ this.outputDir = path.resolve(this.config.outputDir);
606
+ }
607
+
608
+ await this.analyze();
609
+ } catch (error) {
610
+ this.closeReadline();
611
+ throw error;
612
+ }
613
+ }
614
+ }
615
+
616
+ // Run if called directly
617
+ if (require.main === module) {
618
+ const analyzer = new I18nAnalyzer();
619
+ analyzer.run().catch(error => {
620
+ console.error('āŒ Analysis failed:', error.message);
621
+ process.exit(1);
622
+ });
623
+ }
624
+
625
+ module.exports = I18nAnalyzer;