i18ntk 1.7.2 → 1.7.4

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.
@@ -0,0 +1,653 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * I18N TRANSLATION FIXER
4
+ *
5
+ * Replaces placeholder translations with English source text prefixed by language code
6
+ * and optionally fills missing keys.
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const { getUnifiedConfig, displayHelp } = require('../utils/config-helper');
12
+ const { loadTranslations } = require('../utils/i18n-helper');
13
+ loadTranslations(process.env.I18NTK_LANG);
14
+
15
+ class I18nFixer {
16
+ constructor(config = {}) {
17
+ this.config = config;
18
+ this.sourceDir = null;
19
+ this.sourceLanguageDir = null;
20
+ this.markers = [];
21
+ this.languages = [];
22
+ this.locale = this.loadLocale();
23
+ }
24
+
25
+ loadLocale() {
26
+ const uiLocalesDir = path.join(__dirname, '..', 'ui-locales');
27
+ const localeFile = path.join(uiLocalesDir, 'en.json');
28
+
29
+ try {
30
+ const localeContent = fs.readFileSync(localeFile, 'utf8');
31
+ return JSON.parse(localeContent);
32
+ } catch (error) {
33
+ // Fallback to basic English strings if locale file not found
34
+ return {
35
+ fixer: {
36
+ help_message: "\nI18n Translation Fixer\n\nUsage: node i18ntk-fixer.js [options]\n\nOptions:\n --source-dir <dir> Source directory to scan (default: ./locales)\n --languages <langs> Comma separated list of languages to fix\n --markers <markers> Comma separated markers to treat as untranslated\n --no-backup Skip automatic backup creation\n --help Show this help\n\nExamples:\n node i18ntk-fixer.js --languages de,fr\n node i18ntk-fixer.js --source-dir=./locales --markers NOT_TRANSLATED\n node i18ntk-fixer.js --no-backup\n",
37
+ starting: "🚀 Starting translation fixing for languages: {languages}",
38
+ sourceDirectory: "📁 Source directory: {sourceDir}",
39
+ sourceLanguage: "🔤 Source language: {sourceLanguage}",
40
+ markers: "🏷️ Markers to fix: {markers}",
41
+ scanningLanguage: "📊 Scanning {language}...",
42
+ noLanguages: "❌ No languages specified for fixing.",
43
+ allComplete: "🎉 All translations are already complete!",
44
+ fullReportSaved: "📊 Full report saved to: {reportPath}",
45
+ reviewReport: "Please review the report before proceeding.",
46
+ backupCreated: "💾 Backup created successfully.",
47
+ applyingFixes: "🔄 Applying fixes...",
48
+ fixingComplete: "✅ Translation fixing complete!",
49
+ operationCancelled: "❌ Operation cancelled by user.",
50
+ analysisTitle: "🔍 TRANSLATION FIXING ANALYSIS",
51
+ analysisSeparator: "==================================================",
52
+ totalIssues: "Total issues found: {totalIssues}",
53
+ missingTranslations: "Missing translations: {missing}",
54
+ placeholderTranslations: "Placeholder translations: {placeholder}",
55
+ noIssues: "✅ No issues found. All translations are complete.",
56
+ detailedIssues: "📋 DETAILED ISSUES:",
57
+ detailedSeparator: "--------------------------------------------------",
58
+ filePath: "📄 {file} → {path}",
59
+ missingKey: "❌ MISSING: {source} → {new}",
60
+ placeholderKey: "⚠️ PLACEHOLDER: \"{target}\" → \"{new}\"",
61
+ moreIssues: "... and {count} more issues. Check the report file for complete details.",
62
+ confirmationTitle: "🤔 Do you want to proceed with these fixes?",
63
+ confirmationOptions: "Options:",
64
+ optionYes: "y - Yes, apply all fixes",
65
+ optionNo: "n - No, cancel operation",
66
+ optionShow: "s - Show detailed issues",
67
+ choicePrompt: "Your choice (y/n/s): ",
68
+ nonInteractiveMode: "⚡ Non-interactive mode detected - applying fixes automatically...",
69
+ reportGenerated: "📊 Fixer report generated: {path}",
70
+ summary: {
71
+ totalIssues: "Total issues: {total}",
72
+ missingKeys: "Missing keys: {missing}",
73
+ placeholderKeys: "Placeholder keys: {placeholder}",
74
+ languages: "Languages: {languages}"
75
+ }
76
+ }
77
+ };
78
+ }
79
+ }
80
+
81
+ t(key, params = {}) {
82
+ const keys = key.split('.');
83
+ let value = this.locale;
84
+
85
+ for (const k of keys) {
86
+ value = value?.[k];
87
+ if (value === undefined) break;
88
+ }
89
+
90
+ if (typeof value !== 'string') {
91
+ return key; // Fallback to key if translation not found
92
+ }
93
+
94
+ return value.replace(/\{([^}]+)\}/g, (match, param) => {
95
+ return params[param] !== undefined ? params[param] : match;
96
+ });
97
+ }
98
+
99
+ parseArgs() {
100
+ const args = process.argv.slice(2);
101
+ const parsed = {};
102
+ args.forEach(arg => {
103
+ if (arg.startsWith('--')) {
104
+ const [key, ...valueParts] = arg.substring(2).split('=');
105
+ const value = valueParts.join('=');
106
+
107
+ if (key === 'source-dir') {
108
+ parsed.sourceDir = value || '';
109
+ } else if (key === 'source-language') {
110
+ parsed.sourceLanguage = value || '';
111
+ } else if (key === 'languages') {
112
+ parsed.languages = value ? value.split(',').map(l => l.trim()).filter(Boolean) : [];
113
+ } else if (key === 'markers') {
114
+ parsed.markers = value ? value.split(',').map(m => m.trim()).filter(Boolean) : [];
115
+ } else if (key === 'no-backup') {
116
+ parsed.noBackup = true;
117
+ } else if (key === 'help' || key === 'h') {
118
+ parsed.help = true;
119
+ }
120
+ }
121
+ });
122
+ return parsed;
123
+ }
124
+
125
+ async promptForMarkers() {
126
+ const readline = require('readline');
127
+ const rl = readline.createInterface({
128
+ input: process.stdin,
129
+ output: process.stdout
130
+ });
131
+
132
+ return new Promise(resolve => {
133
+ const defaultMarkers = ['__NOT_TRANSLATED__', 'NOT_TRANSLATED', 'TODO_TRANSLATE'];
134
+ console.log(`\n${this.t('fixer.markerPrompt.title')}`);
135
+ console.log(this.t('fixer.markerPrompt.description'));
136
+ console.log(this.t('fixer.markerPrompt.currentDefaults', { markers: defaultMarkers.join(', ') }));
137
+
138
+ rl.question(this.t('fixer.markerPrompt.input'), answer => {
139
+ rl.close();
140
+ if (answer.trim()) {
141
+ const markers = answer.split(',').map(m => m.trim()).filter(Boolean);
142
+ resolve(markers);
143
+ } else {
144
+ resolve(defaultMarkers);
145
+ }
146
+ });
147
+ });
148
+ }
149
+
150
+ async promptForLanguages() {
151
+ const readline = require('readline');
152
+ const rl = readline.createInterface({
153
+ input: process.stdin,
154
+ output: process.stdout
155
+ });
156
+
157
+ return new Promise(resolve => {
158
+ const availableLanguages = this.getAvailableLanguages().filter(l => l !== this.config.sourceLanguage);
159
+
160
+ if (availableLanguages.length === 0) {
161
+ console.log(this.t('fixer.languagePrompt.noLanguages'));
162
+ resolve([]);
163
+ return;
164
+ }
165
+
166
+ console.log(`\n${this.t('fixer.languagePrompt.title')}`);
167
+ console.log(this.t('fixer.languagePrompt.available', { languages: availableLanguages.join(', ') }));
168
+ console.log(this.t('fixer.languagePrompt.description'));
169
+
170
+ rl.question(this.t('fixer.languagePrompt.input'), answer => {
171
+ rl.close();
172
+ if (answer.trim()) {
173
+ const languages = answer.split(',').map(l => l.trim()).filter(Boolean);
174
+ // Validate languages exist
175
+ const validLanguages = languages.filter(l => availableLanguages.includes(l));
176
+ resolve(validLanguages);
177
+ } else {
178
+ resolve(availableLanguages);
179
+ }
180
+ });
181
+ });
182
+ }
183
+
184
+ async promptForDirectory() {
185
+ const readline = require('readline');
186
+ const rl = readline.createInterface({
187
+ input: process.stdin,
188
+ output: process.stdout
189
+ });
190
+
191
+ return new Promise(resolve => {
192
+ const defaultDir = this.config.sourceDir || './locales';
193
+ console.log(`\n${this.t('fixer.directoryPrompt.title')}`);
194
+ console.log(this.t('fixer.directoryPrompt.current', { dir: defaultDir }));
195
+ console.log(this.t('fixer.directoryPrompt.description'));
196
+
197
+ rl.question(this.t('fixer.directoryPrompt.input'), answer => {
198
+ rl.close();
199
+ if (answer.trim()) {
200
+ resolve(answer.trim());
201
+ } else {
202
+ resolve(defaultDir);
203
+ }
204
+ });
205
+ });
206
+ }
207
+
208
+ async initialize() {
209
+ const args = this.parseArgs();
210
+ if (args.help) {
211
+ displayHelp('i18ntk-fixer', {
212
+ 'markers': this.t('fixer.help_options.markers'),
213
+ 'languages': this.t('fixer.help_options.languages'),
214
+ 'no-backup': this.t('fixer.help_options.no_backup')
215
+ });
216
+ process.exit(0);
217
+ }
218
+
219
+ const baseConfig = await getUnifiedConfig('fixer', args);
220
+ this.config = { ...baseConfig, ...(this.config || {}) };
221
+
222
+ // Interactive mode - prompt for settings if not provided via CLI
223
+ if (!args['source-dir'] && !args.languages && !args.markers && !this.config.noBackup) {
224
+ console.log(`\n${this.t('fixer.welcome.title')}`);
225
+ console.log(this.t('fixer.welcome.description'));
226
+
227
+ // Prompt for directory
228
+ const customDir = await this.promptForDirectory();
229
+ let sourceDir = customDir;
230
+ sourceDir = sourceDir.replace(/^["']|["']$/g, '');
231
+
232
+ if (path.isAbsolute(sourceDir)) {
233
+ this.sourceDir = sourceDir;
234
+ } else {
235
+ this.sourceDir = path.resolve(process.cwd(), sourceDir);
236
+ }
237
+
238
+ // Prompt for markers
239
+ const customMarkers = await this.promptForMarkers();
240
+ this.markers = customMarkers;
241
+
242
+ // Prompt for languages
243
+ const customLanguages = await this.promptForLanguages();
244
+ this.languages = customLanguages;
245
+ } else {
246
+ // CLI mode - use provided arguments or defaults
247
+ let sourceDir = args['source-dir'] || this.config.sourceDir || './locales';
248
+ sourceDir = sourceDir.replace(/^["']|["']$/g, '');
249
+
250
+ if (path.isAbsolute(sourceDir)) {
251
+ this.sourceDir = sourceDir;
252
+ } else {
253
+ this.sourceDir = path.resolve(process.cwd(), sourceDir);
254
+ }
255
+
256
+ const baseMarkers = this.config.notTranslatedMarkers || [this.config.notTranslatedMarker || '__NOT_TRANSLATED__'];
257
+ let markerArg = args.markers;
258
+ if (typeof markerArg === 'string') {
259
+ markerArg = markerArg.split(',').map(m => m.trim()).filter(Boolean);
260
+ } else if (!Array.isArray(markerArg)) {
261
+ markerArg = [];
262
+ }
263
+ this.markers = [...baseMarkers, ...markerArg].filter(Boolean);
264
+
265
+ const langArg = args.languages || this.config.languages;
266
+ if (typeof langArg === 'string') {
267
+ this.languages = langArg.split(',').map(l => l.trim()).filter(Boolean);
268
+ } else if (Array.isArray(langArg)) {
269
+ this.languages = langArg;
270
+ } else {
271
+ this.languages = this.getAvailableLanguages().filter(l => l !== this.config.sourceLanguage);
272
+ }
273
+ }
274
+
275
+ this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage);
276
+ this.config.outputDir = this.config.outputDir || './i18ntk-reports';
277
+ this.config.noBackup = args['no-backup'] || false;
278
+ }
279
+
280
+ getAvailableLanguages() {
281
+ if (!fs.existsSync(this.sourceDir)) return [];
282
+ const entries = fs.readdirSync(this.sourceDir);
283
+ const langs = new Set();
284
+ entries.forEach(item => {
285
+ const full = path.join(this.sourceDir, item);
286
+ if (fs.statSync(full).isDirectory()) {
287
+ langs.add(item);
288
+ } else if (item.endsWith('.json')) {
289
+ langs.add(path.basename(item, '.json'));
290
+ }
291
+ });
292
+ return Array.from(langs);
293
+ }
294
+
295
+ createBackup() {
296
+ try {
297
+ const ts = new Date().toISOString().replace(/[:.]/g, '-');
298
+ const backupPath = path.join(this.config.backupDir, `fixer-${ts}`);
299
+ fs.cpSync(this.sourceDir, backupPath, { recursive: true });
300
+ console.log(`Backup created at ${path.relative(process.cwd(), backupPath)}`);
301
+ } catch (e) {
302
+ console.warn(`Backup failed: ${e.message}`);
303
+ }
304
+ }
305
+
306
+ getAllFiles(dir) {
307
+ const results = [];
308
+ if (!fs.existsSync(dir)) return results;
309
+ fs.readdirSync(dir).forEach(item => {
310
+ const full = path.join(dir, item);
311
+ const stat = fs.statSync(full);
312
+ if (stat.isDirectory()) {
313
+ results.push(...this.getAllFiles(full));
314
+ } else if (stat.isFile() && item.endsWith('.json')) {
315
+ results.push(full);
316
+ }
317
+ });
318
+ return results;
319
+ }
320
+
321
+ fixObject(target, source, lang) {
322
+ Object.keys(source).forEach(key => {
323
+ const srcVal = source[key];
324
+ const tgtVal = target[key];
325
+ if (srcVal && typeof srcVal === 'object' && !Array.isArray(srcVal)) {
326
+ target[key] = this.fixObject(
327
+ tgtVal && typeof tgtVal === 'object' ? tgtVal : {},
328
+ srcVal,
329
+ lang
330
+ );
331
+ } else {
332
+ const placeholder = `[${lang.toUpperCase()}] ${srcVal}`;
333
+ if (tgtVal === undefined) {
334
+ target[key] = placeholder;
335
+ } else if (typeof tgtVal === 'string' && this.markers.some(m => tgtVal.includes(m))) {
336
+ target[key] = placeholder;
337
+ }
338
+ }
339
+ });
340
+ return target;
341
+ }
342
+
343
+ processLanguage(lang) {
344
+ const files = this.getAllFiles(this.sourceLanguageDir);
345
+ files.forEach(file => {
346
+ const rel = path.relative(this.sourceLanguageDir, file);
347
+ const srcData = JSON.parse(fs.readFileSync(file, 'utf8'));
348
+ const targetFile = path.join(this.sourceDir, lang, rel);
349
+ let tgtData = {};
350
+ if (fs.existsSync(targetFile)) {
351
+ try {
352
+ tgtData = JSON.parse(fs.readFileSync(targetFile, 'utf8'));
353
+ } catch {
354
+ tgtData = {};
355
+ }
356
+ } else {
357
+ fs.mkdirSync(path.dirname(targetFile), { recursive: true });
358
+ }
359
+ const fixed = this.fixObject(tgtData, srcData, lang);
360
+ fs.writeFileSync(targetFile, JSON.stringify(fixed, null, 2));
361
+ });
362
+ }
363
+
364
+ scanForIssues(lang) {
365
+ const issues = [];
366
+ const files = this.getAllFiles(this.sourceLanguageDir);
367
+
368
+ files.forEach(file => {
369
+ const rel = path.relative(this.sourceLanguageDir, file);
370
+ const srcData = JSON.parse(fs.readFileSync(file, 'utf8'));
371
+ const targetFile = path.join(this.sourceDir, lang, rel);
372
+ let tgtData = {};
373
+
374
+ if (fs.existsSync(targetFile)) {
375
+ try {
376
+ tgtData = JSON.parse(fs.readFileSync(targetFile, 'utf8'));
377
+ } catch {
378
+ tgtData = {};
379
+ }
380
+ }
381
+
382
+ this.scanObject(issues, srcData, tgtData, lang, rel, []);
383
+ });
384
+
385
+ return issues;
386
+ }
387
+
388
+ scanObject(issues, source, target, lang, file, pathStack) {
389
+ Object.keys(source).forEach(key => {
390
+ const srcVal = source[key];
391
+ const tgtVal = target[key];
392
+ const currentPath = [...pathStack, key];
393
+
394
+ if (srcVal && typeof srcVal === 'object' && !Array.isArray(srcVal)) {
395
+ this.scanObject(issues, srcVal, tgtVal || {}, lang, file, currentPath);
396
+ } else {
397
+ const placeholder = `[${lang.toUpperCase()}] ${srcVal}`;
398
+
399
+ if (tgtVal === undefined) {
400
+ issues.push({
401
+ type: 'missing',
402
+ file,
403
+ path: currentPath.join('.'),
404
+ sourceValue: srcVal,
405
+ targetValue: null,
406
+ action: 'add',
407
+ newValue: placeholder
408
+ });
409
+ } else if (typeof tgtVal === 'string') {
410
+ // Check if any marker is present in the target value
411
+ const hasMarker = this.markers.some(m => {
412
+ if (m === '__NOT_TRANSLATED__') {
413
+ return tgtVal === '__NOT_TRANSLATED__' || tgtVal.includes('__NOT_TRANSLATED__');
414
+ }
415
+ return tgtVal.includes(m);
416
+ });
417
+
418
+ if (hasMarker) {
419
+ issues.push({
420
+ type: 'placeholder',
421
+ file,
422
+ path: currentPath.join('.'),
423
+ sourceValue: srcVal,
424
+ targetValue: tgtVal,
425
+ action: 'replace',
426
+ newValue: placeholder
427
+ });
428
+ }
429
+ }
430
+ }
431
+ });
432
+ }
433
+
434
+ generateReport(issues) {
435
+ const report = {
436
+ totalIssues: issues.length,
437
+ missingKeys: issues.filter(i => i.type === 'missing').length,
438
+ placeholderKeys: issues.filter(i => i.type === 'placeholder').length,
439
+ languages: {}
440
+ };
441
+
442
+ issues.forEach(issue => {
443
+ const lang = issue.newValue.match(/\[([A-Z-]+)\]/)?.[1];
444
+ if (lang) {
445
+ if (!report.languages[lang]) report.languages[lang] = 0;
446
+ report.languages[lang]++;
447
+ }
448
+ });
449
+
450
+ return report;
451
+ }
452
+
453
+ printDetailedReport(issues, report) {
454
+ console.log(`\n${this.t('fixer.analysisTitle')}`);
455
+ console.log(this.t('fixer.analysisSeparator'));
456
+ console.log(this.t('fixer.totalIssues', { totalIssues: report.totalIssues }));
457
+ console.log(this.t('fixer.missingTranslations', { missing: report.missingKeys }));
458
+ console.log(this.t('fixer.placeholderTranslations', { placeholder: report.placeholderKeys }));
459
+
460
+ if (report.totalIssues === 0) {
461
+ console.log(`\n${this.t('fixer.noIssues')}`);
462
+ return;
463
+ }
464
+
465
+ console.log(`\n${this.t('fixer.detailedIssues')}`);
466
+ console.log(this.t('fixer.detailedSeparator'));
467
+
468
+ const groupedIssues = issues.reduce((acc, issue) => {
469
+ const key = `${issue.file}:${issue.path}`;
470
+ if (!acc[key]) acc[key] = [];
471
+ acc[key].push(issue);
472
+ return acc;
473
+ }, {});
474
+
475
+ Object.entries(groupedIssues).forEach(([key, keyIssues]) => {
476
+ const [file, path] = key.split(':');
477
+ console.log(`\n${this.t('fixer.filePath', { file, path })}`);
478
+
479
+ keyIssues.forEach(issue => {
480
+ if (issue.type === 'missing') {
481
+ console.log(` ${this.t('fixer.missingKey', { source: issue.sourceValue, new: issue.newValue })}`);
482
+ } else {
483
+ console.log(` ${this.t('fixer.placeholderKey', { target: issue.targetValue, new: issue.newValue })}`);
484
+ }
485
+ });
486
+ });
487
+ }
488
+
489
+ async getUserConfirmation() {
490
+ const readline = require('readline');
491
+ const rl = readline.createInterface({
492
+ input: process.stdin,
493
+ output: process.stdout
494
+ });
495
+
496
+ return new Promise(resolve => {
497
+ console.log(`\n${this.t('fixer.confirmationTitle')}`);
498
+ console.log(this.t('fixer.confirmationOptions'));
499
+ console.log(` ${this.t('fixer.optionYes')}`);
500
+ console.log(` ${this.t('fixer.optionNo')}`);
501
+ console.log(` ${this.t('fixer.optionShow')}`);
502
+
503
+ rl.question(this.t('fixer.choicePrompt'), answer => {
504
+ rl.close();
505
+ resolve(answer.toLowerCase());
506
+ });
507
+ });
508
+ }
509
+
510
+ generateFixerReport(issues, report) {
511
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
512
+ const reportDir = path.join(this.config.outputDir || './i18ntk-reports', 'fixer-reports');
513
+
514
+ // Ensure report directory exists
515
+ if (!fs.existsSync(reportDir)) {
516
+ fs.mkdirSync(reportDir, { recursive: true });
517
+ }
518
+
519
+ const reportFile = path.join(reportDir, `fixer-report-${timestamp}.json`);
520
+
521
+ const reportData = {
522
+ timestamp: new Date().toISOString(),
523
+ summary: {
524
+ totalIssues: report.totalIssues,
525
+ missingTranslations: report.missingKeys,
526
+ placeholderTranslations: report.placeholderKeys,
527
+ languages: report.languages
528
+ },
529
+ issues: issues.map(issue => ({
530
+ type: issue.type,
531
+ file: issue.file,
532
+ path: issue.path,
533
+ sourceValue: issue.sourceValue,
534
+ targetValue: issue.targetValue,
535
+ newValue: issue.newValue,
536
+ action: issue.action
537
+ }))
538
+ };
539
+
540
+ fs.writeFileSync(reportFile, JSON.stringify(reportData, null, 2));
541
+
542
+ console.log(this.t('fixer.reportGenerated', { path: path.relative(process.cwd(), reportFile) }));
543
+
544
+ return {
545
+ file: reportFile,
546
+ relativePath: path.relative(process.cwd(), reportFile)
547
+ };
548
+ }
549
+
550
+ printLimitedReport(issues, report) {
551
+ const MAX_DISPLAY = 10;
552
+ const displayIssues = issues.slice(0, MAX_DISPLAY);
553
+
554
+ console.log(`\n${this.t('fixer.analysisTitle')}`);
555
+ console.log(this.t('fixer.analysisSeparator'));
556
+ console.log(this.t('fixer.totalIssues', { totalIssues: report.totalIssues }));
557
+ console.log(this.t('fixer.missingTranslations', { missing: report.missingKeys }));
558
+ console.log(this.t('fixer.placeholderTranslations', { placeholder: report.placeholderKeys }));
559
+
560
+ if (report.totalIssues === 0) {
561
+ console.log(`\n${this.t('fixer.noIssues')}`);
562
+ return;
563
+ }
564
+
565
+ console.log(`\n${this.t('fixer.detailedIssues')}`);
566
+ console.log(this.t('fixer.detailedSeparator'));
567
+
568
+ displayIssues.forEach(issue => {
569
+ if (issue.type === 'missing') {
570
+ console.log(this.t('fixer.filePath', { file: issue.file, path: issue.path }));
571
+ console.log(` ${this.t('fixer.missingKey', { source: issue.sourceValue, new: issue.newValue })}`);
572
+ } else {
573
+ console.log(this.t('fixer.filePath', { file: issue.file, path: issue.path }));
574
+ console.log(` ${this.t('fixer.placeholderKey', { target: issue.targetValue, new: issue.newValue })}`);
575
+ }
576
+ });
577
+
578
+ if (issues.length > MAX_DISPLAY) {
579
+ const remaining = issues.length - MAX_DISPLAY;
580
+ console.log(`\n${this.t('fixer.moreIssues', { count: remaining })}`);
581
+ }
582
+ }
583
+
584
+ async run() {
585
+ await this.initialize();
586
+
587
+ if (this.languages.length === 0) {
588
+ console.log(this.t('fixer.noLanguages'));
589
+ return;
590
+ }
591
+
592
+ console.log(`\n${this.t('fixer.starting', { languages: this.languages.join(', ') })}`);
593
+ console.log(this.t('fixer.sourceDirectory', { sourceDir: this.sourceDir }));
594
+ console.log(this.t('fixer.sourceLanguage', { sourceLanguage: this.config.sourceLanguage }));
595
+ console.log(this.t('fixer.markers', { markers: this.markers.join(', ') }));
596
+
597
+ const allIssues = [];
598
+ for (const lang of this.languages) {
599
+ console.log(this.t('fixer.scanningLanguage', { language: lang }));
600
+ const issues = this.scanForIssues(lang);
601
+ allIssues.push(...issues);
602
+ }
603
+
604
+ const report = this.generateReport(allIssues);
605
+
606
+ if (report.totalIssues === 0) {
607
+ console.log(`\n${this.t('fixer.allComplete')}`);
608
+ return;
609
+ }
610
+
611
+ // Generate and save report
612
+ const reportInfo = this.generateFixerReport(allIssues, report);
613
+
614
+ // Print limited report to console
615
+ this.printLimitedReport(allIssues, report);
616
+
617
+ // Non-interactive mode (for tests)
618
+ if (this.config.noBackup) {
619
+ console.log(`\n${this.t('fixer.nonInteractiveMode')}`);
620
+ this.languages.forEach(lang => this.processLanguage(lang));
621
+ console.log(this.t('fixer.fixingComplete'));
622
+ return;
623
+ }
624
+
625
+ // Interactive mode
626
+ console.log(this.t('fixer.fullReportSaved', { reportPath: reportInfo.relativePath }));
627
+ console.log(this.t('fixer.reviewReport'));
628
+
629
+ const answer = await this.getUserConfirmation();
630
+
631
+ if (answer === 'y' || answer === 'yes') {
632
+ this.createBackup();
633
+ console.log(this.t('fixer.backupCreated'));
634
+
635
+ console.log(`\n${this.t('fixer.applyingFixes')}`);
636
+ this.languages.forEach(lang => this.processLanguage(lang));
637
+ console.log(this.t('fixer.fixingComplete'));
638
+ } else {
639
+ console.log(this.t('fixer.operationCancelled'));
640
+ }
641
+ }
642
+ }
643
+
644
+ // Run if executed directly
645
+ if (require.main === module) {
646
+ const fixer = new I18nFixer();
647
+ fixer.run().catch(err => {
648
+ console.error(err.message);
649
+ process.exit(1);
650
+ });
651
+ }
652
+
653
+ module.exports = I18nFixer;