i18ntk 4.5.4 → 4.6.1

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.
@@ -29,29 +29,56 @@ const { createPrompt, isInteractive } = require('../utils/prompt-helper');
29
29
  const { parseConfirmation } = require('../utils/localized-confirm');
30
30
  const { normalizeReportFormat, writeReportFile } = require('../utils/report-writer');
31
31
 
32
- // Language configurations with native names
33
- const LANGUAGE_CONFIG = {
34
- 'en': { name: 'English', nativeName: 'English' },
35
- 'de': { name: 'German', nativeName: 'Deutsch' },
36
- 'es': { name: 'Spanish', nativeName: 'Espa\u00f1ol' },
37
- 'fr': { name: 'French', nativeName: 'Fran\u00e7ais' },
38
- 'ru': { name: 'Russian', nativeName: '\u0420\u0443\u0441\u0441\u043a\u0438\u0439' },
39
- 'it': { name: 'Italian', nativeName: 'Italiano' },
40
- 'ja': { name: 'Japanese', nativeName: '\u65e5\u672c\u8a9e' },
41
- 'ko': { name: 'Korean', nativeName: '\ud55c\uad6d\uc5b4' },
42
- 'zh': { name: 'Chinese', nativeName: '\u4e2d\u6587' },
43
- 'ar': { name: 'Arabic', nativeName: '\u0627\u0644\u0639\u0631\u0628\u064a\u0629' },
44
- 'hi': { name: 'Hindi', nativeName: '\u0939\u093f\u0928\u094d\u0926\u0940' },
45
- 'nl': { name: 'Dutch', nativeName: 'Nederlands' },
46
- 'sv': { name: 'Swedish', nativeName: 'Svenska' },
47
- 'da': { name: 'Danish', nativeName: 'Dansk' },
48
- 'no': { name: 'Norwegian', nativeName: 'Norsk' },
49
- 'fi': { name: 'Finnish', nativeName: 'Suomi' },
50
- 'pl': { name: 'Polish', nativeName: 'Polski' },
51
- 'cs': { name: 'Czech', nativeName: '\u010ce\u0161tina' },
52
- 'hu': { name: 'Hungarian', nativeName: 'Magyar' },
53
- 'tr': { name: 'Turkish', nativeName: 'T\u00fcrk\u00e7e' }
54
- };
32
+ // Language configurations with native names — comprehensive ISO 639-1 coverage
33
+ const LANGUAGE_CONFIG = {
34
+ 'af': { name: 'Afrikaans', nativeName: 'Afrikaans' }, 'sq': { name: 'Albanian', nativeName: 'Shqip' },
35
+ 'ar': { name: 'Arabic', nativeName: '\u0627\u0644\u0639\u0631\u0628\u064a\u0629' }, 'hy': { name: 'Armenian', nativeName: '\u0540\u0561\u0575\u0565\u0580\u0565\u0576' },
36
+ 'az': { name: 'Azerbaijani', nativeName: 'Az\u0259rbaycan' }, 'eu': { name: 'Basque', nativeName: 'Euskara' },
37
+ 'be': { name: 'Belarusian', nativeName: '\u0411\u0435\u043b\u0430\u0440\u0443\u0441\u043a\u0430\u044f' },
38
+ 'bn': { name: 'Bengali', nativeName: '\u09ac\u09be\u0982\u09b2\u09be' },
39
+ 'bs': { name: 'Bosnian', nativeName: 'Bosanski' }, 'bg': { name: 'Bulgarian', nativeName: '\u0411\u044a\u043b\u0433\u0430\u0440\u0441\u043a\u0438' },
40
+ 'ca': { name: 'Catalan', nativeName: 'Catal\u00e0' }, 'zh': { name: 'Chinese', nativeName: '\u4e2d\u6587' },
41
+ 'hr': { name: 'Croatian', nativeName: 'Hrvatski' }, 'cs': { name: 'Czech', nativeName: '\u010ce\u0161tina' },
42
+ 'da': { name: 'Danish', nativeName: 'Dansk' }, 'nl': { name: 'Dutch', nativeName: 'Nederlands' },
43
+ 'en': { name: 'English', nativeName: 'English' }, 'et': { name: 'Estonian', nativeName: 'Eesti' },
44
+ 'fi': { name: 'Finnish', nativeName: 'Suomi' }, 'fr': { name: 'French', nativeName: 'Fran\u00e7ais' },
45
+ 'ka': { name: 'Georgian', nativeName: '\u10e5\u10d0\u10e0\u10d7\u10e3\u10da\u10d8' },
46
+ 'de': { name: 'German', nativeName: 'Deutsch' }, 'el': { name: 'Greek', nativeName: '\u0395\u03bb\u03bb\u03b7\u03bd\u03b9\u03ba\u03ac' },
47
+ 'gu': { name: 'Gujarati', nativeName: '\u0a97\u0ac1\u0a9c\u0ab0\u0abe\u0aa4\u0ac0' },
48
+ 'he': { name: 'Hebrew', nativeName: '\u05e2\u05d1\u05e8\u05d9\u05ea' },
49
+ 'hi': { name: 'Hindi', nativeName: '\u0939\u093f\u0928\u094d\u0926\u0940' },
50
+ 'hu': { name: 'Hungarian', nativeName: 'Magyar' }, 'is': { name: 'Icelandic', nativeName: '\u00cdslenska' },
51
+ 'id': { name: 'Indonesian', nativeName: 'Bahasa Indonesia' },
52
+ 'ga': { name: 'Irish', nativeName: 'Gaeilge' }, 'it': { name: 'Italian', nativeName: 'Italiano' },
53
+ 'ja': { name: 'Japanese', nativeName: '\u65e5\u672c\u8a9e' },
54
+ 'kk': { name: 'Kazakh', nativeName: '\u049a\u0430\u0437\u0430\u049b' },
55
+ 'ko': { name: 'Korean', nativeName: '\ud55c\uad6d\uc5b4' },
56
+ 'lv': { name: 'Latvian', nativeName: 'Latvie\u0161u' }, 'lt': { name: 'Lithuanian', nativeName: 'Lietuvi\u0173' },
57
+ 'mk': { name: 'Macedonian', nativeName: '\u041c\u0430\u043a\u0435\u0434\u043e\u043d\u0441\u043a\u0438' },
58
+ 'ms': { name: 'Malay', nativeName: 'Bahasa Melayu' },
59
+ 'ml': { name: 'Malayalam', nativeName: '\u0d2e\u0d32\u0d2f\u0d3e\u0d33\u0d02' },
60
+ 'mr': { name: 'Marathi', nativeName: '\u092e\u0930\u093e\u0920\u0940' },
61
+ 'mn': { name: 'Mongolian', nativeName: '\u041c\u043e\u043d\u0433\u043e\u043b' },
62
+ 'no': { name: 'Norwegian', nativeName: 'Norsk' },
63
+ 'fa': { name: 'Persian', nativeName: '\u0641\u0627\u0631\u0633\u06cc' },
64
+ 'pl': { name: 'Polish', nativeName: 'Polski' }, 'pt': { name: 'Portuguese', nativeName: 'Portugu\u00eas' },
65
+ 'pa': { name: 'Punjabi', nativeName: '\u0a2a\u0a70\u0a1c\u0a3e\u0a2c\u0a40' },
66
+ 'ro': { name: 'Romanian', nativeName: 'Rom\u00e2n\u0103' },
67
+ 'ru': { name: 'Russian', nativeName: '\u0420\u0443\u0441\u0441\u043a\u0438\u0439' },
68
+ 'sr': { name: 'Serbian', nativeName: '\u0421\u0440\u043f\u0441\u043a\u0438' },
69
+ 'sk': { name: 'Slovak', nativeName: 'Sloven\u010dina' },
70
+ 'sl': { name: 'Slovenian', nativeName: 'Sloven\u0161\u010dina' },
71
+ 'es': { name: 'Spanish', nativeName: 'Espa\u00f1ol' },
72
+ 'sw': { name: 'Swahili', nativeName: 'Kiswahili' }, 'sv': { name: 'Swedish', nativeName: 'Svenska' },
73
+ 'tl': { name: 'Filipino', nativeName: 'Filipino' },
74
+ 'ta': { name: 'Tamil', nativeName: '\u0ba4\u0bae\u0bbf\u0bb4\u0bcd' },
75
+ 'te': { name: 'Telugu', nativeName: '\u0c24\u0c46\u0c32\u0c41\u0c17\u0c41' },
76
+ 'th': { name: 'Thai', nativeName: '\u0e44\u0e17\u0e22' },
77
+ 'tr': { name: 'Turkish', nativeName: 'T\u00fcrk\u00e7e' },
78
+ 'uk': { name: 'Ukrainian', nativeName: '\u0423\u043a\u0440\u0430\u0457\u043d\u0441\u044c\u043a\u0430' },
79
+ 'ur': { name: 'Urdu', nativeName: '\u0627\u0631\u062f\u0648' },
80
+ 'vi': { name: 'Vietnamese', nativeName: 'Ti\u1ebfng Vi\u1ec7t' },
81
+ };
55
82
 
