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.
- package/LICENSE +1 -1
- package/README.md +141 -1191
- package/main/i18ntk-analyze.js +65 -84
- package/main/i18ntk-backup-class.js +420 -0
- package/main/i18ntk-backup.js +3 -3
- package/main/i18ntk-complete.js +90 -65
- package/main/i18ntk-doctor.js +123 -103
- package/main/i18ntk-fixer.js +61 -725
- package/main/i18ntk-go.js +14 -15
- package/main/i18ntk-init.js +77 -26
- package/main/i18ntk-java.js +27 -32
- package/main/i18ntk-js.js +70 -68
- package/main/i18ntk-manage.js +129 -30
- package/main/i18ntk-php.js +75 -75
- package/main/i18ntk-py.js +55 -56
- package/main/i18ntk-scanner.js +59 -57
- package/main/i18ntk-setup.js +9 -404
- package/main/i18ntk-sizing.js +6 -6
- package/main/i18ntk-summary.js +21 -18
- package/main/i18ntk-ui.js +11 -10
- package/main/i18ntk-usage.js +54 -18
- package/main/i18ntk-validate.js +13 -13
- package/main/manage/commands/AnalyzeCommand.js +1124 -0
- package/main/manage/commands/BackupCommand.js +62 -0
- package/main/manage/commands/CommandRouter.js +295 -0
- package/main/manage/commands/CompleteCommand.js +61 -0
- package/main/manage/commands/DoctorCommand.js +60 -0
- package/main/manage/commands/FixerCommand.js +624 -0
- package/main/manage/commands/InitCommand.js +62 -0
- package/main/manage/commands/ScannerCommand.js +654 -0
- package/main/manage/commands/SizingCommand.js +60 -0
- package/main/manage/commands/SummaryCommand.js +61 -0
- package/main/manage/commands/UsageCommand.js +60 -0
- package/main/manage/commands/ValidateCommand.js +978 -0
- package/main/manage/index-fixed.js +1447 -0
- package/main/manage/index.js +1462 -0
- package/main/manage/managers/DebugMenu.js +140 -0
- package/main/manage/managers/InteractiveMenu.js +177 -0
- package/main/manage/managers/LanguageMenu.js +62 -0
- package/main/manage/managers/SettingsMenu.js +53 -0
- package/main/manage/services/AuthenticationService.js +263 -0
- package/main/manage/services/ConfigurationService-fixed.js +449 -0
- package/main/manage/services/ConfigurationService.js +449 -0
- package/main/manage/services/FileManagementService.js +368 -0
- package/main/manage/services/FrameworkDetectionService.js +458 -0
- package/main/manage/services/InitService.js +1051 -0
- package/main/manage/services/SetupService.js +462 -0
- package/main/manage/services/SummaryService.js +450 -0
- package/main/manage/services/UsageService.js +1502 -0
- package/package.json +32 -29
- package/runtime/enhanced.d.ts +221 -221
- package/runtime/index.d.ts +29 -29
- package/runtime/index.full.d.ts +331 -331
- package/runtime/index.js +7 -6
- package/scripts/build-lite.js +17 -17
- package/scripts/deprecate-versions.js +23 -6
- package/scripts/export-translations.js +5 -5
- package/scripts/fix-all-i18n.js +3 -3
- package/scripts/fix-and-purify-i18n.js +3 -2
- package/scripts/fix-locale-control-chars.js +110 -0
- package/scripts/lint-locales.js +80 -0
- package/scripts/locale-optimizer.js +8 -8
- package/scripts/prepublish.js +21 -21
- package/scripts/security-check.js +117 -117
- package/scripts/sync-translations.js +4 -4
- package/scripts/sync-ui-locales.js +9 -8
- package/scripts/validate-all-translations.js +8 -7
- package/scripts/verify-deprecations.js +157 -161
- package/scripts/verify-translations.js +6 -5
- package/settings/i18ntk-config.json +282 -282
- package/settings/language-config.json +5 -5
- package/settings/settings-cli.js +9 -9
- package/settings/settings-manager.js +18 -18
- package/ui-locales/de.json +2417 -2348
- package/ui-locales/en.json +2415 -2352
- package/ui-locales/es.json +2425 -2353
- package/ui-locales/fr.json +2418 -2348
- package/ui-locales/ja.json +2463 -2361
- package/ui-locales/ru.json +2463 -2359
- package/ui-locales/zh.json +2418 -2351
- package/utils/admin-auth.js +2 -2
- package/utils/admin-cli.js +297 -297
- package/utils/admin-pin.js +9 -9
- package/utils/cli-helper.js +9 -9
- package/utils/config-helper.js +73 -104
- package/utils/config-manager.js +204 -171
- package/utils/config.js +5 -4
- package/utils/env-manager.js +249 -263
- package/utils/framework-detector.js +27 -24
- package/utils/i18n-helper.js +85 -41
- package/utils/init-helper.js +152 -94
- package/utils/json-output.js +98 -98
- package/utils/mini-commander.js +179 -0
- package/utils/missing-key-validator.js +5 -5
- package/utils/plugin-loader.js +40 -29
- package/utils/prompt.js +14 -44
- package/utils/safe-json.js +40 -0
- package/utils/secure-errors.js +3 -3
- package/utils/security-check-improved.js +390 -0
- package/utils/security-config.js +5 -5
- package/utils/security-fixed.js +607 -0
- package/utils/security.js +652 -602
- package/utils/setup-enforcer.js +136 -44
- package/utils/setup-validator.js +33 -32
- package/utils/ultra-performance-optimizer.js +11 -9
- package/utils/watch-locales.js +2 -1
- package/utils/prompt-fixed.js +0 -55
- package/utils/security-check.js +0 -454
|
@@ -0,0 +1,1124 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* I18NTK ANALYZE COMMAND
|
|
5
|
+
*
|
|
6
|
+
* Handles translation analysis functionality.
|
|
7
|
+
* Contains embedded business logic from I18nAnalyzer.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const cliHelper = require('../../../utils/cli-helper');
|
|
12
|
+
const { loadTranslations, t } = require('../../../utils/i18n-helper');
|
|
13
|
+
const { getUnifiedConfig, parseCommonArgs, displayHelp } = require('../../../utils/config-helper');
|
|
14
|
+
const SecurityUtils = require('../../../utils/security');
|
|
15
|
+
const AdminCLI = require('../../../utils/admin-cli');
|
|
16
|
+
const watchLocales = require('../../../utils/watch-locales');
|
|
17
|
+
const JsonOutput = require('../../../utils/json-output');
|
|
18
|
+
|
|
19
|
+
loadTranslations('en', path.resolve(__dirname, '../../../resources', 'i18n', 'ui-locales'));
|
|
20
|
+
|
|
21
|
+
const PROJECT_ROOT = process.cwd();
|
|
22
|
+
|
|
23
|
+
class AnalyzeCommand {
|
|
24
|
+
constructor(config = {}, ui = null) {
|
|
25
|
+
this.config = config;
|
|
26
|
+
this.ui = ui;
|
|
27
|
+
this.prompt = null;
|
|
28
|
+
this.isNonInteractiveMode = false;
|
|
29
|
+
this.safeClose = null;
|
|
30
|
+
|
|
31
|
+
// Initialize analysis properties
|
|
32
|
+
this.sourceDir = null;
|
|
33
|
+
this.sourceLanguageDir = null;
|
|
34
|
+
this.outputDir = null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Set runtime dependencies for interactive operations
|
|
39
|
+
*/
|
|
40
|
+
setRuntimeDependencies(prompt, isNonInteractiveMode, safeClose) {
|
|
41
|
+
this.prompt = prompt;
|
|
42
|
+
this.isNonInteractiveMode = isNonInteractiveMode;
|
|
43
|
+
this.safeClose = safeClose;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Initialize the analyzer with configuration
|
|
48
|
+
*/
|
|
49
|
+
async initialize() {
|
|
50
|
+
try {
|
|
51
|
+
const args = this.parseArgs();
|
|
52
|
+
if (args.help) {
|
|
53
|
+
displayHelp('i18ntk-analyze', {
|
|
54
|
+
'language': 'Analyze specific language only',
|
|
55
|
+
'output-reports': 'Generate detailed reports',
|
|
56
|
+
'setup-admin': 'Configure admin PIN protection',
|
|
57
|
+
'disable-admin': 'Disable admin PIN protection',
|
|
58
|
+
'admin-status': 'Check admin PIN status',
|
|
59
|
+
'setup-wizard': 'Interactive setup wizard for analysis configuration',
|
|
60
|
+
'wizard': 'Alias for --setup-wizard',
|
|
61
|
+
'source': 'Source directory path',
|
|
62
|
+
'output': 'Output directory for reports',
|
|
63
|
+
'json': 'Output results as JSON',
|
|
64
|
+
'exclude-files': 'Comma-separated list of files to exclude',
|
|
65
|
+
'exclude': 'Pattern to exclude files'
|
|
66
|
+
});
|
|
67
|
+
process.exit(0);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Configuration is handled by getUnifiedConfig - no need for .i18ntk directory check
|
|
71
|
+
|
|
72
|
+
// Initialize i18n with UI language first
|
|
73
|
+
const baseConfig = await getUnifiedConfig('analyze', args);
|
|
74
|
+
this.config = { ...baseConfig, ...(this.config || {}) };
|
|
75
|
+
|
|
76
|
+
const uiLanguage = (this.config && this.config.uiLanguage) || 'en';
|
|
77
|
+
loadTranslations(uiLanguage, path.resolve(__dirname, '../../../resources', 'i18n', 'ui-locales'));
|
|
78
|
+
|
|
79
|
+
this.sourceDir = this.config.sourceDir;
|
|
80
|
+
this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage);
|
|
81
|
+
this.outputDir = this.config.outputDir;
|
|
82
|
+
|
|
83
|
+
// Validate source directory exists
|
|
84
|
+
const { validateSourceDir } = require('../../../utils/config-helper');
|
|
85
|
+
validateSourceDir(this.sourceDir, 'i18ntk-analyze');
|
|
86
|
+
|
|
87
|
+
} catch (error) {
|
|
88
|
+
console.error(`Fatal analysis error: ${error.message}`);
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Initialize readline interface (deprecated - use cliHelper directly)
|
|
94
|
+
initReadline() {
|
|
95
|
+
return cliHelper.getInterface();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Close readline interface (deprecated - use cliHelper.close directly)
|
|
99
|
+
closeReadline() {
|
|
100
|
+
cliHelper.close();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Prompt for user input
|
|
104
|
+
async prompt(question) {
|
|
105
|
+
return cliHelper.prompt(question);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Parse command line arguments
|
|
109
|
+
parseArgs() {
|
|
110
|
+
try {
|
|
111
|
+
const args = process.argv.slice(2);
|
|
112
|
+
const parsed = parseCommonArgs(args);
|
|
113
|
+
|
|
114
|
+
// Add script-specific arguments
|
|
115
|
+
args.forEach(arg => {
|
|
116
|
+
if (arg.startsWith('--')) {
|
|
117
|
+
const [key, value] = arg.substring(2).split('=');
|
|
118
|
+
const sanitizedKey = SecurityUtils.sanitizeInput(key);
|
|
119
|
+
const sanitizedValue = value ? SecurityUtils.sanitizeInput(value) : true;
|
|
120
|
+
|
|
121
|
+
if (sanitizedKey === 'language') {
|
|
122
|
+
parsed.language = sanitizedValue;
|
|
123
|
+
} else if (sanitizedKey === 'output-reports') {
|
|
124
|
+
parsed.outputReports = true;
|
|
125
|
+
} else if (sanitizedKey === 'setup-admin') {
|
|
126
|
+
parsed.setupAdmin = true;
|
|
127
|
+
} else if (sanitizedKey === 'disable-admin') {
|
|
128
|
+
parsed.disableAdmin = true;
|
|
129
|
+
} else if (sanitizedKey === 'admin-status') {
|
|
130
|
+
parsed.adminStatus = true;
|
|
131
|
+
} else if (sanitizedKey === 'json') {
|
|
132
|
+
parsed.json = true;
|
|
133
|
+
} else if (sanitizedKey === 'sort-keys') {
|
|
134
|
+
parsed.sortKeys = true;
|
|
135
|
+
} else if (sanitizedKey === 'indent') {
|
|
136
|
+
parsed.indent = parseInt(value) || 2;
|
|
137
|
+
} else if (sanitizedKey === 'newline') {
|
|
138
|
+
parsed.newline = value || 'lf';
|
|
139
|
+
} else if (sanitizedKey === 'setup-wizard' || sanitizedKey === 'wizard') {
|
|
140
|
+
parsed['setup-wizard'] = true;
|
|
141
|
+
parsed.wizard = true;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
return parsed;
|
|
147
|
+
} catch (error) {
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
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');
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const languages = [];
|
|
162
|
+
|
|
163
|
+
// 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);
|
|
168
|
+
|
|
169
|
+
// Check for monolith files (language.json files)
|
|
170
|
+
const files = items
|
|
171
|
+
.filter(item => item.isFile() && item.name.endsWith('.json'))
|
|
172
|
+
.map(item => item.name);
|
|
173
|
+
|
|
174
|
+
// Add directories as languages
|
|
175
|
+
languages.push(...directories);
|
|
176
|
+
|
|
177
|
+
// 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();
|
|
206
|
+
} catch (error) {
|
|
207
|
+
console.error('Error reading source directory:', error.message);
|
|
208
|
+
return [];
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Get all JSON files from a language directory
|
|
213
|
+
getLanguageFiles(language) {
|
|
214
|
+
if (!this.sourceDir) {
|
|
215
|
+
console.warn('Source directory not set');
|
|
216
|
+
return [];
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const languageDir = path.resolve(this.sourceDir, language);
|
|
220
|
+
const languageFile = path.resolve(this.sourceDir, `${language}.json`);
|
|
221
|
+
const files = [];
|
|
222
|
+
|
|
223
|
+
// Handle monolith file structure
|
|
224
|
+
const languageFileStat = SecurityUtils.safeStatSync(languageFile, this.sourceDir);
|
|
225
|
+
if (languageFileStat && languageFileStat.isFile()) {
|
|
226
|
+
return [path.basename(languageFile)];
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Handle directory-based structure
|
|
230
|
+
const languageDirStat = SecurityUtils.safeStatSync(languageDir, this.sourceDir);
|
|
231
|
+
if (languageDirStat && languageDirStat.isDirectory()) {
|
|
232
|
+
try {
|
|
233
|
+
// Ensure the path is within the source directory for security
|
|
234
|
+
const validatedPath = SecurityUtils.validatePath(languageDir, this.sourceDir);
|
|
235
|
+
if (!validatedPath) {
|
|
236
|
+
console.warn(`Language directory not found or invalid: ${languageDir}`);
|
|
237
|
+
return [];
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const findJsonFiles = (dir) => {
|
|
241
|
+
const results = [];
|
|
242
|
+
const items = SecurityUtils.safeReaddirSync(dir, this.sourceDir, { withFileTypes: true });
|
|
243
|
+
|
|
244
|
+
if (!items) return results;
|
|
245
|
+
|
|
246
|
+
for (const item of items) {
|
|
247
|
+
const fullPath = path.join(dir, item.name);
|
|
248
|
+
|
|
249
|
+
if (item.isDirectory() && !item.name.startsWith('.') && item.name !== 'node_modules') {
|
|
250
|
+
// Recursively search subdirectories
|
|
251
|
+
results.push(...findJsonFiles(fullPath));
|
|
252
|
+
} else if (item.isFile() && item.name.endsWith('.json')) {
|
|
253
|
+
// Check exclusion patterns
|
|
254
|
+
const relativePath = path.relative(this.sourceDir, fullPath);
|
|
255
|
+
const shouldExclude = (this.config.excludeFiles || []).some(pattern => {
|
|
256
|
+
if (typeof pattern === 'string') {
|
|
257
|
+
return relativePath === pattern || relativePath.endsWith(path.sep + pattern);
|
|
258
|
+
}
|
|
259
|
+
if (pattern instanceof RegExp) {
|
|
260
|
+
return pattern.test(relativePath);
|
|
261
|
+
}
|
|
262
|
+
return false;
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
if (!shouldExclude && !item.name.startsWith('.')) {
|
|
266
|
+
results.push(path.relative(languageDir, fullPath));
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return results;
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
return findJsonFiles(validatedPath);
|
|
275
|
+
} catch (error) {
|
|
276
|
+
console.error(`Error reading language directory ${languageDir}:`, error.message);
|
|
277
|
+
return [];
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Check for namespace-based structure (language.json files in any subdirectory)
|
|
282
|
+
const findNamespaceFiles = () => {
|
|
283
|
+
const results = [];
|
|
284
|
+
const searchDir = this.sourceDir;
|
|
285
|
+
|
|
286
|
+
try {
|
|
287
|
+
const searchDirExists = SecurityUtils.safeExistsSync(searchDir, this.sourceDir);
|
|
288
|
+
if (searchDirExists) {
|
|
289
|
+
const items = SecurityUtils.safeReaddirSync(searchDir, this.sourceDir, { withFileTypes: true });
|
|
290
|
+
|
|
291
|
+
if (items) {
|
|
292
|
+
for (const item of items) {
|
|
293
|
+
if (item.isDirectory() && !item.name.startsWith('.') && item.name !== 'node_modules') {
|
|
294
|
+
const namespaceDir = path.join(searchDir, item.name);
|
|
295
|
+
const namespaceFile = path.join(namespaceDir, `${language}.json`);
|
|
296
|
+
|
|
297
|
+
const namespaceFileExists = SecurityUtils.safeExistsSync(namespaceFile, this.sourceDir);
|
|
298
|
+
if (namespaceFileExists) {
|
|
299
|
+
results.push(path.relative(path.join(this.sourceDir, item.name), namespaceFile));
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
} catch (error) {
|
|
306
|
+
console.warn(`Error searching for namespace files: ${error.message}`);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return results;
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
const namespaceFiles = findNamespaceFiles();
|
|
313
|
+
if (namespaceFiles.length > 0) {
|
|
314
|
+
return namespaceFiles;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return files;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Get all keys recursively from an object
|
|
321
|
+
getAllKeys(obj, prefix = '') {
|
|
322
|
+
const keys = new Set();
|
|
323
|
+
|
|
324
|
+
if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {
|
|
325
|
+
// Log a warning instead of crashing
|
|
326
|
+
console.warn(`⚠️ Skipping invalid translation object at prefix '${prefix}'`);
|
|
327
|
+
return keys;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
331
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
332
|
+
keys.add(fullKey);
|
|
333
|
+
|
|
334
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
335
|
+
const nestedKeys = this.getAllKeys(value, fullKey);
|
|
336
|
+
nestedKeys.forEach(k => keys.add(k));
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return keys;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Get value by key path
|
|
344
|
+
getValueByPath(obj, keyPath) {
|
|
345
|
+
// Ensure keyPath is a string
|
|
346
|
+
const keyPathStr = String(keyPath || '');
|
|
347
|
+
const keys = keyPathStr.split('.');
|
|
348
|
+
let current = obj;
|
|
349
|
+
|
|
350
|
+
for (const key of keys) {
|
|
351
|
+
if (current && typeof current === 'object' && key in current) {
|
|
352
|
+
current = current[key];
|
|
353
|
+
} else {
|
|
354
|
+
return undefined;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return current;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Analyze translation issues in an object
|
|
362
|
+
analyzeTranslationIssues(obj, sourceObj = null, prefix = '') {
|
|
363
|
+
const issues = [];
|
|
364
|
+
|
|
365
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
366
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
367
|
+
const sourceValue = sourceObj ? this.getValueByPath(sourceObj, fullKey) : null;
|
|
368
|
+
|
|
369
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
370
|
+
issues.push(...this.analyzeTranslationIssues(value, sourceObj, fullKey));
|
|
371
|
+
} else if (typeof value === 'string') {
|
|
372
|
+
const markers = this.config.notTranslatedMarkers || [this.config.notTranslatedMarker];
|
|
373
|
+
if (markers.some(m => value === m)) {
|
|
374
|
+
issues.push({
|
|
375
|
+
type: 'not_translated',
|
|
376
|
+
key: fullKey,
|
|
377
|
+
value,
|
|
378
|
+
sourceValue: sourceValue || 'N/A'
|
|
379
|
+
});
|
|
380
|
+
} else if (value === '') {
|
|
381
|
+
issues.push({
|
|
382
|
+
type: 'empty_value',
|
|
383
|
+
key: fullKey,
|
|
384
|
+
value,
|
|
385
|
+
sourceValue: sourceValue || 'N/A'
|
|
386
|
+
});
|
|
387
|
+
} else if (markers.some(m => value.includes(m))) {
|
|
388
|
+
issues.push({
|
|
389
|
+
type: 'partial_translation',
|
|
390
|
+
key: fullKey,
|
|
391
|
+
value,
|
|
392
|
+
sourceValue: sourceValue || 'N/A'
|
|
393
|
+
});
|
|
394
|
+
} else if (sourceValue && value === sourceValue) {
|
|
395
|
+
issues.push({
|
|
396
|
+
type: 'same_as_source',
|
|
397
|
+
key: fullKey,
|
|
398
|
+
value,
|
|
399
|
+
sourceValue
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return issues;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Get translation statistics for an object
|
|
409
|
+
getTranslationStats(obj) {
|
|
410
|
+
let total = 0;
|
|
411
|
+
let translated = 0;
|
|
412
|
+
let notTranslated = 0;
|
|
413
|
+
let empty = 0;
|
|
414
|
+
let partial = 0;
|
|
415
|
+
|
|
416
|
+
const markers = this.config.notTranslatedMarkers || [this.config.notTranslatedMarker];
|
|
417
|
+
const count = (item) => {
|
|
418
|
+
if (typeof item === 'string') {
|
|
419
|
+
total++;
|
|
420
|
+
if (markers.some(m => item === m)) {
|
|
421
|
+
notTranslated++;
|
|
422
|
+
} else if (item === '') {
|
|
423
|
+
empty++;
|
|
424
|
+
} else if (markers.some(m => item.includes(m))) {
|
|
425
|
+
partial++;
|
|
426
|
+
} else {
|
|
427
|
+
translated++;
|
|
428
|
+
}
|
|
429
|
+
} else if (Array.isArray(item)) {
|
|
430
|
+
item.forEach(count);
|
|
431
|
+
} else if (item && typeof item === 'object') {
|
|
432
|
+
Object.values(item).forEach(count);
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
count(obj);
|
|
437
|
+
|
|
438
|
+
return {
|
|
439
|
+
total,
|
|
440
|
+
translated,
|
|
441
|
+
notTranslated,
|
|
442
|
+
empty,
|
|
443
|
+
partial,
|
|
444
|
+
percentage: total > 0 ? Math.round((translated / total) * 100) : 0,
|
|
445
|
+
missing: notTranslated + empty + partial
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Check structural consistency between source and target
|
|
450
|
+
checkStructuralConsistency(sourceObj, targetObj) {
|
|
451
|
+
const sourceKeys = this.getAllKeys(sourceObj);
|
|
452
|
+
const targetKeys = this.getAllKeys(targetObj);
|
|
453
|
+
|
|
454
|
+
const missingKeys = [...sourceKeys].filter(key => !targetKeys.has(key));
|
|
455
|
+
const extraKeys = [...targetKeys].filter(key => !sourceKeys.has(key));
|
|
456
|
+
|
|
457
|
+
return {
|
|
458
|
+
isConsistent: missingKeys.length === 0 && extraKeys.length === 0,
|
|
459
|
+
missingKeys,
|
|
460
|
+
extraKeys,
|
|
461
|
+
sourceKeyCount: sourceKeys.size,
|
|
462
|
+
targetKeyCount: targetKeys.size
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Analyze a single language
|
|
467
|
+
analyzeLanguage(language) {
|
|
468
|
+
const languageDir = path.join(this.sourceDir, language);
|
|
469
|
+
const sourceFiles = this.getLanguageFiles(this.config.sourceLanguage);
|
|
470
|
+
const targetFiles = this.getLanguageFiles(language);
|
|
471
|
+
|
|
472
|
+
const analysis = {
|
|
473
|
+
language,
|
|
474
|
+
files: {},
|
|
475
|
+
summary: {
|
|
476
|
+
totalFiles: sourceFiles.length,
|
|
477
|
+
analyzedFiles: 0,
|
|
478
|
+
totalKeys: 0,
|
|
479
|
+
translatedKeys: 0,
|
|
480
|
+
missingKeys: 0,
|
|
481
|
+
issues: []
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
for (const fileName of sourceFiles) {
|
|
486
|
+
const sourceFilePath = path.join(this.config.sourceLanguage, fileName);
|
|
487
|
+
const targetFilePath = path.join(language, fileName);
|
|
488
|
+
|
|
489
|
+
const sourceFullPath = path.join(this.sourceDir, sourceFilePath);
|
|
490
|
+
const targetFullPath = path.join(this.sourceDir, targetFilePath);
|
|
491
|
+
|
|
492
|
+
const sourceExists = SecurityUtils.safeExistsSync(sourceFullPath, this.sourceDir);
|
|
493
|
+
if (!sourceExists) {
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
let sourceContent, targetContent;
|
|
498
|
+
|
|
499
|
+
try {
|
|
500
|
+
const sourceFileContent = SecurityUtils.safeReadFileSync(sourceFullPath, this.sourceDir, 'utf8');
|
|
501
|
+
if (!sourceFileContent) {
|
|
502
|
+
analysis.files[fileName] = {
|
|
503
|
+
error: `Failed to read source file: File not accessible or empty`
|
|
504
|
+
};
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
sourceContent = SecurityUtils.safeParseJSON(sourceFileContent);
|
|
508
|
+
if (!sourceContent) {
|
|
509
|
+
analysis.files[fileName] = {
|
|
510
|
+
error: `Failed to parse source file: Invalid JSON format`
|
|
511
|
+
};
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
} catch (error) {
|
|
515
|
+
analysis.files[fileName] = {
|
|
516
|
+
error: `Failed to parse source file: ${error.message}`
|
|
517
|
+
};
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const targetExists = SecurityUtils.safeExistsSync(targetFullPath, this.sourceDir);
|
|
522
|
+
if (!targetExists) {
|
|
523
|
+
analysis.files[fileName] = {
|
|
524
|
+
status: 'missing',
|
|
525
|
+
sourceKeys: this.getAllKeys(sourceContent).size
|
|
526
|
+
};
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
try {
|
|
531
|
+
const targetFileContent = SecurityUtils.safeReadFileSync(targetFullPath, this.sourceDir, 'utf8');
|
|
532
|
+
if (!targetFileContent) {
|
|
533
|
+
analysis.files[fileName] = {
|
|
534
|
+
error: `Failed to read target file: File not accessible or empty`
|
|
535
|
+
};
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const parsed = SecurityUtils.safeParseJSON(targetFileContent);
|
|
540
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
541
|
+
analysis.files[fileName] = {
|
|
542
|
+
error: `Invalid structure in target file: must be a plain object (not array/null/type)`
|
|
543
|
+
};
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
targetContent = parsed;
|
|
548
|
+
|
|
549
|
+
} catch (error) {
|
|
550
|
+
analysis.files[fileName] = {
|
|
551
|
+
error: `Failed to parse target file: ${error.message}`
|
|
552
|
+
};
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Analyze this file
|
|
557
|
+
const stats = this.getTranslationStats(targetContent);
|
|
558
|
+
const structural = this.checkStructuralConsistency(sourceContent, targetContent);
|
|
559
|
+
const issues = this.analyzeTranslationIssues(targetContent, sourceContent);
|
|
560
|
+
|
|
561
|
+
analysis.files[fileName] = {
|
|
562
|
+
status: 'analyzed',
|
|
563
|
+
stats,
|
|
564
|
+
structural,
|
|
565
|
+
issues,
|
|
566
|
+
sourceFilePath,
|
|
567
|
+
targetFilePath
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
// Update summary
|
|
571
|
+
analysis.summary.analyzedFiles++;
|
|
572
|
+
analysis.summary.totalKeys += stats.total;
|
|
573
|
+
analysis.summary.translatedKeys += stats.translated;
|
|
574
|
+
analysis.summary.missingKeys += stats.missing;
|
|
575
|
+
analysis.summary.issues.push(...issues);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Calculate overall percentage
|
|
579
|
+
analysis.summary.percentage = analysis.summary.totalKeys > 0
|
|
580
|
+
? Math.round((analysis.summary.translatedKeys / analysis.summary.totalKeys) * 100)
|
|
581
|
+
: 0;
|
|
582
|
+
|
|
583
|
+
return analysis;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// Generate detailed report for a language
|
|
587
|
+
generateLanguageReport(analysis) {
|
|
588
|
+
const { language } = analysis;
|
|
589
|
+
const timestamp = new Date().toISOString();
|
|
590
|
+
|
|
591
|
+
let report = `${t('analyze.reportTitle', { language: language.toUpperCase() })}
|
|
592
|
+
`;
|
|
593
|
+
report += `${t('analyze.generated', { timestamp })}
|
|
594
|
+
`;
|
|
595
|
+
report += `${t('analyze.status', { translated: analysis.summary.translatedKeys, total: analysis.summary.totalKeys, percentage: analysis.summary.percentage })}
|
|
596
|
+
`;
|
|
597
|
+
report += `${t('analyze.filesAnalyzed', { analyzed: analysis.summary.analyzedFiles, total: analysis.summary.totalFiles })}
|
|
598
|
+
`;
|
|
599
|
+
report += `${t('analyze.keysNeedingTranslation', { count: analysis.summary.missingKeys })}
|
|
600
|
+
|
|
601
|
+
`;
|
|
602
|
+
|
|
603
|
+
report += `${t('analyze.fileBreakdown')}
|
|
604
|
+
`;
|
|
605
|
+
report += `${'='.repeat(50)}\n\n`;
|
|
606
|
+
|
|
607
|
+
Object.entries(analysis.files).forEach(([fileName, fileData]) => {
|
|
608
|
+
report += `\uD83D\uDCC4 ${fileName}\n`;
|
|
609
|
+
|
|
610
|
+
if (fileData.error) {
|
|
611
|
+
report += ` \u274C ${t('analyze.error')}: ${fileData.error}\n\n`;
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
if (fileData.status === 'missing') {
|
|
616
|
+
report += ` \u274C ${t('analyze.statusFileMissing')}\n`;
|
|
617
|
+
report += ` 📊 ${t('analyze.sourceKeys', { count: fileData.sourceKeys })}\n\n`;
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const { stats, structural, issues } = fileData;
|
|
622
|
+
|
|
623
|
+
report += ` \uD83D\uDCCA ${t('analyze.translation', { translated: stats.translated, total: stats.total, percentage: stats.percentage })}\n`;
|
|
624
|
+
report += ` \uD83C\uDFD7️ ${t('analyze.structure', { status: structural.isConsistent ? t('analyze.consistent') : t('analyze.inconsistent') })}\n`;
|
|
625
|
+
|
|
626
|
+
if (!structural.isConsistent) {
|
|
627
|
+
if (structural.missingKeys.length > 0) {
|
|
628
|
+
report += ` ${t('analyze.missingKeys', { count: structural.missingKeys.length })}\n`;
|
|
629
|
+
}
|
|
630
|
+
if (structural.extraKeys.length > 0) {
|
|
631
|
+
report += ` ${t('analyze.extraKeys', { count: structural.extraKeys.length })}\n`;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
if (issues.length > 0) {
|
|
636
|
+
report += ` ⚠️ ${t('analyze.issues', { count: issues.length })}\n`;
|
|
637
|
+
|
|
638
|
+
const issueTypes = {
|
|
639
|
+
not_translated: issues.filter(i => i.type === 'not_translated').length,
|
|
640
|
+
empty_value: issues.filter(i => i.type === 'empty_value').length,
|
|
641
|
+
partial_translation: issues.filter(i => i.type === 'partial_translation').length,
|
|
642
|
+
same_as_source: issues.filter(i => i.type === 'same_as_source').length
|
|
643
|
+
};
|
|
644
|
+
|
|
645
|
+
Object.entries(issueTypes).forEach(([type, count]) => {
|
|
646
|
+
if (count > 0) {
|
|
647
|
+
report += ` ${t('analyze.issueType.' + type, { count })}\n`;
|
|
648
|
+
}
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
report += `\n`;
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
// Keys needing translation
|
|
656
|
+
const notTranslatedIssues = analysis.summary.issues.filter(issue =>
|
|
657
|
+
issue.type === 'not_translated' || issue.type === 'empty_value'
|
|
658
|
+
);
|
|
659
|
+
|
|
660
|
+
if (notTranslatedIssues.length > 0) {
|
|
661
|
+
report += `${t('analyze.keysToTranslate')}\n`;
|
|
662
|
+
report += `${'='.repeat(50)}\n\n`;
|
|
663
|
+
|
|
664
|
+
notTranslatedIssues.slice(0, 50).forEach(issue => {
|
|
665
|
+
report += `${t('analyze.key')}: ${issue.key}\n`;
|
|
666
|
+
report += `${t('analyze.english')}: "${issue.sourceValue}"\n`;
|
|
667
|
+
report += `${language}: [${t('analyze.needsTranslation')}]\n\n`;
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
if (notTranslatedIssues.length > 50) {
|
|
671
|
+
report += `${t('analyze.andMoreKeys', { count: notTranslatedIssues.length - 50 })}\n\n`;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
return report;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// Save analysis report to a file
|
|
679
|
+
async saveReport(language, report) {
|
|
680
|
+
try {
|
|
681
|
+
// Ensure we have a valid output directory
|
|
682
|
+
if (!this.outputDir) {
|
|
683
|
+
this.outputDir = path.join(process.cwd(), 'i18n-reports');
|
|
684
|
+
console.warn(`No output directory specified, using default: ${this.outputDir}`);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// Ensure the output directory exists
|
|
688
|
+
const dirCreated = SecurityUtils.safeMkdirSync(this.outputDir, process.cwd(), { recursive: true });
|
|
689
|
+
if (!dirCreated) {
|
|
690
|
+
console.error(`Failed to create output directory ${this.outputDir}`);
|
|
691
|
+
return null;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// Validate the output directory is within the project
|
|
695
|
+
const validatedOutputDir = SecurityUtils.validatePath(this.outputDir, process.cwd());
|
|
696
|
+
if (!validatedOutputDir) {
|
|
697
|
+
console.error(`Invalid output directory: ${this.outputDir} is outside project root`);
|
|
698
|
+
return null;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// Create a safe filename
|
|
702
|
+
const safeLanguage = language.replace(/[^\w-]/g, '_');
|
|
703
|
+
const reportPath = path.resolve(validatedOutputDir, `translation-report-${safeLanguage}.json`);
|
|
704
|
+
|
|
705
|
+
// Ensure the final path is still within the output directory
|
|
706
|
+
if (!reportPath.startsWith(validatedOutputDir)) {
|
|
707
|
+
console.error('Invalid report path detected, potential directory traversal attack');
|
|
708
|
+
return null;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// Use safeWriteFile for secure file writing
|
|
712
|
+
const success = await SecurityUtils.safeWriteFile(reportPath, JSON.stringify(report, null, 2), process.cwd(), 'utf8');
|
|
713
|
+
if (!success) {
|
|
714
|
+
throw new Error(t('analyze.failedToWriteReportFile') || 'Failed to write report file securely');
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
console.log(`Report saved to: ${reportPath}`);
|
|
718
|
+
return reportPath;
|
|
719
|
+
|
|
720
|
+
} catch (error) {
|
|
721
|
+
console.error(`Failed to save report for ${language}:`, error.message);
|
|
722
|
+
return null;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Show help message
|
|
727
|
+
showHelp() {
|
|
728
|
+
console.log(t('analyze.help_message'));
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// Main analyze method
|
|
732
|
+
async analyze() {
|
|
733
|
+
try {
|
|
734
|
+
const results = [];
|
|
735
|
+
const args = this.parseArgs();
|
|
736
|
+
const jsonOutput = new JsonOutput('analyze');
|
|
737
|
+
|
|
738
|
+
if (!args.json) {
|
|
739
|
+
console.log(t('analyze.starting') || '🔍 Starting translation analysis...');
|
|
740
|
+
console.log(t('analyze.sourceDirectoryLabel', { sourceDir: path.resolve(this.sourceDir) }));
|
|
741
|
+
console.log(t('analyze.sourceLanguageLabel', { sourceLanguage: this.config.sourceLanguage }));
|
|
742
|
+
console.log(t('analyze.strictModeLabel', { mode: this.config.processing?.strictMode || this.config.strictMode ? 'ON' : 'OFF' }));
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// Ensure output directory exists
|
|
746
|
+
const outputDirExists = SecurityUtils.safeExistsSync(this.outputDir, process.cwd());
|
|
747
|
+
if (!outputDirExists) {
|
|
748
|
+
SecurityUtils.safeMkdirSync(this.outputDir, process.cwd(), { recursive: true });
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
const languages = this.getAvailableLanguages();
|
|
752
|
+
|
|
753
|
+
if (languages.length === 0) {
|
|
754
|
+
const error = t('analyze.noLanguages') || '⚠️ No target languages found.';
|
|
755
|
+
const guidance = this.provideSetupGuidance();
|
|
756
|
+
|
|
757
|
+
if (args.json) {
|
|
758
|
+
jsonOutput.setStatus('error', error);
|
|
759
|
+
jsonOutput.setOutput({
|
|
760
|
+
error,
|
|
761
|
+
guidance,
|
|
762
|
+
structure: this.detectStructureType()
|
|
763
|
+
});
|
|
764
|
+
console.log(JSON.stringify(jsonOutput.data, null, args.indent || 2));
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
console.log(error);
|
|
768
|
+
console.log('\n' + guidance);
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
if (!args.json) {
|
|
773
|
+
console.log(t('analyze.foundLanguages', { count: languages.length, languages: languages.join(', ') }) || `📋 Found ${languages.length} languages to analyze: ${languages.join(', ')}`);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
let totalMissing = 0;
|
|
777
|
+
let totalExtra = 0;
|
|
778
|
+
let totalFiles = 0;
|
|
779
|
+
|
|
780
|
+
for (const language of languages) {
|
|
781
|
+
if (!args.json) {
|
|
782
|
+
console.log(t('analyze.analyzing', { language }) || `\n🔄 Analyzing ${language}...`);
|
|
783
|
+
}
|
|
784
|
+
|
|
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
|
+
}
|
|
799
|
+
|
|
800
|
+
results.push({
|
|
801
|
+
language,
|
|
802
|
+
analysis,
|
|
803
|
+
reportPath
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
// Add issues to JSON output
|
|
807
|
+
Object.values(analysis.files).forEach(fileData => {
|
|
808
|
+
if (fileData.structural) {
|
|
809
|
+
fileData.structural.missingKeys?.forEach(key => {
|
|
810
|
+
jsonOutput.addIssue('missing', key, language);
|
|
811
|
+
totalMissing++;
|
|
812
|
+
});
|
|
813
|
+
fileData.structural.extraKeys?.forEach(key => {
|
|
814
|
+
jsonOutput.addIssue('extra', key, language);
|
|
815
|
+
totalExtra++;
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
});
|
|
819
|
+
totalFiles += analysis.summary.analyzedFiles;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// Set JSON output
|
|
823
|
+
jsonOutput.setStats({
|
|
824
|
+
missing: totalMissing,
|
|
825
|
+
extra: totalExtra,
|
|
826
|
+
files: totalFiles,
|
|
827
|
+
languages: languages.length
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
if (totalMissing > 0 || totalExtra > 0) {
|
|
831
|
+
jsonOutput.setStatus('warn');
|
|
832
|
+
} else {
|
|
833
|
+
jsonOutput.setStatus('ok');
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
if (args.json) {
|
|
837
|
+
jsonOutput.output();
|
|
838
|
+
return results;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// Summary
|
|
842
|
+
console.log(t('analyze.summary') || '\n📊 ANALYSIS SUMMARY');
|
|
843
|
+
console.log('='.repeat(50));
|
|
844
|
+
|
|
845
|
+
results.forEach(({ language, analysis }) => {
|
|
846
|
+
console.log(t('analyze.languageStats', {
|
|
847
|
+
language,
|
|
848
|
+
percentage: analysis.summary.percentage,
|
|
849
|
+
translated: analysis.summary.translatedKeys,
|
|
850
|
+
total: analysis.summary.totalKeys
|
|
851
|
+
}) || `${language}: ${analysis.summary.percentage}% complete (${analysis.summary.translatedKeys}/${analysis.summary.totalKeys} keys)`);
|
|
852
|
+
});
|
|
853
|
+
|
|
854
|
+
console.log(t('analyze.finished') || '\n✅ Analysis completed successfully!');
|
|
855
|
+
|
|
856
|
+
// Only prompt for input if running standalone and not in no-prompt mode
|
|
857
|
+
if (require.main === module && !this.noPrompt) {
|
|
858
|
+
await this.prompt('\nPress Enter to continue...');
|
|
859
|
+
}
|
|
860
|
+
this.closeReadline();
|
|
861
|
+
|
|
862
|
+
return results;
|
|
863
|
+
|
|
864
|
+
} catch (error) {
|
|
865
|
+
console.error(t('analyze.error') || '❌ Analysis failed:', error.message);
|
|
866
|
+
this.closeReadline();
|
|
867
|
+
throw error;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// Main analysis process
|
|
872
|
+
async run(options = {}) {
|
|
873
|
+
const fromMenu = options.fromMenu || false;
|
|
874
|
+
|
|
875
|
+
try {
|
|
876
|
+
const args = this.parseArgs();
|
|
877
|
+
|
|
878
|
+
if (args.help) {
|
|
879
|
+
this.showHelp();
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// Handle setup wizard
|
|
884
|
+
if (args['setup-wizard'] || args.wizard) {
|
|
885
|
+
return await this.runSetupWizard();
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// Initialize configuration properly when called from menu
|
|
889
|
+
if (fromMenu && !this.sourceDir) {
|
|
890
|
+
const baseConfig = await getUnifiedConfig('analyze', args);
|
|
891
|
+
this.config = { ...baseConfig, ...this.config };
|
|
892
|
+
|
|
893
|
+
const uiLanguage = this.config.uiLanguage || 'en';
|
|
894
|
+
loadTranslations(uiLanguage, path.resolve(__dirname, '../../../resources', 'i18n', 'ui-locales'));
|
|
895
|
+
|
|
896
|
+
this.sourceDir = this.config.sourceDir;
|
|
897
|
+
this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage);
|
|
898
|
+
this.outputDir = this.config.outputDir;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// Skip admin authentication when called from menu system (i18ntk-manage.js) or when --no-prompt is used
|
|
902
|
+
// Authentication is handled by the menu system
|
|
903
|
+
const isCalledDirectly = require.main === module;
|
|
904
|
+
if (isCalledDirectly && !args.noPrompt && !fromMenu) {
|
|
905
|
+
// Only check admin authentication when running directly and not in no-prompt mode
|
|
906
|
+
const AdminAuth = require('../../../utils/admin-auth');
|
|
907
|
+
const adminAuth = new AdminAuth();
|
|
908
|
+
await adminAuth.initialize();
|
|
909
|
+
|
|
910
|
+
const isRequired = await adminAuth.isAuthRequired();
|
|
911
|
+
if (isRequired) {
|
|
912
|
+
console.log('\n' + t('adminCli.authRequiredForOperation', { operation: 'analyze translations' }));
|
|
913
|
+
const cliHelper = require('../../../utils/cli-helper');
|
|
914
|
+
const pin = await cliHelper.promptPin(t('adminCli.enterPin'));
|
|
915
|
+
const isValid = await this.adminAuth.verifyPin(pin);
|
|
916
|
+
|
|
917
|
+
if (!isValid) {
|
|
918
|
+
console.log(t('adminCli.invalidPin'));
|
|
919
|
+
this.closeReadline();
|
|
920
|
+
if (!fromMenu) process.exit(1);
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
console.log(t('adminCli.authenticationSuccess'));
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
// Set noPrompt flag - skip prompts when called from menu
|
|
929
|
+
this.noPrompt = args.noPrompt || fromMenu;
|
|
930
|
+
|
|
931
|
+
// Handle UI language change
|
|
932
|
+
if (args.uiLanguage) {
|
|
933
|
+
loadTranslations(args.uiLanguage, path.resolve(__dirname, '../../../ui-locales'));}
|
|
934
|
+
|
|
935
|
+
// Update config if source directory is provided
|
|
936
|
+
if (args.sourceDir) {
|
|
937
|
+
this.config.sourceDir = args.sourceDir;
|
|
938
|
+
this.sourceDir = path.resolve(PROJECT_ROOT, this.config.sourceDir);
|
|
939
|
+
this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage);
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
if (args.outputDir) {
|
|
943
|
+
this.config.outputDir = args.outputDir;
|
|
944
|
+
this.outputDir = path.resolve(this.config.outputDir);
|
|
945
|
+
}
|
|
946
|
+
const execute = async () => {
|
|
947
|
+
await this.analyze();
|
|
948
|
+
};
|
|
949
|
+
|
|
950
|
+
if (args.watch) {
|
|
951
|
+
await execute();
|
|
952
|
+
let running = false;
|
|
953
|
+
watchLocales(this.sourceDir, async () => {
|
|
954
|
+
if (running) return;
|
|
955
|
+
running = true;
|
|
956
|
+
try {
|
|
957
|
+
await execute();
|
|
958
|
+
} finally {
|
|
959
|
+
running = false;
|
|
960
|
+
}
|
|
961
|
+
});
|
|
962
|
+
console.log('��� Watching for translation changes. Press Ctrl+C to exit.');
|
|
963
|
+
} else {
|
|
964
|
+
await execute();
|
|
965
|
+
if (!fromMenu && require.main === module) {
|
|
966
|
+
process.exit(0);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
} catch (error) {
|
|
970
|
+
console.error(t('analyze.error') || '❌ Analysis failed:', error.message);
|
|
971
|
+
this.closeReadline();
|
|
972
|
+
if (!fromMenu && require.main === module) {
|
|
973
|
+
process.exit(1);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
async runSetupWizard() {
|
|
979
|
+
console.log('Translation Analysis Setup Wizard');
|
|
980
|
+
console.log('='.repeat(50));
|
|
981
|
+
|
|
982
|
+
try {
|
|
983
|
+
const structure = this.detectStructureType();
|
|
984
|
+
console.log(`Current structure detected: ${structure.type}`);
|
|
985
|
+
|
|
986
|
+
const structureOptions = ['monolith', 'directory', 'namespace', 'mixed'];
|
|
987
|
+
const defaultStructureIndex = structureOptions.indexOf(structure.type);
|
|
988
|
+
const defaultStructureChoice = defaultStructureIndex >= 0 ? String(defaultStructureIndex + 1) : '4';
|
|
989
|
+
|
|
990
|
+
console.log('Choose your translation file structure:');
|
|
991
|
+
console.log('1) Monolith files (en.json, de.json, etc.)');
|
|
992
|
+
console.log('2) Directory structure (en/common.json, de/common.json)');
|
|
993
|
+
console.log('3) Namespace structure (common/en.json, forms/en.json)');
|
|
994
|
+
console.log('4) Mixed structure (auto-detect)');
|
|
995
|
+
const structureChoiceInput = await this.prompt(`Select option [${defaultStructureChoice}]: `);
|
|
996
|
+
const structureChoice = structureChoiceInput.trim() || defaultStructureChoice;
|
|
997
|
+
const structureType = structureOptions[Number(structureChoice) - 1] || 'mixed';
|
|
998
|
+
|
|
999
|
+
const sourceDirInput = await this.prompt(`Enter source directory path [${this.sourceDir}]: `);
|
|
1000
|
+
const sourceDir = sourceDirInput.trim() || this.sourceDir;
|
|
1001
|
+
if (!SecurityUtils.safeExistsSync(sourceDir, process.cwd())) {
|
|
1002
|
+
console.log('Setup cancelled: directory does not exist.');
|
|
1003
|
+
return { success: false, cancelled: true };
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
const languagesInput = await this.prompt('Enter languages to analyze (comma-separated) [de,fr,es,ja,ru]: ');
|
|
1007
|
+
const languagesValue = languagesInput.trim() || 'de,fr,es,ja,ru';
|
|
1008
|
+
const languages = languagesValue.split(',').map(lang => lang.trim()).filter(Boolean);
|
|
1009
|
+
if (languages.length === 0) {
|
|
1010
|
+
console.log('Setup cancelled: no languages provided.');
|
|
1011
|
+
return { success: false, cancelled: true };
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
const outputReportsInput = await this.prompt('Generate detailed reports for each language? (Y/n): ');
|
|
1015
|
+
const outputReports = !['n', 'no'].includes(outputReportsInput.trim().toLowerCase());
|
|
1016
|
+
|
|
1017
|
+
const outputDirInput = await this.prompt(`Enter output directory for reports [${this.outputDir}]: `);
|
|
1018
|
+
const outputDir = outputDirInput.trim() || this.outputDir;
|
|
1019
|
+
|
|
1020
|
+
const response = {
|
|
1021
|
+
structureType,
|
|
1022
|
+
sourceDir,
|
|
1023
|
+
languages: languagesValue,
|
|
1024
|
+
outputReports,
|
|
1025
|
+
outputDir
|
|
1026
|
+
};
|
|
1027
|
+
|
|
1028
|
+
this.sourceDir = path.resolve(response.sourceDir);
|
|
1029
|
+
this.outputDir = path.resolve(response.outputDir);
|
|
1030
|
+
this.outputReports = response.outputReports;
|
|
1031
|
+
|
|
1032
|
+
console.log('\nConfiguration Summary:');
|
|
1033
|
+
console.log(`Source: ${this.sourceDir}`);
|
|
1034
|
+
console.log(`Output: ${this.outputDir}`);
|
|
1035
|
+
console.log(`Languages: ${languages.join(", ")}`);
|
|
1036
|
+
console.log(`Structure: ${response.structureType}`);
|
|
1037
|
+
|
|
1038
|
+
const proceedInput = await this.prompt('Proceed with analysis? (Y/n): ');
|
|
1039
|
+
const proceed = !['n', 'no'].includes(proceedInput.trim().toLowerCase());
|
|
1040
|
+
if (!proceed) {
|
|
1041
|
+
console.log('Setup cancelled.');
|
|
1042
|
+
return { success: false, cancelled: true };
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
const results = [];
|
|
1046
|
+
for (const language of languages) {
|
|
1047
|
+
try {
|
|
1048
|
+
console.log(`\nAnalyzing ${language}...`);
|
|
1049
|
+
const result = await this.analyzeLanguage(language);
|
|
1050
|
+
results.push({ language, ...result });
|
|
1051
|
+
|
|
1052
|
+
if (this.outputReports) {
|
|
1053
|
+
await this.saveReport(language, result);
|
|
1054
|
+
console.log(`Report saved: ${language}.json`);
|
|
1055
|
+
}
|
|
1056
|
+
} catch (error) {
|
|
1057
|
+
console.error(`Error analyzing ${language}:`, error.message);
|
|
1058
|
+
results.push({ language, error: error.message });
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
const summary = {
|
|
1063
|
+
totalLanguages: results.length,
|
|
1064
|
+
successful: results.filter(r => !r.error).length,
|
|
1065
|
+
failed: results.filter(r => r.error).length,
|
|
1066
|
+
results
|
|
1067
|
+
};
|
|
1068
|
+
|
|
1069
|
+
await this.saveReport('wizard-summary', {
|
|
1070
|
+
summary,
|
|
1071
|
+
configuration: response,
|
|
1072
|
+
timestamp: new Date().toISOString()
|
|
1073
|
+
});
|
|
1074
|
+
|
|
1075
|
+
console.log('\nSetup complete.');
|
|
1076
|
+
console.log(`Analyzed ${summary.successful}/${summary.totalLanguages} languages successfully`);
|
|
1077
|
+
|
|
1078
|
+
return {
|
|
1079
|
+
success: true,
|
|
1080
|
+
summary,
|
|
1081
|
+
configuration: response
|
|
1082
|
+
};
|
|
1083
|
+
|
|
1084
|
+
} catch (error) {
|
|
1085
|
+
console.error('Setup wizard error:', error.message);
|
|
1086
|
+
return { success: false, error: error.message };
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
/**
|
|
1091
|
+
* Execute the analyze command
|
|
1092
|
+
*/
|
|
1093
|
+
async execute(options = {}) {
|
|
1094
|
+
try {
|
|
1095
|
+
await this.initialize();
|
|
1096
|
+
await this.run(options);
|
|
1097
|
+
return { success: true, command: 'analyze' };
|
|
1098
|
+
} catch (error) {
|
|
1099
|
+
console.error(`Analyze command failed: ${error.message}`);
|
|
1100
|
+
throw error;
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
/**
|
|
1105
|
+
* Get command metadata
|
|
1106
|
+
*/
|
|
1107
|
+
getMetadata() {
|
|
1108
|
+
return {
|
|
1109
|
+
name: 'analyze',
|
|
1110
|
+
description: 'Analyze translation files for issues and completeness',
|
|
1111
|
+
category: 'analysis',
|
|
1112
|
+
aliases: [],
|
|
1113
|
+
usage: 'analyze [options]',
|
|
1114
|
+
examples: [
|
|
1115
|
+
'analyze',
|
|
1116
|
+
'analyze --source-dir=./src/locales',
|
|
1117
|
+
'analyze --output-dir=./reports'
|
|
1118
|
+
]
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
module.exports = AnalyzeCommand;
|
|
1124
|
+
|