i18ntk 4.5.4 → 4.6.0

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.
@@ -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;
@@ -9,6 +9,7 @@
9
9
 
10
10
  const fs = require('fs');
11
11
  const path = require('path');
12
+ const { detectProjectFramework, getFrameworkPatterns, getFrameworkSuggestions, SCANNER_EXTENSIONS, WRAPPER_SKIP_PATTERNS } = require('../../../utils/framework-detector');
12
13
  const { getUnifiedConfig, displayHelp } = require('../../../utils/config-helper');
13
14
  const { loadTranslations } = require('../../../utils/i18n-helper');
14
15
  const SecurityUtils = require('../../../utils/security');
@@ -207,11 +208,24 @@ class ScannerCommand {
207
208
  const packageJson = SecurityUtils.safeParseJSON(packageJsonContent);
208
209
  const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };
209
210
 
211
+ if (deps.next || deps['next-intl'] || deps['next-i18next']) return 'next';
210
212
  if (deps.react || deps['react-dom']) return 'react';
211
213
  if (deps.vue || deps['vue-router']) return 'vue';
212
214
  if (deps['@angular/core'] || deps.angular) return 'angular';
213
- if (deps.next) return 'next';
214
215
  if (deps.svelte) return 'svelte';
216
+ if (deps.astro || deps['astro-i18next'] || deps['@astrojs/i18n']) return 'astro';
217
+ if (deps['@remix-run/react'] || deps['remix-i18next']) return 'remix';
218
+ if (deps.gatsby || deps['gatsby-plugin-react-i18next']) return 'gatsby';
219
+ if (deps['@builder.io/qwik'] || deps['qwik-speak']) return 'qwik';
220
+ if (deps['solid-js'] || deps['@solid-primitives/i18n']) return 'solid';
221
+ if (deps['ember-source'] || deps['ember-intl']) return 'ember';
222
+ if (deps.nuxt || deps['@nuxtjs/i18n']) return 'nuxt';
223
+
224
+ const cargoTomlPath = path.join(projectRoot, 'Cargo.toml');
225
+ if (SecurityUtils.safeExistsSync(cargoTomlPath, projectRoot)) return 'rust';
226
+
227
+ const goModPath = path.join(projectRoot, 'go.mod');
228
+ if (SecurityUtils.safeExistsSync(goModPath, projectRoot)) return 'go';
215
229
 
216
230
  return 'vanilla';
