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.
Files changed (108) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +141 -1191
  3. package/main/i18ntk-analyze.js +65 -84
  4. package/main/i18ntk-backup-class.js +420 -0
  5. package/main/i18ntk-backup.js +3 -3
  6. package/main/i18ntk-complete.js +90 -65
  7. package/main/i18ntk-doctor.js +123 -103
  8. package/main/i18ntk-fixer.js +61 -725
  9. package/main/i18ntk-go.js +14 -15
  10. package/main/i18ntk-init.js +77 -26
  11. package/main/i18ntk-java.js +27 -32
  12. package/main/i18ntk-js.js +70 -68
  13. package/main/i18ntk-manage.js +129 -30
  14. package/main/i18ntk-php.js +75 -75
  15. package/main/i18ntk-py.js +55 -56
  16. package/main/i18ntk-scanner.js +59 -57
  17. package/main/i18ntk-setup.js +9 -404
  18. package/main/i18ntk-sizing.js +6 -6
  19. package/main/i18ntk-summary.js +21 -18
  20. package/main/i18ntk-ui.js +11 -10
  21. package/main/i18ntk-usage.js +54 -18
  22. package/main/i18ntk-validate.js +13 -13
  23. package/main/manage/commands/AnalyzeCommand.js +1124 -0
  24. package/main/manage/commands/BackupCommand.js +62 -0
  25. package/main/manage/commands/CommandRouter.js +295 -0
  26. package/main/manage/commands/CompleteCommand.js +61 -0
  27. package/main/manage/commands/DoctorCommand.js +60 -0
  28. package/main/manage/commands/FixerCommand.js +624 -0
  29. package/main/manage/commands/InitCommand.js +62 -0
  30. package/main/manage/commands/ScannerCommand.js +654 -0
  31. package/main/manage/commands/SizingCommand.js +60 -0
  32. package/main/manage/commands/SummaryCommand.js +61 -0
  33. package/main/manage/commands/UsageCommand.js +60 -0
  34. package/main/manage/commands/ValidateCommand.js +978 -0
  35. package/main/manage/index-fixed.js +1447 -0
  36. package/main/manage/index.js +1462 -0
  37. package/main/manage/managers/DebugMenu.js +140 -0
  38. package/main/manage/managers/InteractiveMenu.js +177 -0
  39. package/main/manage/managers/LanguageMenu.js +62 -0
  40. package/main/manage/managers/SettingsMenu.js +53 -0
  41. package/main/manage/services/AuthenticationService.js +263 -0
  42. package/main/manage/services/ConfigurationService-fixed.js +449 -0
  43. package/main/manage/services/ConfigurationService.js +449 -0
  44. package/main/manage/services/FileManagementService.js +368 -0
  45. package/main/manage/services/FrameworkDetectionService.js +458 -0
  46. package/main/manage/services/InitService.js +1051 -0
  47. package/main/manage/services/SetupService.js +462 -0
  48. package/main/manage/services/SummaryService.js +450 -0
  49. package/main/manage/services/UsageService.js +1502 -0
  50. package/package.json +32 -29
  51. package/runtime/enhanced.d.ts +221 -221
  52. package/runtime/index.d.ts +29 -29
  53. package/runtime/index.full.d.ts +331 -331
  54. package/runtime/index.js +7 -6
  55. package/scripts/build-lite.js +17 -17
  56. package/scripts/deprecate-versions.js +23 -6
  57. package/scripts/export-translations.js +5 -5
  58. package/scripts/fix-all-i18n.js +3 -3
  59. package/scripts/fix-and-purify-i18n.js +3 -2
  60. package/scripts/fix-locale-control-chars.js +110 -0
  61. package/scripts/lint-locales.js +80 -0
  62. package/scripts/locale-optimizer.js +8 -8
  63. package/scripts/prepublish.js +21 -21
  64. package/scripts/security-check.js +117 -117
  65. package/scripts/sync-translations.js +4 -4
  66. package/scripts/sync-ui-locales.js +9 -8
  67. package/scripts/validate-all-translations.js +8 -7
  68. package/scripts/verify-deprecations.js +157 -161
  69. package/scripts/verify-translations.js +6 -5
  70. package/settings/i18ntk-config.json +282 -282
  71. package/settings/language-config.json +5 -5
  72. package/settings/settings-cli.js +9 -9
  73. package/settings/settings-manager.js +18 -18
  74. package/ui-locales/de.json +2417 -2348
  75. package/ui-locales/en.json +2415 -2352
  76. package/ui-locales/es.json +2425 -2353
  77. package/ui-locales/fr.json +2418 -2348
  78. package/ui-locales/ja.json +2463 -2361
  79. package/ui-locales/ru.json +2463 -2359
  80. package/ui-locales/zh.json +2418 -2351
  81. package/utils/admin-auth.js +2 -2
  82. package/utils/admin-cli.js +297 -297
  83. package/utils/admin-pin.js +9 -9
  84. package/utils/cli-helper.js +9 -9
  85. package/utils/config-helper.js +73 -104
  86. package/utils/config-manager.js +204 -171
  87. package/utils/config.js +5 -4
  88. package/utils/env-manager.js +249 -263
  89. package/utils/framework-detector.js +27 -24
  90. package/utils/i18n-helper.js +85 -41
  91. package/utils/init-helper.js +152 -94
  92. package/utils/json-output.js +98 -98
  93. package/utils/mini-commander.js +179 -0
  94. package/utils/missing-key-validator.js +5 -5
  95. package/utils/plugin-loader.js +40 -29
  96. package/utils/prompt.js +14 -44
  97. package/utils/safe-json.js +40 -0
  98. package/utils/secure-errors.js +3 -3
  99. package/utils/security-check-improved.js +390 -0
  100. package/utils/security-config.js +5 -5
  101. package/utils/security-fixed.js +607 -0
  102. package/utils/security.js +652 -602
  103. package/utils/setup-enforcer.js +136 -44
  104. package/utils/setup-validator.js +33 -32
  105. package/utils/ultra-performance-optimizer.js +11 -9
  106. package/utils/watch-locales.js +2 -1
  107. package/utils/prompt-fixed.js +0 -55
  108. 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;