i18ntk 1.10.2 ā 2.0.3
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,1502 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* I18NTK USAGE SERVICE
|
|
5
|
+
*
|
|
6
|
+
* Core business logic for i18n usage analysis and translation key management.
|
|
7
|
+
* Handles source code scanning, translation completeness analysis, and reporting.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const { loadTranslations, t } = require('../../../utils/i18n-helper');
|
|
13
|
+
const { getGlobalReadline, closeGlobalReadline, askHidden } = require('../../../utils/cli');
|
|
14
|
+
const { detectFramework } = require('../../../utils/framework-detector');
|
|
15
|
+
const { getExtractor } = require('../../../utils/extractor-manager');
|
|
16
|
+
const configManager = require('../../../utils/config-manager');
|
|
17
|
+
const SecurityUtils = require('../../../utils/security');
|
|
18
|
+
const AdminCLI = require('../../../utils/admin-cli');
|
|
19
|
+
const SettingsManager = require('../../../settings/settings-manager');
|
|
20
|
+
const settingsManager = new SettingsManager();
|
|
21
|
+
const { getUnifiedConfig, parseCommonArgs, displayHelp, validateSourceDir, displayPaths } = require('../../../utils/config-helper');
|
|
22
|
+
const I18nInitializer = require('../../i18ntk-init');
|
|
23
|
+
const JsonOutput = require('../../../utils/json-output');
|
|
24
|
+
const SetupEnforcer = require('../../../utils/setup-enforcer');
|
|
25
|
+
|
|
26
|
+
class UsageService {
|
|
27
|
+
constructor(config = {}) {
|
|
28
|
+
this.config = config;
|
|
29
|
+
this.sourceDir = null;
|
|
30
|
+
this.i18nDir = null;
|
|
31
|
+
this.sourceLanguageDir = null;
|
|
32
|
+
|
|
33
|
+
// Initialize class properties
|
|
34
|
+
this.availableKeys = new Set();
|
|
35
|
+
this.usedKeys = new Set();
|
|
36
|
+
this.fileUsage = new Map();
|
|
37
|
+
this.translationFiles = new Map(); // Track all translation files
|
|
38
|
+
this.translationStats = new Map(); // Track translation completeness
|
|
39
|
+
this.extractor = getExtractor(config.extractor);
|
|
40
|
+
this.placeholderKeys = new Set();
|
|
41
|
+
this.placeholderStyles = settingsManager.getDefaultSettings().placeholderStyles || {};
|
|
42
|
+
|
|
43
|
+
// Enhanced analysis properties
|
|
44
|
+
this.frameworkUsage = new Map(); // Track framework usage per file
|
|
45
|
+
this.keyComplexity = new Map(); // Track key complexity analysis
|
|
46
|
+
this.startTime = Date.now(); // Track performance metrics
|
|
47
|
+
this.version = '1.10.1'; // Version tracking
|
|
48
|
+
|
|
49
|
+
// Use global translation function
|
|
50
|
+
this.rl = null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Initialize readline interface
|
|
54
|
+
initReadline() {
|
|
55
|
+
if (!this.rl) {
|
|
56
|
+
return getGlobalReadline();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Close readline interface
|
|
61
|
+
closeReadline() {
|
|
62
|
+
const { closeGlobalReadline } = require('../../../utils/cli');
|
|
63
|
+
closeGlobalReadline();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Prompt for user input
|
|
67
|
+
async prompt(question) {
|
|
68
|
+
const rl = getGlobalReadline();
|
|
69
|
+
return new Promise(resolve => {
|
|
70
|
+
rl.question(question, (answer) => {
|
|
71
|
+
resolve(answer);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async initialize() {
|
|
77
|
+
try {
|
|
78
|
+
const cliArgs = parseCommonArgs(process.argv.slice(2));
|
|
79
|
+
const defaultConfig = await getUnifiedConfig('usage', cliArgs);
|
|
80
|
+
this.config = { ...defaultConfig, ...(this.config || {}) };
|
|
81
|
+
|
|
82
|
+
// Load translations for UI
|
|
83
|
+
const uiLanguage = (this.config && this.config.uiLanguage) || 'en';
|
|
84
|
+
loadTranslations(uiLanguage, path.resolve(__dirname, '..', '..', '..', 'resources', 'i18n', 'ui-locales'));
|
|
85
|
+
const projectRoot = path.resolve(this.config.projectRoot || '.');
|
|
86
|
+
const detected = detectFramework(projectRoot);
|
|
87
|
+
if (detected) {
|
|
88
|
+
this.config.translationPatterns = detected.patterns;
|
|
89
|
+
if (!this.config.includeExtensions) {
|
|
90
|
+
this.config.includeExtensions = ['.js', '.jsx', '.ts', '.tsx', '.py', '.pyx', '.pyi'];
|
|
91
|
+
}
|
|
92
|
+
if (!this.config.excludeDirs) {
|
|
93
|
+
this.config.excludeDirs = [];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
this.sourceDir = this.config.sourceDir;
|
|
97
|
+
this.i18nDir = this.config.i18nDir;
|
|
98
|
+
this.sourceLanguageDir = path.join(this.i18nDir, this.config.sourceLanguage);
|
|
99
|
+
|
|
100
|
+
if (!SecurityUtils.safeExistsSync(this.i18nDir, process.cwd())) {
|
|
101
|
+
console.warn(t('usage.i18nDirectoryNotFound', { i18nDir: this.i18nDir }));
|
|
102
|
+
this.i18nDir = this.sourceDir;
|
|
103
|
+
this.config.i18nDir = this.i18nDir;
|
|
104
|
+
await configManager.updateConfig({ i18nDir: configManager.toRelative(this.sourceDir) });
|
|
105
|
+
this.sourceLanguageDir = path.join(this.i18nDir, this.config.sourceLanguage);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
displayPaths({ sourceDir: this.sourceDir, i18nDir: this.i18nDir, outputDir: this.config.outputDir });
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
// Ensure translation patterns are defined
|
|
112
|
+
this.config = this.config || {};
|
|
113
|
+
this.config.translationPatterns = this.config.translationPatterns || [
|
|
114
|
+
/t\(['"`]([^'"`]+)['"`]/g,
|
|
115
|
+
/i18n\.t\(['"`]([^'"`]+)['"`]/g,
|
|
116
|
+
/useTranslation\(\)\.t\(['"`]([^'"`]+)['"`]/g,
|
|
117
|
+
/t\(`([^`]+)`\)/g,
|
|
118
|
+
/i18nKey=['"`]([^'"`]+)['"`]/g,
|
|
119
|
+
/\$t\(['"`]([^'"`]+)['"`]/g,
|
|
120
|
+
/getTranslation\(['"`]([^'"`]+)['"`]/g
|
|
121
|
+
];
|
|
122
|
+
this.extractor = getExtractor(this.config.extractor);
|
|
123
|
+
|
|
124
|
+
// Ensure defaults for other config values
|
|
125
|
+
this.config = this.config || {};
|
|
126
|
+
if (!Array.isArray(this.config.excludeDirs)) {
|
|
127
|
+
this.config.excludeDirs = ['node_modules', '.git'];
|
|
128
|
+
}
|
|
129
|
+
if (!Array.isArray(this.config.includeExtensions) && !Array.isArray(this.config.supportedExtensions)) {
|
|
130
|
+
this.config.includeExtensions = ['.js', '.jsx', '.ts', '.tsx', '.py', '.pyx', '.pyi'];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
await SecurityUtils.logSecurityEvent(t('usage.analyzerInitialized'), { component: 'i18ntk-usage' });
|
|
134
|
+
} catch (error) {
|
|
135
|
+
await SecurityUtils.logSecurityEvent(t('usage.analyzerInitFailed'), { component: 'i18ntk-usage', error: error.message });
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Normalize CLI arguments to handle both camelCase and hyphenated flags
|
|
141
|
+
normalizeArgs(a) {
|
|
142
|
+
return {
|
|
143
|
+
sourceDir: a.sourceDir ?? a['source-dir'],
|
|
144
|
+
i18nDir: a.i18nDir ?? a['i18n-dir'],
|
|
145
|
+
outputReport: a.outputReport ?? a['output-report'],
|
|
146
|
+
outputDir: a.outputDir ?? a['output-dir'],
|
|
147
|
+
uiLanguage: a.uiLanguage ?? a['ui-language'],
|
|
148
|
+
help: a.help || a.h,
|
|
149
|
+
noPrompt: a.noPrompt ?? a['no-prompt'],
|
|
150
|
+
strict: a.strict,
|
|
151
|
+
debug: a.debug
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Parse command line arguments
|
|
156
|
+
async parseArgs() {
|
|
157
|
+
const args = process.argv.slice(2);
|
|
158
|
+
const parsed = {};
|
|
159
|
+
|
|
160
|
+
for (const arg of args) {
|
|
161
|
+
if (arg.startsWith('--')) {
|
|
162
|
+
const [key, value] = arg.substring(2).split('=');
|
|
163
|
+
if (value !== undefined) {
|
|
164
|
+
parsed[key] = value;
|
|
165
|
+
} else {
|
|
166
|
+
parsed[key] = true;
|
|
167
|
+
}
|
|
168
|
+
} else if (arg.startsWith('-')) {
|
|
169
|
+
const key = arg.substring(1);
|
|
170
|
+
parsed[key] = true;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return this.normalizeArgs(parsed);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Recursively discover all translation files in modular structure
|
|
178
|
+
async discoverTranslationFiles(baseDir, language = (this.config && this.config.sourceLanguage) || 'en') {
|
|
179
|
+
const translationFiles = [];
|
|
180
|
+
|
|
181
|
+
const traverse = async (currentDir) => {
|
|
182
|
+
try {
|
|
183
|
+
const absoluteDir = path.resolve(currentDir);
|
|
184
|
+
const validatedPath = SecurityUtils.validatePath(absoluteDir, process.cwd());
|
|
185
|
+
|
|
186
|
+
if (!validatedPath || !SecurityUtils.safeExistsSync(validatedPath)) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const items = fs.readdirSync(validatedPath);
|
|
191
|
+
|
|
192
|
+
for (const item of items) {
|
|
193
|
+
const itemPath = path.join(validatedPath, item);
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
const stat = fs.statSync(itemPath);
|
|
197
|
+
|
|
198
|
+
if (stat.isDirectory()) {
|
|
199
|
+
// Skip excluded directories with null-safety
|
|
200
|
+
const excludes = Array.isArray(this.config.excludeDirs) ? this.config.excludeDirs : [];
|
|
201
|
+
if (!excludes.includes(item)) {
|
|
202
|
+
await traverse(itemPath);
|
|
203
|
+
}
|
|
204
|
+
} else if (stat.isFile()) {
|
|
205
|
+
// Look for translation files:
|
|
206
|
+
// 1. Direct language files: en.json, de.json, etc.
|
|
207
|
+
// 2. Language directory files: en/common.json, de/auth.json, etc.
|
|
208
|
+
// 3. Nested modular files: components/en.json, features/auth/en.json, etc.
|
|
209
|
+
|
|
210
|
+
const fileName = path.basename(item, '.json');
|
|
211
|
+
const parentDir = path.basename(path.dirname(itemPath));
|
|
212
|
+
|
|
213
|
+
if (item.endsWith('.json')) {
|
|
214
|
+
// Case 1: Direct language files (en.json)
|
|
215
|
+
if (fileName === language) {
|
|
216
|
+
translationFiles.push({
|
|
217
|
+
filePath: itemPath,
|
|
218
|
+
namespace: path.relative(baseDir, path.dirname(itemPath)).replace(/[\\/]/g, '.') || 'root',
|
|
219
|
+
language: language,
|
|
220
|
+
type: 'direct'
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
// Case 2: Files in language directories (en/common.json)
|
|
224
|
+
else if (parentDir === language) {
|
|
225
|
+
translationFiles.push({
|
|
226
|
+
filePath: itemPath,
|
|
227
|
+
namespace: fileName,
|
|
228
|
+
language: language,
|
|
229
|
+
type: 'namespaced'
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
} catch (statError) {
|
|
235
|
+
// Skip files that can't be accessed
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
} catch (error) {
|
|
240
|
+
await SecurityUtils.logSecurityEvent(t('usage.translationDiscoveryError'), {
|
|
241
|
+
component: 'i18ntk-usage',
|
|
242
|
+
directory: currentDir,
|
|
243
|
+
error: error.message
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
await traverse(baseDir);
|
|
249
|
+
return translationFiles;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Get all files recursively from a directory with enhanced filtering
|
|
253
|
+
async getAllFiles(dir, extensions = (this.config && (this.config.includeExtensions || this.config.supportedExtensions)) || ['.js', '.jsx', '.ts', '.tsx', '.py', '.pyx', '.pyi']) {
|
|
254
|
+
const files = [];
|
|
255
|
+
|
|
256
|
+
// Enhanced list of toolkit files to exclude from analysis
|
|
257
|
+
const excludeFiles = [
|
|
258
|
+
'console-translations.js', 'console-key-checker.js',
|
|
259
|
+
'complete-console-translations.js', 'detect-language-mismatches.js',
|
|
260
|
+
'export-missing-keys.js', 'maintain-language-purity.js',
|
|
261
|
+
'native-translations.js', 'settings-cli.js', 'settings-manager.js',
|
|
262
|
+
'test-complete-system.js', 'test-console-i18n.js', 'test-features.js',
|
|
263
|
+
'translate-mismatches.js', 'i18ntk-ui.js', 'update-console-i18n.js',
|
|
264
|
+
'validate-language-purity.js', 'debugger.js', 'admin-auth.js',
|
|
265
|
+
'admin-cli.js'
|
|
266
|
+
];
|
|
267
|
+
|
|
268
|
+
// Null-safe extensions handling
|
|
269
|
+
const safeExtensions = Array.isArray(extensions) ? extensions : ['.js', '.jsx', '.ts', '.tsx', '.py', '.pyx', '.pyi'];
|
|
270
|
+
const skipRoot = path.resolve(this.i18nDir || '');
|
|
271
|
+
const traverse = async (currentDir) => {
|
|
272
|
+
try {
|
|
273
|
+
const absoluteDir = path.resolve(currentDir);
|
|
274
|
+
const validatedPath = SecurityUtils.validatePath(absoluteDir, process.cwd());
|
|
275
|
+
|
|
276
|
+
if (!validatedPath || !SecurityUtils.safeExistsSync(validatedPath)) {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const items = fs.readdirSync(validatedPath);
|
|
281
|
+
|
|
282
|
+
for (const item of items) {
|
|
283
|
+
const itemPath = path.join(validatedPath, item);
|
|
284
|
+
|
|
285
|
+
try {
|
|
286
|
+
const stat = fs.statSync(itemPath);
|
|
287
|
+
|
|
288
|
+
if (stat.isDirectory()) {
|
|
289
|
+
const excludes = Array.isArray(this.config.excludeDirs) ? this.config.excludeDirs : [];
|
|
290
|
+
if (!excludes.includes(item)) {
|
|
291
|
+
// hard-skip the locales root to avoid reading JSON
|
|
292
|
+
if (skipRoot && path.resolve(itemPath).startsWith(skipRoot)) continue;
|
|
293
|
+
await traverse(itemPath);
|
|
294
|
+
}
|
|
295
|
+
} else if (stat.isFile()) {
|
|
296
|
+
// Skip JSON files entirely to prevent scanning translation files
|
|
297
|
+
if (itemPath.endsWith('.json')) continue;
|
|
298
|
+
|
|
299
|
+
// Include files with specified extensions, but exclude toolkit files
|
|
300
|
+
const ext = path.extname(item);
|
|
301
|
+
// Ensure extension has dot prefix
|
|
302
|
+
const normalizedExt = ext.startsWith('.') ? ext : `.${ext}`;
|
|
303
|
+
const normalizedExtensions = safeExtensions.map(ext => ext.startsWith('.') ? ext : `.${ext}`);
|
|
304
|
+
if (normalizedExtensions.includes(normalizedExt) && !excludeFiles.includes(item)) {
|
|
305
|
+
files.push(itemPath);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
} catch (statError) {
|
|
309
|
+
// Skip files that can't be accessed
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
} catch (error) {
|
|
314
|
+
await SecurityUtils.logSecurityEvent(t('usage.fileTraversalError'), {
|
|
315
|
+
component: 'i18ntk-usage',
|
|
316
|
+
directory: currentDir,
|
|
317
|
+
error: error.message
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
await traverse(dir);
|
|
323
|
+
return files;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Analyze usage in source files
|
|
327
|
+
async analyzeUsage() {
|
|
328
|
+
try {
|
|
329
|
+
console.log(t('usage.checkUsage.analyzing_source_files'));
|
|
330
|
+
|
|
331
|
+
// Check if source directory exists
|
|
332
|
+
if (!SecurityUtils.safeExistsSync(this.sourceDir)) {
|
|
333
|
+
throw new Error(this.t('usage.sourceDirectoryDoesNotExist', { dir: this.sourceDir }) || `Source directory not found: ${this.sourceDir}`);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const sourceFiles = await this.getAllFiles(this.sourceDir);
|
|
337
|
+
console.log(t('usage.checkUsage.found_files_in_source', { numFiles: sourceFiles.length }));
|
|
338
|
+
|
|
339
|
+
// If no files found, exit gracefully
|
|
340
|
+
if (sourceFiles.length === 0) {
|
|
341
|
+
console.warn(t('usage.noSourceFilesFound'));
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
let totalKeysFound = 0;
|
|
346
|
+
let processedFiles = 0;
|
|
347
|
+
|
|
348
|
+
for (const filePath of sourceFiles) {
|
|
349
|
+
try {
|
|
350
|
+
const keys = this.extractKeysFromFile(filePath);
|
|
351
|
+
|
|
352
|
+
if (keys.length > 0) {
|
|
353
|
+
const relativePath = path.relative(this.sourceDir, filePath);
|
|
354
|
+
this.fileUsage.set(relativePath, keys);
|
|
355
|
+
|
|
356
|
+
keys.forEach(key => {
|
|
357
|
+
this.usedKeys.add(key);
|
|
358
|
+
totalKeysFound++;
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
processedFiles++;
|
|
363
|
+
|
|
364
|
+
// Progress indicator for large numbers of files
|
|
365
|
+
if (sourceFiles.length > 10 && processedFiles % Math.ceil(sourceFiles.length / 10) === 0) {
|
|
366
|
+
console.log(t('usage.processedFiles', { processedFiles, totalFiles: sourceFiles.length }));
|
|
367
|
+
}
|
|
368
|
+
} catch (fileError) {
|
|
369
|
+
console.warn(`${t('usage.failedToProcessFile')} ${filePath}: ${fileError.message}`);
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
console.log(t("usage.checkUsage.found_thisusedkeyssize_unique_", { usedKeysSize: this.usedKeys.size }));
|
|
375
|
+
console.log(t("usage.checkUsage.total_key_usages_totalkeysfoun", { totalKeysFound }));
|
|
376
|
+
|
|
377
|
+
} catch (error) {
|
|
378
|
+
console.error(t('usage.failedToAnalyzeUsage', { error: error.message }));
|
|
379
|
+
throw error;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Load available translation keys
|
|
384
|
+
async loadAvailableKeys() {
|
|
385
|
+
console.log(t("usage.checkUsage.loading_available_translation_"));
|
|
386
|
+
|
|
387
|
+
this.availableKeys = await this.getAllTranslationKeys();
|
|
388
|
+
console.log(t("usage.checkUsage.found_thisavailablekeyssize_av", { availableKeysSize: this.availableKeys.size }));
|
|
389
|
+
if (this.placeholderKeys.size > 0) {
|
|
390
|
+
console.log('Placeholder translation keys detected: ' + Array.from(this.placeholderKeys).join(', '));
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Get all translation keys recursively from translation object
|
|
395
|
+
async getAllTranslationKeys() {
|
|
396
|
+
const keys = new Set();
|
|
397
|
+
const isStrict = process.argv.includes('--strict');
|
|
398
|
+
const isDebug = process.argv.includes('--debug');
|
|
399
|
+
|
|
400
|
+
try {
|
|
401
|
+
// Discover all translation files in the i18n directory
|
|
402
|
+
const translationFiles = await this.discoverTranslationFiles(this.i18nDir, this.config.sourceLanguage);
|
|
403
|
+
|
|
404
|
+
console.log(t('usage.foundTranslationFiles', { count: translationFiles.length }));
|
|
405
|
+
|
|
406
|
+
for (const fileInfo of translationFiles) {
|
|
407
|
+
try {
|
|
408
|
+
await SecurityUtils.validatePath(fileInfo.filePath);
|
|
409
|
+
|
|
410
|
+
// Check if file exists and is readable
|
|
411
|
+
if (!SecurityUtils.safeExistsSync(fileInfo.filePath)) {
|
|
412
|
+
if (isDebug || isStrict) {
|
|
413
|
+
console.warn(`ā ļø File not found: ${path.basename(fileInfo.filePath)}`);
|
|
414
|
+
}
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const content = await SecurityUtils.safeReadFile(fileInfo.filePath);
|
|
419
|
+
|
|
420
|
+
// Handle empty files
|
|
421
|
+
if (!content || content.trim() === '') {
|
|
422
|
+
if (isDebug || isStrict) {
|
|
423
|
+
console.warn(`ā ļø Empty file: ${path.basename(fileInfo.filePath)}`);
|
|
424
|
+
}
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const jsonData = await SecurityUtils.safeParseJSON(content);
|
|
429
|
+
|
|
430
|
+
// Validate JSON structure before processing
|
|
431
|
+
if (jsonData === null || jsonData === undefined) {
|
|
432
|
+
if (isDebug || isStrict) {
|
|
433
|
+
console.warn(`ā ļø Null/undefined JSON data: ${path.basename(fileInfo.filePath)}`);
|
|
434
|
+
}
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (typeof jsonData !== 'object') {
|
|
439
|
+
if (isDebug || isStrict) {
|
|
440
|
+
console.warn(`ā ļø Invalid JSON structure (not an object): ${path.basename(fileInfo.filePath)}`);
|
|
441
|
+
}
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (Array.isArray(jsonData)) {
|
|
446
|
+
if (isDebug || isStrict) {
|
|
447
|
+
console.warn(`ā ļø Invalid JSON structure (array instead of object): ${path.basename(fileInfo.filePath)}`);
|
|
448
|
+
}
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Store file info for later analysis
|
|
453
|
+
this.translationFiles.set(fileInfo.filePath, fileInfo);
|
|
454
|
+
|
|
455
|
+
const fileKeys = this.extractKeysFromObject(jsonData, '', fileInfo.namespace);
|
|
456
|
+
fileKeys.forEach(key => keys.add(key));
|
|
457
|
+
this.collectPlaceholderKeys(jsonData, '', fileInfo.language);
|
|
458
|
+
|
|
459
|
+
if (isDebug) {
|
|
460
|
+
console.log(t('usage.fileInfo', { namespace: fileInfo.namespace, keys: fileKeys.length }));
|
|
461
|
+
}
|
|
462
|
+
} catch (error) {
|
|
463
|
+
if (isDebug || isStrict) {
|
|
464
|
+
console.warn(`ā Failed to extract keys from ${path.basename(fileInfo.filePath)}: ${error.message}`);
|
|
465
|
+
}
|
|
466
|
+
if (isDebug) {
|
|
467
|
+
console.error(error.stack);
|
|
468
|
+
}
|
|
469
|
+
await SecurityUtils.logSecurityEvent(t('usage.translationFileParseError'), {
|
|
470
|
+
component: 'i18ntk-usage',
|
|
471
|
+
file: fileInfo.filePath,
|
|
472
|
+
error: error.message
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
} catch (error) {
|
|
477
|
+
await SecurityUtils.logSecurityEvent(t('usage.translationKeysLoadError'), {
|
|
478
|
+
component: 'i18ntk-usage',
|
|
479
|
+
error: error.message
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return keys;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Extract keys recursively from translation object
|
|
487
|
+
extractKeysFromObject(obj, prefix = '', namespace = '') {
|
|
488
|
+
const keys = [];
|
|
489
|
+
|
|
490
|
+
// Validate input object before processing
|
|
491
|
+
if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {
|
|
492
|
+
return keys; // Return empty array for invalid input
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
try {
|
|
496
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
497
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
498
|
+
|
|
499
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
500
|
+
keys.push(...this.extractKeysFromObject(value, fullKey, namespace));
|
|
501
|
+
} else {
|
|
502
|
+
// Add dot notation key (e.g., "pagination.showing")
|
|
503
|
+
keys.push(fullKey);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
} catch (error) {
|
|
507
|
+
// Handle any unexpected errors during key extraction
|
|
508
|
+
console.warn(`ā ļø Error during key extraction: ${error.message}`);
|
|
509
|
+
return keys;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
return keys;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
collectPlaceholderKeys(obj, prefix = '', language) {
|
|
516
|
+
const patterns = this.placeholderStyles[language] || [];
|
|
517
|
+
const regexes = patterns.map(p => new RegExp(p));
|
|
518
|
+
if (typeof obj !== 'object' || obj === null) return;
|
|
519
|
+
|
|
520
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
521
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
522
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
523
|
+
this.collectPlaceholderKeys(value, fullKey, language);
|
|
524
|
+
} else if (typeof value === 'string' && regexes.some(r => r.test(value))) {
|
|
525
|
+
this.placeholderKeys.add(fullKey);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Extract translation keys from source code with enhanced patterns
|
|
531
|
+
extractKeysFromFile(filePath) {
|
|
532
|
+
try {
|
|
533
|
+
const content = SecurityUtils.safeReadFileSync(filePath, path.dirname(filePath), 'utf8');
|
|
534
|
+
if (!content) return [];
|
|
535
|
+
|
|
536
|
+
// Skip JSON files entirely to prevent scanning translation files
|
|
537
|
+
if (filePath.endsWith('.json')) return [];
|
|
538
|
+
const rawPatterns = Array.isArray(this.config.translationPatterns) ? this.config.translationPatterns : [];
|
|
539
|
+
if (rawPatterns.length === 0) return [];
|
|
540
|
+
|
|
541
|
+
return this.extractor.extract(content, rawPatterns);
|
|
542
|
+
|
|
543
|
+
// Null-safe translation patterns handling
|
|
544
|
+
} catch (error) {
|
|
545
|
+
console.warn(`${t('usage.failedToExtractKeys')} ${filePath}: ${error.message}`);
|
|
546
|
+
return [];
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Analyze translation completeness across all languages
|
|
551
|
+
async analyzeTranslationCompleteness() {
|
|
552
|
+
try {
|
|
553
|
+
console.log('\nš Analyzing translation completeness...');
|
|
554
|
+
|
|
555
|
+
const isDebug = process.argv.includes('--debug');
|
|
556
|
+
const isStrict = process.argv.includes('--strict');
|
|
557
|
+
|
|
558
|
+
// Check if i18n directory exists
|
|
559
|
+
if (!SecurityUtils.safeExistsSync(this.i18nDir, process.cwd())) {
|
|
560
|
+
console.warn(t('usage.i18nDirectoryNotFound', { i18nDir: this.i18nDir }));
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Get all available languages
|
|
565
|
+
const languages = new Set();
|
|
566
|
+
|
|
567
|
+
try {
|
|
568
|
+
// Discover translation files for all languages
|
|
569
|
+
const allLanguageDirs = fs.readdirSync(this.i18nDir)
|
|
570
|
+
.filter(item => {
|
|
571
|
+
try {
|
|
572
|
+
const itemPath = path.join(this.i18nDir, item);
|
|
573
|
+
return SecurityUtils.safeExistsSync(itemPath) && fs.statSync(itemPath).isDirectory();
|
|
574
|
+
} catch (error) {
|
|
575
|
+
return false;
|
|
576
|
+
}
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
for (const lang of allLanguageDirs) {
|
|
580
|
+
if (['en', 'de', 'es', 'fr', 'ru', 'ja', 'zh'].includes(lang)) {
|
|
581
|
+
languages.add(lang);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// Also check for direct language files (en.json, de.json, etc.)
|
|
586
|
+
const directFiles = fs.readdirSync(this.i18nDir)
|
|
587
|
+
.filter(file => file.endsWith('.json'))
|
|
588
|
+
.map(file => path.basename(file, '.json'))
|
|
589
|
+
.filter(lang => ['en', 'de', 'es', 'fr', 'ru', 'ja', 'zh'].includes(lang));
|
|
590
|
+
|
|
591
|
+
directFiles.forEach(lang => languages.add(lang));
|
|
592
|
+
} catch (error) {
|
|
593
|
+
console.warn(`${t('usage.errorReadingI18nDirectory')} ${error.message}`);
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// If no languages found, exit gracefully
|
|
598
|
+
if (languages.size === 0) {
|
|
599
|
+
console.warn(t('usage.checkUsage.noTranslationLanguagesFound'));
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// Analyze each language
|
|
604
|
+
for (const language of languages) {
|
|
605
|
+
try {
|
|
606
|
+
const translationFiles = await this.discoverTranslationFiles(this.i18nDir, language);
|
|
607
|
+
let totalKeys = 0;
|
|
608
|
+
let translatedKeys = 0;
|
|
609
|
+
|
|
610
|
+
for (const fileInfo of translationFiles) {
|
|
611
|
+
try {
|
|
612
|
+
if (!SecurityUtils.safeExistsSync(fileInfo.filePath)) {
|
|
613
|
+
if (isDebug || isStrict) {
|
|
614
|
+
console.warn(`ā ļø File not found: ${path.basename(fileInfo.filePath)}`);
|
|
615
|
+
}
|
|
616
|
+
continue;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
const content = await SecurityUtils.safeReadFile(fileInfo.filePath);
|
|
620
|
+
|
|
621
|
+
// Handle empty files
|
|
622
|
+
if (!content || content.trim() === '') {
|
|
623
|
+
if (isDebug || isStrict) {
|
|
624
|
+
console.warn(`ā ļø Empty file: ${path.basename(fileInfo.filePath)}`);
|
|
625
|
+
}
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
const jsonData = await SecurityUtils.safeParseJSON(content);
|
|
630
|
+
|
|
631
|
+
// Validate JSON structure before processing
|
|
632
|
+
if (jsonData === null || jsonData === undefined) {
|
|
633
|
+
if (isDebug || isStrict) {
|
|
634
|
+
console.warn(`ā ļø Null/undefined JSON data: ${path.basename(fileInfo.filePath)}`);
|
|
635
|
+
}
|
|
636
|
+
continue;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
if (typeof jsonData !== 'object') {
|
|
640
|
+
if (isDebug || isStrict) {
|
|
641
|
+
console.warn(`ā ļø Invalid JSON structure (not an object): ${path.basename(fileInfo.filePath)}`);
|
|
642
|
+
}
|
|
643
|
+
continue;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
if (Array.isArray(jsonData)) {
|
|
647
|
+
if (isDebug || isStrict) {
|
|
648
|
+
console.warn(`ā ļø Invalid JSON structure (array instead of object): ${path.basename(fileInfo.filePath)}`);
|
|
649
|
+
}
|
|
650
|
+
continue;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
const stats = this.analyzeFileCompleteness(jsonData);
|
|
654
|
+
totalKeys += stats.total;
|
|
655
|
+
translatedKeys += stats.translated;
|
|
656
|
+
} catch (error) {
|
|
657
|
+
if (isDebug || isStrict) {
|
|
658
|
+
console.warn(`ā Failed to analyze file ${path.basename(fileInfo.filePath)}: ${error.message}`);
|
|
659
|
+
}
|
|
660
|
+
if (isDebug) {
|
|
661
|
+
console.error(error.stack);
|
|
662
|
+
}
|
|
663
|
+
continue;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
this.translationStats.set(language, {
|
|
668
|
+
total: totalKeys,
|
|
669
|
+
translated: translatedKeys,
|
|
670
|
+
notTranslated: totalKeys - translatedKeys
|
|
671
|
+
});
|
|
672
|
+
} catch (error) {
|
|
673
|
+
if (isDebug || isStrict) {
|
|
674
|
+
console.warn(t('usage.failedToAnalyzeLanguage', { language, error: error.message }));
|
|
675
|
+
}
|
|
676
|
+
continue;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
} catch (error) {
|
|
680
|
+
console.warn(t('usage.translationCompletenessAnalysisFailed', { error: error.message }));
|
|
681
|
+
// Don't throw error, just continue with the rest of the analysis
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// Analyze completeness of a single translation file
|
|
686
|
+
analyzeFileCompleteness(obj) {
|
|
687
|
+
let total = 0;
|
|
688
|
+
let translated = 0;
|
|
689
|
+
|
|
690
|
+
// Validate input object before processing
|
|
691
|
+
if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {
|
|
692
|
+
return { total: 0, translated: 0 }; // Return empty stats for invalid input
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
const traverse = (current) => {
|
|
696
|
+
// Validate current object before processing
|
|
697
|
+
if (typeof current !== 'object' || current === null || Array.isArray(current)) {
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
for (const [key, value] of Object.entries(current)) {
|
|
702
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
703
|
+
traverse(value);
|
|
704
|
+
} else {
|
|
705
|
+
total++;
|
|
706
|
+
if (value !== 'NOT_TRANSLATED' && value !== '(NOT TRANSLATED)' &&
|
|
707
|
+
value !== 'TRANSLATED' && value !== '(TRANSLATED)' &&
|
|
708
|
+
value && value.toString().trim() !== '') {
|
|
709
|
+
translated++;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
};
|
|
714
|
+
|
|
715
|
+
traverse(obj);
|
|
716
|
+
return { total, translated };
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// Get statistics about NOT_TRANSLATED values
|
|
720
|
+
getNotTranslatedStats() {
|
|
721
|
+
let total = 0;
|
|
722
|
+
const byLanguage = new Map();
|
|
723
|
+
|
|
724
|
+
for (const [language, stats] of this.translationStats) {
|
|
725
|
+
const notTranslated = stats.notTranslated;
|
|
726
|
+
total += notTranslated;
|
|
727
|
+
byLanguage.set(language, notTranslated);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
return { total, byLanguage };
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// Find unused keys
|
|
734
|
+
findUnusedKeys() {
|
|
735
|
+
const unused = [];
|
|
736
|
+
|
|
737
|
+
for (const key of this.availableKeys) {
|
|
738
|
+
let isUsed = false;
|
|
739
|
+
|
|
740
|
+
// Check exact match
|
|
741
|
+
if (this.usedKeys.has(key)) {
|
|
742
|
+
isUsed = true;
|
|
743
|
+
} else {
|
|
744
|
+
// Check if any dynamic key could match this
|
|
745
|
+
for (const usedKey of this.usedKeys) {
|
|
746
|
+
if (usedKey.endsWith('*')) {
|
|
747
|
+
const prefix = usedKey.slice(0, -1);
|
|
748
|
+
if (key.startsWith(prefix)) {
|
|
749
|
+
isUsed = true;
|
|
750
|
+
break;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
if (!isUsed) {
|
|
757
|
+
unused.push(key);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
return unused;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// Find missing keys (used but not available)
|
|
765
|
+
findMissingKeys() {
|
|
766
|
+
const missing = [];
|
|
767
|
+
|
|
768
|
+
for (const key of this.usedKeys) {
|
|
769
|
+
// Skip dynamic keys for missing check
|
|
770
|
+
if (key.endsWith('*')) {
|
|
771
|
+
continue;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
if (!this.availableKeys.has(key)) {
|
|
775
|
+
missing.push(key);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
return missing;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// Find files that use specific keys
|
|
783
|
+
findKeyUsage(searchKey) {
|
|
784
|
+
const usage = [];
|
|
785
|
+
|
|
786
|
+
for (const [filePath, keys] of this.fileUsage) {
|
|
787
|
+
const matchingKeys = keys.filter(key => {
|
|
788
|
+
if (key.endsWith('*')) {
|
|
789
|
+
const prefix = key.slice(0, -1);
|
|
790
|
+
return searchKey.startsWith(prefix);
|
|
791
|
+
}
|
|
792
|
+
return key === searchKey;
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
if (matchingKeys.length > 0) {
|
|
796
|
+
usage.push({ filePath, keys: matchingKeys });
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
return usage;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// Enhanced usage report generation
|
|
804
|
+
generateUsageReport() {
|
|
805
|
+
const unusedKeys = this.findUnusedKeys();
|
|
806
|
+
const missingKeys = this.findMissingKeys();
|
|
807
|
+
const dynamicKeys = Array.from(this.usedKeys).filter(key => key.endsWith('*'));
|
|
808
|
+
const notTranslatedStats = this.getNotTranslatedStats();
|
|
809
|
+
|
|
810
|
+
const timestamp = new Date().toISOString();
|
|
811
|
+
|
|
812
|
+
let report = `${t('summary.usageReportTitle')}\n`;
|
|
813
|
+
report += `${t('summary.usageReportGenerated', { timestamp })}\n`;
|
|
814
|
+
report += `${t('summary.usageReportSourceDir', { sourceDir: this.sourceDir })}\n`;
|
|
815
|
+
report += `${t('summary.usageReportI18nDir', { i18nDir: this.i18nDir })}\n`;
|
|
816
|
+
report += `Version: ${this.version}\n\n`;
|
|
817
|
+
|
|
818
|
+
// Performance metrics
|
|
819
|
+
const analysisTime = Date.now() - this.startTime;
|
|
820
|
+
report += `ā” Performance Metrics:\n`;
|
|
821
|
+
report += ` Analysis completed in: ${analysisTime}ms\n`;
|
|
822
|
+
report += ` Memory usage: ${Math.round(process.memoryUsage().heapUsed / 1024 / 1024)}MB\n\n`;
|
|
823
|
+
|
|
824
|
+
// Summary
|
|
825
|
+
report += `${t('summary.usageReportSummary')}\n`;
|
|
826
|
+
report += `${'='.repeat(50)}\n`;
|
|
827
|
+
report += `${t('summary.usageReportSourceFilesScanned', { count: this.fileUsage.size })}\n`;
|
|
828
|
+
report += `${t('summary.usageReportTranslationFilesFound', { count: this.translationFiles.size })}\n`;
|
|
829
|
+
report += `${t('summary.usageReportAvailableKeys', { count: this.availableKeys.size })}\n`;
|
|
830
|
+
report += `${t('summary.usageReportUsedKeys', { count: this.usedKeys.size - dynamicKeys.length })}\n`;
|
|
831
|
+
report += `${t('summary.usageReportDynamicKeys', { count: dynamicKeys.length })}\n`;
|
|
832
|
+
report += `${t('summary.usageReportUnusedKeys', { count: unusedKeys.length })}\n`;
|
|
833
|
+
report += `${t('summary.usageReportMissingKeys', { count: missingKeys.length })}\n`;
|
|
834
|
+
report += `${t('summary.usageReportNotTranslatedKeys', { count: notTranslatedStats.total })}\n\n`;
|
|
835
|
+
|
|
836
|
+
// Framework usage analysis
|
|
837
|
+
if (this.frameworkUsage && this.frameworkUsage.size > 0) {
|
|
838
|
+
report += `šļø Framework Usage Analysis:\n`;
|
|
839
|
+
const frameworkCounts = {};
|
|
840
|
+
this.frameworkUsage.forEach((data, filePath) => {
|
|
841
|
+
const framework = data.framework;
|
|
842
|
+
if (!frameworkCounts[framework]) frameworkCounts[framework] = 0;
|
|
843
|
+
frameworkCounts[framework]++;
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
Object.entries(frameworkCounts).forEach(([framework, count]) => {
|
|
847
|
+
report += ` ${framework}: ${count} files\n`;
|
|
848
|
+
});
|
|
849
|
+
report += `\n`;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
// Translation completeness with advanced scoring
|
|
853
|
+
report += `${t('summary.usageReportTranslationCompleteness')}\n`;
|
|
854
|
+
report += `${'='.repeat(50)}\n`;
|
|
855
|
+
for (const [language, stats] of this.translationStats) {
|
|
856
|
+
const translations = this.translationsByLanguage[language] || {};
|
|
857
|
+
const score = this.calculateTranslationScore ? this.calculateTranslationScore(language, translations) : {
|
|
858
|
+
completeness: ((stats.translated / stats.total) * 100).toFixed(1),
|
|
859
|
+
quality: ((stats.translated / stats.total) * 100).toFixed(1),
|
|
860
|
+
placeholderAccuracy: 100
|
|
861
|
+
};
|
|
862
|
+
|
|
863
|
+
report += `${t('summary.usageReportLanguageCompleteness', { language: language.toUpperCase(), completeness: score.completeness, translated: stats.translated, total: stats.total })}\n`;
|
|
864
|
+
report += ` Quality: ${score.quality}%\n`;
|
|
865
|
+
report += ` Placeholder Accuracy: ${score.placeholderAccuracy}%\n`;
|
|
866
|
+
|
|
867
|
+
if (stats.notTranslated > 0) {
|
|
868
|
+
report += `${t('summary.usageReportNotTranslatedInLanguage', { count: stats.notTranslated })}\n`;
|
|
869
|
+
}
|
|
870
|
+
report += `\n`;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// Translation files discovered
|
|
874
|
+
report += `${t('summary.usageReportTranslationFilesDiscovered')}\n`;
|
|
875
|
+
report += `${'='.repeat(50)}\n`;
|
|
876
|
+
for (const [filePath, fileInfo] of this.translationFiles) {
|
|
877
|
+
const relativePath = path.relative(this.i18nDir, filePath);
|
|
878
|
+
report += `${t('summary.usageReportFileInfo', { relativePath, namespace: fileInfo.namespace, type: fileInfo.type })}\n`;
|
|
879
|
+
}
|
|
880
|
+
report += `\n`;
|
|
881
|
+
|
|
882
|
+
// Key complexity analysis
|
|
883
|
+
if (this.keyComplexity && this.keyComplexity.size > 0) {
|
|
884
|
+
report += `š Key Complexity Analysis:\n`;
|
|
885
|
+
const complexityStats = { simple: 0, moderate: 0, complex: 0 };
|
|
886
|
+
this.keyComplexity.forEach((data, key) => {
|
|
887
|
+
complexityStats[data.level]++;
|
|
888
|
+
});
|
|
889
|
+
|
|
890
|
+
report += ` Simple keys: ${complexityStats.simple}\n`;
|
|
891
|
+
report += ` Moderate keys: ${complexityStats.moderate}\n`;
|
|
892
|
+
report += ` Complex keys: ${complexityStats.complex}\n\n`;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// Unused keys with complexity
|
|
896
|
+
if (unusedKeys.length > 0) {
|
|
897
|
+
report += `${t('summary.usageReportUnusedTranslationKeys')}\n`;
|
|
898
|
+
report += `${'='.repeat(50)}\n`;
|
|
899
|
+
report += `${t('summary.usageReportUnusedKeysDescription')}\n\n`;
|
|
900
|
+
|
|
901
|
+
unusedKeys.slice(0, 100).forEach(key => {
|
|
902
|
+
const complexity = this.keyComplexity && this.keyComplexity.get(key);
|
|
903
|
+
const complexityLevel = complexity ? ` (${complexity.level})` : '';
|
|
904
|
+
report += `${t('summary.usageReportUnusedKey', { key: key + complexityLevel })}\n`;
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
if (unusedKeys.length > 100) {
|
|
908
|
+
report += `${t('summary.usageReportMoreUnusedKeys', { count: unusedKeys.length - 100 })}\n`;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
report += `\n`;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
// Missing keys with location and framework
|
|
915
|
+
if (missingKeys.length > 0) {
|
|
916
|
+
report += `${t('summary.usageReportMissingTranslationKeys')}\n`;
|
|
917
|
+
report += `${'='.repeat(50)}\n`;
|
|
918
|
+
report += `${t('summary.usageReportMissingKeysDescription')}\n\n`;
|
|
919
|
+
|
|
920
|
+
missingKeys.forEach(key => {
|
|
921
|
+
report += `${t('summary.usageReportMissingKey', { key })}\n`;
|
|
922
|
+
|
|
923
|
+
// Show where it's used
|
|
924
|
+
const usage = this.findKeyUsage(key);
|
|
925
|
+
usage.slice(0, 3).forEach(({ filePath }) => {
|
|
926
|
+
const framework = this.frameworkUsage && this.frameworkUsage.get(filePath);
|
|
927
|
+
const frameworkInfo = framework ? ` [${framework.framework}]` : '';
|
|
928
|
+
report += ` ${t('summary.usageReportUsedIn', { filePath: filePath + frameworkInfo })}\n`;
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
if (usage.length > 3) {
|
|
932
|
+
report += ` ${t('summary.usageReportMoreFiles', { count: usage.length - 3 })}\n`;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
report += `\n`;
|
|
936
|
+
});
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
// Dynamic keys
|
|
940
|
+
if (dynamicKeys.length > 0) {
|
|
941
|
+
report += `${t('summary.usageReportDynamicTranslationKeys')}\n`;
|
|
942
|
+
report += `${'='.repeat(50)}\n`;
|
|
943
|
+
report += `${t('summary.usageReportDynamicKeysDescription')}\n\n`;
|
|
944
|
+
|
|
945
|
+
dynamicKeys.forEach(key => {
|
|
946
|
+
const complexity = this.keyComplexity && this.keyComplexity.get(key);
|
|
947
|
+
const complexityLevel = complexity ? ` (${complexity.level})` : '';
|
|
948
|
+
report += `${t('summary.usageReportDynamicKey', { key: key + complexityLevel })}\n`;
|
|
949
|
+
|
|
950
|
+
// Show where it's used
|
|
951
|
+
const usage = this.findKeyUsage(key);
|
|
952
|
+
usage.slice(0, 2).forEach(({ filePath }) => {
|
|
953
|
+
const framework = this.frameworkUsage && this.frameworkUsage.get(filePath);
|
|
954
|
+
const frameworkInfo = framework ? ` [${framework.framework}]` : '';
|
|
955
|
+
report += ` ${t('summary.usageReportUsedIn', { filePath: filePath + frameworkInfo })}\n`;
|
|
956
|
+
});
|
|
957
|
+
|
|
958
|
+
report += `\n`;
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
// Placeholder validation results
|
|
963
|
+
const placeholderValidations = [];
|
|
964
|
+
Object.entries(this.translationsByLanguage || {}).forEach(([lang, translations]) => {
|
|
965
|
+
Object.entries(translations).forEach(([key, value]) => {
|
|
966
|
+
if (this.validatePlaceholderKeys) {
|
|
967
|
+
const validation = this.validatePlaceholderKeys(key, value);
|
|
968
|
+
if (validation.hasPlaceholders) {
|
|
969
|
+
placeholderValidations.push({ lang, key, validation });
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
});
|
|
973
|
+
});
|
|
974
|
+
|
|
975
|
+
if (placeholderValidations.length > 0) {
|
|
976
|
+
report += `š§ Placeholder Validation Results:\n`;
|
|
977
|
+
placeholderValidations.forEach(({ lang, key, validation }) => {
|
|
978
|
+
const status = validation.isValid ? 'ā
' : 'ā';
|
|
979
|
+
report += ` ${status} ${lang}.${key}: ${validation.placeholders.join(', ')}\n`;
|
|
980
|
+
if (!validation.isValid) {
|
|
981
|
+
validation.errors.forEach(error => {
|
|
982
|
+
report += ` - ${error}\n`;
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
});
|
|
986
|
+
report += `\n`;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// File usage breakdown with framework info
|
|
990
|
+
report += `${t('summary.usageReportFileUsageBreakdown')}\n`;
|
|
991
|
+
report += `${'='.repeat(50)}\n`;
|
|
992
|
+
|
|
993
|
+
const sortedFiles = Array.from(this.fileUsage.entries())
|
|
994
|
+
.sort(([,a], [,b]) => b.length - a.length)
|
|
995
|
+
.slice(0, 20);
|
|
996
|
+
|
|
997
|
+
sortedFiles.forEach(([filePath, keys]) => {
|
|
998
|
+
const framework = this.frameworkUsage && this.frameworkUsage.get(filePath);
|
|
999
|
+
const frameworkInfo = framework ? ` [${framework.framework}]` : '';
|
|
1000
|
+
report += `${t('summary.usageReportFileUsage', { filePath: filePath + frameworkInfo, count: keys.length })}\n`;
|
|
1001
|
+
});
|
|
1002
|
+
|
|
1003
|
+
if (this.fileUsage.size > 20) {
|
|
1004
|
+
report += `${t('summary.usageReportMoreFiles', { count: this.fileUsage.size - 20 })}\n`;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
return report;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
// Save report to file
|
|
1011
|
+
async saveReport(report, outputDir = './i18ntk-reports/usage') {
|
|
1012
|
+
try {
|
|
1013
|
+
// Ensure output directory exists
|
|
1014
|
+
if (!SecurityUtils.safeExistsSync(outputDir)) {
|
|
1015
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
1019
|
+
const filename = `usage-analysis-${timestamp}.txt`;
|
|
1020
|
+
const filepath = path.join(outputDir, filename);
|
|
1021
|
+
|
|
1022
|
+
await SecurityUtils.safeWriteFile(filepath, report);
|
|
1023
|
+
console.log(t('usage.reportSavedTo', { reportPath: filepath }));
|
|
1024
|
+
return filepath;
|
|
1025
|
+
} catch (error) {
|
|
1026
|
+
console.error(t('usage.failedToSaveReport', { error: error.message }));
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// Enhanced placeholder key detection with validation
|
|
1031
|
+
validatePlaceholderKeys(key, value) {
|
|
1032
|
+
if (typeof value !== 'string') {
|
|
1033
|
+
return {
|
|
1034
|
+
key,
|
|
1035
|
+
hasPlaceholders: false,
|
|
1036
|
+
placeholders: [],
|
|
1037
|
+
isValid: true,
|
|
1038
|
+
errors: []
|
|
1039
|
+
};
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
const placeholderRegex = /\{\{[^}]+\}\}|\{[^}]+\}|\$\{[^}]+\}/g;
|
|
1043
|
+
const placeholders = (typeof value === 'string' ? value : String(value || '')).match(placeholderRegex) || [];
|
|
1044
|
+
|
|
1045
|
+
const validation = {
|
|
1046
|
+
key,
|
|
1047
|
+
hasPlaceholders: placeholders.length > 0,
|
|
1048
|
+
placeholders,
|
|
1049
|
+
isValid: true,
|
|
1050
|
+
errors: []
|
|
1051
|
+
};
|
|
1052
|
+
|
|
1053
|
+
// Check for common placeholder issues
|
|
1054
|
+
placeholders.forEach(placeholder => {
|
|
1055
|
+
if (typeof placeholder === 'string') {
|
|
1056
|
+
if (placeholder.includes('undefined') || placeholder.includes('null')) {
|
|
1057
|
+
validation.isValid = false;
|
|
1058
|
+
validation.errors.push(`Invalid placeholder: ${placeholder}`);
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
// Check for matching opening/closing brackets
|
|
1062
|
+
const placeholderStr = String(placeholder || '');
|
|
1063
|
+
const openCount = (placeholderStr.match(/\{/g) || []).length;
|
|
1064
|
+
const closeCount = (placeholderStr.match(/\}/g) || []).length;
|
|
1065
|
+
if (openCount !== closeCount) {
|
|
1066
|
+
validation.isValid = false;
|
|
1067
|
+
validation.errors.push(`Mismatched brackets in: ${placeholder}`);
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
});
|
|
1071
|
+
|
|
1072
|
+
return validation;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
// Framework-specific pattern recognition
|
|
1076
|
+
detectFrameworkPatterns(content, filePath) {
|
|
1077
|
+
const frameworkPatterns = {
|
|
1078
|
+
react: {
|
|
1079
|
+
patterns: [
|
|
1080
|
+
/useTranslation\(\)/g,
|
|
1081
|
+
/Trans\s+component/g,
|
|
1082
|
+
/i18nKey\s*=/g,
|
|
1083
|
+
/withTranslation\(/g
|
|
1084
|
+
],
|
|
1085
|
+
score: 0
|
|
1086
|
+
},
|
|
1087
|
+
vue: {
|
|
1088
|
+
patterns: [
|
|
1089
|
+
/\$t\(/g,
|
|
1090
|
+
/this\.\$t\(/g,
|
|
1091
|
+
/v-t\s*=/g,
|
|
1092
|
+
/\$i18n/g
|
|
1093
|
+
],
|
|
1094
|
+
score: 0
|
|
1095
|
+
},
|
|
1096
|
+
angular: {
|
|
1097
|
+
patterns: [
|
|
1098
|
+
/translate\s*\|/g,
|
|
1099
|
+
/ngx-translate/g,
|
|
1100
|
+
/TranslateService/g,
|
|
1101
|
+
/\.instant\(/g
|
|
1102
|
+
],
|
|
1103
|
+
score: 0
|
|
1104
|
+
}
|
|
1105
|
+
};
|
|
1106
|
+
|
|
1107
|
+
const contentStr = String(content || '');
|
|
1108
|
+
Object.keys(frameworkPatterns).forEach(framework => {
|
|
1109
|
+
const config = frameworkPatterns[framework];
|
|
1110
|
+
config.patterns.forEach(pattern => {
|
|
1111
|
+
const matches = contentStr.match(pattern);
|
|
1112
|
+
if (matches) {
|
|
1113
|
+
config.score += matches.length;
|
|
1114
|
+
}
|
|
1115
|
+
});
|
|
1116
|
+
});
|
|
1117
|
+
|
|
1118
|
+
// Find dominant framework
|
|
1119
|
+
let dominantFramework = 'generic';
|
|
1120
|
+
let maxScore = 0;
|
|
1121
|
+
|
|
1122
|
+
Object.keys(frameworkPatterns).forEach(framework => {
|
|
1123
|
+
if (frameworkPatterns[framework].score > maxScore) {
|
|
1124
|
+
maxScore = frameworkPatterns[framework].score;
|
|
1125
|
+
dominantFramework = framework;
|
|
1126
|
+
}
|
|
1127
|
+
});
|
|
1128
|
+
|
|
1129
|
+
this.frameworkUsage.set(filePath, {
|
|
1130
|
+
framework: dominantFramework,
|
|
1131
|
+
score: maxScore,
|
|
1132
|
+
patterns: frameworkPatterns[dominantFramework]?.patterns || []
|
|
1133
|
+
});
|
|
1134
|
+
|
|
1135
|
+
return dominantFramework;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
// Advanced translation completeness scoring
|
|
1139
|
+
calculateTranslationScore(language, translations) {
|
|
1140
|
+
const score = {
|
|
1141
|
+
completeness: 0,
|
|
1142
|
+
quality: 0,
|
|
1143
|
+
consistency: 0,
|
|
1144
|
+
placeholderAccuracy: 0
|
|
1145
|
+
};
|
|
1146
|
+
|
|
1147
|
+
const totalKeys = Object.keys(translations).length;
|
|
1148
|
+
const translatedKeys = Object.keys(translations).filter(key =>
|
|
1149
|
+
translations[key] &&
|
|
1150
|
+
translations[key] !== 'NOT_TRANSLATED' &&
|
|
1151
|
+
translations[key] !== key
|
|
1152
|
+
).length;
|
|
1153
|
+
|
|
1154
|
+
score.completeness = totalKeys > 0 ? (translatedKeys / totalKeys) * 100 : 0;
|
|
1155
|
+
|
|
1156
|
+
// Quality scoring based on placeholder accuracy
|
|
1157
|
+
const placeholderScores = Object.entries(translations).map(([key, value]) => {
|
|
1158
|
+
const validation = this.validatePlaceholderKeys(key, value);
|
|
1159
|
+
return validation.isValid ? 1 : 0;
|
|
1160
|
+
});
|
|
1161
|
+
|
|
1162
|
+
score.placeholderAccuracy = placeholderScores.length > 0
|
|
1163
|
+
? (placeholderScores.reduce((sum, score) => sum + score, 0) / placeholderScores.length) * 100
|
|
1164
|
+
: 0;
|
|
1165
|
+
|
|
1166
|
+
score.quality = (score.completeness + score.placeholderAccuracy) / 2;
|
|
1167
|
+
|
|
1168
|
+
return score;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
// Key complexity analysis
|
|
1172
|
+
analyzeKeyComplexity(key) {
|
|
1173
|
+
// Ensure key is a string
|
|
1174
|
+
const keyStr = String(key || '');
|
|
1175
|
+
|
|
1176
|
+
const complexity = {
|
|
1177
|
+
level: 'simple',
|
|
1178
|
+
segments: keyStr.split('.').length,
|
|
1179
|
+
length: keyStr.length,
|
|
1180
|
+
hasPlaceholders: false,
|
|
1181
|
+
patterns: []
|
|
1182
|
+
};
|
|
1183
|
+
|
|
1184
|
+
if (complexity.segments > 3) complexity.level = 'complex';
|
|
1185
|
+
else if (complexity.segments > 1) complexity.level = 'moderate';
|
|
1186
|
+
|
|
1187
|
+
if (keyStr.includes('{{') || keyStr.includes('${')) {
|
|
1188
|
+
complexity.hasPlaceholders = true;
|
|
1189
|
+
complexity.level = 'complex';
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
this.keyComplexity.set(keyStr, complexity);
|
|
1193
|
+
return complexity;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
// Main analysis process
|
|
1197
|
+
async run(options = {}) {
|
|
1198
|
+
const { fromMenu = false } = options;
|
|
1199
|
+
|
|
1200
|
+
// Parse command line arguments for strict/debug flags
|
|
1201
|
+
const args = await this.parseArgs();
|
|
1202
|
+
const cliOptions = {
|
|
1203
|
+
strict: process.argv.includes('--strict'),
|
|
1204
|
+
debug: process.argv.includes('--debug')
|
|
1205
|
+
};
|
|
1206
|
+
|
|
1207
|
+
if (cliOptions.debug) {
|
|
1208
|
+
console.log('š Debug mode enabled');
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
try {
|
|
1212
|
+
// Ensure config is always initialized
|
|
1213
|
+
if (!this.config) {
|
|
1214
|
+
this.config = {};
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
// Ensure configuration is loaded - no need for .i18ntk directory check
|
|
1218
|
+
if (!this.config) {
|
|
1219
|
+
this.config = {};
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
// Initialize configuration properly when called from menu
|
|
1223
|
+
if (fromMenu && !this.sourceDir) {
|
|
1224
|
+
const baseConfig = await getUnifiedConfig('usage', args);
|
|
1225
|
+
this.config = { ...baseConfig, ...(this.config || {}) };
|
|
1226
|
+
|
|
1227
|
+
const uiLanguage = (this.config && this.config.uiLanguage) || 'en';
|
|
1228
|
+
loadTranslations(uiLanguage, path.resolve(__dirname, '..', '..', '..', 'resources', 'i18n', 'ui-locales'));
|
|
1229
|
+
if (!Array.isArray(this.config.translationPatterns)) {
|
|
1230
|
+
this.config.translationPatterns = [
|
|
1231
|
+
/t\(['"`]([^'"`]+)['"`]/g,
|
|
1232
|
+
/i18n\.t\(['"`]([^'"`]+)['"`]/g,
|
|
1233
|
+
/useTranslation\(\)\.t\(['"`]([^'"`]+)['"`]/g,
|
|
1234
|
+
/t\(`([^`]+)`\)/g,
|
|
1235
|
+
/i18nKey=['"`]([^'"`]+)['"`]/g,
|
|
1236
|
+
/\$t\(['"`]([^'"`]+)['"`]/g,
|
|
1237
|
+
/getTranslation\(['"`]([^'"`]+)['"`]/g
|
|
1238
|
+
];
|
|
1239
|
+
}
|
|
1240
|
+
if (!Array.isArray(this.config.excludeDirs)) {
|
|
1241
|
+
this.config.excludeDirs = ['node_modules', '.git'];
|
|
1242
|
+
}
|
|
1243
|
+
if (!Array.isArray(this.config.includeExtensions) && !Array.isArray(this.config.supportedExtensions)) {
|
|
1244
|
+
this.config.includeExtensions = ['.js', '.jsx', '.ts', '.tsx', '.py', '.pyx', '.pyi'];
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
this.sourceDir = this.config.sourceDir;
|
|
1248
|
+
this.i18nDir = this.config.i18nDir;
|
|
1249
|
+
this.sourceLanguageDir = path.join(this.i18nDir, this.config.sourceLanguage);
|
|
1250
|
+
if (fromMenu && (!this.config.sourceDir || this.config.sourceDir === this.config.i18nDir)) {
|
|
1251
|
+
console.log('ā ļø Go to Settings ā Directory Settings or run with --source-dir');
|
|
1252
|
+
}
|
|
1253
|
+
} else {
|
|
1254
|
+
await this.initialize();
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
// Skip admin authentication when called from menu
|
|
1258
|
+
if (!fromMenu) {
|
|
1259
|
+
const isCalledDirectly = require.main === module;
|
|
1260
|
+
if (isCalledDirectly && !args.noPrompt) {
|
|
1261
|
+
// Only check admin authentication when running directly and not in no-prompt mode
|
|
1262
|
+
const AdminAuth = require('../../../utils/admin-auth');
|
|
1263
|
+
const adminAuth = new AdminAuth();
|
|
1264
|
+
await adminAuth.initialize();
|
|
1265
|
+
|
|
1266
|
+
const isRequired = await adminAuth.isAuthRequired();
|
|
1267
|
+
if (isRequired) {
|
|
1268
|
+
console.log('\n' + t('adminCli.authRequiredForOperation', { operation: 'analyze usage' }));
|
|
1269
|
+
|
|
1270
|
+
const pin = await askHidden(t('adminCli.enterPin'));
|
|
1271
|
+
|
|
1272
|
+
const isValid = await adminAuth.verifyPin(pin);
|
|
1273
|
+
|
|
1274
|
+
if (!isValid) {
|
|
1275
|
+
console.log(t('adminCli.invalidPin'));
|
|
1276
|
+
if (!fromMenu) process.exit(1);
|
|
1277
|
+
return { success: false, error: 'Authentication failed' };
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
console.log(t('adminCli.authenticationSuccess'));
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
if (args.help) {
|
|
1286
|
+
this.showHelp();
|
|
1287
|
+
return;
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
// Override config with command line arguments
|
|
1291
|
+
if (args.sourceDir) {
|
|
1292
|
+
this.config.sourceDir = args.sourceDir;
|
|
1293
|
+
this.sourceDir = path.resolve(args.sourceDir);
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
if (args.i18nDir) {
|
|
1297
|
+
this.config.i18nDir = args.i18nDir;
|
|
1298
|
+
this.i18nDir = path.resolve(args.i18nDir);
|
|
1299
|
+
this.sourceLanguageDir = path.join(this.i18nDir, this.config.sourceLanguage);
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
if (this.sourceDir || this.i18nDir) {
|
|
1303
|
+
await configManager.updateConfig({
|
|
1304
|
+
sourceDir: configManager.toRelative(this.sourceDir || this.config.sourceDir),
|
|
1305
|
+
i18nDir: configManager.toRelative(this.i18nDir || this.config.i18nDir)
|
|
1306
|
+
});
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
// Ensure sourceDir points to source code, not locales
|
|
1310
|
+
if (!args.sourceDir && this.config.sourceDir === this.config.i18nDir) {
|
|
1311
|
+
// Default to common source directories if not explicitly provided
|
|
1312
|
+
const possibleSourceDirs = ['src', 'lib', 'app', 'source'];
|
|
1313
|
+
|
|
1314
|
+
const projectRoot = this.config.projectRoot || '.';
|
|
1315
|
+
|
|
1316
|
+
for (const dir of possibleSourceDirs) {
|
|
1317
|
+
const testPath = path.resolve(projectRoot, dir);
|
|
1318
|
+
if (SecurityUtils.safeExistsSync(testPath)) {
|
|
1319
|
+
this.config.sourceDir = testPath;
|
|
1320
|
+
this.sourceDir = testPath;
|
|
1321
|
+
break;
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
// If no common source directory found, use current directory
|
|
1326
|
+
if (this.config.sourceDir === this.config.i18nDir) {
|
|
1327
|
+
this.config.sourceDir = projectRoot;
|
|
1328
|
+
this.sourceDir = projectRoot;
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
// š§ prevent scanning locales as source
|
|
1333
|
+
if (path.resolve(this.sourceDir) === path.resolve(this.i18nDir)) {
|
|
1334
|
+
const fallback = path.resolve(this.config.projectRoot || '.', 'src');
|
|
1335
|
+
console.warn(t('usage.sourceEqualsI18nWarn') ||
|
|
1336
|
+
`ā ļø sourceDir equals i18nDir (${this.sourceDir}). Falling back to ${fallback} for source scanning.`);
|
|
1337
|
+
if (SecurityUtils.safeExistsSync(fallback)) {
|
|
1338
|
+
this.sourceDir = fallback;
|
|
1339
|
+
} else {
|
|
1340
|
+
console.warn(`ā ļø Fallback directory ${fallback} does not exist. Using project root for source scanning.`);
|
|
1341
|
+
this.sourceDir = path.resolve(this.config.projectRoot || '.');
|
|
1342
|
+
}
|
|
1343
|
+
this.config.sourceDir = this.sourceDir;
|
|
1344
|
+
await configManager.updateConfig({
|
|
1345
|
+
sourceDir: configManager.toRelative(this.sourceDir)
|
|
1346
|
+
});
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
console.log(t('usage.detectedSourceDirectory', { sourceDir: this.sourceDir }));
|
|
1350
|
+
console.log(t('usage.detectedI18nDirectory', { i18nDir: this.i18nDir }));
|
|
1351
|
+
|
|
1352
|
+
// Load available translation keys first
|
|
1353
|
+
await this.loadAvailableKeys();
|
|
1354
|
+
|
|
1355
|
+
// NEW: Detect framework patterns before analysis
|
|
1356
|
+
await this.detectFrameworkPatterns();
|
|
1357
|
+
|
|
1358
|
+
// Perform usage analysis with enhanced features
|
|
1359
|
+
await this.analyzeUsage();
|
|
1360
|
+
|
|
1361
|
+
// NEW: Validate placeholder keys
|
|
1362
|
+
await this.validatePlaceholderKeys();
|
|
1363
|
+
|
|
1364
|
+
// Analyze translation completeness with enhanced scoring
|
|
1365
|
+
await this.analyzeTranslationCompleteness();
|
|
1366
|
+
|
|
1367
|
+
// Calculate key complexity analysis
|
|
1368
|
+
await this.analyzeKeyComplexity();
|
|
1369
|
+
|
|
1370
|
+
// Generate and display results
|
|
1371
|
+
const unusedKeys = this.findUnusedKeys();
|
|
1372
|
+
const missingKeys = this.findMissingKeys();
|
|
1373
|
+
const notTranslatedStats = this.getNotTranslatedStats();
|
|
1374
|
+
|
|
1375
|
+
// Calculate performance metrics
|
|
1376
|
+
const duration = Date.now() - this.startTime;
|
|
1377
|
+
|
|
1378
|
+
console.log('\n' + t('usage.analysisResults'));
|
|
1379
|
+
console.log(' ' + t('usage.availableKeysCount', { count: this.availableKeys.size }));
|
|
1380
|
+
console.log(' ' + t('usage.usedKeysCount', { count: this.usedKeys.size }));
|
|
1381
|
+
console.log(t('usage.unusedKeysCount', { count: unusedKeys.length }));
|
|
1382
|
+
console.log(t('usage.missingKeysCount', { count: missingKeys.length }));
|
|
1383
|
+
console.log(t('usage.notTranslatedKeysTotal', { total: notTranslatedStats.total }));
|
|
1384
|
+
|
|
1385
|
+
// NEW: Display performance metrics
|
|
1386
|
+
console.log(`\nš Performance: ${duration}ms (${this.availableKeys.size} keys processed)`);
|
|
1387
|
+
|
|
1388
|
+
// NEW: Display framework usage
|
|
1389
|
+
if (this.frameworkUsage.size > 0) {
|
|
1390
|
+
console.log('\nš ļø Framework Detection:');
|
|
1391
|
+
const frameworkCounts = new Map();
|
|
1392
|
+
|
|
1393
|
+
// Aggregate framework counts
|
|
1394
|
+
for (const [filePath, frameworkInfo] of this.frameworkUsage) {
|
|
1395
|
+
const frameworkName = frameworkInfo.framework || 'generic';
|
|
1396
|
+
frameworkCounts.set(frameworkName, (frameworkCounts.get(frameworkName) || 0) + 1);
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
if (frameworkCounts.size > 0) {
|
|
1400
|
+
for (const [framework, count] of frameworkCounts) {
|
|
1401
|
+
console.log(` ${framework}: ${count} files`);
|
|
1402
|
+
}
|
|
1403
|
+
} else {
|
|
1404
|
+
console.log(' No Framework: 0 files');
|
|
1405
|
+
}
|
|
1406
|
+
} else {
|
|
1407
|
+
console.log('\nš ļø Framework Detection:');
|
|
1408
|
+
console.log(' No Framework: 0 files');
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
// NEW: Display key complexity analysis
|
|
1412
|
+
const complexityValues = Array.from(this.keyComplexity.values()).map(c => c.segments || 0);
|
|
1413
|
+
const avgComplexity = complexityValues.length > 0 ?
|
|
1414
|
+
complexityValues.reduce((a, b) => a + b, 0) / complexityValues.length : 0;
|
|
1415
|
+
console.log(`\nš Key Complexity: ${avgComplexity.toFixed(2)} avg depth`);
|
|
1416
|
+
|
|
1417
|
+
// Sanity check: warn if 0 used keys but available keys exist
|
|
1418
|
+
if (this.availableKeys.size > 0 && this.usedKeys.size === 0) {
|
|
1419
|
+
console.warn('\nā ļø ' + (t('operations.usage.noUsedKeysHint') || 'Found translations but no usage in source. Check --source-dir and translationPatterns.'));
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
// Display translation completeness by language with enhanced scoring
|
|
1423
|
+
console.log(t('common.languageCompletenessTitle'));
|
|
1424
|
+
for (const [language, stats] of this.translationStats) {
|
|
1425
|
+
const completeness = ((stats.translated / stats.total) * 100).toFixed(1);
|
|
1426
|
+
const score = this.calculateTranslationScore(language, stats);
|
|
1427
|
+
console.log(t('summary.usageReportLanguageCompleteness', {
|
|
1428
|
+
language: language.toUpperCase(),
|
|
1429
|
+
completeness,
|
|
1430
|
+
translated: stats.translated,
|
|
1431
|
+
total: stats.total
|
|
1432
|
+
}));
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
if (args.outputReport) {
|
|
1436
|
+
const report = this.generateUsageReport();
|
|
1437
|
+
await this.saveReport(report, args.outputDir);
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
console.log('\n' + t('usage.analysisCompletedSuccessfully'));
|
|
1441
|
+
|
|
1442
|
+
if (require.main === module && !args.noPrompt) {
|
|
1443
|
+
await this.prompt('\nPress Enter to continue...');
|
|
1444
|
+
}
|
|
1445
|
+
this.closeReadline();
|
|
1446
|
+
|
|
1447
|
+
} catch (error) {
|
|
1448
|
+
console.error(t('usage.analysisFailedError'), error.message);
|
|
1449
|
+
this.closeReadline();
|
|
1450
|
+
SecurityUtils.logSecurityEvent(t('usage.usageAnalysisFailed'), {
|
|
1451
|
+
component: 'i18ntk-usage',
|
|
1452
|
+
error: error.message
|
|
1453
|
+
});
|
|
1454
|
+
throw error;
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
// Show help message
|
|
1459
|
+
showHelp() {
|
|
1460
|
+
console.log(`
|
|
1461
|
+
š i18ntk usage - Translation key usage analysis (v1.8.3)
|
|
1462
|
+
|
|
1463
|
+
Usage:
|
|
1464
|
+
node i18ntk-usage.js [options]
|
|
1465
|
+
npm run i18ntk:usage -- [options]
|
|
1466
|
+
|
|
1467
|
+
Options:
|
|
1468
|
+
--source-dir=<path> Source code directory to scan (default: ./src)
|
|
1469
|
+
--i18n-dir=<path> Directory containing translation files (default: ./src/i18n/locales)
|
|
1470
|
+
--output-report Generate detailed usage report
|
|
1471
|
+
--output-dir=<path> Directory for output reports (default: ./i18ntk-reports/usage)
|
|
1472
|
+
--strict Show all warnings and errors during analysis
|
|
1473
|
+
--debug Enable debug mode with stack traces
|
|
1474
|
+
--no-prompt Skip interactive prompts (useful for CI/CD)
|
|
1475
|
+
--validate-placeholders Enable placeholder key validation
|
|
1476
|
+
--framework-detect Enable framework-specific pattern detection
|
|
1477
|
+
--performance-mode Enable performance metrics tracking
|
|
1478
|
+
--help, -h Show this help message
|
|
1479
|
+
|
|
1480
|
+
Examples:
|
|
1481
|
+
node i18ntk-usage.js --source-dir=./src --i18n-dir=./translations --output-report
|
|
1482
|
+
npm run i18ntk:usage -- --strict --debug --validate-placeholders
|
|
1483
|
+
node i18ntk-usage.js --no-prompt --performance-mode --output-dir=./reports
|
|
1484
|
+
|
|
1485
|
+
Analysis Features (v1.8.3):
|
|
1486
|
+
⢠Detects unused translation keys
|
|
1487
|
+
⢠Identifies missing translation keys
|
|
1488
|
+
⢠Shows translation completeness by language
|
|
1489
|
+
⢠Reports NOT_TRANSLATED values
|
|
1490
|
+
⢠Supports modular folder structures
|
|
1491
|
+
⢠Enhanced placeholder key detection
|
|
1492
|
+
⢠Framework-specific pattern recognition (React, Vue, Angular)
|
|
1493
|
+
⢠Advanced translation completeness scoring
|
|
1494
|
+
⢠Performance metrics and optimization tracking
|
|
1495
|
+
⢠Key complexity analysis
|
|
1496
|
+
⢠Security-enhanced path validation
|
|
1497
|
+
⢠Detailed reporting with validation errors
|
|
1498
|
+
`);
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
module.exports = UsageService;
|