217
231
  } catch (error) {
@@ -237,57 +251,110 @@ class ScannerCommand {
237
251
 
238
252
  const frameworkSpecific = {
239
253
  react: [
240
- // React specific patterns - enhanced for i18next detection
241
254
  /children:\s*["']([^"']{2,99})["']/g,
242
255
  /dangerouslySetInnerHTML={{\s*__html:\s*["']([^"']{2,99})["']/g,
243
- // JSX text content without translation
244
256
  />([^<{][^<>{]*[^}>])</g,
245
- // Button text
246
257
  /<button[^>]*>([^<]{2,99})<\/button>/g,
247
- // Span text
248
- /<span[^>]*>([^<]{2,99})<\/span>/g
258
+ /<span[^>]*>([^<]{2,99})<\/span>/g,
259
+ /<(?:FormattedMessage|Trans)[\s\S]*?\b(?:id|defaultMessage|i18nKey)\s*=\s*(?:\{|)(['"`])([^'"`}]+)\1[^>]*>/g,
260
+ /<(?:FormattedMessage|Trans)[\s\S]*?\b(?:id|defaultMessage|i18nKey)\s*=\s*\{[']\s*([^'}]+\.[^'}]+)\s*[']/g
249
261
  ],
250
262
  vue: [
251
- // Vue specific patterns - enhanced for vue-i18n detection
252
263
  /v-text=["']([^"']{2,99})["']/g,
253
264
  /v-html=["']([^"']{2,99})["']/g,
254
- // Vue template text
255
265
  />([^<{][^<>{]*[^}>])</g,
256
- // Button text
257
266
  /<button[^>]*>([^<]{2,99})<\/button>/g,
258
- // Span text
259
- /<span[^>]*>([^<]{2,99})<\/span>/g
267
+ /<span[^>]*>([^<]{2,99})<\/span>/g,
268
+ /\$t\(["']([^"']{2,99})["']\)/g,
269
+ /v-t=["']([^"']{2,99})["']/g
260
270
  ],
261
271
  angular: [
262
- // Angular specific patterns - enhanced for ngx-translate detection
263
272
  /\[innerHTML\]=["']([^"']{2,99})["']/g,
264
273
  /\[textContent\]=["']([^"']{2,99})["']/g,
265
- // Angular template text
266
274
  />([^<{][^<>{]*[^}>])</g,
267
- // Button text
268
275
  /<button[^>]*>([^<]{2,99})<\/button>/g,
269
- // Span text
270
- /<span[^>]*>([^<]{2,99})<\/span>/g
276
+ /<span[^>]*>([^<]{2,99})<\/span>/g,
277
+ /i18n=["']([^"']{2,99})["']/g,
278
+ /\[attr\.title\]=["']([^"']{2,99})["']/g
279
+ ],
280
+ next: [
281
+ /children:\s*["']([^"']{2,99})["']/g,
282
+ />([^<{][^<>{]*[^}>])</g,
283
+ /<button[^>]*>([^<]{2,99})<\/button>/g,
284
+ /<(?:FormattedMessage|Trans)[\s\S]*?\b(?:id|defaultMessage|i18nKey)\s*=\s*(?:\{|)(['"`])([^'"`}]+)\1[^>]*>/g
285
+ ],
286
+ svelte: [
287
+ />([^<{][^<>{]*[^}>])</g,
288
+ /<button[^>]*>([^<]{2,99})<\/button>/g,
289
+ /\$_\(["']([^"']{2,99})["']\)/g,
290
+ /t\.set\(["']([^"']{2,99})["']/g,
291
+ /\{#if[^}]*\}([^<]{2,99})\{\/if\}/g
292
+ ],
293
+ astro: [
294
+ />([^<{][^<>{]*[^}>])</g,
295
+ /<button[^>]*>([^<]{2,99})<\/button>/g,
296
+ /t\(["']([^"']{2,99})["']\)/g
297
+ ],
298
+ remix: [
299
+ /children:\s*["']([^"']{2,99})["']/g,
300
+ />([^<{][^<>{]*[^}>])</g,
301
+ /<button[^>]*>([^<]{2,99})<\/button>/g,
302
+ /<(?:FormattedMessage|Trans)[\s\S]*?\b(?:id|defaultMessage|i18nKey)\s*=\s*(?:\{|)(['"`])([^'"`}]+)\1[^>]*>/g
303
+ ],
304
+ qwik: [
305
+ />([^<{][^<>{]*[^}>])</g,
306
+ /<button[^>]*>([^<]{2,99})<\/button>/g,
307
+ /t\(["']([^"']{2,99})["']\)/g,
308
+ /useTranslate\(\)/g
309
+ ],
310
+ solid: [
311
+ />([^<{][^<>{]*[^}>])</g,
312
+ /<button[^>]*>([^<]{2,99})<\/button>/g,
313
+ /t\(["']([^"']{2,99})["']\)/g,
314
+ /useI18n\(\)\.t\(["']([^"']{2,99})["']\)/g
315
+ ],
316
+ ember: [
317
+ />([^<{][^<>{]*[^}>])</g,
318
+ /\{\{t\s+["']([^"']{2,99})["']\s*\}\}/g,
319
+ /\{\{intl\.t\s+["']([^"']{2,99})["']\s*\}\}/g
320
+ ],
321
+ gatsby: [
322
+ /children:\s*["']([^"']{2,99})["']/g,
323
+ />([^<{][^<>{]*[^}>])</g,
324
+ /<(?:Trans)[\s\S]*?\bi18nKey\s*=\s*(?:\{|)(['"`])([^'"`}]+)\1[^>]*>/g
271
325
  ],
272
326
  django: [
273
- // Django template patterns
274
327
  /\{\%\s*trans\s+["']([^"']{2,99})["']\s*%\}/g,
275
328
  /\{\%\s*blocktrans\s*%\}([^%]{2,99})\{\%\s*endblocktrans\s*%\}/g,
276
329
  /{{\s*_["']([^"']{2,99})["']\s*}}/g,
277
330
  /{{\s*gettext\(["']([^"']{2,99})["']\)\s*}}/g
278
331
  ],
279
332
  flask: [
280
- // Flask/Jinja2 template patterns
281
333
  /\{\{\s*_["']([^"']{2,99})["']\s*}}/g,
282
334
  /\{\{\s*gettext\(["']([^"']{2,99})["']\)\s*}}/g,
283
335
  /\{\{\s*lazy_gettext\(["']([^"']{2,99})["']\)\s*}}/g
284
336
  ],
285
337
  python: [
286
- // Python source patterns
287
338
  /gettext\(["']([^"']{2,99})["']\)/g,
288
339
  /_\(["']([^"']{2,99})["']\)/g,
289
340
  /gettext_lazy\(["']([^"']{2,99})["']\)/g,
290
341
  /lazy_gettext\(["']([^"']{2,99})["']\)/g
342
+ ],
343
+ rust: [
344
+ /bundle\.get_message\(\s*["']([^"']{2,99})["']\)/g,
345
+ /\.get_message\s*\(\s*["']([^"']{2,99})["']\)/g,
346
+ /ts!\s*\(\s*["']([^"']{2,99})["']/g,
347
+ /fluent!\s*\(\s*["']([^"']{2,99})["']/g
348
+ ],
349
+ go: [
350
+ /i18n\.Translate\([^,]+,\s*["']([^"']{2,99})["']\)/g,
351
+ /i18n\.NewMessage\([^,]+,\s*["']([^"']{2,99})["']\)/g,
352
+ /t\.Get\([^,]+,\s*["']([^"']{2,99})["']\)/g
353
+ ],
354
+ vanilla: [
355
+ /t\(["']([^"']{2,99})["']\)/g,
356
+ /i18n\.t\(["']([^"']{2,99})["']\)/g,
357
+ /translate\(["']([^"']{2,99})["']\)/g
291
358
  ]
292
359
  };
293
360
 
@@ -380,24 +447,63 @@ class ScannerCommand {
380
447
  key: `ui.${key}`,
381
448
  original: text,
382
449
  translationKey: `t('ui.${key}')`,
383
- frameworkSpecific: this.getFrameworkSpecific(text)
450
+ frameworkSpecific: getFrameworkSuggestions(this.framework, text)
384
451
  };
385
452
  }
386
453
 
387
454
  getFrameworkSpecific(text) {
455
+ const keySnippet = (str) => str.toLowerCase()
456
+ .replace(/[^a-z0-9\s]/g, '')
457
+ .replace(/\s+/g, '_')
458
+ .substring(0, 40);
388
459
  const frameworks = {
389
460
  react: {
390
461
  hook: `const { t } = useTranslation();`,
391
- usage: `{t('ui.${text.toLowerCase().replace(/[^a-z0-9\s]/g, '').replace(/\s+/g, '_')}')}`,
392
- component: `<Trans i18nKey="ui.${text.toLowerCase().replace(/[^a-z0-9\s]/g, '').replace(/\s+/g, '_')}">${text}</Trans>`
462
+ usage: `{t('ui.${keySnippet(text)}')}`,
463
+ component: `<Trans i18nKey="ui.${keySnippet(text)}">${text}</Trans>`
464
+ },
465
+ next: {
466
+ hook: `const t = useTranslations();`,
467
+ usage: `{t('ui.${keySnippet(text)}')}`,
468
+ component: `<Trans i18nKey="ui.${keySnippet(text)}">${text}</Trans>`
393
469
  },
394
470
  vue: {
395
- directive: `{{ $t('ui.${text.toLowerCase().replace(/[^a-z0-9\s]/g, '').replace(/\s+/g, '_')}') }}`,
396
- method: `this.$t('ui.${text.toLowerCase().replace(/[^a-z0-9\s]/g, '').replace(/\s+/g, '_')}')`
471
+ directive: `{{ $t('ui.${keySnippet(text)}') }}`,
472
+ method: `this.$t('ui.${keySnippet(text)}')`
397
473
  },
398
474
  angular: {
399
475
  pipe: `{{ '${text}' | translate }}`,
400
- service: `this.translateService.instant('ui.${text.toLowerCase().replace(/[^a-z0-9\s]/g, '').replace(/\s+/g, '_')}')`
476
+ service: `this.translateService.instant('ui.${keySnippet(text)}')`
477
+ },
478
+ svelte: {
479
+ store: `$_(('ui.${keySnippet(text)}'))`,
480
+ method: `t.set('ui.${keySnippet(text)}', '${text}')`
481
+ },
482
+ astro: {
483
+ import: `import { t } from 'astro-i18next';`,
484
+ usage: `{t('ui.${keySnippet(text)}')}`
485
+ },
486
+ remix: {
487
+ hook: `const { t } = useTranslation();`,
488
+ usage: `{t('ui.${keySnippet(text)}')}`,
489
+ server: `export const handle = i18next.handle;`
490
+ },
491
+ qwik: {
492
+ hook: `const t = useTranslate();`,
493
+ usage: `{t('ui.${keySnippet(text)}')}`
494
+ },
495
+ solid: {
496
+ hook: `const [t] = useI18n();`,
497
+ usage: `{t('ui.${keySnippet(text)}')}`
498
+ },
499
+ ember: {
500
+ template: `{{t 'ui.${keySnippet(text)}'}}`,
501
+ helper: `this.intl.t('ui.${keySnippet(text)}')`
502
+ },
503
+ gatsby: {
504
+ hook: `const { t } = useTranslation();`,
505
+ usage: `{t('ui.${keySnippet(text)}')}`,
506
+ plugin: `'gatsby-plugin-react-i18next'`
401
507
  },
402
508
  django: {
403
509
  template: `{% trans '${text}' %}`,
@@ -413,6 +519,17 @@ class ScannerCommand {
413
519
  gettext: `import gettext\ngettext.gettext('${text}')`,
414
520
  underscore: `from gettext import gettext as _\n_('${text}')`,
415
521
  lazy: `from gettext import gettext_lazy as _\n_('${text}')`
522
+ },
523
+ rust: {
524
+ fluent: `bundle.get_message("ui_${keySnippet(text)}")`,
525
+ gettext: `gettext("ui_${keySnippet(text)}")`
526
+ },
527
+ go: {
528
+ translate: `i18n.Translate("ui_${keySnippet(text)}")`,
529
+ message: `i18n.NewMessage("ui_${keySnippet(text)}")`
530
+ },
531
+ vanilla: {
532
+ generic: `t('ui.${keySnippet(text)}')`
416
533
  }
417
534
  };
418
535
 
@@ -433,7 +550,7 @@ class ScannerCommand {
433
550
  }
434
551
 
435
552
  const allResults = [];
436
- const extensions = ['.js', '.jsx', '.ts', '.tsx', '.vue', '.html', '.svelte', '.py', '.pyx', '.pyi'];
553
+ const extensions = [...SCANNER_EXTENSIONS];
437
554
 
438
555
  const scanRecursive = (currentDir) => {
439
556
  const items = SecurityUtils.safeReaddirSync(currentDir, path.dirname(currentDir), { withFileTypes: true });
@@ -586,7 +703,7 @@ class ScannerCommand {
586
703
  } else if (fwPref && fwPref !== 'auto') {
587
704
  this.framework = fwPref;
588
705
  } else if (fwDetectEnabled) {
589
- const detected = this.detectFramework(process.cwd());
706
+ const detected = detectProjectFramework(process.cwd());
590
707
  this.framework = detected || fwFallback;
591
708
  } else {
592
709
  this.framework = fwFallback;
@@ -612,7 +729,7 @@ class ScannerCommand {
612
729
  console.log(this.t('scanner.starting', { framework: this.framework }));
613
730
  console.log(this.t('scanner.sourceDirectory', { sourceDir: this.sourceDir }));
614
731
 
615
- const patterns = this.getFrameworkPatterns(this.framework);
732
+ const patterns = getFrameworkPatterns(this.framework);
616
733
  const exclusions = this.config.exclude || ['node_modules', '.git', 'dist', 'build'];
617
734
  const minLength = this.config.minLength || 3;
618
735
  const maxLength = this.config.maxLength || 100;