i18ntk 4.6.1 → 4.7.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.
Files changed (42) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +17 -11
  3. package/main/i18ntk-init.js +9 -7
  4. package/main/i18ntk-ui.js +1 -1
  5. package/main/i18ntk-usage.js +2 -2
  6. package/main/i18ntk-validate.js +1 -1
  7. package/main/manage/commands/ValidateCommand.js +1 -1
  8. package/main/manage/index.js +16 -20
  9. package/main/manage/services/ConfigurationService.js +1 -1
  10. package/main/manage/services/FrameworkDetectionService.js +1 -1
  11. package/main/manage/services/UsageService.js +2 -2
  12. package/package.json +12 -9
  13. package/settings/settings-manager.js +19 -3
  14. package/ui-locales/ar.json +2636 -0
  15. package/ui-locales/cs.json +2636 -0
  16. package/ui-locales/de.json +17 -1
  17. package/ui-locales/el.json +2636 -0
  18. package/ui-locales/en.json +17 -1
  19. package/ui-locales/es.json +17 -1
  20. package/ui-locales/fr.json +17 -1
  21. package/ui-locales/he.json +2636 -0
  22. package/ui-locales/hi.json +2636 -0
  23. package/ui-locales/hu.json +2636 -0
  24. package/ui-locales/it.json +2636 -0
  25. package/ui-locales/ja.json +17 -1
  26. package/ui-locales/ko.json +2636 -0
  27. package/ui-locales/nl.json +2636 -0
  28. package/ui-locales/pl.json +2636 -0
  29. package/ui-locales/pt.json +2636 -0
  30. package/ui-locales/ru.json +17 -1
  31. package/ui-locales/sv.json +2636 -0
  32. package/ui-locales/th.json +2636 -0
  33. package/ui-locales/tr.json +2636 -0
  34. package/ui-locales/uk.json +2636 -0
  35. package/ui-locales/vi.json +2636 -0
  36. package/ui-locales/zh.json +17 -1
  37. package/utils/config-helper.js +2 -2
  38. package/utils/config-manager.js +3 -4
  39. package/utils/env-manager.js +1 -1
  40. package/utils/framework-detector.js +252 -65
  41. package/utils/locale-optimizer.js +1 -1
  42. package/utils/report-model.js +16 -5
@@ -2057,7 +2057,23 @@
2057
2057
  "fr": "法语",
2058
2058
  "ru": "俄语",
2059
2059
  "ja": "日本人",
2060
- "zh": "中国人"
2060
+ "zh": "中国人",
2061
+ "it": "意大利语",
2062
+ "pt": "葡萄牙语",
2063
+ "nl": "荷兰语",
2064
+ "pl": "波兰语",
2065
+ "sv": "瑞典语",
2066
+ "uk": "乌克兰语",
2067
+ "cs": "捷克语",
2068
+ "tr": "土耳其语",
2069
+ "ko": "韩语",
2070
+ "ar": "阿拉伯语",
2071
+ "hi": "印地语",
2072
+ "th": "泰语",
2073
+ "vi": "越南语",
2074
+ "he": "希伯来语",
2075
+ "el": "希腊语",
2076
+ "hu": "匈牙利语"
2061
2077
  },
