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,686 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * I18N INITIALIZATION SCRIPT
4
+ *
5
+ * This script initializes a new i18n project or adds new languages to an existing one.
6
+ * It uses the English (en) locale as the source of truth and generates translation files
7
+ * for specified languages with proper structure and __NOT_TRANSLATED__ markers.
8
+ *
9
+ * Usage:
10
+ * node scripts/i18n/01-init-i18n.js
11
+ * node scripts/i18n/01-init-i18n.js --languages=de,es,fr,ru
12
+ * node scripts/i18n/01-init-i18n.js --source-dir=./src/i18n/locales --target-languages=de,es
13
+ */
14
+
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+ const readline = require('readline');
18
+ const settingsManager = require('../settings/settings-manager');
19
+ const SecurityUtils = require('../utils/security');
20
+ const AdminAuth = require('../utils/admin-auth');
21
+ const UIi18n = require('./ui-i18n');
22
+
23
+ // Get configuration from settings manager
24
+ function getConfig() {
25
+ const settings = settingsManager.getSettings();
26
+ return {
27
+ sourceDir: settings.directories?.sourceDir || './locales',
28
+ sourceLanguage: settings.directories?.sourceLanguage || 'en',
29
+ defaultLanguages: settings.processing?.defaultLanguages || ['de', 'es', 'fr', 'ru'],
30
+ notTranslatedMarker: settings.processing?.notTranslatedMarker || 'NOT_TRANSLATED',
31
+ excludeFiles: settings.processing?.excludeFiles || ['.DS_Store', 'Thumbs.db'],
32
+ uiLanguage: settings.language || 'en'
33
+ };
34
+ }
35
+
36
+ // Language configurations with native names
37
+ const LANGUAGE_CONFIG = {
38
+ 'de': { name: 'German', nativeName: 'Deutsch' },
39
+ 'es': { name: 'Spanish', nativeName: 'Español' },
40
+ 'fr': { name: 'French', nativeName: 'Français' },
41
+ 'ru': { name: 'Russian', nativeName: 'Русский' },
42
+ 'it': { name: 'Italian', nativeName: 'Italiano' },
43
+ 'pt': { name: 'Portuguese', nativeName: 'Português' },
44
+ 'ja': { name: 'Japanese', nativeName: '日本語' },
45
+ 'ko': { name: 'Korean', nativeName: '한국어' },
46
+ 'zh': { name: 'Chinese', nativeName: '中文' },
47
+ 'ar': { name: 'Arabic', nativeName: 'العربية' },
48
+ 'hi': { name: 'Hindi', nativeName: 'हिन्दी' },
49
+ 'nl': { name: 'Dutch', nativeName: 'Nederlands' },
50
+ 'sv': { name: 'Swedish', nativeName: 'Svenska' },
51
+ 'da': { name: 'Danish', nativeName: 'Dansk' },
52
+ 'no': { name: 'Norwegian', nativeName: 'Norsk' },
53
+ 'fi': { name: 'Finnish', nativeName: 'Suomi' },
54
+ 'pl': { name: 'Polish', nativeName: 'Polski' },
55
+ 'cs': { name: 'Czech', nativeName: 'Čeština' },
56
+ 'hu': { name: 'Hungarian', nativeName: 'Magyar' },
57
+ 'tr': { name: 'Turkish', nativeName: 'Türkçe' }
58
+ };
59
+
60
+ class I18nInitializer {
61
+ constructor(config = {}) {
62
+ this.ui = new UIi18n();
63
+ this.config = { ...getConfig(), ...config };
64
+ this.sourceDir = path.resolve(this.config.sourceDir);
65
+ this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage);
66
+
67
+ // Use global readline interface to prevent doubling
68
+ if (global.activeReadlineInterface) {
69
+ this.rl = global.activeReadlineInterface;
70
+ this.shouldCloseRL = false;
71
+ } else {
72
+ this.rl = readline.createInterface({
73
+ input: process.stdin,
74
+ output: process.stdout,
75
+ terminal: true,
76
+ historySize: 0
77
+ });
78
+ global.activeReadlineInterface = this.rl;
79
+ this.shouldCloseRL = true;
80
+ }
81
+ }
82
+
83
+ // Add the missing checkI18nDependencies method
84
+ async checkI18nDependencies() {
85
+ const packageJsonPath = path.resolve('./package.json');
86
+
87
+ if (!fs.existsSync(packageJsonPath)) {
88
+ console.log(this.ui.t('init.warnings.noPackageJson'));
89
+ return await this.promptContinueWithoutI18n();
90
+ }
91
+
92
+ try {
93
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
94
+ // Include peerDependencies in the check
95
+ const dependencies = {
96
+ ...packageJson.dependencies,
97
+ ...packageJson.devDependencies,
98
+ ...packageJson.peerDependencies
99
+ };
100
+
101
+ const i18nFrameworks = [
102
+ 'react-i18next',
103
+ 'vue-i18n',
104
+ 'angular-i18n',
105
+ 'i18next',
106
+ 'next-i18next',
107
+ 'svelte-i18n',
108
+ '@nuxtjs/i18n'
109
+ ];
110
+
111
+ const installedFrameworks = i18nFrameworks.filter(framework => dependencies[framework]);
112
+
113
+ if (installedFrameworks.length > 0) {
114
+ console.log(this.ui.t('init.detectedI18nFrameworks', { frameworks: installedFrameworks.join(', ') }));
115
+ return true;
116
+ } else {
117
+ console.log(this.ui.t('init.suggestions.noFramework'));
118
+ console.log(this.ui.t('init.frameworks.react'));
119
+ console.log(this.ui.t('init.frameworks.vue'));
120
+ console.log(this.ui.t('init.frameworks.i18next'));
121
+ console.log(this.ui.t('init.frameworks.nuxt'));
122
+ console.log(this.ui.t('init.frameworks.svelte'));
123
+ return await this.promptContinueWithoutI18n();
124
+ }
125
+ } catch (error) {
126
+ console.log(this.ui.t('init.errors.packageJsonRead'));
127
+ return await this.promptContinueWithoutI18n();
128
+ }
129
+ }
130
+
131
+ // Add the missing promptContinueWithoutI18n method
132
+ async promptContinueWithoutI18n() {
133
+ const answer = await this.prompt('\n' + this.ui.t('init.continueWithoutI18nPrompt'));
134
+ return answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes';
135
+ }
136
+
137
+ // Add the missing prompt method
138
+ async prompt(question) {
139
+ return new Promise((resolve) => {
140
+ this.rl.question(question, (answer) => {
141
+ resolve(answer.trim());
142
+ });
143
+ });
144
+ }
145
+
146
+ // Parse command line arguments
147
+ parseArgs() {
148
+ const args = process.argv.slice(2);
149
+ const parsed = {};
150
+
151
+ args.forEach(arg => {
152
+ if (arg.startsWith('--')) {
153
+ const [key, value] = arg.substring(2).split('=');
154
+ if (key === 'languages' || key === 'target-languages') {
155
+ parsed.languages = value ? value.split(',').map(l => l.trim()) : [];
156
+ } else if (key === 'source-dir') {
157
+ parsed.sourceDir = value;
158
+ } else if (key === 'source-language') {
159
+ parsed.sourceLanguage = value;
160
+ }
161
+ }
162
+ });
163
+
164
+ return parsed;
165
+ }
166
+
167
+ // Setup initial directory structure if needed
168
+ async setupInitialStructure() {
169
+ // Validate paths
170
+ const validatedSourceDir = SecurityUtils.validatePath(this.sourceDir, process.cwd());
171
+ const validatedSourceLanguageDir = SecurityUtils.validatePath(this.sourceLanguageDir, process.cwd());
172
+
173
+ if (!validatedSourceDir || !validatedSourceLanguageDir) {
174
+ SecurityUtils.logSecurityEvent('Invalid directory paths in setupInitialStructure', 'error', { sourceDir: this.sourceDir, sourceLanguageDir: this.sourceLanguageDir });
175
+ throw new Error('Invalid directory paths detected');
176
+ }
177
+
178
+ // Create source directory if it doesn't exist
179
+ if (!fs.existsSync(validatedSourceDir)) {
180
+ console.log(this.ui.t('init.creatingSourceDirectory', { dir: validatedSourceDir }));
181
+ fs.mkdirSync(validatedSourceDir, { recursive: true });
182
+ SecurityUtils.logSecurityEvent('Source directory created', 'info', { dir: validatedSourceDir });
183
+ }
184
+
185
+ // Create source language directory if it doesn't exist
186
+ if (!fs.existsSync(validatedSourceLanguageDir)) {
187
+ console.log(this.ui.t('init.creatingSourceLanguageDirectory', { dir: validatedSourceLanguageDir }));
188
+ fs.mkdirSync(validatedSourceLanguageDir, { recursive: true });
189
+
190
+ // Create a sample common.json file
191
+ const sampleTranslations = {
192
+ "common": {
193
+ "welcome": "Welcome",
194
+ "hello": "Hello",
195
+ "goodbye": "Goodbye",
196
+ "yes": "Yes",
197
+ "no": "No",
198
+ "save": "Save",
199
+ "cancel": "Cancel",
200
+ "delete": "Delete",
201
+ "edit": "Edit",
202
+ "loading": "Loading..."
203
+ },
204
+ "navigation": {
205
+ "home": "Home",
206
+ "about": "About",
207
+ "contact": "Contact",
208
+ "settings": "Settings"
209
+ }
210
+ };
211
+
212
+ const sampleFilePath = path.join(validatedSourceLanguageDir, 'common.json');
213
+ const validatedSampleFilePath = SecurityUtils.validatePath(sampleFilePath, process.cwd());
214
+
215
+ if (!validatedSampleFilePath) {
216
+ SecurityUtils.logSecurityEvent('Invalid sample file path', 'error', { path: sampleFilePath });
217
+ throw new Error('Invalid sample file path');
218
+ }
219
+
220
+ const success = await SecurityUtils.safeWriteFile(validatedSampleFilePath, JSON.stringify(sampleTranslations, null, 2), process.cwd());
221
+
222
+ if (success) {
223
+ console.log(this.ui.t('init.createdSampleTranslationFile', { file: validatedSampleFilePath }));
224
+ SecurityUtils.logSecurityEvent('Sample translation file created', 'info', { file: validatedSampleFilePath });
225
+ } else {
226
+ SecurityUtils.logSecurityEvent('Failed to create sample translation file', 'error', { file: validatedSampleFilePath });
227
+ throw new Error('Failed to create sample translation file');
228
+ }
229
+ }
230
+ }
231
+
232
+ // Check if source directory and language exist
233
+ validateSource() {
234
+ if (!fs.existsSync(this.sourceDir)) {
235
+ throw new Error(`Source directory not found: ${this.sourceDir}`);
236
+ }
237
+
238
+ if (!fs.existsSync(this.sourceLanguageDir)) {
239
+ throw new Error(`Source language directory not found: ${this.sourceLanguageDir}`);
240
+ }
241
+
242
+ return true;
243
+ }
244
+
245
+ // Get all JSON files from source language directory
246
+ getSourceFiles() {
247
+ const files = fs.readdirSync(this.sourceLanguageDir)
248
+ .filter(file => {
249
+ return file.endsWith('.json') &&
250
+ !this.config.excludeFiles.includes(file);
251
+ });
252
+
253
+ if (files.length === 0) {
254
+ throw new Error(`No JSON files found in source directory: ${this.sourceLanguageDir}`);
255
+ }
256
+
257
+ return files;
258
+ }
259
+
260
+ // Recursively mark all string values as not translated
261
+ markAsNotTranslated(obj) {
262
+ if (typeof obj === 'string') {
263
+ return this.config.notTranslatedMarker;
264
+ }
265
+
266
+ if (Array.isArray(obj)) {
267
+ return obj.map(item => this.markAsNotTranslated(item));
268
+ }
269
+
270
+ if (obj && typeof obj === 'object') {
271
+ const result = {};
272
+ for (const [key, value] of Object.entries(obj)) {
273
+ result[key] = this.markAsNotTranslated(value);
274
+ }
275
+ return result;
276
+ }
277
+
278
+ return obj;
279
+ }
280
+
281
+ // Create or update a language file securely
282
+ async createLanguageFile(sourceFile, targetLanguage, sourceContent) {
283
+ const targetDir = path.join(this.sourceDir, targetLanguage);
284
+ const targetFile = path.join(targetDir, sourceFile);
285
+
286
+ // Validate paths
287
+ const validatedTargetDir = SecurityUtils.validatePath(targetDir, this.sourceDir);
288
+ const validatedTargetFile = SecurityUtils.validatePath(targetFile, this.sourceDir);
289
+
290
+ if (!validatedTargetDir || !validatedTargetFile) {
291
+ SecurityUtils.logSecurityEvent('Invalid path detected in createLanguageFile', 'error', { targetDir, targetFile });
292
+ throw new Error('Invalid file path detected');
293
+ }
294
+
295
+ // Create target directory if it doesn't exist
296
+ if (!fs.existsSync(validatedTargetDir)) {
297
+ fs.mkdirSync(validatedTargetDir, { recursive: true });
298
+ }
299
+
300
+ let targetContent;
301
+
302
+ // If target file exists, preserve existing translations
303
+ if (fs.existsSync(validatedTargetFile)) {
304
+ try {
305
+ const existingContent = await SecurityUtils.safeReadFile(validatedTargetFile, this.sourceDir);
306
+ if (existingContent) {
307
+ targetContent = this.mergeTranslations(sourceContent, JSON.parse(existingContent));
308
+ } else {
309
+ targetContent = this.markAsNotTranslated(sourceContent);
310
+ }
311
+ } catch (error) {
312
+ console.warn(`⚠️ Warning: Could not parse existing file ${validatedTargetFile}, creating new one`);
313
+ SecurityUtils.logSecurityEvent('File parse error', 'warn', { file: validatedTargetFile, error: error.message });
314
+ targetContent = this.markAsNotTranslated(sourceContent);
315
+ }
316
+ } else {
317
+ targetContent = this.markAsNotTranslated(sourceContent);
318
+ }
319
+
320
+ // Write the file securely
321
+ const success = await SecurityUtils.safeWriteFile(validatedTargetFile, JSON.stringify(targetContent, null, 2), this.sourceDir);
322
+
323
+ if (!success) {
324
+ SecurityUtils.logSecurityEvent('Failed to write language file', 'error', { file: validatedTargetFile });
325
+ throw new Error(`Failed to write file: ${validatedTargetFile}`);
326
+ }
327
+
328
+ SecurityUtils.logSecurityEvent('Language file created/updated', 'info', { file: validatedTargetFile, language: targetLanguage });
329
+ return validatedTargetFile;
330
+ }
331
+
332
+ // Merge existing translations with new structure
333
+ mergeTranslations(sourceObj, existingObj) {
334
+ if (typeof sourceObj === 'string') {
335
+ // If existing translation exists and is not the marker, keep it
336
+ if (typeof existingObj === 'string' &&
337
+ existingObj !== this.config.notTranslatedMarker &&
338
+ existingObj.trim() !== '') {
339
+ return existingObj;
340
+ }
341
+ return this.config.notTranslatedMarker;
342
+ }
343
+
344
+ if (Array.isArray(sourceObj)) {
345
+ return sourceObj.map((item, index) => {
346
+ const existingItem = Array.isArray(existingObj) ? existingObj[index] : undefined;
347
+ return this.mergeTranslations(item, existingItem);
348
+ });
349
+ }
350
+
351
+ if (sourceObj && typeof sourceObj === 'object') {
352
+ const result = {};
353
+ for (const [key, value] of Object.entries(sourceObj)) {
354
+ const existingValue = existingObj && typeof existingObj === 'object' ? existingObj[key] : undefined;
355
+ result[key] = this.mergeTranslations(value, existingValue);
356
+ }
357
+ return result;
358
+ }
359
+
360
+ return sourceObj;
361
+ }
362
+
363
+ // Get translation statistics
364
+ getTranslationStats(obj) {
365
+ let total = 0;
366
+ let translated = 0;
367
+
368
+ const count = (item) => {
369
+ if (typeof item === 'string') {
370
+ total++;
371
+ if (item !== this.config.notTranslatedMarker && item.trim() !== '') {
372
+ translated++;
373
+ }
374
+ } else if (Array.isArray(item)) {
375
+ item.forEach(count);
376
+ } else if (item && typeof item === 'object') {
377
+ Object.values(item).forEach(count);
378
+ }
379
+ };
380
+
381
+ count(obj);
382
+
383
+ return {
384
+ total,
385
+ translated,
386
+ percentage: total > 0 ? Math.round((translated / total) * 100) : 0,
387
+ missing: total - translated
388
+ };
389
+ }
390
+
391
+ // Interactive admin PIN setup
392
+ async promptAdminPinSetup() {
393
+ const readline = require('readline');
394
+ const rl = readline.createInterface({
395
+ input: process.stdin,
396
+ output: process.stdout,
397
+ terminal: true,
398
+ historySize: 0
399
+ });
400
+
401
+ const question = (query) => new Promise(resolve => rl.question(query, resolve));
402
+
403
+ console.log('\n' + this.ui.t('init.adminPinSetupOptional'));
404
+ console.log(this.ui.t('init.adminPinSeparator'));
405
+ console.log(this.ui.t('init.adminPinDescription1'));
406
+ console.log(this.ui.t('init.adminPinDescription2'));
407
+ console.log(this.ui.t('init.adminPinDescription3'));
408
+ console.log(this.ui.t('init.adminPinDescription4'));
409
+
410
+ const setupPin = await question('\n' + this.ui.t('init.adminPinSetupPrompt'));
411
+
412
+ if (setupPin.toLowerCase() === 'y' || setupPin.toLowerCase() === 'yes') {
413
+ try {
414
+ const adminAuth = new AdminAuth();
415
+
416
+ // Enable admin PIN in settings
417
+ settingsManager.setSecurity({ adminPinEnabled: true, adminPinPromptOnInit: true });
418
+
419
+ console.log('\n' + this.ui.t('init.settingUpAdminPin'));
420
+
421
+ let pin1, pin2;
422
+ do {
423
+ pin1 = await question(this.ui.t('init.enterAdminPin'));
424
+
425
+ if (!/^\d{4,8}$/.test(pin1)) {
426
+ console.log(this.ui.t('init.pinMustBe4To8Digits'));
427
+ continue;
428
+ }
429
+
430
+ pin2 = await question(this.ui.t('init.confirmAdminPin'));
431
+
432
+ if (pin1 !== pin2) {
433
+ console.log(this.ui.t('init.pinsDoNotMatch'));
434
+ }
435
+ } while (pin1 !== pin2 || !/^\d{4,8}$/.test(pin1));
436
+
437
+ await adminAuth.setupPin(pin1);
438
+ console.log(this.ui.t('init.adminPinSetupSuccess'));
439
+ console.log(this.ui.t('init.adminProtectionEnabled'));
440
+
441
+ } catch (error) {
442
+ console.error(this.ui.t('init.errorSettingUpAdminPin', { error: error.message }));
443
+ console.log(this.ui.t('init.continuingWithoutAdminPin'));
444
+ }
445
+ } else {
446
+ console.log(this.ui.t('init.skippingAdminPinSetup'));
447
+ }
448
+
449
+ rl.close();
450
+ }
451
+
452
+ // Interactive language selection
453
+ async selectLanguages() {
454
+ // Use the global readline interface if available, otherwise create one
455
+ let rl = this.rl;
456
+ let shouldClose = false;
457
+
458
+ if (!rl) {
459
+ const readline = require('readline');
460
+ rl = readline.createInterface({
461
+ input: process.stdin,
462
+ output: process.stdout,
463
+ terminal: true,
464
+ historySize: 0
465
+ });
466
+ shouldClose = true;
467
+ }
468
+
469
+ const question = (query) => new Promise(resolve => rl.question(query, resolve));
470
+
471
+ console.log('\n' + this.ui.t('init.languageSelectionTitle'));
472
+ console.log('=' .repeat(50));
473
+ console.log(this.ui.t('language.available'));
474
+
475
+ Object.entries(LANGUAGE_CONFIG).forEach(([code, config], index) => {
476
+ console.log(` ${(index + 1).toString().padStart(2)}. ${code} - ${config.name} (${config.nativeName})`);
477
+ });
478
+
479
+ console.log('\n' + this.ui.t('init.defaultLanguages', { languages: this.config.defaultLanguages.join(', ') }));
480
+
481
+ const answer = await question('\n' + this.ui.t('init.enterLanguageCodes'));
482
+
483
+ // Only close if we created our own readline interface
484
+ if (shouldClose) {
485
+ rl.close();
486
+ }
487
+
488
+ if (answer.trim() === '') {
489
+ return this.config.defaultLanguages;
490
+ }
491
+
492
+ const selectedLanguages = answer.split(',').map(lang => lang.trim().toLowerCase());
493
+ const validLanguages = selectedLanguages.filter(lang => LANGUAGE_CONFIG[lang]);
494
+ const invalidLanguages = selectedLanguages.filter(lang => !LANGUAGE_CONFIG[lang]);
495
+
496
+ if (invalidLanguages.length > 0) {
497
+ console.warn(this.ui.t('init.warningInvalidLanguageCodes', { languages: invalidLanguages.join(', ') }));
498
+ }
499
+
500
+ return validLanguages.length > 0 ? validLanguages : this.config.defaultLanguages;
501
+ }
502
+
503
+ // Main initialization process
504
+ async init() {
505
+ try {
506
+ console.log(this.ui.t('init.initializationTitle'));
507
+ console.log('=' .repeat(50));
508
+
509
+ // Parse command line arguments
510
+ const args = this.parseArgs();
511
+ if (args.sourceDir) this.config.sourceDir = args.sourceDir;
512
+ if (args.sourceLanguage) this.config.sourceLanguage = args.sourceLanguage;
513
+
514
+ // Update paths
515
+ this.sourceDir = path.resolve(this.config.sourceDir);
516
+ this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage);
517
+
518
+ console.log(this.ui.t('init.sourceDirectoryLabel', { dir: this.sourceDir }));
519
+ console.log(this.ui.t('init.sourceLanguageLabel', { language: this.config.sourceLanguage }));
520
+
521
+ // Check i18n dependencies first and exit if user chooses not to continue
522
+ const hasI18n = await this.checkI18nDependencies();
523
+
524
+ if (!hasI18n) {
525
+ console.log(this.ui.t('init.errors.noFramework'));
526
+ console.log(this.ui.t('init.suggestions.installFramework'));
527
+ if (this.shouldCloseRL) {
528
+ this.rl.close();
529
+ global.activeReadlineInterface = null;
530
+ }
531
+ process.exit(0);
532
+ }
533
+
534
+ // Call the enhanced initialize method with args
535
+ await this.initialize(hasI18n, args);
536
+
537
+ } catch (error) {
538
+ console.error('❌ Initialization failed:', error.message);
539
+ throw error;
540
+ } finally {
541
+ if (this.shouldCloseRL && this.rl) {
542
+ this.rl.close();
543
+ global.activeReadlineInterface = null;
544
+ }
545
+ }
546
+ }
547
+
548
+ // Enhanced initialization with dependency checking
549
+ async initialize(hasI18n = true, args = {}) {
550
+ console.log(this.ui.t('init.initializingProject'));
551
+
552
+ if (!hasI18n) {
553
+ console.log(this.ui.t('init.warningProceedingWithoutFramework'));
554
+ console.log(this.ui.t('init.translationFilesCreatedWarning'));
555
+ }
556
+
557
+ // Continue with existing initialization logic
558
+ await this.setupInitialStructure();
559
+
560
+ // Validate source
561
+ this.validateSource();
562
+
563
+ // Prompt for admin PIN setup if not already configured
564
+ const securitySettings = settingsManager.getSecurity();
565
+
566
+ if (!securitySettings.adminPinEnabled && securitySettings.adminPinPromptOnInit !== false) {
567
+ await this.promptAdminPinSetup();
568
+ }
569
+
570
+ // Get target languages - use args.languages if provided
571
+ const targetLanguages = args.languages || await this.selectLanguages();
572
+
573
+ if (targetLanguages.length === 0) {
574
+ console.log(this.ui.t('init.noTargetLanguagesSpecified'));
575
+ return;
576
+ }
577
+
578
+ console.log('\n' + this.ui.t('init.targetLanguages', { languages: targetLanguages.map(lang => `${lang} (${LANGUAGE_CONFIG[lang]?.name || 'Unknown'})`).join(', ') }));
579
+
580
+ // Get source files
581
+ const sourceFiles = this.getSourceFiles();
582
+ console.log('\n' + this.ui.t('init.foundSourceFiles', { count: sourceFiles.length, files: sourceFiles.join(', ') }));
583
+
584
+ // Process each language
585
+ const results = {};
586
+
587
+ for (const targetLanguage of targetLanguages) {
588
+ console.log('\n' + this.ui.t('init.processingLanguage', { language: targetLanguage, name: LANGUAGE_CONFIG[targetLanguage]?.name || 'Unknown' }));
589
+
590
+ const languageResults = {
591
+ files: [],
592
+ totalStats: { total: 0, translated: 0, missing: 0 }
593
+ };
594
+
595
+ for (const sourceFile of sourceFiles) {
596
+ const sourceFilePath = path.join(this.sourceLanguageDir, sourceFile);
597
+ const validatedSourceFilePath = SecurityUtils.validatePath(sourceFilePath, process.cwd());
598
+
599
+ if (!validatedSourceFilePath) {
600
+ SecurityUtils.logSecurityEvent('Invalid source file path', 'error', { path: sourceFilePath });
601
+ continue;
602
+ }
603
+
604
+ const sourceContentRaw = await SecurityUtils.safeReadFile(validatedSourceFilePath, process.cwd());
605
+ if (!sourceContentRaw) {
606
+ SecurityUtils.logSecurityEvent('Failed to read source file', 'error', { file: validatedSourceFilePath });
607
+ continue;
608
+ }
609
+
610
+ const sourceContent = JSON.parse(sourceContentRaw);
611
+
612
+ const targetFilePath = await this.createLanguageFile(sourceFile, targetLanguage, sourceContent);
613
+
614
+ // Get stats for this file
615
+ const targetContentRaw = await SecurityUtils.safeReadFile(targetFilePath, process.cwd());
616
+ if (!targetContentRaw) {
617
+ SecurityUtils.logSecurityEvent('Failed to read target file for stats', 'error', { file: targetFilePath });
618
+ continue;
619
+ }
620
+
621
+ const targetContent = JSON.parse(targetContentRaw);
622
+ const stats = this.getTranslationStats(targetContent);
623
+
624
+ languageResults.files.push({
625
+ name: sourceFile,
626
+ path: targetFilePath,
627
+ stats
628
+ });
629
+
630
+ // Add to total stats
631
+ languageResults.totalStats.total += stats.total;
632
+ languageResults.totalStats.translated += stats.translated;
633
+ languageResults.totalStats.missing += stats.missing;
634
+
635
+ console.log(this.ui.t('init.fileProcessingResult', { file: sourceFile, translated: stats.translated, total: stats.total, percentage: stats.percentage }));
636
+ }
637
+
638
+ // Calculate overall percentage
639
+ languageResults.totalStats.percentage = languageResults.totalStats.total > 0
640
+ ? Math.round((languageResults.totalStats.translated / languageResults.totalStats.total) * 100)
641
+ : 0;
642
+
643
+ results[targetLanguage] = languageResults;
644
+
645
+ console.log(this.ui.t('init.overallProgress', { translated: languageResults.totalStats.translated, total: languageResults.totalStats.total, percentage: languageResults.totalStats.percentage }));
646
+ }
647
+
648
+ // Summary report
649
+ console.log('\n' + '=' .repeat(50));
650
+ console.log(this.ui.t('init.initializationSummaryTitle'));
651
+ console.log('=' .repeat(50));
652
+
653
+ Object.entries(results).forEach(([lang, data]) => {
654
+ const langName = LANGUAGE_CONFIG[lang]?.name || 'Unknown';
655
+ const statusIcon = data.totalStats.percentage === 100 ? '✅' : data.totalStats.percentage >= 80 ? '🟡' : '🔴';
656
+
657
+ console.log(this.ui.t('init.languageSummary', { icon: statusIcon, name: langName, code: lang, percentage: data.totalStats.percentage }));
658
+ console.log(this.ui.t('init.languageFiles', { count: data.files.length }));
659
+ console.log(this.ui.t('init.languageKeys', { translated: data.totalStats.translated, total: data.totalStats.total }));
660
+ console.log(this.ui.t('init.languageMissing', { count: data.totalStats.missing }));
661
+ });
662
+
663
+ console.log('\n' + this.ui.t('init.initializationCompletedSuccessfully'));
664
+ console.log('\n' + this.ui.t('init.nextStepsTitle'));
665
+ console.log(this.ui.t('init.nextStep1'));
666
+ console.log(this.ui.t('init.nextStep2'));
667
+ console.log(this.ui.t('init.nextStep3'));
668
+ }
669
+
670
+ // Add run method for compatibility with manager
671
+ async run() {
672
+ return await this.init();
673
+ }
674
+ }
675
+
676
+ module.exports = I18nInitializer;
677
+
678
+ // Run if called directly
679
+ if (require.main === module) {
680
+ const initializer = new I18nInitializer();
681
+ initializer.init().catch(error => {
682
+ console.error('❌ Initialization failed:', error.message);
683
+ console.error('Stack trace:', error.stack);
684
+ process.exit(1);
685
+ });
686
+ }