i18ntk 1.10.2 → 2.0.2

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 (108) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +141 -1191
  3. package/main/i18ntk-analyze.js +65 -84
  4. package/main/i18ntk-backup-class.js +420 -0
  5. package/main/i18ntk-backup.js +3 -3
  6. package/main/i18ntk-complete.js +90 -65
  7. package/main/i18ntk-doctor.js +123 -103
  8. package/main/i18ntk-fixer.js +61 -725
  9. package/main/i18ntk-go.js +14 -15
  10. package/main/i18ntk-init.js +77 -26
  11. package/main/i18ntk-java.js +27 -32
  12. package/main/i18ntk-js.js +70 -68
  13. package/main/i18ntk-manage.js +129 -30
  14. package/main/i18ntk-php.js +75 -75
  15. package/main/i18ntk-py.js +55 -56
  16. package/main/i18ntk-scanner.js +59 -57
  17. package/main/i18ntk-setup.js +9 -404
  18. package/main/i18ntk-sizing.js +6 -6
  19. package/main/i18ntk-summary.js +21 -18
  20. package/main/i18ntk-ui.js +11 -10
  21. package/main/i18ntk-usage.js +54 -18
  22. package/main/i18ntk-validate.js +13 -13
  23. package/main/manage/commands/AnalyzeCommand.js +1124 -0
  24. package/main/manage/commands/BackupCommand.js +62 -0
  25. package/main/manage/commands/CommandRouter.js +295 -0
  26. package/main/manage/commands/CompleteCommand.js +61 -0
  27. package/main/manage/commands/DoctorCommand.js +60 -0
  28. package/main/manage/commands/FixerCommand.js +624 -0
  29. package/main/manage/commands/InitCommand.js +62 -0
  30. package/main/manage/commands/ScannerCommand.js +654 -0
  31. package/main/manage/commands/SizingCommand.js +60 -0
  32. package/main/manage/commands/SummaryCommand.js +61 -0
  33. package/main/manage/commands/UsageCommand.js +60 -0
  34. package/main/manage/commands/ValidateCommand.js +978 -0
  35. package/main/manage/index-fixed.js +1447 -0
  36. package/main/manage/index.js +1462 -0
  37. package/main/manage/managers/DebugMenu.js +140 -0
  38. package/main/manage/managers/InteractiveMenu.js +177 -0
  39. package/main/manage/managers/LanguageMenu.js +62 -0
  40. package/main/manage/managers/SettingsMenu.js +53 -0
  41. package/main/manage/services/AuthenticationService.js +263 -0
  42. package/main/manage/services/ConfigurationService-fixed.js +449 -0
  43. package/main/manage/services/ConfigurationService.js +449 -0
  44. package/main/manage/services/FileManagementService.js +368 -0
  45. package/main/manage/services/FrameworkDetectionService.js +458 -0
  46. package/main/manage/services/InitService.js +1051 -0
  47. package/main/manage/services/SetupService.js +462 -0
  48. package/main/manage/services/SummaryService.js +450 -0
  49. package/main/manage/services/UsageService.js +1502 -0
  50. package/package.json +32 -29
  51. package/runtime/enhanced.d.ts +221 -221
  52. package/runtime/index.d.ts +29 -29
  53. package/runtime/index.full.d.ts +331 -331
  54. package/runtime/index.js +7 -6
  55. package/scripts/build-lite.js +17 -17
  56. package/scripts/deprecate-versions.js +23 -6
  57. package/scripts/export-translations.js +5 -5
  58. package/scripts/fix-all-i18n.js +3 -3
  59. package/scripts/fix-and-purify-i18n.js +3 -2
  60. package/scripts/fix-locale-control-chars.js +110 -0
  61. package/scripts/lint-locales.js +80 -0
  62. package/scripts/locale-optimizer.js +8 -8
  63. package/scripts/prepublish.js +21 -21
  64. package/scripts/security-check.js +117 -117
  65. package/scripts/sync-translations.js +4 -4
  66. package/scripts/sync-ui-locales.js +9 -8
  67. package/scripts/validate-all-translations.js +8 -7
  68. package/scripts/verify-deprecations.js +157 -161
  69. package/scripts/verify-translations.js +6 -5
  70. package/settings/i18ntk-config.json +282 -282
  71. package/settings/language-config.json +5 -5
  72. package/settings/settings-cli.js +9 -9
  73. package/settings/settings-manager.js +18 -18
  74. package/ui-locales/de.json +2417 -2348
  75. package/ui-locales/en.json +2415 -2352
  76. package/ui-locales/es.json +2425 -2353
  77. package/ui-locales/fr.json +2418 -2348
  78. package/ui-locales/ja.json +2463 -2361
  79. package/ui-locales/ru.json +2463 -2359
  80. package/ui-locales/zh.json +2418 -2351
  81. package/utils/admin-auth.js +2 -2
  82. package/utils/admin-cli.js +297 -297
  83. package/utils/admin-pin.js +9 -9
  84. package/utils/cli-helper.js +9 -9
  85. package/utils/config-helper.js +73 -104
  86. package/utils/config-manager.js +204 -171
  87. package/utils/config.js +5 -4
  88. package/utils/env-manager.js +249 -263
  89. package/utils/framework-detector.js +27 -24
  90. package/utils/i18n-helper.js +85 -41
  91. package/utils/init-helper.js +152 -94
  92. package/utils/json-output.js +98 -98
  93. package/utils/mini-commander.js +179 -0
  94. package/utils/missing-key-validator.js +5 -5
  95. package/utils/plugin-loader.js +40 -29
  96. package/utils/prompt.js +14 -44
  97. package/utils/safe-json.js +40 -0
  98. package/utils/secure-errors.js +3 -3
  99. package/utils/security-check-improved.js +390 -0
  100. package/utils/security-config.js +5 -5
  101. package/utils/security-fixed.js +607 -0
  102. package/utils/security.js +652 -602
  103. package/utils/setup-enforcer.js +136 -44
  104. package/utils/setup-validator.js +33 -32
  105. package/utils/ultra-performance-optimizer.js +11 -9
  106. package/utils/watch-locales.js +2 -1
  107. package/utils/prompt-fixed.js +0 -55
  108. package/utils/security-check.js +0 -454