2062
2078
  "viewAll": {
2063
2079
  "title": "查看所有设置"
@@ -534,7 +534,7 @@ async function initializeSourceFiles(sourceDir, sourceLang) {
534
534
  SecurityUtils.safeWriteFileSync(sourceFile, JSON.stringify(defaultContent, null, 2), sourceDir, 'utf8');
535
535
 
536
536
  // Create directories for supported languages
537
- const supportedLanguages = ['es', 'fr', 'de', 'ja', 'ru', 'zh', 'pt'];
537
+ const supportedLanguages = ['es', 'fr', 'de', 'ja', 'ru', 'zh', 'pt', 'it', 'nl', 'pl', 'sv', 'uk', 'cs', 'tr', 'ko', 'ar', 'hi', 'th', 'vi', 'he', 'el', 'hu'];
538
538
 
539
539
  supportedLanguages.forEach(lang => {
540
540
  const langFile = path.join(sourceDir, `${lang}.json`);
@@ -564,7 +564,7 @@ async function initializeSourceFiles(sourceDir, sourceLang) {
564
564
  sourceDir: sourceDir,
565
565
  outputDir: "./i18ntk-reports",
566
566
  defaultLanguage: sourceLang,
567
- supportedLanguages: [sourceLang, 'es', 'fr', 'de', 'ja', 'ru', 'zh', 'pt'],
567
+ supportedLanguages: [sourceLang, 'es', 'fr', 'de', 'ja', 'ru', 'zh', 'pt', 'it', 'nl', 'pl', 'sv', 'uk', 'cs', 'tr', 'ko', 'ar', 'hi', 'th', 'vi', 'he', 'el', 'hu'],
568
568
  setup: {
569
569
  completed: true,
570
570
  completedAt: new Date().toISOString(),
@@ -106,10 +106,9 @@ const DEFAULT_CONFIG = {
106
106
  "setupId": null
107
107
  },
108
108
  "framework": {
109
- "preference": "auto", // one of: auto | vanilla | react | vue | angular | svelte | i18next | nuxt | next
110
- "fallback": "vanilla", // when auto detects nothing, use this value
111
- "detect": true,
112
- "supported": ["react", "vue", "angular", "svelte", "i18next", "nuxt", "next", "vanilla"]
109
+ "preference": "auto",
110
+ "fallback": "vanilla",
111
+ "detect": true
113
112
  },
114
113
  "scriptDirectories": {
115
114
  "init": null,
@@ -71,7 +71,7 @@ const ALLOWED_ENV_VARS = {
71
71
  // UI and interaction
72
72
  'I18NTK_UI_LANGUAGE': {
73
73
  default: 'en',
74
- validate: (value) => ['en', 'de', 'es', 'fr', 'ru', 'ja', 'zh'].includes(value.toLowerCase()),
74
+ validate: (value) => ['en', 'de', 'es', 'fr', 'it', 'pt', 'nl', 'pl', 'sv', 'uk', 'cs', 'tr', 'ru', 'ja', 'ko', 'zh', 'ar', 'hi', 'th', 'vi', 'he', 'el', 'hu'].includes(value.toLowerCase()),
75
75
  transform: (value) => value.toLowerCase()
76
76
  },
77
77
 
@@ -22,9 +22,17 @@ const FRAMEWORK_COMPATIBILITY = {
22
22
  'gatsby': { minVersion: '4.0.0' },
23
23
  'astro': { minVersion: '2.0.0' },
24
24
  'qwik': { minVersion: '1.0.0' },
25
+ 'nuxt': { minVersion: '7.0.0' },
26
+ 'nuxt-i18n': { minVersion: '7.0.0' },
27
+ 'next-intl': { minVersion: '2.0.0' },
28
+ 'ngx-translate': { minVersion: '13.0.0' },
29
+ 'svelte-i18n': { minVersion: '3.0.0' },
30
+ 'solid-i18n': { minVersion: '1.0.0' },
25
31
  'ember-intl': { minVersion: '5.0.0' },
26
32
  'react-native-localize': { minVersion: '2.0.0' },
27
- 'ionic': { minVersion: '6.0.0' }
33
+ 'ionic': { minVersion: '6.0.0' },
34
+ 'expo': { minVersion: '48.0.0' },
35
+ 'vanilla': { minVersion: '1.0.0' }
28
36
  };
29
37
 
30
38
  // Define framework detection in order of specificity
@@ -462,6 +470,13 @@ const SOURCE_DIRS = [
462
470
  ];
463
471
 
464
472
  const FRAMEWORK_PATTERNS = {
473
+ 'i18ntk-runtime': [
474
+ />([^<{][^<>{]*[^}>])</g,
475
+ /<button[^>]*>([^<]{2,99})<\/button>/g,
476
+ /i18n\.t\(["']([^"']{2,99})["']\)/g,
477
+ /useI18n\(\)\.t\(["']([^"']{2,99})["']\)/g,
478
+ /<t\s[\s\S]*?\bmessage\s*=\s*(?:\{|)(['"`])([^'"`}]+)\1[^>]*>/g
479
+ ],
465
480
  react: [
466
481
  /children:\s*["']([^"']{2,99})["']/g,
467
482
  /dangerouslySetInnerHTML={{\s*__html:\s*["']([^"']{2,99})["']/g,
@@ -476,6 +491,14 @@ const FRAMEWORK_PATTERNS = {
476
491
  /<button[^>]*>([^<]{2,99})<\/button>/g,
477
492
  /<(?:FormattedMessage|Trans)[\s\S]*?\b(?:id|defaultMessage|i18nKey)\s*=\s*(?:\{|)(['"`])([^'"`}]+)\1[^>]*>/g
478
493
  ],
494
+ nuxt: [
495
+ />([^<{][^<>{]*[^}>])</g,
496
+ /<button[^>]*>([^<]{2,99})<\/button>/g,
497
+ /\$t\(["']([^"']{2,99})["']\)/g,
498
+ /\$tc\(["']([^"']{2,99})["']\)/g,
499
+ /v-t=["']([^"']{2,99})["']/g,
500
+ /\blocalePath\(\s*\)/g
501
+ ],
479
502
  vue: [
480
503
  /v-text=["']([^"']{2,99})["']/g,
481
504
  /v-html=["']([^"']{2,99})["']/g,
@@ -562,6 +585,68 @@ const FRAMEWORK_PATTERNS = {
562
585
  /i18n\.NewMessage\([^,]+,\s*["']([^"']{2,99})["']\)/g,
563
586
  /t\.Get\([^,]+,\s*["']([^"']{2,99})["']\)/g
564
587
  ],
588
+ lingui: [
589
+ />([^<{][^<>{]*[^}>])</g,
590
+ /(?<![\w$.])t\s*\(\s*['"`]([^'"`]{2,99})['"`]/g,
591
+ /_\(\s*['"`]([^'"`]{2,99})['"`]/g,
592
+ /<Translate\s[\s\S]*?\bid\s*=\s*(?:\{|)(['"`])([^'"`}]+)\1[^>]*>/g
593
+ ],
594
+ formatjs: [
595
+ />([^<{][^<>{]*[^}>])</g,
596
+ /formatMessage\(\s*\{\s*id:\s*['"`]([^'"`]{2,99})['"`]/g,
597
+ /<FormattedMessage\s[\s\S]*?\bid\s*=\s*(?:\{|)(['"`])([^'"`}]+)\1[^>]*>/g
598
+ ],
599
+ 'ngx-translate': [
600
+ />([^<{][^<>{]*[^}>])</g,
601
+ /\|\s*translate\s*[^}]*\}\}/g,
602
+ /translateService\.instant\(\s*['"`]([^'"`]{2,99})['"`]/g,
603
+ /translateService\.get\(\s*['"`]([^'"`]{2,99})['"`]/g,
604
+ /translate\s*=\s*["'`]([^"'`]{2,99})["'`]/g,
605
+ /\[innerHTML\]=["']([^"']{2,99})["']/g
606
+ ],
607
+ 'next-intl': [
608
+ />([^<{][^<>{]*[^}>])</g,
609
+ /t\(\s*['"`]([^'"`]{2,99})['"`]/g,
610
+ /\buseTranslations\(\s*()\)/g
611
+ ],
612
+ 'svelte-i18n': [
613
+ />([^<{][^<>{]*[^}>])</g,
614
+ /\$_\(\s*['"`]([^'"`]{2,99})['"`]/g,
615
+ /\$_\w*\(\s*['"`]([^'"`]{2,99})['"`]/g,
616
+ /\bt\.get\(\s*['"`]([^'"`]{2,99})['"`]/g
617
+ ],
618
+ 'solid-i18n': [
619
+ />([^<{][^<>{]*[^}>])</g,
620
+ /\buseI18n\(\s*\)\s*\.\s*t\(\s*['"`]([^'"`]{2,99})['"`]/g,
621
+ /t\(\s*['"`]([^'"`]{2,99})['"`]/g,
622
+ /<Translate\s[\s\S]*?\bid\s*=\s*(?:\{|)(['"`])([^'"`}]+)\1[^>]*>/g
623
+ ],
624
+ fastapi: [
625
+ /_\(\s*['"`]([^'"`]{2,99})['"`]/g,
626
+ /i18n\.t\(\s*['"`]([^'"`]{2,99})['"`]/g,
627
+ /\{\{\s*_['"]([^'"]{2,99})['"]\s*\}\}/g,
628
+ /\{\{\s*gettext\(['"`]([^'"`]{2,99})['"`]\)\s*\}\}/g
629
+ ],
630
+ 'ruby-on-rails': [
631
+ /I18n\.t\(\s*['"]([^'"]{2,99})['"]/g,
632
+ /I18n\.translate\(\s*['"]([^'"]{2,99})['"]/g,
633
+ /I18n\.l\(\s*['"]([^'"]{2,99})['"]/g,
634
+ /<%= t\(['"]([^'"]{2,99})['"]/g,
635
+ /<%= I18n\.t\(['"]([^'"]{2,99})['"]/g,
636
+ /(?<![\w$.])t\s*\(\s*['"]([^'"]{2,99})['"]/g
637
+ ],
638
+ 'react-native-localize': [
639
+ />([^<{][^<>{]*[^}>])</g,
640
+ /\bi18n\.t\(\s*['"`]([^'"`]{2,99})['"`]/g,
641
+ /\bt\(\s*['"`]([^'"`]{2,99})['"`]/g,
642
+ /\btranslate\(\s*['"`]([^'"`]{2,99})['"`]/g
643
+ ],
644
+ ionic: [
645
+ />([^<{][^<>{]*[^}>])</g,
646
+ /\|\s*translate\s*[^}]*\}\}/g,
647
+ /translateService\.instant\(\s*['"`]([^'"`]{2,99})['"`]/g,
648
+ /translateService\.get\(\s*['"`]([^'"`]{2,99})['"`]/g
649
+ ],
565
650
  vanilla: [
566
651
  /t\(["']([^"']{2,99})["']\)/g,
567
652
  /i18n\.t\(["']([^"']{2,99})["']\)/g,
@@ -581,6 +666,18 @@ const FRAMEWORK_SUGGESTIONS = {
581
666
  solid: { hook: 'const [t] = useI18n();', usage: "{t('ui.KEY')}" },
582
667
  ember: { template: "{{t 'ui.KEY'}}", helper: "this.intl.t('ui.KEY')" },
583
668
  gatsby: { hook: 'const { t } = useTranslation();', usage: "{t('ui.KEY')}", plugin: "'gatsby-plugin-react-i18next'" },
669
+ nuxt: { directive: "{{ $t('ui.KEY') }}", method: "this.$t('ui.KEY')", component: "<NuxtLink :to=\"localePath('ui.KEY')\">" },
670
+ 'i18ntk-runtime': { hook: 'const { t } = useI18n();', usage: "{t('ui.KEY')}", component: "<t message=\"ui.KEY\">text</t>" },
671
+ lingui: { hook: 'import { t } from \"@lingui/macro\";', usage: "{t('ui.KEY')}", component: "<Trans id=\"ui.KEY\">text</Trans>" },
672
+ formatjs: { hook: 'import { FormattedMessage, useIntl } from \"react-intl\";', usage: "intl.formatMessage({ id: 'ui.KEY' })", component: "<FormattedMessage id=\"ui.KEY\" />" },
673
+ 'ngx-translate': { pipe: "{{ 'ui.KEY' | translate }}", service: "this.translateService.instant('ui.KEY')" },
674
+ 'next-intl': { hook: 'const t = useTranslations();', usage: "{t('ui.KEY')}" },
675
+ 'svelte-i18n': { store: "$_('ui.KEY')", method: "t.get('ui.KEY')" },
676
+ 'solid-i18n': { hook: 'const [t] = useI18n();', usage: "{t('ui.KEY')}", component: "<Translate id=\"ui.KEY\" />" },
677
+ fastapi: { python: "from fastapi_i18n import _\n_('text')", template: "{{ _('text') }}" },
678
+ 'ruby-on-rails': { helper: "t('ui.KEY')", method: "I18n.t('ui.KEY')", template: "<%= t('ui.KEY') %>" },
679
+ 'react-native-localize': { hook: 'import { t } from \"i18n-js\";', usage: "{t('ui.KEY')}" },
680
+ ionic: { pipe: "{{ 'ui.KEY' | translate }}", service: "this.translateService.instant('ui.KEY')" },
584
681
  django: { template: "{% trans 'text' %}", python: "from django.utils.translation import gettext as _\n_('text')", model: "from django.utils.translation import gettext_lazy as _\n_('text')" },
585
682
  flask: { template: "{{ _('text') }}", python: "from flask_babel import gettext as _\n_('text')", lazy: "from flask_babel import lazy_gettext as _\n_('text')" },
586
683
  python: { gettext: "import gettext\ngettext.gettext('text')", underscore: "from gettext import gettext as _\n_('text')", lazy: "from gettext import gettext_lazy as _\n_('text')" },
@@ -591,8 +688,13 @@ const FRAMEWORK_SUGGESTIONS = {
591
688
 
592
689
  const WRAPPER_SKIP_PATTERNS = [
593
690
  't(', 'tx(', 'i18n.t(', 'i18n.translate(', 'translate(',
594
- 'useI18n(', 'useTranslation(', '__', '__t', '$_(', '$t(',
595
- 'gettext(', 'gettext_lazy(', 'lazy_gettext(', 'pgettext(', 'ngettext('
691
+ 'useI18n(', 'useTranslation(', '__', '__t', '$_(', '$t(', '_(',
692
+ '$tc(', 'gettext(', 'gettext_lazy(', 'lazy_gettext(', 'pgettext(', 'ngettext(',
693
+ 'I18n.t(', 'I18n.translate(', 'I18n.l(', 'I18n.localize(',
694
+ 't.get(', 'useTranslate(', 'useSpeak(', 'withTranslation(',
695
+ 'formatMessage(', 'bundle.get_message(', 'i18n.NewMessage(',
696
+ 'i18n.Translate(', '_l(', '_n(', 't.set(', 'fluent!', 'ts!',
697
+ 'translateService.instant(', 'translateService.get('
596
698
  ];
597
699
 
598
700
  function _keySnippet(text) {
@@ -643,83 +745,168 @@ function detectFramework(projectRoot) {
643
745
  throw new Error('Invalid project root path');
644
746
  }
645
747
 
646
- const packageJsonPath = path.join(projectRoot, 'package.json');
647
748
  const detectedFrameworks = [];
648
749
 
649
- // Only proceed if package.json exists
650
- if (!SecurityUtils.safeExistsSync(packageJsonPath, projectRoot)) {
651
- return null;
750
+ // Phase 1: Check package.json for Node.js-based i18n frameworks
751
+ const packageJsonPath = path.join(projectRoot, 'package.json');
752
+ if (SecurityUtils.safeExistsSync(packageJsonPath, projectRoot)) {
753
+ try {
754
+ const packageJsonContent = SecurityUtils.safeReadFileSync(packageJsonPath, projectRoot, 'utf8');
755
+ if (packageJsonContent) {
756
+ const packageJson = SecurityUtils.safeParseJSON(packageJsonContent);
757
+ if (packageJson) {
758
+ const deps = {
759
+ ...(packageJson.dependencies || {}),
760
+ ...(packageJson.devDependencies || {}),
761
+ ...(packageJson.peerDependencies || {})
762
+ };
763
+
764
+ const sortedFrameworks = Object.entries(FRAMEWORKS).sort((a, b) =>
765
+ (b[1].priority || 0) - (a[1].priority || 0)
766
+ );
767
+
768
+ for (const [id, framework] of sortedFrameworks) {
769
+ try {
770
+ const frameworkDeps = framework.deps || framework.dependencies || [];
771
+ const hasAnyDep = frameworkDeps.some(dep => dep in deps);
772
+ if (hasAnyDep) {
773
+ const mainDep = frameworkDeps[0];
774
+ const frameworkInfo = {
775
+ id, name: framework.name || id, description: framework.description,
776
+ confidence: id === 'i18ntk-runtime' ? 0.95 : 0.9,
777
+ version: deps[mainDep] || '', priority: framework.priority || 0
778
+ };
779
+ detectedFrameworks.push(frameworkInfo);
780
+ }
781
+ } catch (_) { /* skip */ }
782
+ }
783
+ }
784
+ }
785
+ } catch (_) { /* skip */ }
652
786
  }
653
787
 
654
- try {
655
- // Read and parse package.json
656
- const packageJsonContent = SecurityUtils.safeReadFileSync(packageJsonPath, projectRoot, 'utf8');
657
- if (!packageJsonContent) {
658
- return null;
659
- }
660
- const packageJson = SecurityUtils.safeParseJSON(packageJsonContent);
661
- if (!packageJson) {
662
- return null;
788
+ // Phase 2: Check Python project files (no package.json or no Node matches)
789
+ if (detectedFrameworks.length === 0) {
790
+ const pyProjectFiles = ['requirements.txt', 'setup.py', 'setup.cfg', 'pyproject.toml', 'Pipfile', 'Pipfile.lock'];
791
+ const hasPyProject = pyProjectFiles.some(f => SecurityUtils.safeExistsSync(path.join(projectRoot, f), projectRoot));
792
+ if (hasPyProject) {
793
+ const pyDeps = readPyProjectDeps(projectRoot);
794
+ const pyFrameworks = ['django', 'flask', 'fastapi', 'python'];
795
+ for (const id of pyFrameworks) {
796
+ const framework = FRAMEWORKS[id];
797
+ if (!framework) continue;
798
+ const frameworkDeps = framework.deps || [];
799
+ const hasAnyDep = frameworkDeps.some(dep => pyDeps.includes(dep.toLowerCase().replace(/[_-]/g, '')));
800
+ if (hasAnyDep) {
801
+ detectedFrameworks.push({
802
+ id, name: framework.name || id, description: framework.description,
803
+ confidence: 0.8, version: '', priority: framework.priority || 0
804
+ });
805
+ }
806
+ }
807
+ if (detectedFrameworks.length === 0 && hasPyProject) {
808
+ detectedFrameworks.push({
809
+ id: 'python', name: 'Python', description: 'Python project with gettext/i18n',
810
+ confidence: 0.5, version: '', priority: 0
811
+ });
812
+ }
663
813
  }
814
+ }
664
815
 
665
- const deps = {
666
- ...(packageJson.dependencies || {}),
667
- ...(packageJson.devDependencies || {}),
668
- ...(packageJson.peerDependencies || {})
669
- };
670
-
671
- // Sort frameworks by priority (highest first)
672
- const sortedFrameworks = Object.entries(FRAMEWORKS).sort((a, b) =>
673
- (b[1].priority || 0) - (a[1].priority || 0)
674
- );
675
-
676
- // Check each framework's dependencies
677
- for (const [id, framework] of sortedFrameworks) {
816
+ // Phase 3: Check Rust project (Cargo.toml)
817
+ if (detectedFrameworks.length === 0) {
818
+ const cargoPath = path.join(projectRoot, 'Cargo.toml');
819
+ if (SecurityUtils.safeExistsSync(cargoPath, projectRoot)) {
678
820
  try {
679
- const frameworkDeps = framework.deps || framework.dependencies || [];
680
- const hasAnyDep = frameworkDeps.some(dep => dep in deps);
681
-
682
- if (hasAnyDep) {
683
- const mainDep = frameworkDeps[0];
684
- const frameworkInfo = {
685
- id,
686
- name: framework.name || id,
687
- description: framework.description,
688
- confidence: 0.9, // Base confidence when dependencies are found
689
- version: deps[mainDep] || '',
690
- priority: framework.priority || 0
691
- };
692
-
693
- // Boost confidence for i18ntk-runtime
694
- if (id === 'i18ntk-runtime') {
695
- frameworkInfo.confidence = 0.95;
821
+ const cargoContent = SecurityUtils.safeReadFileSync(cargoPath, projectRoot, 'utf8');
822
+ if (cargoContent) {
823
+ const hasRustI18n = /fluent|rust-i18n|gettext/.test(cargoContent);
824
+ if (hasRustI18n || /^\[package\]/m.test(cargoContent)) {
825
+ detectedFrameworks.push({
826
+ id: 'rust', name: 'Rust', description: 'Rust project with i18n',
827
+ confidence: hasRustI18n ? 0.85 : 0.4, version: '', priority: 0
828
+ });
696
829
  }
697
-
698
- detectedFrameworks.push(frameworkInfo);
699
830
  }
700
- } catch (error) {
701
- console.warn(`Error checking framework ${id}:`, error.message);
702
- continue;
703
- }
831
+ } catch (_) { /* skip */ }
704
832
  }
833
+ }
705
834
 
706
- // Return the framework with highest confidence, if any
707
- if (detectedFrameworks.length > 0) {
708
- return detectedFrameworks.sort((a, b) => {
709
- // First sort by confidence
710
- const confidenceDiff = b.confidence - a.confidence;
711
- if (confidenceDiff !== 0) return confidenceDiff;
835
+ // Phase 4: Check Go project (go.mod)
836
+ if (detectedFrameworks.length === 0) {
837
+ const goModPath = path.join(projectRoot, 'go.mod');
838
+ if (SecurityUtils.safeExistsSync(goModPath, projectRoot)) {
839
+ try {
840
+ const goModContent = SecurityUtils.safeReadFileSync(goModPath, projectRoot, 'utf8');
841
+ if (goModContent) {
842
+ const hasGoI18n = /go-i18n|x-text|i18n/.test(goModContent);
843
+ detectedFrameworks.push({
844
+ id: 'go', name: 'Go', description: 'Go project with i18n',
845
+ confidence: hasGoI18n ? 0.85 : 0.4, version: '', priority: 0
846
+ });
847
+ }
848
+ } catch (_) { /* skip */ }
849
+ }
850
+ }
712
851
 
713
- // If confidence is equal, sort by priority
714
- return (b.priority || 0) - (a.priority || 0);
715
- })[0];
852
+ // Phase 5: Check Ruby project (Gemfile)
853
+ if (detectedFrameworks.length === 0) {
854
+ const gemfilePaths = ['Gemfile', 'gems.rb'];
855
+ for (const gf of gemfilePaths) {
856
+ const gemfilePath = path.join(projectRoot, gf);
857
+ if (SecurityUtils.safeExistsSync(gemfilePath, projectRoot)) {
858
+ try {
859
+ const gemfileContent = SecurityUtils.safeReadFileSync(gemfilePath, projectRoot, 'utf8');
860
+ if (gemfileContent) {
861
+ const hasRails = /rails/.test(gemfileContent);
862
+ const hasI18n = /i18n/.test(gemfileContent);
863
+ if (hasRails) {
864
+ detectedFrameworks.push({
865
+ id: 'ruby-on-rails', name: 'Ruby on Rails', description: 'Rails project with i18n',
866
+ confidence: hasI18n ? 0.9 : 0.7, version: '', priority: 0
867
+ });
868
+ break;
869
+ }
870
+ if (hasI18n) {
871
+ detectedFrameworks.push({
872
+ id: 'ruby-on-rails', name: 'Ruby on Rails', description: 'Ruby project with i18n',
873
+ confidence: 0.6, version: '', priority: 0
874
+ });
875
+ break;
876
+ }
877
+ }
878
+ } catch (_) { /* skip */ }
879
+ }
716
880
  }
881
+ }
717
882
 
718
- return null;
719
- } catch (error) {
720
- console.error('Error detecting framework:', error);
721
- return null;
883
+ // Return the framework with highest confidence, if any
884
+ if (detectedFrameworks.length > 0) {
885
+ return detectedFrameworks.sort((a, b) => {
886
+ const confidenceDiff = b.confidence - a.confidence;
887
+ if (confidenceDiff !== 0) return confidenceDiff;
888
+ return (b.priority || 0) - (a.priority || 0);
889
+ })[0];
722
890
  }
891
+
892
+ return null;
893
+ }
894
+
895
+ function readPyProjectDeps(projectRoot) {
896
+ const deps = [];
897
+ try {
898
+ const reqPath = path.join(projectRoot, 'requirements.txt');
899
+ if (SecurityUtils.safeExistsSync(reqPath, projectRoot)) {
900
+ const content = SecurityUtils.safeReadFileSync(reqPath, projectRoot, 'utf8');
901
+ if (content) {
902
+ for (const line of content.split('\n')) {
903
+ const stripped = line.replace(/[<>=!~].*$/, '').trim();
904
+ if (stripped && !stripped.startsWith('#')) deps.push(stripped.toLowerCase().replace(/[_-]/g, ''));
905
+ }
906
+ }
907
+ }
908
+ } catch (_) { /* skip */ }
909
+ return deps;
723
910
  }
724
911
 
725
912
  module.exports = { detectFramework, FRAMEWORKS, FRAMEWORK_COMPATIBILITY,
@@ -23,7 +23,7 @@ const { getGlobalReadline } = require('./cli');
23
23
  class LocaleOptimizer {
24
24
  constructor() {
25
25
  this.uiLocalesDir = path.join(__dirname, '..', 'resources', 'i18n', 'ui-locales');
26
- this.allLocales = ['en', 'de', 'es', 'fr', 'ja', 'ru', 'zh'];
26
+ this.allLocales = ['en', 'de', 'es', 'fr', 'it', 'pt', 'nl', 'pl', 'sv', 'uk', 'cs', 'tr', 'ru', 'ja', 'ko', 'zh', 'ar', 'hi', 'th', 'vi', 'he', 'el', 'hu'];
27
27
  this.backupDir = path.join(__dirname, '..', 'backups', 'locales');
28
28
  this.rl = getGlobalReadline();
29
29
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
- const { getSourceExtensions, getExcludeDirs } = require('./framework-detector');
5
+ const { getSourceExtensions, getExcludeDirs, getFrameworkPatterns } = require('./framework-detector');
6
6
  const SecurityUtils = require('./security');
7
7
 
8
8
  const EXCLUDE_DIRS = new Set(getExcludeDirs());
@@ -83,7 +83,8 @@ function generateI18ntkReport(options = {}) {
83
83
  Object.keys(values).forEach(key => allKeys.add(key));
84
84
  }
85
85
 
86
- const sourceScan = scanSourceFiles(sourceDir, new Set(Object.keys(sourceValues)));
86
+ const framework = options.framework || 'vanilla';
87
+ const sourceScan = scanSourceFiles(sourceDir, new Set(Object.keys(sourceValues)), framework);
87
88
  const usedKeys = sourceScan.usedKeys;
88
89
  const issues = [];
89
90
  const addIssue = issue => issues.push(normalizeIssue(issue, issues.length + 1, projectRoot));
@@ -288,7 +289,7 @@ function flattenObject(value, prefix = '', out = {}) {
288
289
  return out;
289
290
  }
290
291
 
291
- function scanSourceFiles(sourceDir, availableKeys) {
292
+ function scanSourceFiles(sourceDir, availableKeys, framework) {
292
293
  const usedKeys = new Set();
293
294
  const usageLocations = new Map();
294
295
  const hardcodedTexts = [];
@@ -299,15 +300,25 @@ function scanSourceFiles(sourceDir, availableKeys) {
299
300
  for (const file of files) {
300
301
  const content = SecurityUtils.safeReadFileSync(file, basePath, 'utf8');
301
302
  const lines = content.split(/\r?\n/);
303
+ const frameworkPatterns = framework ? getFrameworkPatterns(framework) : [];
302
304
  const keyPatterns = [
303
305
  /\b(?:t|tx|__|_t)\s*\(\s*['"`]([^'"`]+)['"`]/g,
304
306
  /\bi18n\.t\s*\(\s*['"`]([^'"`]+)['"`]/g,
305
307
  /\bi18nKey\s*=\s*['"`]([^'"`]+)['"`]/g,
308
+ /\$t\(\s*['"`]([^'"`]+)['"`]/g,
309
+ /\|\s*translate/g,
310
+ /\{\{\s*t\s+['"`]([^'"`]+)['"`]/g,
311
+ /\b(I18n|i18n)\.(t|translate|localize|l)\s*\(\s*['"`]([^'"`]+)['"`]/g,
312
+ /\{\%\s*trans\s+['"`]([^'"`]+)['"`]\s*%\}/g,
313
+ /gettext\(['"`]([^'"`]+)['"`]\)/g,
314
+ /_\(['"`]([^'"`]+)['"`]\)/g,
315
+ /\$_\(\s*['"`]([^'"`]+)['"`]/g
306
316
  ];
307
- for (const pattern of keyPatterns) {
317
+ for (const pattern of [...keyPatterns, ...frameworkPatterns]) {
308
318
  let match;
309
319
  while ((match = pattern.exec(content))) {
310
- const key = match[1];
320
+ const key = match[1] || match[2];
321
+ if (!key) continue;
311
322
  usedKeys.add(key);
312
323
  const location = offsetToLocation(content, match.index);
313
324
  const list = usageLocations.get(key) || [];