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,654 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* I18NTK SCANNER COMMAND
|
|
5
|
+
*
|
|
6
|
+
* Handles scanning functionality for translation keys.
|
|
7
|
+
* Contains embedded business logic from I18nTextScanner.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const { getUnifiedConfig, displayHelp } = require('../../../utils/config-helper');
|
|
13
|
+
const { loadTranslations } = require('../../../utils/i18n-helper');
|
|
14
|
+
const SecurityUtils = require('../../../utils/security');
|
|
15
|
+
const SetupEnforcer = require('../../../utils/setup-enforcer');
|
|
16
|
+
|
|
17
|
+
class ScannerCommand {
|
|
18
|
+
constructor(config = {}, ui = null) {
|
|
19
|
+
this.config = config;
|
|
20
|
+
this.ui = ui;
|
|
21
|
+
this.prompt = null;
|
|
22
|
+
this.isNonInteractiveMode = false;
|
|
23
|
+
this.safeClose = null;
|
|
24
|
+
|
|
25
|
+
// Initialize scanner properties
|
|
26
|
+
this.sourceDir = null;
|
|
27
|
+
this.patterns = [];
|
|
28
|
+
this.exclusions = [];
|
|
29
|
+
this.locale = this.loadLocale();
|
|
30
|
+
this.results = [];
|
|
31
|
+
this.framework = null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Set runtime dependencies for interactive operations
|
|
36
|
+
*/
|
|
37
|
+
setRuntimeDependencies(prompt, isNonInteractiveMode, safeClose) {
|
|
38
|
+
this.prompt = prompt;
|
|
39
|
+
this.isNonInteractiveMode = isNonInteractiveMode;
|
|
40
|
+
this.safeClose = safeClose;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
loadLocale() {
|
|
44
|
+
const uiLocalesDir = path.join(__dirname, '../../../resources', 'i18n', 'ui-locales');
|
|
45
|
+
const localeFile = path.join(uiLocalesDir, 'en.json');
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const localeContent = SecurityUtils.safeReadFileSync(localeFile, uiLocalesDir, 'utf8');
|
|
49
|
+
return SecurityUtils.safeParseJSON(localeContent);
|
|
50
|
+
} catch (error) {
|
|
51
|
+
return {
|
|
52
|
+
scanner: {
|
|
53
|
+
help_options: {
|
|
54
|
+
source_dir: "Source directory to scan (default: ./src)",
|
|
55
|
+
framework: "Framework type: react, vue, angular, vanilla (auto-detected)",
|
|
56
|
+
patterns: "Custom patterns to match (comma-separated)",
|
|
57
|
+
exclude: "Exclude patterns (comma-separated)",
|
|
58
|
+
output_report: "Generate detailed report",
|
|
59
|
+
output_dir: "Report output directory (default: ./reports)",
|
|
60
|
+
min_length: "Minimum text length to consider (default: 3)",
|
|
61
|
+
max_length: "Maximum text length to consider (default: 100)",
|
|
62
|
+
include_tests: "Include test files in scan"
|
|
63
|
+
},
|
|
64
|
+
starting: "🔍 Starting text analysis for {framework} project...",
|
|
65
|
+
sourceDirectory: "📁 Source directory: {sourceDir}",
|
|
66
|
+
framework: "🏗️ Framework: {framework}",
|
|
67
|
+
scanningFiles: "📊 Scanning {count} files...",
|
|
68
|
+
foundText: "📝 Found {count} potential hardcoded text instances",
|
|
69
|
+
reportGenerated: "📊 Report generated: {path}",
|
|
70
|
+
noTextFound: "✅ No hardcoded text found!",
|
|
71
|
+
analysisTitle: "🔍 TEXT ANALYSIS RESULTS",
|
|
72
|
+
summary: {
|
|
73
|
+
totalFiles: "📄 Total files scanned: {count}",
|
|
74
|
+
textInstances: "📝 Text instances found: {count}",
|
|
75
|
+
filesWithText: "📂 Files with hardcoded text: {count}",
|
|
76
|
+
framework: "🏗️ Framework detected: {framework}"
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
t(key, params = {}) {
|
|
84
|
+
const keyStr = String(key || '');
|
|
85
|
+
const keys = keyStr.split('.');
|
|
86
|
+
let value = this.locale;
|
|
87
|
+
|
|
88
|
+
for (const k of keys) {
|
|
89
|
+
value = value?.[k];
|
|
90
|
+
if (value === undefined) break;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (typeof value !== 'string') {
|
|
94
|
+
return key;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return value.replace(/\{([^}]+)\}/g, (match, param) => {
|
|
98
|
+
return params[param] !== undefined ? params[param] : match;
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
parseArgs() {
|
|
103
|
+
const args = process.argv.slice(2);
|
|
104
|
+
const parsed = {};
|
|
105
|
+
|
|
106
|
+
args.forEach(arg => {
|
|
107
|
+
if (arg.startsWith('--')) {
|
|
108
|
+
const [key, ...valueParts] = arg.substring(2).split('=');
|
|
109
|
+
const value = valueParts.join('=');
|
|
110
|
+
|
|
111
|
+
switch (key) {
|
|
112
|
+
case 'source-dir':
|
|
113
|
+
parsed.sourceDir = value || '';
|
|
114
|
+
break;
|
|
115
|
+
case 'framework':
|
|
116
|
+
parsed.framework = value || '';
|
|
117
|
+
break;
|
|
118
|
+
case 'patterns':
|
|
119
|
+
parsed.patterns = value ? value.split(',').map(p => p.trim()).filter(Boolean) : [];
|
|
120
|
+
break;
|
|
121
|
+
case 'exclude':
|
|
122
|
+
parsed.exclude = value ? value.split(',').map(e => e.trim()).filter(Boolean) : [];
|
|
123
|
+
break;
|
|
124
|
+
case 'output-dir':
|
|
125
|
+
parsed.outputDir = value || '';
|
|
126
|
+
break;
|
|
127
|
+
case 'min-length':
|
|
128
|
+
parsed.minLength = parseInt(value) || 3;
|
|
129
|
+
break;
|
|
130
|
+
case 'max-length':
|
|
131
|
+
parsed.maxLength = parseInt(value) || 100;
|
|
132
|
+
break;
|
|
133
|
+
case 'output-report':
|
|
134
|
+
parsed.outputReport = true;
|
|
135
|
+
break;
|
|
136
|
+
case 'include-tests':
|
|
137
|
+
parsed.includeTests = true;
|
|
138
|
+
break;
|
|
139
|
+
case 'help':
|
|
140
|
+
case 'h':
|
|
141
|
+
parsed.help = true;
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
return parsed;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
detectFramework(projectRoot) {
|
|
151
|
+
const packagePath = path.join(projectRoot, 'package.json');
|
|
152
|
+
|
|
153
|
+
// Check for Python frameworks
|
|
154
|
+
const requirementsPath = path.join(projectRoot, 'requirements.txt');
|
|
155
|
+
const setupPath = path.join(projectRoot, 'setup.py');
|
|
156
|
+
const pyprojectPath = path.join(projectRoot, 'pyproject.toml');
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
// Check Python frameworks first
|
|
160
|
+
if (SecurityUtils.safeExistsSync(requirementsPath, projectRoot)) {
|
|
161
|
+
const requirements = SecurityUtils.safeReadFileSync(requirementsPath, projectRoot, 'utf8');
|
|
162
|
+
if (requirements.includes('Django')) return 'django';
|
|
163
|
+
if (requirements.includes('Flask') || requirements.includes('flask-babel')) return 'flask';
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (SecurityUtils.safeExistsSync(setupPath, projectRoot)) {
|
|
167
|
+
const setup = SecurityUtils.safeReadFileSync(setupPath, projectRoot, 'utf8');
|
|
168
|
+
if (setup.includes('Django')) return 'django';
|
|
169
|
+
if (setup.includes('Flask')) return 'flask';
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (SecurityUtils.safeExistsSync(pyprojectPath, projectRoot)) {
|
|
173
|
+
const pyproject = SecurityUtils.safeReadFileSync(pyprojectPath, projectRoot, 'utf8');
|
|
174
|
+
if (pyproject.includes('Django')) return 'django';
|
|
175
|
+
if (pyproject.includes('Flask')) return 'flask';
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Check for Python files
|
|
179
|
+
const hasPythonFiles = fs.readdirSync(projectRoot, { recursive: true })
|
|
180
|
+
.some(file => file.endsWith && file.endsWith('.py'));
|
|
181
|
+
if (hasPythonFiles) return 'python';
|
|
182
|
+
} catch (error) {
|
|
183
|
+
// Continue to JS frameworks
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
const packageJsonContent = SecurityUtils.safeReadFileSync(packagePath, projectRoot, 'utf8');
|
|
188
|
+
const packageJson = SecurityUtils.safeParseJSON(packageJsonContent);
|
|
189
|
+
const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };
|
|
190
|
+
|
|
191
|
+
if (deps.react || deps['react-dom']) return 'react';
|
|
192
|
+
if (deps.vue || deps['vue-router']) return 'vue';
|
|
193
|
+
if (deps['@angular/core'] || deps.angular) return 'angular';
|
|
194
|
+
if (deps.next) return 'next';
|
|
195
|
+
if (deps.svelte) return 'svelte';
|
|
196
|
+
|
|
197
|
+
return 'vanilla';
|
|
198
|
+
} catch (error) {
|
|
199
|
+
return 'vanilla';
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
getFrameworkPatterns(framework) {
|
|
204
|
+
const basePatterns = [
|
|
205
|
+
// String literals in JSX/TSX - enhanced for Unicode
|
|
206
|
+
/(?<![\w])["'`]([^"'`]{2,99})["'`]/g,
|
|
207
|
+
// Template literals - enhanced for Unicode
|
|
208
|
+
/`([^`]{2,99})`/g,
|
|
209
|
+
// Text content in HTML - enhanced for Unicode
|
|
210
|
+
/>([^<]{2,99})</g,
|
|
211
|
+
// Title attributes - enhanced for Unicode
|
|
212
|
+
/title=["']([^"']{2,99})["']/g,
|
|
213
|
+
// Alt attributes - enhanced for Unicode
|
|
214
|
+
/alt=["']([^"']{2,99})["']/g,
|
|
215
|
+
// Placeholder attributes - enhanced for Unicode
|
|
216
|
+
/placeholder=["']([^"']{2,99})["']/g
|
|
217
|
+
];
|
|
218
|
+
|
|
219
|
+
const frameworkSpecific = {
|
|
220
|
+
react: [
|
|
221
|
+
// React specific patterns - enhanced for i18next detection
|
|
222
|
+
/children:\s*["']([^"']{2,99})["']/g,
|
|
223
|
+
/dangerouslySetInnerHTML={{\s*__html:\s*["']([^"']{2,99})["']/g,
|
|
224
|
+
// JSX text content without translation
|
|
225
|
+
/>([^<{][^<>{]*[^}>])</g,
|
|
226
|
+
// Button text
|
|
227
|
+
/<button[^>]*>([^<]{2,99})<\/button>/g,
|
|
228
|
+
// Span text
|
|
229
|
+
/<span[^>]*>([^<]{2,99})<\/span>/g
|
|
230
|
+
],
|
|
231
|
+
vue: [
|
|
232
|
+
// Vue specific patterns - enhanced for vue-i18n detection
|
|
233
|
+
/v-text=["']([^"']{2,99})["']/g,
|
|
234
|
+
/v-html=["']([^"']{2,99})["']/g,
|
|
235
|
+
// Vue template text
|
|
236
|
+
/>([^<{][^<>{]*[^}>])</g,
|
|
237
|
+
// Button text
|
|
238
|
+
/<button[^>]*>([^<]{2,99})<\/button>/g,
|
|
239
|
+
// Span text
|
|
240
|
+
/<span[^>]*>([^<]{2,99})<\/span>/g
|
|
241
|
+
],
|
|
242
|
+
angular: [
|
|
243
|
+
// Angular specific patterns - enhanced for ngx-translate detection
|
|
244
|
+
/\[innerHTML\]=["']([^"']{2,99})["']/g,
|
|
245
|
+
/\[textContent\]=["']([^"']{2,99})["']/g,
|
|
246
|
+
// Angular template text
|
|
247
|
+
/>([^<{][^<>{]*[^}>])</g,
|
|
248
|
+
// Button text
|
|
249
|
+
/<button[^>]*>([^<]{2,99})<\/button>/g,
|
|
250
|
+
// Span text
|
|
251
|
+
/<span[^>]*>([^<]{2,99})<\/span>/g
|
|
252
|
+
],
|
|
253
|
+
django: [
|
|
254
|
+
// Django template patterns
|
|
255
|
+
/\{\%\s*trans\s+["']([^"']{2,99})["']\s*%\}/g,
|
|
256
|
+
/\{\%\s*blocktrans\s*%\}([^%]{2,99})\{\%\s*endblocktrans\s*%\}/g,
|
|
257
|
+
/{{\s*_["']([^"']{2,99})["']\s*}}/g,
|
|
258
|
+
/{{\s*gettext\(["']([^"']{2,99})["']\)\s*}}/g
|
|
259
|
+
],
|
|
260
|
+
flask: [
|
|
261
|
+
// Flask/Jinja2 template patterns
|
|
262
|
+
/\{\{\s*_["']([^"']{2,99})["']\s*}}/g,
|
|
263
|
+
/\{\{\s*gettext\(["']([^"']{2,99})["']\)\s*}}/g,
|
|
264
|
+
/\{\{\s*lazy_gettext\(["']([^"']{2,99})["']\)\s*}}/g
|
|
265
|
+
],
|
|
266
|
+
python: [
|
|
267
|
+
// Python source patterns
|
|
268
|
+
/gettext\(["']([^"']{2,99})["']\)/g,
|
|
269
|
+
/_\(["']([^"']{2,99})["']\)/g,
|
|
270
|
+
/gettext_lazy\(["']([^"']{2,99})["']\)/g,
|
|
271
|
+
/lazy_gettext\(["']([^"']{2,99})["']\)/g
|
|
272
|
+
]
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
return [...basePatterns, ...(frameworkSpecific[framework] || [])];
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
shouldExcludeFile(filePath, exclusions) {
|
|
279
|
+
const fileName = path.basename(filePath);
|
|
280
|
+
return exclusions.some(pattern => {
|
|
281
|
+
if (pattern.includes('*')) {
|
|
282
|
+
const regex = new RegExp(pattern.replace(/\*/g, '.*'));
|
|
283
|
+
return regex.test(fileName) || regex.test(filePath);
|
|
284
|
+
}
|
|
285
|
+
return fileName.includes(pattern) || filePath.includes(pattern);
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
isEnglishText(text) {
|
|
290
|
+
// Enhanced text detection for Unicode and multilingual support
|
|
291
|
+
const trimmed = text.trim();
|
|
292
|
+
if (trimmed.length < 3) return false;
|
|
293
|
+
|
|
294
|
+
// Skip if it's just numbers or special characters
|
|
295
|
+
if (/^\d+$/.test(trimmed)) return false;
|
|
296
|
+
if (/^[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>?]+$/.test(trimmed)) return false;
|
|
297
|
+
|
|
298
|
+
// Allow Unicode characters including CJK, Cyrillic, etc.
|
|
299
|
+
const validChars = trimmed.match(/[\p{L}\p{N}\s\-,.!?':"()\[\]{}]/gu) || [];
|
|
300
|
+
const validRatio = validChars.length / trimmed.length;
|
|
301
|
+
|
|
302
|
+
// Must have at least 50% valid characters and some alphabetic characters
|
|
303
|
+
const hasAlpha = /[a-zA-Z\u00C0-\u024F\u1E00-\u1EFF\u0400-\u04FF\u4E00-\u9FFF\uAC00-\uD7AF]/u.test(trimmed);
|
|
304
|
+
|
|
305
|
+
return validRatio >= 0.5 && hasAlpha;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
scanFile(filePath, patterns, minLength, maxLength) {
|
|
309
|
+
try {
|
|
310
|
+
const content = SecurityUtils.safeReadFileSync(filePath, path.dirname(filePath), 'utf8');
|
|
311
|
+
const lines = content.split('\n');
|
|
312
|
+
const results = [];
|
|
313
|
+
|
|
314
|
+
patterns.forEach(pattern => {
|
|
315
|
+
let match;
|
|
316
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
317
|
+
const text = match[1] || match[0];
|
|
318
|
+
|
|
319
|
+
// Skip translation function calls
|
|
320
|
+
const beforeMatch = content.substring(Math.max(0, match.index - 20), match.index);
|
|
321
|
+
if (beforeMatch.includes('t(') || beforeMatch.includes('i18next.t(') ||
|
|
322
|
+
beforeMatch.includes('$t(') || beforeMatch.includes('translate(')) {
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (text && this.isEnglishText(text) &&
|
|
327
|
+
text.length >= minLength && text.length <= maxLength) {
|
|
328
|
+
|
|
329
|
+
const lineNumber = content.substring(0, match.index).split('\n').length;
|
|
330
|
+
const lineContent = lines[lineNumber - 1] || '';
|
|
331
|
+
|
|
332
|
+
results.push({
|
|
333
|
+
text: text.trim(),
|
|
334
|
+
line: lineNumber,
|
|
335
|
+
column: match.index - content.lastIndexOf('\n', match.index),
|
|
336
|
+
context: lineContent.trim(),
|
|
337
|
+
pattern: pattern.toString(),
|
|
338
|
+
suggestion: this.generateSuggestion(text)
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
return results;
|
|
345
|
+
} catch (error) {
|
|
346
|
+
console.warn(`Warning: Could not read file ${filePath}: ${error.message}`);
|
|
347
|
+
return [];
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
generateSuggestion(text) {
|
|
352
|
+
const key = text.toLowerCase()
|
|
353
|
+
.replace(/[^a-z0-9\s]/g, '')
|
|
354
|
+
.replace(/\s+/g, '_')
|
|
355
|
+
.substring(0, 50);
|
|
356
|
+
|
|
357
|
+
return {
|
|
358
|
+
key: `ui.${key}`,
|
|
359
|
+
original: text,
|
|
360
|
+
translationKey: `t('ui.${key}')`,
|
|
361
|
+
frameworkSpecific: this.getFrameworkSpecific(text)
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
getFrameworkSpecific(text) {
|
|
366
|
+
const frameworks = {
|
|
367
|
+
react: {
|
|
368
|
+
hook: `const { t } = useTranslation();`,
|
|
369
|
+
usage: `{t('ui.${text.toLowerCase().replace(/[^a-z0-9\s]/g, '').replace(/\s+/g, '_')}')}`,
|
|
370
|
+
component: `<Trans i18nKey="ui.${text.toLowerCase().replace(/[^a-z0-9\s]/g, '').replace(/\s+/g, '_')}">${text}</Trans>`
|
|
371
|
+
},
|
|
372
|
+
vue: {
|
|
373
|
+
directive: `{{ $t('ui.${text.toLowerCase().replace(/[^a-z0-9\s]/g, '').replace(/\s+/g, '_')}') }}`,
|
|
374
|
+
method: `this.$t('ui.${text.toLowerCase().replace(/[^a-z0-9\s]/g, '').replace(/\s+/g, '_')}')`
|
|
375
|
+
},
|
|
376
|
+
angular: {
|
|
377
|
+
pipe: `{{ '${text}' | translate }}`,
|
|
378
|
+
service: `this.translateService.instant('ui.${text.toLowerCase().replace(/[^a-z0-9\s]/g, '').replace(/\s+/g, '_')}')`
|
|
379
|
+
},
|
|
380
|
+
django: {
|
|
381
|
+
template: `{% trans '${text}' %}`,
|
|
382
|
+
python: `from django.utils.translation import gettext as _\n_('${text}')`,
|
|
383
|
+
model: `from django.utils.translation import gettext_lazy as _\n_('${text}')`
|
|
384
|
+
},
|
|
385
|
+
flask: {
|
|
386
|
+
template: `{{ _('${text}') }}`,
|
|
387
|
+
python: `from flask_babel import gettext as _\n_('${text}')`,
|
|
388
|
+
lazy: `from flask_babel import lazy_gettext as _\n_('${text}')`
|
|
389
|
+
},
|
|
390
|
+
python: {
|
|
391
|
+
gettext: `import gettext\ngettext.gettext('${text}')`,
|
|
392
|
+
underscore: `from gettext import gettext as _\n_('${text}')`,
|
|
393
|
+
lazy: `from gettext import gettext_lazy as _\n_('${text}')`
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
return frameworks[this.framework] || frameworks.vanilla;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
async scanDirectory(dir, options = {}) {
|
|
401
|
+
const {
|
|
402
|
+
patterns = [],
|
|
403
|
+
exclusions = [],
|
|
404
|
+
minLength = 3,
|
|
405
|
+
maxLength = 100,
|
|
406
|
+
includeTests = false
|
|
407
|
+
} = options;
|
|
408
|
+
|
|
409
|
+
if (!SecurityUtils.safeExistsSync(dir, path.dirname(dir))) {
|
|
410
|
+
throw new Error(`Directory does not exist: ${dir}`);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const allResults = [];
|
|
414
|
+
const extensions = ['.js', '.jsx', '.ts', '.tsx', '.vue', '.html', '.svelte', '.py', '.pyx', '.pyi'];
|
|
415
|
+
|
|
416
|
+
const scanRecursive = (currentDir) => {
|
|
417
|
+
const items = fs.readdirSync(currentDir);
|
|
418
|
+
|
|
419
|
+
for (const item of items) {
|
|
420
|
+
const fullPath = path.join(currentDir, item);
|
|
421
|
+
const stat = fs.statSync(fullPath);
|
|
422
|
+
|
|
423
|
+
if (stat.isDirectory()) {
|
|
424
|
+
if (!item.startsWith('.') && !this.shouldExcludeFile(fullPath, exclusions)) {
|
|
425
|
+
scanRecursive(fullPath);
|
|
426
|
+
}
|
|
427
|
+
} else if (stat.isFile()) {
|
|
428
|
+
const ext = path.extname(item);
|
|
429
|
+
if (extensions.includes(ext) && !this.shouldExcludeFile(fullPath, exclusions)) {
|
|
430
|
+
if (!includeTests && (item.includes('.test.') || item.includes('.spec.'))) {
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const results = this.scanFile(fullPath, patterns, minLength, maxLength);
|
|
435
|
+
if (results.length > 0) {
|
|
436
|
+
allResults.push({
|
|
437
|
+
file: fullPath,
|
|
438
|
+
results
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
scanRecursive(dir);
|
|
447
|
+
return allResults;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
async generateReport(results, outputDir) {
|
|
451
|
+
if (!SecurityUtils.safeExistsSync(outputDir, path.dirname(outputDir))) {
|
|
452
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
456
|
+
const reportFile = path.join(outputDir, `text-analysis-${timestamp}.json`);
|
|
457
|
+
const summaryFile = path.join(outputDir, `text-analysis-${timestamp}.md`);
|
|
458
|
+
|
|
459
|
+
const summary = {
|
|
460
|
+
totalFiles: results.length,
|
|
461
|
+
totalInstances: results.reduce((sum, file) => sum + file.results.length, 0),
|
|
462
|
+
filesWithText: results.length,
|
|
463
|
+
framework: this.framework,
|
|
464
|
+
timestamp: new Date().toISOString(),
|
|
465
|
+
results
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
// JSON report
|
|
469
|
+
SecurityUtils.safeWriteFileSync(reportFile, JSON.stringify(summary, null, 2), outputDir);
|
|
470
|
+
|
|
471
|
+
// Markdown summary
|
|
472
|
+
const mdContent = this.generateMarkdownReport(summary);
|
|
473
|
+
SecurityUtils.safeWriteFileSync(summaryFile, mdContent, outputDir);
|
|
474
|
+
|
|
475
|
+
return { reportFile, summaryFile, summary };
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
generateMarkdownReport(summary) {
|
|
479
|
+
let content = `# Text Analysis Report
|
|
480
|
+
|
|
481
|
+
**Framework:** ${summary.framework}
|
|
482
|
+
**Total Files Scanned:** ${summary.totalFiles}
|
|
483
|
+
**Text Instances Found:** ${summary.totalInstances}
|
|
484
|
+
**Files with Hardcoded Text:** ${summary.filesWithText}
|
|
485
|
+
**Generated:** ${summary.timestamp}
|
|
486
|
+
|
|
487
|
+
## Summary
|
|
488
|
+
|
|
489
|
+
| Metric | Count |
|
|
490
|
+
|--------|-------|
|
|
491
|
+
| Total Files | ${summary.totalFiles} |
|
|
492
|
+
| Text Instances | ${summary.totalInstances} |
|
|
493
|
+
| Files with Text | ${summary.filesWithText} |
|
|
494
|
+
|
|
495
|
+
## Files with Hardcoded Text
|
|
496
|
+
|
|
497
|
+
`;
|
|
498
|
+
|
|
499
|
+
summary.results.forEach(file => {
|
|
500
|
+
content += `### ${file.file}
|
|
501
|
+
|
|
502
|
+
| Text | Line | Suggestion |
|
|
503
|
+
|------|------|------------|
|
|
504
|
+
`;
|
|
505
|
+
file.results.forEach(result => {
|
|
506
|
+
const suggestion = result.suggestion;
|
|
507
|
+
content += `| "${result.text}" | ${result.line} | \`${suggestion.translationKey}\` |
|
|
508
|
+
`;
|
|
509
|
+
});
|
|
510
|
+
content += '\n';
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
content += `
|
|
514
|
+
## Recommendations
|
|
515
|
+
|
|
516
|
+
1. **Create Translation Keys**: Add the suggested keys to your translation files
|
|
517
|
+
2. **Replace Text**: Replace hardcoded text with the suggested translation patterns
|
|
518
|
+
3. **Test Changes**: Verify translations work correctly in your application
|
|
519
|
+
4. **Update Framework**: Ensure your i18n framework is properly configured
|
|
520
|
+
|
|
521
|
+
## Next Steps
|
|
522
|
+
|
|
523
|
+
- Run \`i18ntk init\` to set up translation infrastructure if needed
|
|
524
|
+
- Use \`i18ntk fixer\` to fix any placeholder translations
|
|
525
|
+
- Run \`i18ntk validate\` to ensure all translations are properly configured
|
|
526
|
+
`;
|
|
527
|
+
|
|
528
|
+
return content;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
async initialize() {
|
|
532
|
+
const args = this.parseArgs();
|
|
533
|
+
if (args.help) {
|
|
534
|
+
displayHelp('i18ntk-scanner', {
|
|
535
|
+
'source-dir': this.t('scanner.help_options.source_dir'),
|
|
536
|
+
'framework': this.t('scanner.help_options.framework'),
|
|
537
|
+
'patterns': this.t('scanner.help_options.patterns'),
|
|
538
|
+
'exclude': this.t('scanner.help_options.exclude'),
|
|
539
|
+
'output-report': this.t('scanner.help_options.output_report'),
|
|
540
|
+
'output-dir': this.t('scanner.help_options.output_dir'),
|
|
541
|
+
'min-length': this.t('scanner.help_options.min_length'),
|
|
542
|
+
'max-length': this.t('scanner.help_options.max_length'),
|
|
543
|
+
'include-tests': this.t('scanner.help_options.include_tests')
|
|
544
|
+
});
|
|
545
|
+
process.exit(0);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const baseConfig = await getUnifiedConfig('scanner', args);
|
|
549
|
+
this.config = { ...baseConfig, ...(this.config || {}) };
|
|
550
|
+
|
|
551
|
+
this.sourceDir = this.config.sourceDir || './src';
|
|
552
|
+
|
|
553
|
+
// Resolve framework with precedence: CLI arg > config.framework.preference|string > auto-detect > fallback
|
|
554
|
+
const cliFramework = args.framework;
|
|
555
|
+
const cfgFramework = this.config.framework;
|
|
556
|
+
const fwPref = typeof cfgFramework === 'string' ? cfgFramework : (cfgFramework?.preference || 'auto');
|
|
557
|
+
const fwDetectEnabled = typeof cfgFramework === 'object' ? (cfgFramework.detect !== false) : true;
|
|
558
|
+
const fwFallback = typeof cfgFramework === 'object' ? (cfgFramework.fallback || 'vanilla') : 'vanilla';
|
|
559
|
+
|
|
560
|
+
if (cliFramework && typeof cliFramework === 'string') {
|
|
561
|
+
this.framework = cliFramework;
|
|
562
|
+
} else if (fwPref && fwPref !== 'auto') {
|
|
563
|
+
this.framework = fwPref;
|
|
564
|
+
} else if (fwDetectEnabled) {
|
|
565
|
+
const detected = this.detectFramework(process.cwd());
|
|
566
|
+
this.framework = detected || fwFallback;
|
|
567
|
+
} else {
|
|
568
|
+
this.framework = fwFallback;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Validate source directory
|
|
572
|
+
if (!SecurityUtils.safeExistsSync(this.sourceDir, path.dirname(this.sourceDir))) {
|
|
573
|
+
console.error(`❌ Source directory does not exist: ${this.sourceDir}`);
|
|
574
|
+
process.exit(1);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const validatedPath = SecurityUtils.validatePath(this.sourceDir);
|
|
578
|
+
if (!validatedPath) {
|
|
579
|
+
console.error(`❌ Security validation failed: Path validation returned null`);
|
|
580
|
+
process.exit(1);
|
|
581
|
+
}
|
|
582
|
+
this.sourceDir = validatedPath;
|
|
583
|
+
|
|
584
|
+
return this;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
async run() {
|
|
588
|
+
console.log(this.t('scanner.starting', { framework: this.framework }));
|
|
589
|
+
console.log(this.t('scanner.sourceDirectory', { sourceDir: this.sourceDir }));
|
|
590
|
+
|
|
591
|
+
const patterns = this.getFrameworkPatterns(this.framework);
|
|
592
|
+
const exclusions = this.config.exclude || ['node_modules', '.git', 'dist', 'build'];
|
|
593
|
+
const minLength = this.config.minLength || 3;
|
|
594
|
+
const maxLength = this.config.maxLength || 100;
|
|
595
|
+
const includeTests = this.config.includeTests || false;
|
|
596
|
+
|
|
597
|
+
try {
|
|
598
|
+
const results = await this.scanDirectory(this.sourceDir, {
|
|
599
|
+
patterns,
|
|
600
|
+
exclusions,
|
|
601
|
+
minLength,
|
|
602
|
+
maxLength,
|
|
603
|
+
includeTests
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
console.log(this.t('scanner.foundText', { count: results.reduce((sum, file) => sum + file.results.length, 0) }));
|
|
607
|
+
|
|
608
|
+
if (results.length > 0 && this.config.outputReport) {
|
|
609
|
+
const outputDir = this.config.outputDir || './reports';
|
|
610
|
+
const { reportFile, summaryFile } = await this.generateReport(results, outputDir);
|
|
611
|
+
console.log(this.t('scanner.reportGenerated', { path: summaryFile }));
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
return results;
|
|
615
|
+
} catch (error) {
|
|
616
|
+
console.error(`❌ Error during scanning: ${error.message}`);
|
|
617
|
+
process.exit(1);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Execute the scanner command
|
|
623
|
+
*/
|
|
624
|
+
async execute(options = {}) {
|
|
625
|
+
try {
|
|
626
|
+
await this.initialize();
|
|
627
|
+
await this.run();
|
|
628
|
+
return { success: true, command: 'scanner' };
|
|
629
|
+
} catch (error) {
|
|
630
|
+
console.error(`Scanner command failed: ${error.message}`);
|
|
631
|
+
throw error;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Get command metadata
|
|
637
|
+
*/
|
|
638
|
+
getMetadata() {
|
|
639
|
+
return {
|
|
640
|
+
name: 'scanner',
|
|
641
|
+
description: 'Scan for translation keys in source code',
|
|
642
|
+
category: 'analysis',
|
|
643
|
+
aliases: [],
|
|
644
|
+
usage: 'scanner [options]',
|
|
645
|
+
examples: [
|
|
646
|
+
'scanner',
|
|
647
|
+
'scanner --source-dir=./src',
|
|
648
|
+
'scanner --output-dir=./reports'
|
|
649
|
+
]
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
module.exports = ScannerCommand;
|