@@ -0,0 +1,1051 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * I18NTK INIT SERVICE
5
+ *
6
+ * Core business logic for i18n project initialization.
7
+ * Handles directory setup, language file creation, and project configuration.
8
+ */
9
+
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+ const SecurityUtils = require('../../../utils/security');
13
+ const configManager = require('../../../utils/config-manager');
14
+ const { loadTranslations, t } = require('../../../utils/i18n-helper');
15
+ const { detectFramework } = require('../../../utils/framework-detector');
16
+ const { getFormatAdapter } = require('../../../utils/format-manager');
17
+ const AdminAuth = require('../../../utils/admin-auth');
18
+
19
+ // Language configurations with native names
20
+ const LANGUAGE_CONFIG = {
21
+ 'de': { name: 'German', nativeName: 'Deutsch' },
22
+ 'es': { name: 'Spanish', nativeName: 'EspaƱol' },
23
+ 'fr': { name: 'French', nativeName: 'FranƧais' },
24
+ 'ru': { name: 'Russian', nativeName: 'Русский' },
25
+ 'it': { name: 'Italian', nativeName: 'Italiano' },
26
+ 'ja': { name: 'Japanese', nativeName: 'ę—„ęœ¬čŖž' },
27
+ 'ko': { name: 'Korean', nativeName: 'ķ•œźµ­ģ–“' },
28
+ 'zh': { name: 'Chinese', nativeName: 'äø­ę–‡' },
29
+ 'ar': { name: 'Arabic', nativeName: 'Ų§Ł„Ų¹Ų±ŲØŁŠŲ©' },
30
+ 'hi': { name: 'Hindi', nativeName: 'ą¤¹ą¤æą¤Øą„ą¤¦ą„€' },
31
+ 'nl': { name: 'Dutch', nativeName: 'Nederlands' },
32
+ 'sv': { name: 'Swedish', nativeName: 'Svenska' },
33
+ 'da': { name: 'Danish', nativeName: 'Dansk' },
34
+ 'no': { name: 'Norwegian', nativeName: 'Norsk' },
35
+ 'fi': { name: 'Finnish', nativeName: 'Suomi' },
36
+ 'pl': { name: 'Polish', nativeName: 'Polski' },
37
+ 'cs': { name: 'Czech', nativeName: 'ČeŔtina' },
38
+ 'hu': { name: 'Hungarian', nativeName: 'Magyar' },
39
+ 'tr': { name: 'Turkish', nativeName: 'Türkçe' }
40
+ };
41
+
42
+ class InitService {
43
+ constructor(config = {}) {
44
+ this.config = {
45
+ sourceLanguage: 'en',
46
+ excludeFiles: ['.DS_Store', 'Thumbs.db'],
47
+ supportedExtensions: ['.json'],
48
+ // Default structure: modular (folder per language)
49
+ structure: 'modular', // one of: 'single' | 'modular' | 'existing'
50
+ perLanguageStructure: {}, // optional map lang -> 'single' | 'modular'
51
+ noPrompt: false,
52
+ ...config
53
+ };
54
+
55
+ this.format = getFormatAdapter(this.config.format);
56
+ this.config.supportedExtensions = [this.format.extension];
57
+ this.detectedFramework = detectFramework(process.cwd());
58
+ if (this.detectedFramework && !this.config.translationPatterns) {
59
+ this.config.translationPatterns = this.detectedFramework.patterns;
60
+ }
61
+ this.sourceDir = this.config.sourceDir || './locales';
62
+ // Source language directory depends on structure
63
+ this.sourceLanguageDir = this.config.structure === 'single'
64
+ ? this.sourceDir
65
+ : path.join(this.sourceDir, this.config.sourceLanguage);
66
+
67
+ // Ensure defaultLanguages is properly initialized from config
68
+ this.config.defaultLanguages = this.config.defaultLanguages || ['de', 'es', 'fr', 'ru'];
69
+ }
70
+
71
+ // Check i18n dependencies
72
+ async checkI18nDependencies() {
73
+ const packageJsonPath = path.resolve('./package.json');
74
+
75
+ if (!SecurityUtils.safeExistsSync(packageJsonPath)) {
76
+ console.log(t('errors.noPackageJson'));
77
+ return true; // Allow to continue without framework
78
+ }
79
+
80
+ try {
81
+ const packageJson = JSON.parse(SecurityUtils.safeReadFileSync(packageJsonPath, path.dirname(packageJsonPath), 'utf8'));
82
+ // Include peerDependencies in the check
83
+ const dependencies = {
84
+ ...packageJson.dependencies,
85
+ ...packageJson.devDependencies,
86
+ ...packageJson.peerDependencies
87
+ };
88
+
89
+ const i18nFrameworks = [
90
+ 'react-i18next',
91
+ 'vue-i18n',
92
+ 'angular-i18n',
93
+ 'i18next',
94
+ 'next-i18next',
95
+ 'svelte-i18n',
96
+ '@nuxtjs/i18n'
97
+ ];
98
+
99
+ const installedFrameworks = i18nFrameworks.filter(framework => dependencies[framework]);
100
+
101
+ if (installedFrameworks.length > 0) {
102
+ console.log(t('init.detectedI18nFrameworks', { frameworks: installedFrameworks.join(', ') }));
103
+ const cfg = configManager.loadSettings ? configManager.loadSettings() : (configManager.getConfig ? configManager.getConfig() : {});
104
+ cfg.framework = cfg.framework || {};
105
+ cfg.framework.detected = true;
106
+ cfg.framework.installed = installedFrameworks;
107
+ if (configManager.saveSettings) {
108
+ configManager.saveSettings(cfg);
109
+ } else if (configManager.saveConfig) {
110
+ configManager.saveConfig(cfg);
111
+ }
112
+ return true;
113
+ } else {
114
+ const cfg = configManager.loadSettings ? configManager.loadSettings() : (configManager.getConfig ? configManager.getConfig() : {});
115
+ if (cfg.framework) {
116
+ cfg.framework.detected = false;
117
+ if (configManager.saveSettings) {
118
+ configManager.saveSettings(cfg);
119
+ } else if (configManager.saveConfig) {
120
+ configManager.saveConfig(cfg);
121
+ }
122
+ }
123
+ return true;
124
+ }
125
+ } catch (error) {
126
+ console.log(t('init.errors.packageJsonRead'));
127
+ return true; // Allow to continue on error
128
+ }
129
+ }
130
+
131
+ // Detect existing translation directories and allow user selection
132
+ async detectAndSelectDirectory(skipPrompt = false) {
133
+ const possibleLocations = [
134
+ './locales',
135
+ './src/locales',
136
+ './src/i18n/locales',
137
+ './app/locales',
138
+ './public/locales',
139
+ './translations',
140
+ './lang',
141
+ './i18n/locales',
142
+ './assets/locales',
143
+ './client/locales',
144
+ './frontend/locales'
145
+ ];
146
+
147
+ const existingLocations = [];
148
+
149
+ // Check for existing translation directories
150
+ for (const location of possibleLocations) {
151
+ if (SecurityUtils.safeExistsSync(location)) {
152
+ try {
153
+ const items = fs.readdirSync(location);
154
+ const englishFormats = ['en', 'en-US', 'en-GB', 'english'];
155
+
156
+ // Check for English directories first
157
+ for (const format of englishFormats) {
158
+ const englishPath = path.join(location, format);
159
+ if (SecurityUtils.safeExistsSync(englishPath) && fs.statSync(englishPath).isDirectory()) {
160
+ const englishFiles = fs.readdirSync(englishPath).filter(file => file.endsWith(this.format.extension));
161
+ if (englishFiles.length > 0) {
162
+ // Found English files, prioritize this
163
+ existingLocations.unshift(location);
164
+ break;
165
+ }
166
+ }
167
+ }
168
+
169
+ // Also check for any language directories or format files
170
+ const hasLanguageDirs = items.some(item => {
171
+ const itemPath = path.join(location, item);
172
+ if (fs.statSync(itemPath).isDirectory()) {
173
+ return ['en', 'de', 'es', 'fr', 'ru', 'ja', 'zh', 'en-US', 'en-GB'].includes(item);
174
+ }
175
+ return item.endsWith(this.format.extension);
176
+ });
177
+
178
+ if (hasLanguageDirs && !existingLocations.includes(location)) {
179
+ existingLocations.push(location);
180
+ }
181
+ } catch (error) {
182
+ // Continue checking other locations
183
+ }
184
+ }
185
+ }
186
+
187
+ if (existingLocations.length > 0) {
188
+ if (skipPrompt || !process.stdin.isTTY) {
189
+ const selectedDir = existingLocations[0];
190
+ this.config.sourceDir = selectedDir;
191
+ this.sourceDir = path.resolve(selectedDir);
192
+ this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage);
193
+ const rel = configManager.toRelative(selectedDir);
194
+ await configManager.updateConfig({ sourceDir: rel, i18nDir: rel });
195
+ return selectedDir;
196
+ }
197
+
198
+ console.log('\n' + t('init.existingDirectoriesFound'));
199
+ console.log(t('common.separator'));
200
+
201
+ // List existing locations
202
+ existingLocations.forEach((location, index) => {
203
+ console.log(` ${index + 1}. ${location}`);
204
+ });
205
+
206
+ // Add options for new directory and exit
207
+ console.log(` ${existingLocations.length + 1}. Create new directory`);
208
+ console.log(` 0. Exit`);
209
+
210
+ let answer;
211
+ let selectedIndex;
212
+
213
+ // Keep asking until we get a valid number
214
+ while (true) {
215
+ answer = await this.prompt('\n' + t('init.selectDirectoryPrompt') + ' (0-' + (existingLocations.length + 1) + '):');
216
+
217
+ // Check for exit (0)
218
+ if (answer === '0') {
219
+ console.log(t('init.initializationCancelled'));
220
+ process.exit(0);
221
+ }
222
+
223
+ // Parse the selection
224
+ selectedIndex = parseInt(answer) - 1;
225
+
226
+ // Validate the selection
227
+ if (!isNaN(selectedIndex) && selectedIndex >= 0 && selectedIndex <= existingLocations.length) {
228
+ break;
229
+ }
230
+
231
+ console.log(t('errors.invalidOption', { option: answer }));
232
+ }
233
+
234
+ if (selectedIndex >= 0 && selectedIndex < existingLocations.length) {
235
+ const selectedDir = existingLocations[selectedIndex];
236
+ if (!this.announcedExistingDir) {
237
+ console.log(t('init.usingExistingDirectory', { dir: selectedDir }));
238
+ this.announcedExistingDir = true;
239
+ }
240
+
241
+ this.config.sourceDir = selectedDir;
242
+ this.sourceDir = path.resolve(selectedDir);
243
+ this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage);
244
+
245
+ const rel = configManager.toRelative(selectedDir);
246
+ await configManager.updateConfig({ sourceDir: rel, i18nDir: rel });
247
+
248
+ return selectedDir;
249
+ } else if (selectedIndex === existingLocations.length) {
250
+ const newDirName = await this.prompt('\n' + t('init.enterNewDirectoryName') + ': ');
251
+ if (newDirName && newDirName.trim()) {
252
+ const newDirPath = path.resolve(newDirName.trim());
253
+
254
+ if (!SecurityUtils.safeExistsSync(newDirPath)) {
255
+ fs.mkdirSync(newDirPath, { recursive: true });
256
+ console.log(t('init.createdNewDirectory', { dir: newDirPath }));
257
+ } else {
258
+ console.log(t('init.directoryAlreadyExists', { dir: newDirPath }));
259
+ }
260
+
261
+ const sourceLangDir = path.join(newDirPath, this.config.sourceLanguage);
262
+ if (!SecurityUtils.safeExistsSync(sourceLangDir)) {
263
+ fs.mkdirSync(sourceLangDir, { recursive: true });
264
+ console.log(t('init.createdSourceLanguageDirectory', { dir: sourceLangDir }));
265
+ await this.createSampleTranslationFile(sourceLangDir);
266
+ }
267
+
268
+ this.config.sourceDir = newDirPath;
269
+ this.sourceDir = newDirPath;
270
+ this.sourceLanguageDir = sourceLangDir;
271
+
272
+ const rel = configManager.toRelative(newDirPath);
273
+ await configManager.updateConfig({ sourceDir: rel, i18nDir: rel });
274
+
275
+ return newDirPath;
276
+ } else {
277
+ console.log(t('init.invalidDirectoryName'));
278
+ return null;
279
+ }
280
+ }
281
+ }
282
+
283
+ if (skipPrompt || !process.stdin.isTTY) {
284
+ return null;
285
+ }
286
+
287
+ return null;
288
+ }
289
+
290
+ // Setup initial directory structure if needed
291
+ async setupInitialStructure(skipPrompt = false) {
292
+ // First, detect if there are existing translation directories with English files
293
+ const usedExisting = await this.detectAndSelectDirectory(skipPrompt);
294
+
295
+ if (usedExisting) {
296
+ console.log(t('init.usingExistingDirectory', { dir: this.sourceDir }));
297
+ // When using existing, sourceLanguageDir might already be set to English directory
298
+ return;
299
+ }
300
+
301
+ // Validate paths
302
+ const validatedSourceDir = SecurityUtils.validatePath(this.sourceDir, process.cwd());
303
+ if (!validatedSourceDir) {
304
+ throw new Error(t('validate.invalidSourceDirectory', { sourceDir: this.sourceDir }) || `Invalid source directory: ${this.sourceDir}`);
305
+ }
306
+
307
+ // For modular structure, ensure per-language subdirectory exists
308
+ let validatedSourceLanguageDir = this.sourceLanguageDir;
309
+ if (this.config.structure !== 'single') {
310
+ validatedSourceLanguageDir = SecurityUtils.validatePath(this.sourceLanguageDir, process.cwd());
311
+ if (!validatedSourceLanguageDir) {
312
+ throw new Error(t('validate.invalidSourceLanguageDirectory', { sourceDir: this.sourceLanguageDir }) || `Invalid source language directory: ${this.sourceLanguageDir}`);
313
+ }
314
+ }
315
+
316
+ // Create directories if they do not exist
317
+ if (!SecurityUtils.safeExistsSync(validatedSourceDir)) {
318
+ fs.mkdirSync(validatedSourceDir, { recursive: true });
319
+ }
320
+ if (this.config.structure !== 'single' && !SecurityUtils.safeExistsSync(validatedSourceLanguageDir)) {
321
+ fs.mkdirSync(validatedSourceLanguageDir, { recursive: true });
322
+ }
323
+
324
+ // Create sample translation file if none exist
325
+ const englishFiles = (this.config.structure === 'single'
326
+ ? fs.readdirSync(validatedSourceDir)
327
+ : fs.readdirSync(validatedSourceLanguageDir))
328
+ .filter(file => file.endsWith(this.format.extension));
329
+
330
+ if (englishFiles.length === 0) {
331
+ await this.createSampleTranslationFile(this.config.structure === 'single' ? validatedSourceDir : validatedSourceLanguageDir);
332
+ } else {
333
+ // Directory exists, check if we need to create a sample file
334
+ const existingFiles = (this.config.structure === 'single' ? fs.readdirSync(validatedSourceDir) : fs.readdirSync(validatedSourceLanguageDir))
335
+ .filter(file => file.endsWith(this.format.extension));
336
+
337
+ if (existingFiles.length === 0) {
338
+ // No format files exist, create sample file
339
+ await this.createSampleTranslationFile(this.config.structure === 'single' ? validatedSourceDir : validatedSourceLanguageDir);
340
+ }
341
+ }
342
+
343
+ const rel = configManager.toRelative(this.sourceDir);
344
+ await configManager.updateConfig({ sourceDir: rel, i18nDir: rel });
345
+ }
346
+
347
+ // Create sample translation file with smart naming
348
+ async createSampleTranslationFile(validatedSourceLanguageDir) {
349
+ const sampleTranslations = {
350
+ "common": {
351
+ "welcome": "Welcome",
352
+ "hello": "Hello",
353
+ "goodbye": "Goodbye",
354
+ "yes": "Yes",
355
+ "no": "No",
356
+ "save": "Save",
357
+ "cancel": "Cancel",
358
+ "delete": "Delete",
359
+ "edit": "Edit",
360
+ "loading": "Loading..."
361
+ },
362
+ "navigation": {
363
+ "home": "Home",
364
+ "about": "About",
365
+ "contact": "Contact",
366
+ "settings": "Settings"
367
+ }
368
+ };
369
+
370
+ // Determine filename: use common.json if it doesn't exist, otherwise i18ntk-common.json
371
+ const commonFilePath = path.join(validatedSourceLanguageDir, `common${this.format.extension}`);
372
+ const i18ntkCommonFilePath = path.join(validatedSourceLanguageDir, `i18ntk-common${this.format.extension}`);
373
+
374
+ let sampleFilePath;
375
+ if (!SecurityUtils.safeExistsSync(commonFilePath)) {
376
+ sampleFilePath = commonFilePath;
377
+ } else {
378
+ sampleFilePath = i18ntkCommonFilePath;
379
+ }
380
+
381
+ const validatedSampleFilePath = SecurityUtils.validatePath(sampleFilePath, process.cwd());
382
+
383
+ if (!validatedSampleFilePath) {
384
+ SecurityUtils.logSecurityEvent('Invalid sample file path', 'error', { path: sampleFilePath });
385
+ throw new Error(t('validate.invalidSampleFilePath') || 'Invalid sample file path');
386
+ }
387
+
388
+ const success = await SecurityUtils.safeWriteFile(validatedSampleFilePath, this.format.serialize(sampleTranslations), process.cwd());
389
+
390
+ if (success) {
391
+ console.log(t('init.createdSampleTranslationFile', { file: validatedSampleFilePath }));
392
+ SecurityUtils.logSecurityEvent('Sample translation file created', 'info', { file: validatedSampleFilePath });
393
+ } else {
394
+ SecurityUtils.logSecurityEvent('Failed to create sample translation file', 'error', { file: validatedSampleFilePath });
395
+ throw new Error(t('validate.failedToCreateSampleTranslationFile') || 'Failed to create sample translation file');
396
+ }
397
+ }
398
+
399
+ // Check if source directory and language exist
400
+ validateSource() {
401
+ if (!SecurityUtils.safeExistsSync(this.sourceDir)) {
402
+ throw new Error(t('validate.sourceLanguageDirectoryNotFound', { sourceDir: this.sourceDir }) || `Source directory not found: ${this.sourceDir}`);
403
+ }
404
+
405
+ if (!SecurityUtils.safeExistsSync(this.sourceLanguageDir)) {
406
+ throw new Error(t('validate.sourceLanguageDirectoryNotFound', { sourceDir: this.sourceLanguageDir }) || `Source language directory not found: ${this.sourceLanguageDir}`);
407
+ }
408
+
409
+ return true;
410
+ }
411
+
412
+ createBootstrapSourceFile(targetDir) {
413
+ try {
414
+ if (!SecurityUtils.safeExistsSync(targetDir)) {
415
+ fs.mkdirSync(targetDir, { recursive: true });
416
+ }
417
+
418
+ const sampleName = `common${this.format.extension}`;
419
+ const samplePath = path.join(targetDir, sampleName);
420
+
421
+ if (SecurityUtils.safeExistsSync(samplePath)) {
422
+ return sampleName;
423
+ }
424
+
425
+ const sampleContent = {
426
+ app: {
427
+ title: 'Application',
428
+ description: 'Application description'
429
+ },
430
+ common: {
431
+ yes: 'Yes',
432
+ no: 'No',
433
+ save: 'Save',
434
+ cancel: 'Cancel'
435
+ }
436
+ };
437
+
438
+ const serializer = typeof this.format?.write === 'function'
439
+ ? this.format.write.bind(this.format)
440
+ : (typeof this.format?.serialize === 'function'
441
+ ? this.format.serialize.bind(this.format)
442
+ : (data) => JSON.stringify(data, null, 2));
443
+
444
+ const serialized = serializer(sampleContent);
445
+ SecurityUtils.safeWriteFileSync(samplePath, `${serialized}\n`, path.dirname(samplePath), 'utf8');
446
+ console.log(t('init.createdSampleTranslationFile', { file: samplePath }) || `Created sample translation file: ${samplePath}`);
447
+
448
+ return sampleName;
449
+ } catch {
450
+ return null;
451
+ }
452
+ }
453
+
454
+ // Get all JSON files from source language directory (supports single/modular)
455
+ getSourceFiles() {
456
+ try {
457
+ if (this.config.structure === 'single') {
458
+ if (!SecurityUtils.safeExistsSync(this.sourceDir)) {
459
+ throw new Error(t('validate.sourceLanguageDirectoryNotFound', { sourceDir: this.sourceDir }) || `Source directory not found: ${this.sourceDir}`);
460
+ }
461
+ const files = fs.readdirSync(this.sourceDir)
462
+ .filter(file => file.endsWith(this.format.extension) && !this.config.excludeFiles.includes(file));
463
+ if (files.length === 0) {
464
+ const sampleFile = this.createBootstrapSourceFile(this.sourceDir);
465
+ if (sampleFile) return [sampleFile];
466
+ throw new Error(t('validate.noJsonFilesFound', { sourceDir: this.sourceDir }) || `No JSON files found in source directory: ${this.sourceDir}`);
467
+ }
468
+ return files;
469
+ }
470
+
471
+ if (!SecurityUtils.safeExistsSync(this.sourceLanguageDir)) {
472
+ // Try to find English files in parent directory or subdirectories
473
+ const parentDir = path.dirname(this.sourceLanguageDir);
474
+ if (SecurityUtils.safeExistsSync(parentDir)) {
475
+ const subdirs = fs.readdirSync(parentDir).filter(item => {
476
+ const fullPath = path.join(parentDir, item);
477
+ return fs.statSync(fullPath).isDirectory();
478
+ });
479
+
480
+ // Look for English files in any subdirectory
481
+ for (const subdir of subdirs) {
482
+ const englishDir = path.join(parentDir, subdir);
483
+ if (SecurityUtils.safeExistsSync(englishDir)) {
484
+ const files = fs.readdirSync(englishDir);
485
+ const formatFiles = files.filter(file =>
486
+ file.endsWith(this.format.extension) &&
487
+ !this.config.excludeFiles.includes(file)
488
+ );
489
+ if (formatFiles.length > 0) {
490
+ // Found English files, use this directory
491
+ this.sourceLanguageDir = englishDir;
492
+ return formatFiles;
493
+ }
494
+ }
495
+ }
496
+ }
497
+ throw new Error(t('validate.noJsonFilesFound', { sourceDir: this.sourceLanguageDir }) || `No JSON files found in source directory: ${this.sourceLanguageDir}`);
498
+ }
499
+
500
+ const files = fs.readdirSync(this.sourceLanguageDir)
501
+ .filter(file => {
502
+ return file.endsWith(this.format.extension) &&
503
+ !this.config.excludeFiles.includes(file);
504
+ });
505
+
506
+ if (files.length === 0) {
507
+ const sampleFile = this.createBootstrapSourceFile(this.sourceLanguageDir);
508
+ if (sampleFile) return [sampleFile];
509
+ throw new Error(t('validate.noJsonFilesFound', { sourceDir: this.sourceLanguageDir }) || `No JSON files found in source directory: ${this.sourceLanguageDir}`);
510
+ }
511
+
512
+ return files;
513
+ } catch (error) {
514
+ console.warn(t('init.warningCannotReadSourceDir', { dir: this.sourceLanguageDir, error: error.message }));
515
+ throw error;
516
+ }
517
+ }
518
+
519
+ // Recursively mark all string values with country code markers
520
+ markWithCountryCode(obj, countryCode) {
521
+ if (typeof obj === 'string') {
522
+ return `[${countryCode.toUpperCase()}] ${obj}`;
523
+ }
524
+
525
+ if (Array.isArray(obj)) {
526
+ return obj.map(item => this.markWithCountryCode(item, countryCode));
527
+ }
528
+
529
+ if (obj && typeof obj === 'object') {
530
+ const result = {};
531
+ for (const [key, value] of Object.entries(obj)) {
532
+ result[key] = this.markWithCountryCode(value, countryCode);
533
+ }
534
+ return result;
535
+ }
536
+
537
+ return obj;
538
+ }
539
+
540
+ // Get the structure type for a specific language
541
+ getLanguageStructure(language) {
542
+ // Check for per-language structure first
543
+ if (this.config.perLanguageStructure && this.config.perLanguageStructure[language]) {
544
+ return this.config.perLanguageStructure[language];
545
+ }
546
+ // Fall back to the global structure
547
+ return this.config.structure || 'modular';
548
+ }
549
+
550
+ // Create or update a language file securely (supports single/modular)
551
+ async createLanguageFile(sourceFile, targetLanguage, sourceContent) {
552
+ try {
553
+ const sourceFilePath = path.join(this.sourceLanguageDir, sourceFile);
554
+ let targetFilePath;
555
+ if (this.getLanguageStructure(targetLanguage) === 'single') {
556
+ // Single-file per language, write to <lang>.json in sourceDir
557
+ const baseName = `${targetLanguage}${this.format.extension}`;
558
+ targetFilePath = path.join(this.sourceDir, baseName);
559
+ } else {
560
+ // Modular: folder per language mirroring source file
561
+ const targetDir = path.join(this.sourceDir, targetLanguage);
562
+ if (!SecurityUtils.safeExistsSync(targetDir)) fs.mkdirSync(targetDir, { recursive: true });
563
+ targetFilePath = path.join(targetDir, sourceFile);
564
+ }
565
+
566
+ // Validate source and target paths
567
+ const validatedSourcePath = SecurityUtils.validatePath(sourceFilePath, process.cwd());
568
+ const validatedTargetPath = SecurityUtils.validatePath(targetFilePath, process.cwd());
569
+ const targetDir = path.dirname(validatedTargetPath);
570
+
571
+ if (!validatedSourcePath || !validatedTargetPath) {
572
+ SecurityUtils.logSecurityEvent('Invalid path detected in createLanguageFile', 'error', {
573
+ sourcePath: sourceFilePath,
574
+ targetPath: targetFilePath
575
+ });
576
+ throw new Error(t('validate.invalidFilePathDetected') || 'Invalid file path detected');
577
+ }
578
+
579
+ // Create target directory if it doesn't exist
580
+ if (!SecurityUtils.safeExistsSync(targetDir)) {
581
+ fs.mkdirSync(targetDir, { recursive: true });
582
+ }
583
+
584
+ let targetContent;
585
+
586
+ // If target file exists, preserve existing translations
587
+ if (SecurityUtils.safeExistsSync(validatedTargetPath)) {
588
+ try {
589
+ const existingContent = await SecurityUtils.safeReadFile(validatedTargetPath, process.cwd());
590
+ if (existingContent) {
591
+ targetContent = this.mergeTranslations(sourceContent, this.format.read(existingContent), targetLanguage);
592
+ } else {
593
+ targetContent = this.markWithCountryCode(sourceContent, targetLanguage);
594
+ }
595
+ } catch (error) {
596
+ console.warn(`āš ļø Warning: Could not parse existing file ${validatedTargetPath}, creating new one`);
597
+ SecurityUtils.logSecurityEvent('File parse error', 'warn', { file: validatedTargetPath, error: error.message });
598
+ targetContent = this.markWithCountryCode(sourceContent, targetLanguage);
599
+ }
600
+ } else {
601
+ targetContent = this.markWithCountryCode(sourceContent, targetLanguage);
602
+ }
603
+
604
+ // Write the file securely
605
+ const success = await SecurityUtils.safeWriteFile(validatedTargetPath, this.format.serialize(targetContent), process.cwd());
606
+
607
+ if (!success) {
608
+ SecurityUtils.logSecurityEvent('Failed to write language file', 'error', { file: validatedTargetPath });
609
+ throw new Error(t('validate.failedToWriteFile', { filePath: validatedTargetPath }) || `Failed to write file: ${validatedTargetPath}`);
610
+ }
611
+
612
+ SecurityUtils.logSecurityEvent('Language file created/updated', 'info', { file: validatedTargetPath, language: targetLanguage });
613
+ return validatedTargetPath;
614
+ } catch (error) {
615
+ console.error(t('init.errors.initializationFailed', { error: error.message }));
616
+ return false;
617
+ }
618
+ }
619
+
620
+ // Merge existing translations with new structure using country code markers
621
+ mergeTranslations(sourceObj, existingObj, countryCode) {
622
+ if (typeof sourceObj === 'string') {
623
+ // If existing translation exists and doesn't contain country code marker, keep it
624
+ if (typeof existingObj === 'string' &&
625
+ !existingObj.startsWith(`[${countryCode.toUpperCase()}]`) &&
626
+ existingObj.trim() !== '') {
627
+ return existingObj;
628
+ }
629
+ return this.markWithCountryCode(sourceObj, countryCode);
630
+ }
631
+
632
+ if (Array.isArray(sourceObj)) {
633
+ return sourceObj.map((item, index) => {
634
+ const existingItem = Array.isArray(existingObj) ? existingObj[index] : undefined;
635
+ return this.mergeTranslations(item, existingItem, countryCode);
636
+ });
637
+ }
638
+
639
+ if (sourceObj && typeof sourceObj === 'object') {
640
+ const result = {};
641
+ for (const [key, value] of Object.entries(sourceObj)) {
642
+ const existingValue = existingObj && typeof existingObj === 'object' ? existingObj[key] : undefined;
643
+ result[key] = this.mergeTranslations(value, existingValue, countryCode);
644
+ }
645
+ return result;
646
+ }
647
+
648
+ return sourceObj;
649
+ }
650
+
651
+ // Get translation statistics
652
+ getTranslationStats(obj) {
653
+ let total = 0;
654
+ let translated = 0;
655
+ let missing = 0;
656
+
657
+ const count = (item) => {
658
+ if (typeof item === 'string') {
659
+ total++;
660
+ const isCountryCodeMarker = /^\[([A-Z]{2})\]/.test(item);
661
+ if (isCountryCodeMarker) {
662
+ missing++;
663
+ } else if (item.trim() !== '') {
664
+ translated++;
665
+ }
666
+ } else if (Array.isArray(item)) {
667
+ item.forEach(count);
668
+ } else if (item && typeof item === 'object') {
669
+ Object.values(item).forEach(count);
670
+ }
671
+ };
672
+
673
+ count(obj);
674
+
675
+ return {
676
+ total,
677
+ translated,
678
+ percentage: total > 0 ? Math.round((translated / total) * 100) : 0,
679
+ missing
680
+ };
681
+ }
682
+
683
+ // Interactive admin PIN setup
684
+ async promptAdminPinSetup() {
685
+ const { ask, askHidden, flushStdout } = require('../../../utils/cli');
686
+
687
+ console.log('\n' + t('init.adminPinSetupOptional'));
688
+ console.log(t('init.adminPinSeparator'));
689
+ console.log(t('init.adminPinDescription1'));
690
+ console.log(t('init.adminPinDescription2'));
691
+ console.log(t('init.adminPinDescription3'));
692
+ console.log(t('init.adminPinDescription4'));
693
+
694
+ await flushStdout();
695
+ const enableProtection = await ask('\n' + t('adminPin.setup_prompt'));
696
+
697
+ if (enableProtection.toLowerCase() === 'y' || enableProtection.toLowerCase() === 'yes') {
698
+ try {
699
+ const adminAuth = new AdminAuth();
700
+ await adminAuth.initialize();
701
+
702
+ let pin = null;
703
+ do {
704
+ pin = await askHidden(t('init.enterAdminPin'));
705
+ if (!/^\d{4}$/.test(pin)) {
706
+ console.log(t('init.pinMustBe4Digits'));
707
+ pin = null;
708
+ continue;
709
+ }
710
+ const confirm = await askHidden(t('init.confirmAdminPin'));
711
+ if (pin !== confirm) {
712
+ console.log(t('init.pinMismatch'));
713
+ pin = null;
714
+ }
715
+ } while (!pin);
716
+
717
+ const saved = await adminAuth.setupPin(pin);
718
+ if (saved) {
719
+ await configManager.updateConfig({
720
+ security: {
721
+ adminPinEnabled: true,
722
+ adminPinPromptOnInit: true,
723
+ pinProtection: { enabled: true }
724
+ }
725
+ });
726
+ console.log(t('init.adminPinSetupSuccess'));
727
+ } else {
728
+ console.error(t('init.errorSettingUpAdminPin', { error: 'Failed to save PIN' }));
729
+ }
730
+ } catch (error) {
731
+ console.error(t('init.errorSettingUpAdminPin', { error: error.message }));
732
+ console.log(t('init.continuingWithoutAdminPin'));
733
+ }
734
+ } else {
735
+ console.log(t('init.skippingAdminPinSetup'));
736
+ }
737
+ }
738
+
739
+ // Interactive language selection
740
+ async selectLanguages(skipPrompt = false) {
741
+ if (skipPrompt || !process.stdin.isTTY) {
742
+ return this.config.defaultLanguages;
743
+ }
744
+
745
+ const { ask } = require('../../../utils/cli');
746
+
747
+ console.log('\n' + t('init.languageSelectionTitle'));
748
+ console.log(t('common.separator'));
749
+ console.log(t('init.available'));
750
+
751
+ Object.entries(LANGUAGE_CONFIG).forEach(([code, config], index) => {
752
+ console.log(` ${(index + 1).toString().padStart(2)}. ${code} - ${config.name} (${config.nativeName})`);
753
+ });
754
+
755
+ console.log('\n' + t('init.defaultLanguages', { languages: this.config.defaultLanguages.join(', ') }));
756
+
757
+ const answer = await ask('\n' + t('init.enterLanguageCodes'));
758
+
759
+ if (answer.trim() === '') {
760
+ return this.config.defaultLanguages;
761
+ }
762
+
763
+ const selectedLanguages = answer.split(',').map(lang => lang.trim().toLowerCase());
764
+ const validLanguages = selectedLanguages.filter(lang => LANGUAGE_CONFIG[lang]);
765
+ const invalidLanguages = selectedLanguages.filter(lang => !LANGUAGE_CONFIG[lang]);
766
+
767
+ if (invalidLanguages.length > 0) {
768
+ console.warn(t('init.warningInvalidLanguageCodes', { languages: invalidLanguages.join(', ') }));
769
+ }
770
+
771
+ return validLanguages.length > 0 ? validLanguages : this.config.defaultLanguages;
772
+ }
773
+
774
+ // Interactive setup configuration (internationalized)
775
+ async promptSetupConfiguration(skipPrompt = false) {
776
+ if (skipPrompt || !process.stdin.isTTY) {
777
+ return { structure: 'modular', duplicateStructure: true };
778
+ }
779
+
780
+ const { ask } = require('../../../utils/cli');
781
+
782
+ console.log('\n' + t('init.setup.title'));
783
+ console.log(t('common.separator'));
784
+ // Determine recommended option
785
+ const recommended = ' (recommended)';
786
+ console.log(t('init.setup.question'));
787
+ console.log(' 1. ' + t('init.setup.opt_single') + (this.config.structure === 'single' ? recommended : ''));
788
+ console.log(' 2. ' + t('init.setup.opt_modular') + (this.config.structure !== 'single' ? recommended : ''));
789
+ console.log(' 3. ' + t('init.setup.opt_existing'));
790
+
791
+ const structureChoice = await ask('\n' + t('init.setup.choice_prompt'));
792
+
793
+ let structure = 'modular';
794
+ if (structureChoice === '1') structure = 'single';
795
+ else if (structureChoice === '2') structure = 'modular';
796
+ else structure = 'existing';
797
+
798
+ let duplicateStructure = true;
799
+ let perLanguage = [];
800
+ if (structure !== 'existing') {
801
+ const duplicateChoice = await ask('\n' + t('init.setup.apply_all_prompt'));
802
+ duplicateStructure = duplicateChoice.toLowerCase() === 'y' || duplicateChoice.toLowerCase() === 'yes';
803
+ if (!duplicateStructure) {
804
+ // Prompt for languages to include/exclude
805
+ console.log(t('init.setup.per_language_intro'));
806
+ const available = Object.keys(LANGUAGE_CONFIG).join(', ');
807
+ console.log(t('init.setup.available_languages', { languages: available }));
808
+ const includeAns = await ask(t('init.setup.include_prompt'));
809
+ perLanguage = includeAns.split(',').map(l => l.trim().toLowerCase()).filter(Boolean);
810
+ }
811
+ }
812
+
813
+ return { structure, duplicateStructure, perLanguage };
814
+ }
815
+
816
+ // Enhanced initialization with dependency checking
817
+ async initialize(hasI18n = true, args = {}) {
818
+ console.log(t('init.initializingProject'));
819
+
820
+ if (!hasI18n) {
821
+ console.log(t('init.warningProceedingWithoutFramework'));
822
+ console.log(t('init.translationFilesCreatedWarning'));
823
+ }
824
+
825
+ // Get setup configuration
826
+ const setupConfig = await this.promptSetupConfiguration(args.noPrompt);
827
+
828
+ // Handle directory selection and structure setup
829
+ const selectedDir = await this.detectAndSelectDirectory(args.noPrompt);
830
+ if (selectedDir) {
831
+ this.config.sourceDir = selectedDir;
832
+ this.sourceDir = path.resolve(selectedDir);
833
+ this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage);
834
+ if (!this.announcedExistingDir) {
835
+ console.log(t('init.usingExistingDirectory', { dir: selectedDir }));
836
+ this.announcedExistingDir = true;
837
+ }
838
+ } else {
839
+ await this.setupInitialStructure(args.noPrompt);
840
+ }
841
+
842
+ // Validate source
843
+ this.validateSource();
844
+
845
+ // Prompt for admin PIN setup if not already configured
846
+ const securitySettings = configManager.getConfig().security || {};
847
+
848
+ if (!securitySettings.adminPinEnabled && securitySettings.adminPinPromptOnInit !== false && !args.noPrompt) {
849
+ const { flushStdout } = require('../../../utils/cli');
850
+ await flushStdout();
851
+ await this.promptAdminPinSetup();
852
+ }
853
+
854
+ // Get target languages - use args.languages if provided
855
+ let targetLanguages = args.languages || await this.selectLanguages(args.noPrompt);
856
+
857
+ // Ensure targetLanguages is always an array
858
+ targetLanguages = Array.isArray(targetLanguages) ? targetLanguages : [];
859
+
860
+ if (targetLanguages.length === 0) {
861
+ console.log(t('init.noTargetLanguagesSpecified'));
862
+ return;
863
+ }
864
+
865
+ console.log('\n' + t('init.targetLanguages', { languages: targetLanguages.map(lang => `${lang} (${LANGUAGE_CONFIG[lang]?.name || 'Unknown'})`).join(', ') }));
866
+
867
+ // Get source files
868
+ const sourceFiles = this.getSourceFiles();
869
+ console.log('\n' + t('init.foundSourceFiles', { count: sourceFiles.length }));
870
+
871
+ // Process each language
872
+ const results = {};
873
+
874
+ for (const targetLanguage of targetLanguages) {
875
+ console.log('\n' + t('init.processingLanguage', { language: targetLanguage, name: LANGUAGE_CONFIG[targetLanguage]?.name || 'Unknown' }));
876
+
877
+ const languageResults = {
878
+ files: [],
879
+ totalStats: { total: 0, translated: 0, missing: 0 }
880
+ };
881
+
882
+ for (const sourceFile of sourceFiles) {
883
+ const sourceFilePath = path.join(this.sourceLanguageDir, sourceFile);
884
+ const validatedSourceFilePath = SecurityUtils.validatePath(sourceFilePath, process.cwd());
885
+
886
+ if (!validatedSourceFilePath) {
887
+ SecurityUtils.logSecurityEvent('Invalid source file path', 'error', { path: sourceFilePath });
888
+ continue;
889
+ }
890
+
891
+ const sourceContentRaw = await SecurityUtils.safeReadFile(validatedSourceFilePath, process.cwd());
892
+ if (!sourceContentRaw) {
893
+ SecurityUtils.logSecurityEvent('Failed to read source file', 'error', { file: validatedSourceFilePath });
894
+ continue;
895
+ }
896
+
897
+ const sourceContent = this.format.read(sourceContentRaw);
898
+
899
+ const targetFilePath = await this.createLanguageFile(sourceFile, targetLanguage, sourceContent);
900
+
901
+ // Get stats for this file
902
+ const targetContentRaw = await SecurityUtils.safeReadFile(targetFilePath, process.cwd());
903
+ if (!targetContentRaw) {
904
+ SecurityUtils.logSecurityEvent('Failed to read target file for stats', 'error', { file: targetFilePath });
905
+ continue;
906
+ }
907
+
908
+ const targetContent = this.format.read(targetContentRaw);
909
+ const stats = this.getTranslationStats(targetContent);
910
+
911
+ languageResults.files.push({
912
+ name: sourceFile,
913
+ path: targetFilePath,
914
+ stats
915
+ });
916
+
917
+ // Add to total stats
918
+ languageResults.totalStats.total += stats.total;
919
+ languageResults.totalStats.translated += stats.translated;
920
+ languageResults.totalStats.missing += stats.missing;
921
+
922
+ console.log(t('init.fileProcessingResult', { file: sourceFile, translated: stats.translated, total: stats.total, percentage: stats.percentage }));
923
+ }
924
+
925
+ // Calculate overall percentage
926
+ languageResults.totalStats.percentage = languageResults.totalStats.total > 0
927
+ ? Math.round((languageResults.totalStats.translated / languageResults.totalStats.total) * 100)
928
+ : 0;
929
+
930
+ results[targetLanguage] = languageResults;
931
+
932
+ console.log(t('init.overallProgress', { translated: languageResults.totalStats.translated, total: languageResults.totalStats.total, percentage: languageResults.totalStats.percentage }));
933
+ }
934
+
935
+ // Generate and display completion summary
936
+ await this.generateCompletionSummary(results, targetLanguages);
937
+
938
+ console.log('\n' + t('init.initializationCompletedSuccessfully'));
939
+ console.log('\n' + t('init.nextStepsTitle'));
940
+ console.log(t('init.nextStep1'));
941
+ console.log(t('init.nextStep2'));
942
+ console.log(t('init.nextStep3'));
943
+ }
944
+
945
+ // Generate completion summary with proper error handling
946
+ async generateCompletionSummary(results, targetLanguages) {
947
+ try {
948
+ console.log('\n' + '='.repeat(50));
949
+ console.log(t('init.initializationSummaryTitle'));
950
+ console.log(t('common.separator'));
951
+
952
+ let totalChanges = 0;
953
+ let languagesProcessed = 0;
954
+ let missingKeysAdded = 0;
955
+
956
+ Object.entries(results || {}).forEach(([lang, data]) => {
957
+ if (!data || typeof data !== 'object') return;
958
+
959
+ const langName = LANGUAGE_CONFIG[lang]?.name || 'Unknown';
960
+ const stats = data.totalStats || { total: 0, translated: 0, percentage: 0, missing: 0 };
961
+
962
+ const statusIcon = stats.percentage === 100 ? 'āœ…' : stats.percentage >= 80 ? '🟔' : 'šŸ”“';
963
+
964
+ console.log(
965
+ t('init.languageSummary', {
966
+ icon: statusIcon,
967
+ name: langName,
968
+ code: lang,
969
+ percentage: stats.percentage || 0,
970
+ })
971
+ );
972
+
973
+ if (data.files && Array.isArray(data.files)) {
974
+ console.log(t('init.languageFiles', { count: data.files.length }));
975
+ }
976
+
977
+ console.log(
978
+ t('init.languageKeys', {
979
+ translated: stats.translated || 0,
980
+ total: stats.total || 0,
981
+ })
982
+ );
983
+
984
+ console.log(t('init.languageMissing', { count: stats.missing || 0 }));
985
+
986
+ totalChanges += (stats.translated || 0) + (stats.missing || 0);
987
+ languagesProcessed += 1;
988
+ missingKeysAdded += stats.missing || 0;
989
+ });
990
+
991
+ console.log('\nšŸ“Š COMPLETION SUMMARY');
992
+ console.log(t('common.separator'));
993
+ console.log(`šŸ“ Total changes: ${totalChanges}`);
994
+ console.log(`šŸŒ Languages processed: ${languagesProcessed}`);
995
+ console.log(`āž• Missing keys added: ${missingKeysAdded}`);
996
+
997
+ if (process.stdin.isTTY && !this.config?.noPrompt) {
998
+ const { ask } = require('../../../utils/cli');
999
+ const generateReport = await ask('\nšŸ¤– Would you like a report generated? (Y/N): ');
1000
+ if (generateReport.toLowerCase() === 'y' || generateReport.toLowerCase() === 'yes') {
1001
+ await this.generateDetailedReport(results, targetLanguages);
1002
+ }
1003
+ }
1004
+ } catch (error) {
1005
+ console.error('\nāŒ Error during completion:', error.message);
1006
+ console.log('šŸ“Š COMPLETION SUMMARY (Basic)');
1007
+ console.log(t('common.separator'));
1008
+ console.log(`šŸŒ Languages processed: ${Object.keys(results || {}).length}`);
1009
+ }
1010
+ }
1011
+
1012
+ // Generate detailed report
1013
+ async generateDetailedReport(results, targetLanguages) {
1014
+ try {
1015
+ const outputDir = this.config.outputDir || path.join(process.cwd(), 'i18ntk-reports');
1016
+ if (!SecurityUtils.safeExistsSync(outputDir)) {
1017
+ fs.mkdirSync(outputDir, { recursive: true });
1018
+ }
1019
+
1020
+ const reportPath = path.join(outputDir, 'init-report.json');
1021
+ const report = {
1022
+ timestamp: new Date().toISOString(),
1023
+ languages: targetLanguages,
1024
+ results: results,
1025
+ summary: {
1026
+ languagesProcessed: targetLanguages.length,
1027
+ totalFiles: Object.values(results).reduce((sum, data) => sum + (data.files?.length || 0), 0),
1028
+ totalKeys: Object.values(results).reduce((sum, data) => sum + (data.totalStats?.total || 0), 0),
1029
+ totalMissing: Object.values(results).reduce((sum, data) => sum + (data.totalStats?.missing || 0), 0)
1030
+ }
1031
+ };
1032
+
1033
+ await fs.promises.writeFile(reportPath, JSON.stringify(report, null, 2));
1034
+ console.log(`āœ… Report generated: ${reportPath}`);
1035
+ } catch (error) {
1036
+ console.error('āŒ Failed to generate report:', error.message);
1037
+ }
1038
+ }
1039
+
1040
+ // Set prompt function for interactive operations
1041
+ setPromptFunction(promptFn) {
1042
+ this.prompt = promptFn;
1043
+ }
1044
+
1045
+ // Set announcedExistingDir flag
1046
+ setAnnouncedExistingDir(value) {
1047
+ this.announcedExistingDir = value;
1048
+ }
1049
+ }
1050
+
1051
+ module.exports = InitService;