56
83
  class I18nInitializer {
57
84
  constructor(config = {}) {
@@ -67,7 +94,9 @@ class I18nInitializer {
67
94
  ...config
68
95
  };
69
96
  this.format = getFormatAdapter(this.config.format);
70
- this.config.supportedExtensions = [this.format.extension];
97
+ if (!this.config.supportedExtensions || !this.config.supportedExtensions.length) {
98
+ this.config.supportedExtensions = [this.format.extension];
99
+ }
71
100
  this.detectedFramework = detectFramework(process.cwd());
72
101
  if (this.detectedFramework && !this.config.translationPatterns) {
73
102
  this.config.translationPatterns = this.detectedFramework.patterns;
@@ -79,7 +108,9 @@ class I18nInitializer {
79
108
  : path.join(this.sourceDir, this.config.sourceLanguage);
80
109
 
81
110
  // Ensure defaultLanguages is properly initialized from config
82
- this.config.defaultLanguages = this.config.defaultLanguages || ['en', 'de', 'es', 'fr', 'ru'];
111
+ if (!this.config.defaultLanguages || !Array.isArray(this.config.defaultLanguages) || this.config.defaultLanguages.length === 0) {
112
+ this.config.defaultLanguages = ['en', 'de', 'es', 'fr', 'ru'];
113
+ }
83
114
 
84
115
  // No longer create readline interface here - use CLI helpers
85
116
  this.rl = null;
@@ -22,6 +22,7 @@ const { getUnifiedConfig, displayHelp } = require('../utils/config-helper');
22
22
  const { loadTranslations } = require('../utils/i18n-helper');
23
23
  const SecurityUtils = require('../utils/security');
24
24
  const SetupEnforcer = require('../utils/setup-enforcer');
25
+ const { detectProjectFramework, getFrameworkPatterns, getFrameworkSuggestions, SCANNER_EXTENSIONS, WRAPPER_SKIP_PATTERNS, getExcludeDirs } = require('../utils/framework-detector');
25
26
 
26
27
  // Ensure setup is complete before running
27
28
  (async () => {
@@ -225,11 +226,26 @@ class I18nTextScanner {
225
226
  const packageJson = SecurityUtils.safeParseJSON(packageJsonContent);
226
227
  const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };
227
228
 
229
+ if (deps.next || deps['next-intl'] || deps['next-i18next']) return 'next';
228
230
  if (deps.react || deps['react-dom']) return 'react';
229
231
  if (deps.vue || deps['vue-router']) return 'vue';
230
232
  if (deps['@angular/core'] || deps.angular) return 'angular';
231
- if (deps.next) return 'next';
232
233
  if (deps.svelte) return 'svelte';
234
+ if (deps.astro || deps['astro-i18next'] || deps['@astrojs/i18n']) return 'astro';
235
+ if (deps['@remix-run/react'] || deps['remix-i18next']) return 'remix';
236
+ if (deps.gatsby || deps['gatsby-plugin-react-i18next']) return 'gatsby';
237
+ if (deps['@builder.io/qwik'] || deps['qwik-speak']) return 'qwik';
238
+ if (deps['solid-js'] || deps['@solid-primitives/i18n']) return 'solid';
239
+ if (deps['ember-source'] || deps['ember-intl']) return 'ember';
240
+ if (deps.nuxt || deps['@nuxtjs/i18n']) return 'nuxt';
241
+
242
+ // Check for Rust project
243
+ const cargoTomlPath = path.join(projectRoot, 'Cargo.toml');
244
+ if (SecurityUtils.safeExistsSync(cargoTomlPath, projectRoot)) return 'rust';
245
+
246
+ // Check for Go project
247
+ const goModPath = path.join(projectRoot, 'go.mod');
248
+ if (SecurityUtils.safeExistsSync(goModPath, projectRoot)) return 'go';
233
249
 
234
250
  return 'vanilla';
235
251
  } catch (error) {
@@ -255,57 +271,110 @@ class I18nTextScanner {
255
271
 
256
272
  const frameworkSpecific = {
257
273
  react: [
258
- // React specific patterns - enhanced for i18next detection
259
274
  /children:\s*["']([^"']{2,99})["']/g,
260
275
  /dangerouslySetInnerHTML={{\s*__html:\s*["']([^"']{2,99})["']/g,
261
- // JSX text content without translation
262
276
  />([^<{][^<>{]*[^}>])</g,
263
- // Button text
264
277
  /<button[^>]*>([^<]{2,99})<\/button>/g,
265
- // Span text
266
- /<span[^>]*>([^<]{2,99})<\/span>/g
278
+ /<span[^>]*>([^<]{2,99})<\/span>/g,
279
+ /<(?:FormattedMessage|Trans)[\s\S]*?\b(?:id|defaultMessage|i18nKey)\s*=\s*(?:\{|)(['"`])([^'"`}]+)\1[^>]*>/g,
280
+ /<(?:FormattedMessage|Trans)[\s\S]*?\b(?:id|defaultMessage|i18nKey)\s*=\s*\{[']\s*([^'}]+\.[^'}]+)\s*[']/g
267
281
  ],
268
282
  vue: [
269
- // Vue specific patterns - enhanced for vue-i18n detection
270
283
  /v-text=["']([^"']{2,99})["']/g,
271
284
  /v-html=["']([^"']{2,99})["']/g,
272
- // Vue template text
273
285
  />([^<{][^<>{]*[^}>])</g,
274
- // Button text
275
286
  /<button[^>]*>([^<]{2,99})<\/button>/g,
276
- // Span text
277
- /<span[^>]*>([^<]{2,99})<\/span>/g
287
+ /<span[^>]*>([^<]{2,99})<\/span>/g,
288
+ /\$t\(["']([^"']{2,99})["']\)/g,
289
+ /v-t=["']([^"']{2,99})["']/g
278
290
  ],
279
291
  angular: [
280
- // Angular specific patterns - enhanced for ngx-translate detection
281
292
  /\[innerHTML\]=["']([^"']{2,99})["']/g,
282
293
  /\[textContent\]=["']([^"']{2,99})["']/g,
283
- // Angular template text
284
294
  />([^<{][^<>{]*[^}>])</g,
285
- // Button text
286
295
  /<button[^>]*>([^<]{2,99})<\/button>/g,
287
- // Span text
288
- /<span[^>]*>([^<]{2,99})<\/span>/g
296
+ /<span[^>]*>([^<]{2,99})<\/span>/g,
297
+ /i18n=["']([^"']{2,99})["']/g,
298
+ /\[attr\.title\]=["']([^"']{2,99})["']/g
299
+ ],
300
+ next: [
301
+ /children:\s*["']([^"']{2,99})["']/g,
302
+ />([^<{][^<>{]*[^}>])</g,
303
+ /<button[^>]*>([^<]{2,99})<\/button>/g,
304
+ /<(?:FormattedMessage|Trans)[\s\S]*?\b(?:id|defaultMessage|i18nKey)\s*=\s*(?:\{|)(['"`])([^'"`}]+)\1[^>]*>/g
305
+ ],
306
+ svelte: [
307
+ />([^<{][^<>{]*[^}>])</g,
308
+ /<button[^>]*>([^<]{2,99})<\/button>/g,
309
+ /\$_\(["']([^"']{2,99})["']\)/g,
310
+ /t\.set\(["']([^"']{2,99})["']/g,
311
+ /\{#if[^}]*\}([^<]{2,99})\{\/if\}/g
312
+ ],
313
+ astro: [
314
+ />([^<{][^<>{]*[^}>])</g,
315
+ /<button[^>]*>([^<]{2,99})<\/button>/g,
316
+ /t\(["']([^"']{2,99})["']\)/g
317
+ ],
318
+ remix: [
319
+ /children:\s*["']([^"']{2,99})["']/g,
320
+ />([^<{][^<>{]*[^}>])</g,
321
+ /<button[^>]*>([^<]{2,99})<\/button>/g,
322
+ /<(?:FormattedMessage|Trans)[\s\S]*?\b(?:id|defaultMessage|i18nKey)\s*=\s*(?:\{|)(['"`])([^'"`}]+)\1[^>]*>/g
323
+ ],
324
+ qwik: [
325
+ />([^<{][^<>{]*[^}>])</g,
326
+ /<button[^>]*>([^<]{2,99})<\/button>/g,
327
+ /t\(["']([^"']{2,99})["']\)/g,
328
+ /useTranslate\(\)/g
329
+ ],
330
+ solid: [
331
+ />([^<{][^<>{]*[^}>])</g,
332
+ /<button[^>]*>([^<]{2,99})<\/button>/g,
333
+ /t\(["']([^"']{2,99})["']\)/g,
334
+ /useI18n\(\)\.t\(["']([^"']{2,99})["']\)/g
335
+ ],
336
+ ember: [
337
+ />([^<{][^<>{]*[^}>])</g,
338
+ /\{\{t\s+["']([^"']{2,99})["']\s*\}\}/g,
339
+ /\{\{intl\.t\s+["']([^"']{2,99})["']\s*\}\}/g
340
+ ],
341
+ gatsby: [
342
+ /children:\s*["']([^"']{2,99})["']/g,
343
+ />([^<{][^<>{]*[^}>])</g,
344
+ /<(?:Trans)[\s\S]*?\bi18nKey\s*=\s*(?:\{|)(['"`])([^'"`}]+)\1[^>]*>/g
289
345
  ],
290
346
  django: [
291
- // Django template patterns
292
347
  /\{\%\s*trans\s+["']([^"']{2,99})["']\s*%\}/g,
293
348
  /\{\%\s*blocktrans\s*%\}([^%]{2,99})\{\%\s*endblocktrans\s*%\}/g,
294
349
  /{{\s*_["']([^"']{2,99})["']\s*}}/g,
295
350
  /{{\s*gettext\(["']([^"']{2,99})["']\)\s*}}/g
296
351
  ],
297
352
  flask: [
298
- // Flask/Jinja2 template patterns
299
353
  /\{\{\s*_["']([^"']{2,99})["']\s*}}/g,
300
354
  /\{\{\s*gettext\(["']([^"']{2,99})["']\)\s*}}/g,
301
355
  /\{\{\s*lazy_gettext\(["']([^"']{2,99})["']\)\s*}}/g
302
356
  ],
303
357
  python: [
304
- // Python source patterns
305
358
  /gettext\(["']([^"']{2,99})["']\)/g,
306
359
  /_\(["']([^"']{2,99})["']\)/g,
307
360
  /gettext_lazy\(["']([^"']{2,99})["']\)/g,
308
361
  /lazy_gettext\(["']([^"']{2,99})["']\)/g
362
+ ],
363
+ rust: [
364
+ /bundle\.get_message\(\s*["']([^"']{2,99})["']\)/g,
365
+ /\.get_message\s*\(\s*["']([^"']{2,99})["']\)/g,
366
+ /ts!\s*\(\s*["']([^"']{2,99})["']/g,
367
+ /fluent!\s*\(\s*["']([^"']{2,99})["']/g
368
+ ],
369
+ go: [
370
+ /i18n\.Translate\([^,]+,\s*["']([^"']{2,99})["']\)/g,
371
+ /i18n\.NewMessage\([^,]+,\s*["']([^"']{2,99})["']\)/g,
372
+ /t\.Get\([^,]+,\s*["']([^"']{2,99})["']\)/g
373
+ ],
374
+ vanilla: [
375
+ /t\(["']([^"']{2,99})["']\)/g,
376
+ /i18n\.t\(["']([^"']{2,99})["']\)/g,
377
+ /translate\(["']([^"']{2,99})["']\)/g
309
378
  ]
310
379
  };
311
380
 
@@ -518,7 +587,7 @@ class I18nTextScanner {
518
587
  key: `ui.${key}`,
519
588
  original: text,
520
589
  translationKey: `t('ui.${key}')`,
521
- frameworkSpecific: this.getFrameworkSpecific(text)
590
+ frameworkSpecific: getFrameworkSuggestions(this.framework, text)
522
591
  };
523
592
  }
524
593
 
@@ -526,16 +595,51 @@ class I18nTextScanner {
526
595
  const frameworks = {
527
596
  react: {
528
597
  hook: `const { t } = useTranslation();`,
529
- usage: `{t('ui.${text.toLowerCase().replace(/[^a-z0-9\s]/g, '').replace(/\s+/g, '_')}')}`,
530
- component: `<Trans i18nKey="ui.${text.toLowerCase().replace(/[^a-z0-9\s]/g, '').replace(/\s+/g, '_')}">${text}</Trans>`
598
+ usage: `{t('ui.${this._keySnippet(text)}')}`,
599
+ component: `<Trans i18nKey="ui.${this._keySnippet(text)}">${text}</Trans>`
600
+ },
601
+ next: {
602
+ hook: `const t = useTranslations();`,
603
+ usage: `{t('ui.${this._keySnippet(text)}')}`,
604
+ component: `<Trans i18nKey="ui.${this._keySnippet(text)}">${text}</Trans>`
531
605
  },
532
606
  vue: {
533
- directive: `{{ $t('ui.${text.toLowerCase().replace(/[^a-z0-9\s]/g, '').replace(/\s+/g, '_')}') }}`,
534
- method: `this.$t('ui.${text.toLowerCase().replace(/[^a-z0-9\s]/g, '').replace(/\s+/g, '_')}')`
607
+ directive: `{{ $t('ui.${this._keySnippet(text)}') }}`,
608
+ method: `this.$t('ui.${this._keySnippet(text)}')`
535
609
  },
536
610
  angular: {
537
611
  pipe: `{{ '${text}' | translate }}`,
538
- service: `this.translateService.instant('ui.${text.toLowerCase().replace(/[^a-z0-9\s]/g, '').replace(/\s+/g, '_')}')`
612
+ service: `this.translateService.instant('ui.${this._keySnippet(text)}')`
613
+ },
614
+ svelte: {
615
+ store: `$_(('ui.${this._keySnippet(text)}'))`,
616
+ method: `t.set('ui.${this._keySnippet(text)}', '${text}')`
617
+ },
618
+ astro: {
619
+ import: `import { t } from 'astro-i18next';`,
620
+ usage: `{t('ui.${this._keySnippet(text)}')}`
621
+ },
622
+ remix: {
623
+ hook: `const { t } = useTranslation();`,
624
+ usage: `{t('ui.${this._keySnippet(text)}')}`,
625
+ server: `export const handle = i18next.handle;`
626
+ },
627
+ qwik: {
628
+ hook: `const t = useTranslate();`,
629
+ usage: `{t('ui.${this._keySnippet(text)}')}`
630
+ },
631
+ solid: {
632
+ hook: `const [t] = useI18n();`,
633
+ usage: `{t('ui.${this._keySnippet(text)}')}`
634
+ },
635
+ ember: {
636
+ template: `{{t 'ui.${this._keySnippet(text)}'}}`,
637
+ helper: `this.intl.t('ui.${this._keySnippet(text)}')`
638
+ },
639
+ gatsby: {
640
+ hook: `const { t } = useTranslation();`,
641
+ usage: `{t('ui.${this._keySnippet(text)}')}`,
642
+ plugin: `'gatsby-plugin-react-i18next'`
539
643
  },
540
644
  django: {
541
645
  template: `{% trans '${text}' %}`,
@@ -552,14 +656,29 @@ class I18nTextScanner {
552
656
  underscore: `from gettext import gettext as _\n_('${text}')`,
553
657
  lazy: `from gettext import gettext_lazy as _\n_('${text}')`
554
658
  },
659
+ rust: {
660
+ fluent: `bundle.get_message("ui_${this._keySnippet(text)}")`,
661
+ gettext: `gettext("ui_${this._keySnippet(text)}")`
662
+ },
663
+ go: {
664
+ translate: `i18n.Translate("ui_${this._keySnippet(text)}")`,
665
+ message: `i18n.NewMessage("ui_${this._keySnippet(text)}")`
666
+ },
555
667
  vanilla: {
556
- generic: `t('ui.${text.toLowerCase().replace(/[^a-z0-9\s]/g, '').replace(/\s+/g, '_')}')`
668
+ generic: `t('ui.${this._keySnippet(text)}')`
557
669
  }
558
670
  };
559
671
 
560
672
  return frameworks[this.framework] || frameworks.vanilla;
561
673
  }
562
674
 
675
+ _keySnippet(text) {
676
+ return text.toLowerCase()
677
+ .replace(/[^a-z0-9\s]/g, '')
678
+ .replace(/\s+/g, '_')
679
+ .substring(0, 40);
680
+ }
681
+
563
682
  async scanDirectory(dir, options = {}) {
564
683
  const {
565
684
  patterns = [],
@@ -574,7 +693,7 @@ class I18nTextScanner {
574
693
  }
575
694
 
576
695
  const allResults = [];
577
- const extensions = ['.js', '.jsx', '.ts', '.tsx', '.vue', '.html', '.svelte', '.py', '.pyx', '.pyi'];
696
+ const extensions = [...SCANNER_EXTENSIONS];
578
697
 
579
698
  const scanRecursive = (currentDir) => {
580
699
  const items = SecurityUtils.safeReaddirSync(currentDir, path.dirname(currentDir), { withFileTypes: true });
@@ -736,7 +855,7 @@ class I18nTextScanner {
736
855
  } else if (fwPref && fwPref !== 'auto') {
737
856
  this.framework = fwPref;
738
857
  } else if (fwDetectEnabled) {
739
- const detected = this.detectFramework(process.cwd());
858
+ const detected = detectProjectFramework(process.cwd());
740
859
  this.framework = detected || fwFallback;
741
860
  } else {
742
861
  this.framework = fwFallback;
@@ -762,7 +881,7 @@ class I18nTextScanner {
762
881
  console.log(this.t('scanner.starting', { framework: this.framework }));
763
882
  console.log(this.t('scanner.sourceDirectory', { sourceDir: this.sourceDir }));
764
883
 
765
- const patterns = this.getFrameworkPatterns(this.framework);
884
+ const patterns = getFrameworkPatterns(this.framework);
766
885
  const exclusions = this.config.exclude || ['node_modules', '.git', 'dist', 'build'];
767
886
  const minLength = this.config.minLength || 3;
768
887
  const maxLength = this.config.maxLength || 100;
@@ -1131,10 +1131,13 @@ async function run(args) {
1131
1131
  }
1132
1132
  }
1133
1133
 
1134
+ const hadRealErrors = grandTranslated === 0 && grandTotal > 0;
1135
+ const hasResiduals = allResidualUntranslated.length > 0;
1136
+
1134
1137
  return {
1135
- success: allResidualUntranslated.length === 0,
1136
- exitCode: allResidualUntranslated.length === 0 ? ExitCodes.SUCCESS : ExitCodes.VALIDATION_FAILED,
1137
- error: allResidualUntranslated.length === 0 ? undefined : 'Auto Translate left untranslated placeholder values',
1138
+ success: !hadRealErrors,
1139
+ exitCode: hadRealErrors ? ExitCodes.VALIDATION_FAILED : ExitCodes.SUCCESS,
1140
+ error: hadRealErrors ? 'Auto Translate failed to translate any values' : undefined,
1138
1141
  total: grandTotal,
1139
1142
  translated: grandTranslated,
1140
1143
  skipped: grandSkipped,