i18ntk 2.0.4 → 2.1.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.
package/README.md CHANGED
@@ -1,74 +1,59 @@
1
- # šŸš€ i18ntk - The Ultra-Fast, Zero-Dependency i18n Translation Toolkit
1
+ # i18ntk v2.1.0
2
2
 
3
- <div align="center">
3
+ Zero-dependency i18n toolkit for initialization, scanning, analysis, validation, usage tracking, and translation completion.
4
4
 
5
5
  ![i18ntk Logo](docs/screenshots/i18ntk-logo-public.PNG)
6
6
 
7
- **The fastest and most comprehensive i18n toolkit ever built.**
8
-
9
7
  [![npm version](https://img.shields.io/npm/v/i18ntk.svg?color=brightgreen)](https://www.npmjs.com/package/i18ntk)
10
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
11
- [![Performance](https://img.shields.io/badge/Performance-97%25%20Faster-blue.svg)](https://github.com/vladnoskv/i18ntk#performance)
12
- [![Zero Dependencies](https://img.shields.io/badge/Dependencies-ZERO-red.svg)](https://github.com/vladnoskv/i18ntk#features)
13
8
  [![npm downloads](https://img.shields.io/npm/dt/i18ntk.svg)](https://www.npmjs.com/package/i18ntk)
14
- [![GitHub stars](https://img.shields.io/github/stars/vladnoskv/i18ntk?style=social)](https://github.com/vladnoskv/i18ntk)
15
- [![Socket Badge](https://socket.dev/api/badge/npm/package/i18ntk/2.0.4)](https://socket.dev/npm/package/i18ntk/overview/2.0.4)
16
-
17
- [šŸ“¦ Install Now](#-installation) • [⚔ Quick Start](#-quick-start) • [šŸ“š Documentation](#-documentation) • [šŸŽÆ Features](#-why-choose-i18ntk)
18
-
19
- ---
20
-
21
- ## ⚔ Lightning Fast Performance
22
-
23
- **15.38ms** for 200k translation keys • **<2MB** memory usage • **97% faster** than traditional tools
9
+ [![node](https://img.shields.io/badge/node-%3E%3D16-339933)](https://nodejs.org)
10
+ [![dependencies](https://img.shields.io/badge/dependencies-0-success)](https://www.npmjs.com/package/i18ntk)
11
+ [![license](https://img.shields.io/badge/license-MIT-yellow.svg)](LICENSE)
12
+ [![socket](https://socket.dev/api/badge/npm/package/i18ntk/2.1.0)](https://socket.dev/npm/package/i18ntk/overview/2.1.0)
24
13
 
25
- **v2.0.4**
14
+ ## Why i18ntk
26
15
 
27
- </div>
16
+ - Zero runtime dependencies
17
+ - Works across JS/TS, React, Vue, Angular, and generic projects
18
+ - Supports non-interactive CI runs (`--no-prompt`)
19
+ - Includes usage/coverage validation and missing-key completion
20
+ - Ships with runtime translation helpers via `i18ntk/runtime`
28
21
 
29
- ## šŸ“¦ Installation
22
+ ## Install
30
23
 
31
24
  ```bash
32
- # Install globally (recommended)
25
+ # global (recommended for CLI use)
33
26
  npm install -g i18ntk
34
27
 
35
- # Or use with npx (no installation required)
36
- npx i18ntk
28
+ # local
29
+ npm install --save-dev i18ntk
37
30
 
38
- # Or install locally in your project
39
- npm install i18ntk --save-dev
31
+ # one-off
32
+ npx i18ntk --help
40
33
  ```
41
34
 
42
- ## ⚔ Quick Start
43
-
44
- Get your i18n project up and running in **60 seconds**:
35
+ ## Quick Start
45
36
 
46
37
  ```bash
47
- # 1. Install i18ntk
48
- npm install -g i18ntk
49
-
50
- # 2. Initialize your project
51
- i18ntk init
52
-
53
- # 3. Analyze your translations
54
- i18ntk analyze
38
+ # initialize locales/project settings
39
+ i18ntk --command=init
55
40
 
56
- # 4. Fix any issues
57
- i18ntk fixer --interactive
41
+ # analyze translation completeness
42
+ i18ntk --command=analyze
58
43
 
59
- # 5. Validate everything
60
- i18ntk validate
44
+ # validate translation structure/content
45
+ i18ntk --command=validate
61
46
 
62
- # 6. Mangage Mre
47
+ # complete missing keys
48
+ i18ntk --command=complete
63
49
  ```
64
50
 
65
- That's it! Your i18n infrastructure is ready. šŸŽ‰
51
+ ## Command Model (v2)
66
52
 
67
- ## v2 Command Model
68
-
69
- Primary CLI commands:
53
+ Primary CLI:
70
54
 
71
55
  ```bash
56
+ i18ntk
72
57
  i18ntk --command=init
73
58
  i18ntk --command=analyze
74
59
  i18ntk --command=validate
@@ -80,7 +65,7 @@ i18ntk --command=summary
80
65
  i18ntk --command=debug
81
66
  ```
82
67
 
83
- Standalone binaries:
68
+ Standalone executables:
84
69
 
85
70
  ```bash
86
71
  i18ntk-init
@@ -96,19 +81,8 @@ i18ntk-fixer
96
81
  i18ntk-backup
97
82
  ```
98
83
 
99
- Backup helper:
100
-
101
- ```bash
102
- i18ntk-backup --help
103
- i18ntk-backup create ./locales
104
- i18ntk-backup list
105
- i18ntk-backup restore <backup-file>
106
- ```
107
-
108
84
  ## Common Flags
109
85
 
110
- Most commands support:
111
-
112
86
  - `--source-dir <path>`
113
87
  - `--i18n-dir <path>`
114
88
  - `--output-dir <path>`
@@ -120,13 +94,13 @@ Most commands support:
120
94
 
121
95
  ## Configuration
122
96
 
123
- i18ntk reads project settings from `.i18ntk-config` in the project root.
97
+ i18ntk reads project settings from `.i18ntk-config` in your project root.
124
98
 
125
99
  Example:
126
100
 
127
101
  ```json
128
102
  {
129
- "version": "2.0.0",
103
+ "version": "2.1.0",
130
104
  "sourceDir": "./locales",
131
105
  "i18nDir": "./locales",
132
106
  "outputDir": "./i18ntk-reports",
@@ -155,7 +129,7 @@ setLanguage('fr');
155
129
  console.log(getLanguage());
156
130
  ```
157
131
 
158
- ## Docs
132
+ ## Documentation
159
133
 
160
134
  - [Documentation Index](docs/README.md)
161
135
  - [API Reference](docs/api/API_REFERENCE.md)
@@ -163,9 +137,9 @@ console.log(getLanguage());
163
137
  - [Runtime API Guide](docs/runtime.md)
164
138
  - [Scanner Guide](docs/scanner-guide.md)
165
139
  - [Environment Variables](docs/environment-variables.md)
166
- - [Migration Guide v2.0.0](docs/migration-guide-v2.0.0.md)
140
+ - [Migration Guide v2.1.0](docs/migration-guide-v2.1.0.md)
167
141
  - [Release Runbook](DEVUPDATE.md)
168
142
 
169
143
  ## License
170
144
 
171
- MIT
145
+ MIT
@@ -142,22 +142,44 @@ class I18nAnalyzer {
142
142
  }
143
143
  }
144
144
 
145
- // Get all available languages
146
- getAvailableLanguages() {
147
- try {
148
- const items = SecurityUtils.safeReaddirSync(this.sourceDir, process.cwd(), { withFileTypes: true });
149
- if (!items) {
150
- console.error('Error reading source directory: Unable to access directory');
145
+ // Get all available languages
146
+ isValidLanguageCode(code) {
147
+ if (!code || typeof code !== 'string') return false;
148
+ return /^[a-z]{2}(?:-[A-Za-z0-9]{2,8})*$/i.test(code.trim());
149
+ }
150
+
151
+ isExcludedLanguageDirectory(name) {
152
+ if (!name || typeof name !== 'string') return true;
153
+ const lowered = name.toLowerCase();
154
+ return lowered.startsWith('backup-') || lowered === 'backup' || lowered === 'reports' || lowered === 'i18ntk-reports';
155
+ }
156
+
157
+ getAvailableLanguages() {
158
+ try {
159
+ const items = SecurityUtils.safeReaddirSync(this.sourceDir, process.cwd(), { withFileTypes: true });
160
+ if (!items) {
161
+ console.error('Error reading source directory: Unable to access directory');
151
162
  return [];
152
163
  }
153
164
 
154
165
  const languages = [];
155
166
 
156
167
  // Check for directory-based structure
157
- const directories = items
158
- .filter(item => item.isDirectory())
159
- .map(item => item.name)
160
- .filter(name => name !== 'node_modules' && !name.startsWith('.') && name !== this.config.sourceLanguage);
168
+ const directories = items
169
+ .filter(item => item.isDirectory())
170
+ .map(item => item.name)
171
+ .filter(name =>
172
+ name !== 'node_modules' &&
173
+ !name.startsWith('.') &&
174
+ name !== this.config.sourceLanguage &&
175
+ !this.isExcludedLanguageDirectory(name) &&
176
+ this.isValidLanguageCode(name)
177
+ )
178
+ .filter(name => {
179
+ const dirPath = path.join(this.sourceDir, name);
180
+ const dirItems = SecurityUtils.safeReaddirSync(dirPath, process.cwd(), { withFileTypes: true }) || [];
181
+ return dirItems.some(item => item.isFile() && item.name.endsWith('.json'));
182
+ });
161
183
 
162
184
  // Check for monolith files (language.json files)
163
185
  const files = items
@@ -168,34 +190,17 @@ class I18nAnalyzer {
168
190
  languages.push(...directories);
169
191
 
170
192
  // Add monolith files as languages (without .json extension)
171
- const monolithLanguages = files
172
- .map(file => file.replace('.json', ''))
173
- .filter(lang => !languages.includes(lang) && lang !== this.config.sourceLanguage);
174
- languages.push(...monolithLanguages);
175
-
176
- // Check for nested structures
177
- for (const dir of directories) {
178
- const dirPath = path.join(this.sourceDir, dir);
179
- try {
180
- const dirItems = SecurityUtils.safeReaddirSync(dirPath, process.cwd(), { withFileTypes: true });
181
- if (dirItems) {
182
- const jsonFiles = dirItems
183
- .filter(item => item.isFile() && item.name.endsWith('.json'))
184
- .map(item => item.name.replace('.json', ''));
185
-
186
- // If directory contains JSON files, it's likely a language directory
187
- if (jsonFiles.length > 0) {
188
- if (!languages.includes(dir)) {
189
- languages.push(dir);
190
- }
191
- }
192
- }
193
- } catch (error) {
194
- // Skip directories we can't read
195
- }
196
- }
197
-
198
- return [...new Set(languages)].sort();
193
+ const monolithLanguages = files
194
+ .map(file => file.replace('.json', ''))
195
+ .filter(lang =>
196
+ !languages.includes(lang) &&
197
+ lang !== this.config.sourceLanguage &&
198
+ !this.isExcludedLanguageDirectory(lang) &&
199
+ this.isValidLanguageCode(lang)
200
+ );
201
+ languages.push(...monolithLanguages);
202
+
203
+ return [...new Set(languages)].sort();
199
204
  } catch (error) {
200
205
  console.error('Error reading source directory:', error.message);
201
206
  return [];
@@ -707,8 +712,7 @@ try {
707
712
  throw new Error(t('analyze.failedToWriteReportFile') || 'Failed to write report file securely');
708
713
  }
709
714
 
710
- console.log(`Report saved to: ${reportPath}`);
711
- return reportPath;
715
+ return reportPath;
712
716
 
713
717
  } catch (error) {
714
718
  console.error(`Failed to save report for ${language}:`, error.message);
@@ -775,20 +779,21 @@ try {
775
779
  console.log(t('analyze.analyzing', { language }) || `\nšŸ”„ Analyzing ${language}...`);
776
780
  }
777
781
 
778
- const analysis = this.analyzeLanguage(language);
779
- const report = this.generateLanguageReport(analysis);
780
-
781
- // Save report
782
- const reportPath = await this.saveReport(language, report);
783
-
784
- if (!args.json) {
785
- console.log(t('analyze.completed', { language }) || `āœ… Analysis completed for ${language}`);
786
- console.log(t('analyze.progress', {
787
- translated: results.length,
788
- total: languages.length
789
- }) || ` Progress: ${results.length}/${languages.length} languages processed`);
790
- console.log(t('analyze.reportSaved', { reportPath }) || ` Report saved: ${reportPath}`);
791
- }
782
+ const analysis = this.analyzeLanguage(language);
783
+ const report = this.generateLanguageReport(analysis);
784
+
785
+ // Save report
786
+ const reportPath = await this.saveReport(language, report);
787
+ const processedCount = results.length + 1;
788
+
789
+ if (!args.json) {
790
+ console.log(t('analyze.completed', { language }) || `āœ… Analysis completed for ${language}`);
791
+ console.log(t('analyze.progress', {
792
+ translated: processedCount,
793
+ total: languages.length
794
+ }) || ` Progress: ${processedCount}/${languages.length} languages processed`);
795
+ console.log(t('analyze.reportSaved', { reportPath }) || ` Report saved: ${reportPath}`);
796
+ }
792
797
 
793
798
  results.push({
794
799
  language,
package/main/i18ntk-ui.js CHANGED
@@ -11,13 +11,13 @@ const legacyConfigManager = require('../utils/config-manager');
11
11
  const { getIcon, isUnicodeSupported } = require('../utils/terminal-icons');
12
12
  const configManager = new SettingsManager();
13
13
 
14
- class UIi18n {
15
- constructor() {
16
- this.currentLanguage = 'en';
17
- this.translations = {};
18
- this.uiLocalesDir = path.resolve(__dirname, '..', 'resources', 'i18n', 'ui-locales');
19
- this.availableLanguages = [];
20
- this.configFile = path.resolve(configManager.configFile);
14
+ class UIi18n {
15
+ constructor() {
16
+ this.currentLanguage = 'en';
17
+ this.translations = {};
18
+ this.uiLocalesDir = this.resolveUiLocalesDir();
19
+ this.availableLanguages = [];
20
+ this.configFile = path.resolve(configManager.configFile);
21
21
 
22
22
 
23
23
  // Initialize with safe defaults
@@ -25,13 +25,13 @@ this.translations = {};
25
25
  }
26
26
 
27
27
 
28
- initialize() {
29
- try {
30
- const config = configManager.loadSettings ? configManager.loadSettings() : configManager.getConfig ? configManager.getConfig() : {};
31
-
32
- // Use safe defaults if config is not available
33
- this.uiLocalesDir = path.resolve(__dirname, '..', 'resources', 'i18n', 'ui-locales');
34
- this.availableLanguages = this.detectAvailableLanguages();
28
+ initialize() {
29
+ try {
30
+ const config = configManager.loadSettings ? configManager.loadSettings() : configManager.getConfig ? configManager.getConfig() : {};
31
+
32
+ // Use safe defaults if config is not available
33
+ this.uiLocalesDir = this.resolveUiLocalesDir();
34
+ this.availableLanguages = this.detectAvailableLanguages();
35
35
 
36
36
 
37
37
  const configuredLanguage = config?.language || config?.uiLanguage || 'en';
@@ -42,25 +42,71 @@ this.translations = {};
42
42
  } else {
43
43
  this.loadLanguage('en');
44
44
  }
45
- } catch (error) {
46
- console.warn('UIi18n: Failed to initialize with config, using defaults:', error.message);
47
- this.uiLocalesDir = path.resolve(__dirname, '..', 'resources', 'i18n', 'ui-locales');
48
- this.availableLanguages = this.detectAvailableLanguages();
49
- this.loadLanguage('en');
50
- }
51
- }
45
+ } catch (error) {
46
+ console.warn('UIi18n: Failed to initialize with config, using defaults:', error.message);
47
+ this.uiLocalesDir = this.resolveUiLocalesDir();
48
+ this.availableLanguages = this.detectAvailableLanguages();
49
+ this.loadLanguage('en');
50
+ }
51
+ }
52
+
53
+ getValidationBase(targetPath) {
54
+ const fallbackBase = path.resolve(__dirname, '..');
55
+ if (!targetPath || typeof targetPath !== 'string') {
56
+ return fallbackBase;
57
+ }
58
+
59
+ let current = path.resolve(path.dirname(targetPath));
60
+ while (true) {
61
+ try {
62
+ if (fs.statSync(current).isDirectory()) {
63
+ return current;
64
+ }
65
+ } catch (_) {
66
+ // Continue walking up until an existing directory is found.
67
+ }
68
+
69
+ const parent = path.dirname(current);
70
+ if (parent === current) {
71
+ break;
72
+ }
73
+ current = parent;
74
+ }
75
+
76
+ return fallbackBase;
77
+ }
78
+
79
+ resolveUiLocalesDir() {
80
+ const candidates = [
81
+ path.resolve(__dirname, '..', 'ui-locales'),
82
+ path.resolve(__dirname, '..', 'resources', 'i18n', 'ui-locales')
83
+ ];
84
+
85
+ for (const candidate of candidates) {
86
+ try {
87
+ const stats = SecurityUtils.safeStatSync(candidate, this.getValidationBase(candidate));
88
+ if (stats && stats.isDirectory()) {
89
+ return candidate;
90
+ }
91
+ } catch (_) {
92
+ // Try next candidate.
93
+ }
94
+ }
95
+
96
+ return candidates[0];
97
+ }
52
98
  /**
53
99
  /**
54
100
  * Detect which UI locales are currently installed
55
101
  * @returns {string[]} Array of available language codes
56
102
  */
57
- detectAvailableLanguages() {
58
- const all = ['en', 'de', 'es', 'fr', 'ru', 'ja', 'zh'];
59
- return all.filter(lang => {
60
- const filePath = path.join(this.uiLocalesDir, `${lang}.json`);
61
- return SecurityUtils.safeExistsSync(filePath);
62
- });
63
- }
103
+ detectAvailableLanguages() {
104
+ const all = ['en', 'de', 'es', 'fr', 'ru', 'ja', 'zh'];
105
+ return all.filter(lang => {
106
+ const filePath = path.join(this.uiLocalesDir, `${lang}.json`);
107
+ return SecurityUtils.safeExistsSync(filePath, this.getValidationBase(filePath));
108
+ });
109
+ }
64
110
 
65
111
  /**
66
112
  * Refresh the list of available languages and ensure current language is valid
@@ -91,13 +137,13 @@ this.translations = {};
91
137
  }
92
138
 
93
139
  try {
94
- // Primary: Use monolith JSON file (en.json, de.json, etc.)
95
- const monolithTranslationFile = path.join(this.uiLocalesDir, `${language}.json`);
96
-
97
- if (SecurityUtils.safeExistsSync(monolithTranslationFile)) {
98
- try {
99
- const content = SecurityUtils.safeReadFileSync(monolithTranslationFile, path.dirname(monolithTranslationFile), 'utf8');
100
- const fullTranslations = JSON.parse(content);
140
+ // Primary: Use monolith JSON file (en.json, de.json, etc.)
141
+ const monolithTranslationFile = path.join(this.uiLocalesDir, `${language}.json`);
142
+
143
+ if (SecurityUtils.safeExistsSync(monolithTranslationFile, this.getValidationBase(monolithTranslationFile))) {
144
+ try {
145
+ const content = SecurityUtils.safeReadFileSync(monolithTranslationFile, path.dirname(monolithTranslationFile), 'utf8');
146
+ const fullTranslations = JSON.parse(content);
101
147
 
102
148
  // Flatten the nested structure for easier key access
103
149
  this.translations = this.flattenTranslations(fullTranslations);
@@ -111,13 +157,14 @@ this.translations = {};
111
157
  this.translations = {};
112
158
  }
113
159
  } else {
114
- // Fallback: Use folder-based structure if monolith file doesn't exist
115
- const langDir = path.join(this.uiLocalesDir, language);
116
-
117
- if (SecurityUtils.safeExistsSync(langDir) && fs.statSync(langDir).isDirectory()) {
118
- const files = fs.readdirSync(langDir).filter(file => file.endsWith('.json'));
119
- if (debugEnabled) {
120
- console.log(`UI: Found files in ${langDir}: ${files.join(', ')}`);
160
+ // Fallback: Use folder-based structure if monolith file doesn't exist
161
+ const langDir = path.join(this.uiLocalesDir, language);
162
+
163
+ const langDirStats = SecurityUtils.safeStatSync(langDir, this.getValidationBase(langDir));
164
+ if (langDirStats && langDirStats.isDirectory()) {
165
+ const files = fs.readdirSync(langDir).filter(file => file.endsWith('.json'));
166
+ if (debugEnabled) {
167
+ console.log(`UI: Found files in ${langDir}: ${files.join(', ')}`);
121
168
  }
122
169
 
123
170
  for (const file of files) {
@@ -381,12 +428,12 @@ this.translations = {};
381
428
  * @param {object} replacements - Object with replacement values
382
429
  * @returns {string|null} English translation or null if not found
383
430
  */
384
- getEnglishFallback(keyPath, replacements = {}) {
385
- try {
386
- const englishFile = path.join(this.uiLocalesDir, 'en.json');
387
- if (SecurityUtils.safeExistsSync(englishFile)) {
388
- const englishContent = SecurityUtils.safeReadFileSync(englishFile, path.dirname(englishFile), 'utf8');
389
- const englishTranslations = JSON.parse(englishContent);
431
+ getEnglishFallback(keyPath, replacements = {}) {
432
+ try {
433
+ const englishFile = path.join(this.uiLocalesDir, 'en.json');
434
+ if (SecurityUtils.safeExistsSync(englishFile, this.getValidationBase(englishFile))) {
435
+ const englishContent = SecurityUtils.safeReadFileSync(englishFile, path.dirname(englishFile), 'utf8');
436
+ const englishTranslations = JSON.parse(englishContent);
390
437
 
391
438
  // Use the same flattening approach for consistency
392
439
  const flattenedEnglish = this.flattenTranslations(englishTranslations);
@@ -523,4 +570,4 @@ this.translations = {};
523
570
  }
524
571
 
525
572
  // Export the class, not a singleton instance
526
- module.exports = UIi18n;
573
+ module.exports = UIi18n;
@@ -149,22 +149,44 @@ class AnalyzeCommand {
149
149
  }
150
150
  }
151
151
 
152
- // Get all available languages
153
- getAvailableLanguages() {
154
- try {
155
- const items = SecurityUtils.safeReaddirSync(this.sourceDir, process.cwd(), { withFileTypes: true });
156
- if (!items) {
157
- console.error('Error reading source directory: Unable to access directory');
152
+ // Get all available languages
153
+ isValidLanguageCode(code) {
154
+ if (!code || typeof code !== 'string') return false;
155
+ return /^[a-z]{2}(?:-[A-Za-z0-9]{2,8})*$/i.test(code.trim());
156
+ }
157
+
158
+ isExcludedLanguageDirectory(name) {
159
+ if (!name || typeof name !== 'string') return true;
160
+ const lowered = name.toLowerCase();
161
+ return lowered.startsWith('backup-') || lowered === 'backup' || lowered === 'reports' || lowered === 'i18ntk-reports';
162
+ }
163
+
164
+ getAvailableLanguages() {
165
+ try {
166
+ const items = SecurityUtils.safeReaddirSync(this.sourceDir, process.cwd(), { withFileTypes: true });
167
+ if (!items) {
168
+ console.error('Error reading source directory: Unable to access directory');
158
169
  return [];
159
170
  }
160
171
 
161
172
  const languages = [];
162
173
 
163
174
  // Check for directory-based structure
164
- const directories = items
165
- .filter(item => item.isDirectory())
166
- .map(item => item.name)
167
- .filter(name => name !== 'node_modules' && !name.startsWith('.') && name !== this.config.sourceLanguage);
175
+ const directories = items
176
+ .filter(item => item.isDirectory())
177
+ .map(item => item.name)
178
+ .filter(name =>
179
+ name !== 'node_modules' &&
180
+ !name.startsWith('.') &&
181
+ name !== this.config.sourceLanguage &&
182
+ !this.isExcludedLanguageDirectory(name) &&
183
+ this.isValidLanguageCode(name)
184
+ )
185
+ .filter(name => {
186
+ const dirPath = path.join(this.sourceDir, name);
187
+ const dirItems = SecurityUtils.safeReaddirSync(dirPath, process.cwd(), { withFileTypes: true }) || [];
188
+ return dirItems.some(item => item.isFile() && item.name.endsWith('.json'));
189
+ });
168
190
 
169
191
  // Check for monolith files (language.json files)
170
192
  const files = items
@@ -175,34 +197,17 @@ class AnalyzeCommand {
175
197
  languages.push(...directories);
176
198
 
177
199
  // Add monolith files as languages (without .json extension)
178
- const monolithLanguages = files
179
- .map(file => file.replace('.json', ''))
180
- .filter(lang => !languages.includes(lang) && lang !== this.config.sourceLanguage);
181
- languages.push(...monolithLanguages);
182
-
183
- // Check for nested structures
184
- for (const dir of directories) {
185
- const dirPath = path.join(this.sourceDir, dir);
186
- try {
187
- const dirItems = SecurityUtils.safeReaddirSync(dirPath, process.cwd(), { withFileTypes: true });
188
- if (dirItems) {
189
- const jsonFiles = dirItems
190
- .filter(item => item.isFile() && item.name.endsWith('.json'))
191
- .map(item => item.name.replace('.json', ''));
192
-
193
- // If directory contains JSON files, it's likely a language directory
194
- if (jsonFiles.length > 0) {
195
- if (!languages.includes(dir)) {
196
- languages.push(dir);
197
- }
198
- }
199
- }
200
- } catch (error) {
201
- // Skip directories we can't read
202
- }
203
- }
204
-
205
- return [...new Set(languages)].sort();
200
+ const monolithLanguages = files
201
+ .map(file => file.replace('.json', ''))
202
+ .filter(lang =>
203
+ !languages.includes(lang) &&
204
+ lang !== this.config.sourceLanguage &&
205
+ !this.isExcludedLanguageDirectory(lang) &&
206
+ this.isValidLanguageCode(lang)
207
+ );
208
+ languages.push(...monolithLanguages);
209
+
210
+ return [...new Set(languages)].sort();
206
211
  } catch (error) {
207
212
  console.error('Error reading source directory:', error.message);
208
213
  return [];
@@ -714,8 +719,7 @@ class AnalyzeCommand {
714
719
  throw new Error(t('analyze.failedToWriteReportFile') || 'Failed to write report file securely');
715
720
  }
716
721
 
717
- console.log(`Report saved to: ${reportPath}`);
718
- return reportPath;
722
+ return reportPath;
719
723
 
720
724
  } catch (error) {
721
725
  console.error(`Failed to save report for ${language}:`, error.message);
@@ -782,20 +786,21 @@ class AnalyzeCommand {
782
786
  console.log(t('analyze.analyzing', { language }) || `\nšŸ”„ Analyzing ${language}...`);
783
787
  }
784
788
 
785
- const analysis = this.analyzeLanguage(language);
786
- const report = this.generateLanguageReport(analysis);
787
-
788
- // Save report
789
- const reportPath = await this.saveReport(language, report);
790
-
791
- if (!args.json) {
792
- console.log(t('analyze.completed', { language }) || `āœ… Analysis completed for ${language}`);
793
- console.log(t('analyze.progress', {
794
- translated: results.length,
795
- total: languages.length
796
- }) || ` Progress: ${results.length}/${languages.length} languages processed`);
797
- console.log(t('analyze.reportSaved', { reportPath }) || ` Report saved: ${reportPath}`);
798
- }
789
+ const analysis = this.analyzeLanguage(language);
790
+ const report = this.generateLanguageReport(analysis);
791
+
792
+ // Save report
793
+ const reportPath = await this.saveReport(language, report);
794
+ const processedCount = results.length + 1;
795
+
796
+ if (!args.json) {
797
+ console.log(t('analyze.completed', { language }) || `āœ… Analysis completed for ${language}`);
798
+ console.log(t('analyze.progress', {
799
+ translated: processedCount,
800
+ total: languages.length
801
+ }) || ` Progress: ${processedCount}/${languages.length} languages processed`);
802
+ console.log(t('analyze.reportSaved', { reportPath }) || ` Report saved: ${reportPath}`);
803
+ }
799
804
 
800
805
  results.push({
801
806
  language,