i18ntk 2.0.3 → 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,44 +1,59 @@
1
- # i18ntk v2.0.3
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
- [![npm version](https://img.shields.io/npm/v/i18ntk.svg)](https://www.npmjs.com/package/i18ntk)
7
+ [![npm version](https://img.shields.io/npm/v/i18ntk.svg?color=brightgreen)](https://www.npmjs.com/package/i18ntk)
8
8
  [![npm downloads](https://img.shields.io/npm/dt/i18ntk.svg)](https://www.npmjs.com/package/i18ntk)
9
9
  [![node](https://img.shields.io/badge/node-%3E%3D16-339933)](https://nodejs.org)
10
10
  [![dependencies](https://img.shields.io/badge/dependencies-0-success)](https://www.npmjs.com/package/i18ntk)
11
11
  [![license](https://img.shields.io/badge/license-MIT-yellow.svg)](LICENSE)
12
- [![socket](https://socket.dev/api/badge/npm/package/i18ntk/2.0.3)](https://socket.dev/npm/package/i18ntk/overview/2.0.0)
12
+ [![socket](https://socket.dev/api/badge/npm/package/i18ntk/2.1.0)](https://socket.dev/npm/package/i18ntk/overview/2.1.0)
13
13
 
14
- Zero-dependency i18n toolkit for initialization, scanning, analysis, validation, and completion workflows.
14
+ ## Why i18ntk
15
15
 
16
- </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`
17
21
 
18
22
  ## Install
19
23
 
20
24
  ```bash
25
+ # global (recommended for CLI use)
21
26
  npm install -g i18ntk
27
+
28
+ # local
29
+ npm install --save-dev i18ntk
30
+
31
+ # one-off
32
+ npx i18ntk --help
22
33
  ```
23
34
 
24
35
  ## Quick Start
25
36
 
26
37
  ```bash
27
- # interactive menu
28
- i18ntk
29
-
30
- # direct workflow
38
+ # initialize locales/project settings
31
39
  i18ntk --command=init
32
- i18ntk --command=analyze --no-prompt
33
- i18ntk --command=validate --no-prompt
34
- i18ntk --command=complete --no-prompt
40
+
41
+ # analyze translation completeness
42
+ i18ntk --command=analyze
43
+
44
+ # validate translation structure/content
45
+ i18ntk --command=validate
46
+
47
+ # complete missing keys
48
+ i18ntk --command=complete
35
49
  ```
36
50
 
37
- ## v2 Command Model
51
+ ## Command Model (v2)
38
52
 
39
- Primary CLI commands:
53
+ Primary CLI:
40
54
 
41
55
  ```bash
56
+ i18ntk
42
57
  i18ntk --command=init
43
58
  i18ntk --command=analyze
44
59
  i18ntk --command=validate
@@ -50,7 +65,7 @@ i18ntk --command=summary
50
65
  i18ntk --command=debug
51
66
  ```
52
67
 
53
- Standalone binaries:
68
+ Standalone executables:
54
69
 
55
70
  ```bash
56
71
  i18ntk-init
@@ -66,19 +81,8 @@ i18ntk-fixer
66
81
  i18ntk-backup
67
82
  ```
68
83
 
69
- Backup helper:
70
-
71
- ```bash
72
- i18ntk-backup --help
73
- i18ntk-backup create ./locales
74
- i18ntk-backup list
75
- i18ntk-backup restore <backup-file>
76
- ```
77
-
78
84
  ## Common Flags
79
85
 
80
- Most commands support:
81
-
82
86
  - `--source-dir <path>`
83
87
  - `--i18n-dir <path>`
84
88
  - `--output-dir <path>`
@@ -90,13 +94,13 @@ Most commands support:
90
94
 
91
95
  ## Configuration
92
96
 
93
- i18ntk reads project settings from `.i18ntk-config` in the project root.
97
+ i18ntk reads project settings from `.i18ntk-config` in your project root.
94
98
 
95
99
  Example:
96
100
 
97
101
  ```json
98
102
  {
99
- "version": "2.0.0",
103
+ "version": "2.1.0",
100
104
  "sourceDir": "./locales",
101
105
  "i18nDir": "./locales",
102
106
  "outputDir": "./i18ntk-reports",
@@ -125,7 +129,7 @@ setLanguage('fr');
125
129
  console.log(getLanguage());
126
130
  ```
127
131
 
128
- ## Docs
132
+ ## Documentation
129
133
 
130
134
  - [Documentation Index](docs/README.md)
131
135
  - [API Reference](docs/api/API_REFERENCE.md)
@@ -133,9 +137,9 @@ console.log(getLanguage());
133
137
  - [Runtime API Guide](docs/runtime.md)
134
138
  - [Scanner Guide](docs/scanner-guide.md)
135
139
  - [Environment Variables](docs/environment-variables.md)
136
- - [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)
137
141
  - [Release Runbook](DEVUPDATE.md)
138
142
 
139
143
  ## License
140
144
 
141
- 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,