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,762 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * I18N TRANSLATION VALIDATION TOOLKIT
4
+ *
5
+ * This script validates translation files for completeness, consistency,
6
+ * and structural integrity across all languages.
7
+ *
8
+ * Usage:
9
+ * npm run i18ntk:validate
10
+ * npm run i18ntk:validate -- --strict
11
+ * npm run i18ntk:validate -- --language=de
12
+ * npm run i18ntk:validate -- --source-dir=./src/i18n/locales
13
+ *
14
+ * Alternative direct usage:
15
+ * node i18ntk-validate.js
16
+ */
17
+
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+ const { loadTranslations, t } = require('../utils/i18n-helper');
21
+ const settingsManager = require('../settings/settings-manager');
22
+ const SecurityUtils = require('../utils/security');
23
+ const AdminCLI = require('../utils/admin-cli');
24
+
25
+ // Get configuration from settings manager
26
+ async function getConfig() {
27
+ try {
28
+ SecurityUtils.logSecurityEvent('config_access', 'info', 'Accessing configuration for validation');
29
+ const settings = settingsManager.getSettings();
30
+ const config = {
31
+ sourceDir: settings.directories?.sourceDir || settings.sourceDir || './locales',
32
+ sourceLanguage: settings.directories?.sourceLanguage || settings.sourceLanguage || 'en',
33
+ notTranslatedMarker: settings.processing?.notTranslatedMarker || 'NOT_TRANSLATED',
34
+ excludeFiles: settings.processing?.excludeFiles || ['.DS_Store', 'Thumbs.db'],
35
+ strictMode: settings.processing?.strictMode || false,
36
+ uiLanguage: settings.language || 'en'
37
+ };
38
+
39
+ // Validate configuration
40
+ SecurityUtils.validateConfig(config);
41
+ SecurityUtils.logSecurityEvent('config_validated', 'info', 'Configuration validated successfully');
42
+
43
+ return config;
44
+ } catch (error) {
45
+ SecurityUtils.logSecurityEvent('config_error', 'error', `Configuration error: ${error.message}`);
46
+ throw error;
47
+ }
48
+ }
49
+
50
+ class I18nValidator {
51
+ constructor(config = {}) {
52
+ this.config = config;
53
+ this.errors = [];
54
+ this.warnings = [];
55
+ this.t = null;
56
+ }
57
+
58
+ async initialize() {
59
+ try {
60
+ SecurityUtils.logSecurityEvent('validator_init', 'info', 'Initializing I18n validator');
61
+
62
+ const defaultConfig = await getConfig();
63
+ this.config = { ...defaultConfig, ...this.config };
64
+
65
+ // Validate configuration values
66
+ if (!this.config.sourceDir) {
67
+ throw new Error('Source directory not configured');
68
+ }
69
+
70
+ if (!this.config.sourceLanguage) {
71
+ throw new Error('Source language not configured');
72
+ }
73
+
74
+ // Validate and resolve paths
75
+ const resolvedSourceDir = path.resolve(this.config.sourceDir);
76
+ this.sourceDir = resolvedSourceDir;
77
+ this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage);
78
+
79
+ // Initialize i18n with UI language - FIX: Properly initialize translation function
80
+ const uiLanguage = SecurityUtils.sanitizeInput(this.config.uiLanguage || 'en');
81
+ loadTranslations(uiLanguage);
82
+ this.t = t; // Assign the translation function
83
+
84
+ // Verify translation function is working
85
+ if (typeof this.t !== 'function') {
86
+ throw new Error('Translation function not properly initialized');
87
+ }
88
+
89
+ SecurityUtils.logSecurityEvent('validator_initialized', 'info', 'I18n validator initialized successfully');
90
+ } catch (error) {
91
+ SecurityUtils.logSecurityEvent('validator_init_error', 'error', `Validator initialization error: ${error.message}`);
92
+ throw error;
93
+ }
94
+ }
95
+
96
+ // Parse command line arguments
97
+ parseArgs() {
98
+ try {
99
+ SecurityUtils.logSecurityEvent('args_parsing', 'info', 'Parsing command line arguments');
100
+
101
+ const args = process.argv.slice(2);
102
+ const parsed = {};
103
+
104
+ args.forEach(arg => {
105
+ const sanitizedArg = SecurityUtils.sanitizeInput(arg);
106
+
107
+ if (sanitizedArg.startsWith('--')) {
108
+ const [key, value] = sanitizedArg.substring(2).split('=');
109
+ const sanitizedKey = SecurityUtils.sanitizeInput(key);
110
+ const sanitizedValue = value ? SecurityUtils.sanitizeInput(value) : true;
111
+
112
+ if (sanitizedKey === 'language') {
113
+ parsed.language = sanitizedValue;
114
+ } else if (sanitizedKey === 'source-dir') {
115
+ parsed.sourceDir = sanitizedValue;
116
+ } else if (sanitizedKey === 'strict') {
117
+ parsed.strictMode = true;
118
+ } else if (sanitizedKey === 'ui-language') {
119
+ parsed.uiLanguage = sanitizedValue;
120
+ } else if (sanitizedKey === 'help') {
121
+ parsed.help = true;
122
+ } else if (sanitizedKey === 'setup-admin') {
123
+ parsed.setupAdmin = true;
124
+ } else if (sanitizedKey === 'disable-admin') {
125
+ parsed.disableAdmin = true;
126
+ } else if (sanitizedKey === 'admin-status') {
127
+ parsed.adminStatus = true;
128
+ } else if (['en', 'de', 'es', 'fr', 'ru', 'ja', 'zh'].includes(sanitizedKey)) {
129
+ // Support shorthand language flags like --de, --fr, etc.
130
+ parsed.uiLanguage = sanitizedKey;
131
+ }
132
+ }
133
+ });
134
+
135
+ SecurityUtils.logSecurityEvent('args_parsed', 'info', 'Command line arguments parsed successfully');
136
+ return parsed;
137
+ } catch (error) {
138
+ SecurityUtils.logSecurityEvent('args_parse_error', 'error', `Argument parsing error: ${error.message}`);
139
+ throw error;
140
+ }
141
+ }
142
+
143
+ // Add error
144
+ addError(message, details = {}) {
145
+ this.errors.push({ message, details, type: 'error' });
146
+ }
147
+
148
+ // Add warning
149
+ addWarning(message, details = {}) {
150
+ this.warnings.push({ message, details, type: 'warning' });
151
+ }
152
+
153
+ // Get all available languages
154
+ getAvailableLanguages() {
155
+ try {
156
+ SecurityUtils.logSecurityEvent('languages_scan', 'info', 'Scanning available languages');
157
+
158
+ if (!fs.existsSync(this.sourceDir)) {
159
+ throw new Error(`Source directory not found: ${this.sourceDir}`);
160
+ }
161
+
162
+ const languages = fs.readdirSync(this.sourceDir)
163
+ .filter(item => {
164
+ const itemPath = path.join(this.sourceDir, item);
165
+ return fs.statSync(itemPath).isDirectory() && item !== this.config.sourceLanguage;
166
+ });
167
+
168
+ SecurityUtils.logSecurityEvent('languages_found', 'info', `Found ${languages.length} languages`);
169
+ return languages;
170
+ } catch (error) {
171
+ SecurityUtils.logSecurityEvent('languages_scan_error', 'error', `Language scanning error: ${error.message}`);
172
+ throw error;
173
+ }
174
+ }
175
+
176
+ // Get all JSON files from a language directory
177
+ getLanguageFiles(language) {
178
+ try {
179
+ const sanitizedLanguage = SecurityUtils.sanitizeInput(language);
180
+ const languageDir = path.join(this.sourceDir, sanitizedLanguage);
181
+
182
+ if (!fs.existsSync(languageDir)) {
183
+ return [];
184
+ }
185
+
186
+ const files = fs.readdirSync(languageDir)
187
+ .filter(file => {
188
+ return file.endsWith('.json') &&
189
+ !this.config.excludeFiles.includes(file);
190
+ });
191
+
192
+ SecurityUtils.logSecurityEvent('files_scan', 'info', `Found ${files.length} files in ${sanitizedLanguage}`);
193
+ return files;
194
+ } catch (error) {
195
+ SecurityUtils.logSecurityEvent('files_scan_error', 'error', `File scanning error: ${error.message}`);
196
+ throw error;
197
+ }
198
+ }
199
+
200
+ // Get all keys recursively from an object
201
+ getAllKeys(obj, prefix = '') {
202
+ const keys = new Set();
203
+
204
+ for (const [key, value] of Object.entries(obj)) {
205
+ const fullKey = prefix ? `${prefix}.${key}` : key;
206
+ keys.add(fullKey);
207
+
208
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
209
+ const nestedKeys = this.getAllKeys(value, fullKey);
210
+ nestedKeys.forEach(k => keys.add(k));
211
+ }
212
+ }
213
+
214
+ return keys;
215
+ }
216
+
217
+ // Get value by key path
218
+ getValueByPath(obj, keyPath) {
219
+ const keys = keyPath.split('.');
220
+ let current = obj;
221
+
222
+ for (const key of keys) {
223
+ if (current && typeof current === 'object' && key in current) {
224
+ current = current[key];
225
+ } else {
226
+ return undefined;
227
+ }
228
+ }
229
+
230
+ return current;
231
+ }
232
+
233
+ // Validate JSON file syntax
234
+ async validateJsonSyntax(filePath) {
235
+ try {
236
+ const content = fs.readFileSync(filePath, 'utf8');
237
+ const parsed = SecurityUtils.safeParseJSON(content);
238
+
239
+ SecurityUtils.logSecurityEvent('json_validated', 'info', `JSON syntax validated: ${filePath}`);
240
+ return { valid: true, data: parsed };
241
+ } catch (error) {
242
+ SecurityUtils.logSecurityEvent('json_validation_error', 'error', `JSON validation error: ${error.message}`);
243
+ return {
244
+ valid: false,
245
+ error: error.message,
246
+ line: error.message.match(/line (\d+)/)?.[1] || 'unknown'
247
+ };
248
+ }
249
+ }
250
+
251
+ // Validate structural consistency
252
+ validateStructure(sourceObj, targetObj, language, fileName) {
253
+ const sourceKeys = this.getAllKeys(sourceObj);
254
+ const targetKeys = this.getAllKeys(targetObj);
255
+
256
+ const missingKeys = [...sourceKeys].filter(key => !targetKeys.has(key));
257
+ const extraKeys = [...targetKeys].filter(key => !sourceKeys.has(key));
258
+
259
+ // Report missing keys as errors
260
+ missingKeys.forEach(key => {
261
+ this.addError(
262
+ `Missing key in ${language}/${fileName}`,
263
+ { key, language, fileName }
264
+ );
265
+ });
266
+
267
+ // Report extra keys as warnings
268
+ extraKeys.forEach(key => {
269
+ this.addWarning(
270
+ `Extra key in ${language}/${fileName}`,
271
+ { key, language, fileName }
272
+ );
273
+ });
274
+
275
+ return {
276
+ isConsistent: missingKeys.length === 0 && extraKeys.length === 0,
277
+ missingKeys,
278
+ extraKeys,
279
+ sourceKeyCount: sourceKeys.size,
280
+ targetKeyCount: targetKeys.size
281
+ };
282
+ }
283
+
284
+ // Validate translation completeness
285
+ validateTranslations(obj, language, fileName, prefix = '') {
286
+ let totalKeys = 0;
287
+ let translatedKeys = 0;
288
+ let issues = [];
289
+
290
+ for (const [key, value] of Object.entries(obj)) {
291
+ const fullKey = prefix ? `${prefix}.${key}` : key;
292
+
293
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
294
+ const nested = this.validateTranslations(value, language, fileName, fullKey);
295
+ totalKeys += nested.totalKeys;
296
+ translatedKeys += nested.translatedKeys;
297
+ issues.push(...nested.issues);
298
+ } else if (typeof value === 'string') {
299
+ totalKeys++;
300
+
301
+ if (value === this.config.notTranslatedMarker) {
302
+ issues.push({
303
+ type: 'not_translated',
304
+ key: fullKey,
305
+ value,
306
+ language,
307
+ fileName
308
+ });
309
+ } else if (value === '') {
310
+ issues.push({
311
+ type: 'empty_value',
312
+ key: fullKey,
313
+ value,
314
+ language,
315
+ fileName
316
+ });
317
+ } else if (value.includes(this.config.notTranslatedMarker)) {
318
+ issues.push({
319
+ type: 'partial_translation',
320
+ key: fullKey,
321
+ value,
322
+ language,
323
+ fileName
324
+ });
325
+ } else {
326
+ translatedKeys++;
327
+ }
328
+ }
329
+ }
330
+
331
+ return { totalKeys, translatedKeys, issues };
332
+ }
333
+
334
+ // Validate a single language
335
+ async validateLanguage(language) {
336
+ try {
337
+ SecurityUtils.logSecurityEvent('language_validation', 'info', `Validating language: ${language}`);
338
+
339
+ const sanitizedLanguage = SecurityUtils.sanitizeInput(language);
340
+ const languageDir = path.join(this.sourceDir, sanitizedLanguage);
341
+ const sourceFiles = this.getLanguageFiles(this.config.sourceLanguage);
342
+ const targetFiles = this.getLanguageFiles(sanitizedLanguage);
343
+
344
+ const validation = {
345
+ language: sanitizedLanguage,
346
+ files: {},
347
+ summary: {
348
+ totalFiles: sourceFiles.length,
349
+ validFiles: 0,
350
+ totalKeys: 0,
351
+ translatedKeys: 0,
352
+ missingFiles: [],
353
+ syntaxErrors: [],
354
+ structuralIssues: [],
355
+ translationIssues: []
356
+ }
357
+ };
358
+
359
+ // Check for missing language directory
360
+ if (!fs.existsSync(languageDir)) {
361
+ this.addError(
362
+ `Language directory missing: ${sanitizedLanguage}`,
363
+ { language: sanitizedLanguage, expectedPath: languageDir }
364
+ );
365
+ return validation;
366
+ }
367
+
368
+ // Validate each file
369
+ for (const fileName of sourceFiles) {
370
+ const sourceFilePath = path.join(this.sourceLanguageDir, fileName);
371
+ const targetFilePath = path.join(languageDir, fileName);
372
+
373
+ // Check if source file exists
374
+ if (!fs.existsSync(sourceFilePath)) {
375
+ this.addWarning(
376
+ `Source file missing: ${this.config.sourceLanguage}/${fileName}`,
377
+ { fileName, language: this.config.sourceLanguage }
378
+ );
379
+ continue;
380
+ }
381
+
382
+ // Check if target file exists
383
+ if (!fs.existsSync(targetFilePath)) {
384
+ this.addError(
385
+ `Translation file missing: ${language}/${fileName}`,
386
+ { fileName, language, expectedPath: targetFilePath }
387
+ );
388
+ validation.summary.missingFiles.push(fileName);
389
+ continue;
390
+ }
391
+
392
+ // Validate JSON syntax for both files
393
+ const sourceValidation = await this.validateJsonSyntax(sourceFilePath);
394
+ const targetValidation = await this.validateJsonSyntax(targetFilePath);
395
+
396
+ if (!sourceValidation.valid) {
397
+ this.addError(
398
+ `Invalid JSON syntax in source file: ${this.config.sourceLanguage}/${fileName}`,
399
+ { fileName, language: this.config.sourceLanguage, error: sourceValidation.error }
400
+ );
401
+ validation.summary.syntaxErrors.push({ fileName, type: 'source', error: sourceValidation.error });
402
+ continue;
403
+ }
404
+
405
+ if (!targetValidation.valid) {
406
+ this.addError(
407
+ `Invalid JSON syntax in target file: ${sanitizedLanguage}/${fileName}`,
408
+ { fileName, language: sanitizedLanguage, error: targetValidation.error }
409
+ );
410
+ validation.summary.syntaxErrors.push({ fileName, type: 'target', error: targetValidation.error });
411
+ continue;
412
+ }
413
+
414
+ // Use parsed data from validation
415
+ const sourceContent = sourceValidation.data;
416
+ const targetContent = targetValidation.data;
417
+
418
+ // Validate structure
419
+ const structural = this.validateStructure(sourceContent, targetContent, language, fileName);
420
+
421
+ // Validate translations
422
+ const translations = this.validateTranslations(targetContent, language, fileName);
423
+
424
+ // Store file validation results
425
+ validation.files[fileName] = {
426
+ status: 'validated',
427
+ structural,
428
+ translations,
429
+ sourceFilePath,
430
+ targetFilePath
431
+ };
432
+
433
+ // Update summary
434
+ validation.summary.validFiles++;
435
+ validation.summary.totalKeys += translations.totalKeys;
436
+ validation.summary.translatedKeys += translations.translatedKeys;
437
+
438
+ if (!structural.isConsistent) {
439
+ validation.summary.structuralIssues.push({
440
+ fileName,
441
+ missingKeys: structural.missingKeys.length,
442
+ extraKeys: structural.extraKeys.length
443
+ });
444
+ }
445
+
446
+ validation.summary.translationIssues.push(...translations.issues);
447
+ }
448
+
449
+ // Calculate completion percentage
450
+ validation.summary.percentage = validation.summary.totalKeys > 0
451
+ ? Math.round((validation.summary.translatedKeys / validation.summary.totalKeys) * 100)
452
+ : 0;
453
+
454
+ return validation;
455
+ } catch (error) {
456
+ SecurityUtils.logSecurityEvent('language_validation_error', 'error', {
457
+ language: language,
458
+ error: error.message,
459
+ timestamp: new Date().toISOString()
460
+ });
461
+
462
+ this.addError(
463
+ `Language validation failed for ${language}: ${error.message}`,
464
+ { language, error: error.message }
465
+ );
466
+
467
+ return {
468
+ language: language,
469
+ files: {},
470
+ summary: {
471
+ totalFiles: 0,
472
+ validFiles: 0,
473
+ totalKeys: 0,
474
+ translatedKeys: 0,
475
+ missingFiles: [],
476
+ syntaxErrors: [],
477
+ structuralIssues: [],
478
+ translationIssues: [],
479
+ percentage: 0
480
+ }
481
+ };
482
+ }
483
+ }
484
+
485
+ // Check for unused translation keys (basic implementation)
486
+ checkUnusedKeys(language) {
487
+ // Note: For comprehensive unused key detection, use the dedicated
488
+ // usage analysis script: i18ntk-usage.js
489
+ const warnings = [];
490
+
491
+ // This method provides basic validation only
492
+ // For detailed usage analysis, run: node i18ntk-usage.js
493
+
494
+ return warnings;
495
+ }
496
+
497
+ // Show help message
498
+ showHelp() {
499
+ console.log(this.t('validateTranslations.help_message'));
500
+ }
501
+
502
+ // Main validation process
503
+ async validate() {
504
+ try {
505
+ console.log(this.t('validateTranslations.title'));
506
+ console.log(this.t("validateTranslations.message"));
507
+
508
+ // Delete old validation report if it exists
509
+ const reportPath = path.join(process.cwd(), 'validation-report.txt');
510
+ SecurityUtils.validatePath(reportPath);
511
+
512
+ if (fs.existsSync(reportPath)) {
513
+ fs.unlinkSync(reportPath);
514
+ console.log(this.t('validateTranslations.deletedOldReport'));
515
+
516
+ SecurityUtils.logSecurityEvent('file_deleted', 'info', {
517
+ path: reportPath,
518
+ timestamp: new Date().toISOString()
519
+ });
520
+ }
521
+
522
+ // Parse command line arguments
523
+ const args = this.parseArgs();
524
+
525
+ // Handle UI language change
526
+ if (args.uiLanguage) {
527
+ loadTranslations(args.uiLanguage);
528
+ this.t = t;
529
+ }
530
+
531
+ if (args.sourceDir) {
532
+ this.config.sourceDir = args.sourceDir;
533
+ this.sourceDir = path.resolve(this.config.sourceDir);
534
+ this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage);
535
+ }
536
+ if (args.strictMode) {
537
+ this.config.strictMode = true;
538
+ }
539
+
540
+ console.log(this.t('validateTranslations.sourceDirectory', { dir: this.sourceDir }));
541
+ console.log(this.t("validateTranslations.source_language_thisconfigsour", { sourceLanguage: this.config.sourceLanguage }));
542
+ console.log(this.t('validateTranslations.strictMode', { mode: this.config.strictMode ? 'ON' : 'OFF' }));
543
+
544
+ // Validate source language directory exists
545
+ SecurityUtils.validatePath(this.sourceLanguageDir);
546
+
547
+ if (!fs.existsSync(this.sourceLanguageDir)) {
548
+ this.addError(
549
+ `Source language directory not found: ${this.sourceLanguageDir}`,
550
+ { sourceLanguage: this.config.sourceLanguage }
551
+ );
552
+
553
+ SecurityUtils.logSecurityEvent('validation_error', 'error', {
554
+ error: 'Source language directory not found',
555
+ path: this.sourceLanguageDir,
556
+ timestamp: new Date().toISOString()
557
+ });
558
+
559
+ throw new Error('Source language directory not found');
560
+ }
561
+
562
+ // Get available languages
563
+ const availableLanguages = this.getAvailableLanguages();
564
+
565
+ if (availableLanguages.length === 0) {
566
+ console.log(this.t('validateTranslations.noTargetLanguages'));
567
+ return { success: true, message: 'No languages to validate' };
568
+ }
569
+
570
+ // Filter languages if specified
571
+ const targetLanguages = args.language
572
+ ? [args.language].filter(lang => availableLanguages.includes(lang))
573
+ : availableLanguages;
574
+
575
+ if (targetLanguages.length === 0) {
576
+ this.addError(
577
+ `Specified language '${args.language}' not found`,
578
+ { requestedLanguage: args.language, availableLanguages }
579
+ );
580
+ throw new Error('Specified language not found');
581
+ }
582
+
583
+ console.log(this.t('validateTranslations.validatingLanguages', { languages: targetLanguages.join(', ') }));
584
+
585
+ const results = {};
586
+
587
+ // Validate each language
588
+ for (const language of targetLanguages) {
589
+ console.log(this.t('validateTranslations.validatingLanguage', { language: language }));
590
+
591
+ const validation = await this.validateLanguage(language);
592
+ results[language] = validation;
593
+
594
+ // Display summary
595
+ const { summary } = validation;
596
+ console.log(this.t('validateTranslations.filesCount', { filesCount: summary.validFiles }));
597
+ console.log(this.t('validateTranslations.keysCount', { keysCount: summary.totalKeys }));
598
+ console.log(this.t('validateTranslations.missingFilesCount', { missingFilesCount: summary.missingFiles.length }));
599
+ console.log(this.t('validateTranslations.syntaxErrorsCount', { syntaxErrorsCount: summary.syntaxErrors.length }));
600
+ console.log(this.t('validateTranslations.translationPercentage', { translationPercentage: summary.percentage, translated: summary.translatedKeys, total: summary.totalKeys }));
601
+ console.log('');
602
+ }
603
+
604
+ // Overall summary
605
+ const hasErrors = this.errors.length > 0;
606
+ const hasWarnings = this.warnings.length > 0;
607
+
608
+ console.log(this.t("validateTranslations.n_validation_summary"));
609
+ console.log(this.t("validateTranslations.total_errors", { totalErrors: this.errors.length }));
610
+ console.log(this.t("validateTranslations.total_warnings", { totalWarnings: this.warnings.length }));
611
+
612
+ // Show errors
613
+ if (hasErrors) {
614
+ console.log(this.t("validateTranslations.n_errors"));
615
+ this.errors.forEach((error, index) => {
616
+ console.log(`${index + 1}. ${error.message}`);
617
+ if (error.details && Object.keys(error.details).length > 0) {
618
+ console.log(` Details: ${JSON.stringify(error.details, null, 2)}`);
619
+ }
620
+ });
621
+ console.log('');
622
+ }
623
+
624
+ // Show warnings
625
+ if (hasWarnings) {
626
+ console.log(this.t("validateTranslations.n_warnings"));
627
+ this.warnings.forEach((warning, index) => {
628
+ console.log(`${index + 1}. ${warning.message}`);
629
+ if (warning.details && Object.keys(warning.details).length > 0) {
630
+ console.log(` Details: ${JSON.stringify(warning.details, null, 2)}`);
631
+ }
632
+ });
633
+ console.log('');
634
+ }
635
+
636
+ // Recommendations
637
+ console.log(this.t("validateTranslations.n_recommendations"));
638
+ console.log(this.t("validateTranslations.message"));
639
+
640
+ if (hasErrors) {
641
+ console.log(this.t("validateTranslations.fix_errors_first"));
642
+ console.log(this.t("validateTranslations.1_resolve_missing_files_and_sy"));
643
+ console.log(this.t("validateTranslations.2_fix_structural_inconsistenci"));
644
+ console.log(this.t("validateTranslations.3_complete_missing_translation"));
645
+ console.log(this.t("validateTranslations.4_rerun_validation"));
646
+ } else if (hasWarnings) {
647
+ console.log(this.t("validateTranslations.address_warnings"));
648
+ console.log(this.t("validateTranslations.review_warnings"));
649
+ console.log(this.t("validateTranslations.2_consider_running_with_strict"));
650
+ } else {
651
+ console.log(this.t("validateTranslations.all_validations_passed"));
652
+ console.log(this.t("validateTranslations.consider_running_usage_analysi"));
653
+ }
654
+
655
+ // Exit with appropriate code
656
+ const success = !hasErrors && (!hasWarnings || !this.config.strictMode);
657
+
658
+ return {
659
+ success,
660
+ errors: this.errors.length,
661
+ warnings: this.warnings.length,
662
+ results
663
+ };
664
+
665
+ } catch (error) {
666
+ console.error(this.t("validateTranslations.validation_failed", { error: error.message }));
667
+ return {
668
+ success: false,
669
+ error: error.message
670
+ };
671
+ }
672
+ }
673
+
674
+ /**
675
+ * Run method for compatibility with manager
676
+ */
677
+ async run() {
678
+ try {
679
+ console.log('🔍 Starting validation process...');
680
+ SecurityUtils.logSecurityEvent('run_started', 'info', 'Starting validation run');
681
+
682
+ await this.initialize();
683
+ const result = await this.validate();
684
+
685
+ console.log('✅ Validation process completed successfully');
686
+ SecurityUtils.logSecurityEvent('run_completed', 'info', 'Validation run completed successfully');
687
+ return result;
688
+ } catch (error) {
689
+ console.error('❌ Validation Error:', error.message);
690
+ console.error('Stack trace:', error.stack);
691
+ SecurityUtils.logSecurityEvent('run_error', 'error', `Validation run failed: ${error.message}`);
692
+ throw error;
693
+ }
694
+ }
695
+ }
696
+
697
+ module.exports = I18nValidator;
698
+
699
+ if (require.main === module) {
700
+ (async () => {
701
+ try {
702
+ SecurityUtils.logSecurityEvent('script_execution', 'info', {
703
+ script: 'i18ntk-validate.js',
704
+ timestamp: new Date().toISOString()
705
+ });
706
+
707
+ const validator = new I18nValidator();
708
+ await validator.initialize();
709
+
710
+ const args = validator.parseArgs();
711
+
712
+ if (args.help) {
713
+ validator.showHelp();
714
+ process.exit(0);
715
+ }
716
+
717
+ if (args.setupAdmin) {
718
+ await AdminCLI.setupAdmin();
719
+ process.exit(0);
720
+ }
721
+
722
+ if (args.disableAdmin) {
723
+ await AdminCLI.disableAdmin();
724
+ process.exit(0);
725
+ }
726
+
727
+ if (args.adminStatus) {
728
+ await AdminCLI.showStatus();
729
+ process.exit(0);
730
+ }
731
+
732
+ if (AdminCLI.requiresAuth('validate')) {
733
+ const authenticated = await AdminCLI.authenticate();
734
+ if (!authenticated) {
735
+ console.log('❌ Authentication failed. Access denied.');
736
+ process.exit(1);
737
+ }
738
+ }
739
+
740
+ const result = await validator.validate();
741
+
742
+ SecurityUtils.logSecurityEvent('validation_completed', 'info', {
743
+ success: result.success,
744
+ errors: result.errors || 0,
745
+ warnings: result.warnings || 0,
746
+ timestamp: new Date().toISOString()
747
+ });
748
+
749
+ process.exit(result.success ? 0 : 1);
750
+ } catch (error) {
751
+ console.error('❌ Fatal validation error:', error.message);
752
+ console.error('Stack trace:', error.stack);
753
+ SecurityUtils.logSecurityEvent('validation_error', 'error', {
754
+ error: error.message,
755
+ stack: error.stack,
756
+ timestamp: new Date().toISOString()
757
+ });
758
+
759
+ process.exit(1);
760
+ }
761
+ })();
762
+ }