i18ntk 4.6.0 → 4.7.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.
package/CHANGELOG.md CHANGED
@@ -3,9 +3,37 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
- and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
-
8
- ## [4.6.0] - 2026-07-04
6
+
7
+ ## [4.7.0] - 2026-07-07
8
+
9
+ ### Added
10
+
11
+ - **13 new FRAMEWORK_PATTERNS:** `i18ntk-runtime` (hooks, `<t>` component), `nuxt` (`$tc()`, `localePath()`), `lingui` (`<Translate>` component), `formatjs` (`<FormattedMessage>`), `ngx-translate` (pipe, service patterns), `next-intl`, `svelte-i18n`, `solid-i18n` (`<Translate>`), `fastapi`, `ruby-on-rails` (`I18n.t/l/translate`), `react-native-localize`, `ionic`. Each has framework-specific scanning regex patterns.
12
+ - **8 missing FRAMEWORK_COMPATIBILITY entries:** `nuxt`, `nuxt-i18n`, `next-intl`, `ngx-translate`, `svelte-i18n`, `solid-i18n`, `expo`, `vanilla` with min versions.
13
+ - **Non-Node project detection:** `detectFramework()` now checks Python (`requirements.txt`, `pyproject.toml`, `setup.py`), Rust (`Cargo.toml`), Go (`go.mod`), Ruby (`Gemfile`) when no `package.json` exists. Detects Django, Flask, FastAPI, Rails, and generic i18n usage.
14
+ - **20+ new WRAPPER_SKIP_PATTERNS:** `I18n.t(`, `I18n.translate(`, `useTranslate(`, `t.get(`, `$tc(`, `translateService.instant(`, `formatMessage(`, `bundle.get_message(`, `i18n.NewMessage(`, `_(`, `fluent!`, `ts!` and more.
15
+ - **Expanded KNOWN_WRAPPERS in Lens scanner:** Added `$_`, `$tc`, `gettext`, `gettext_lazy`, `I18n.t`, `I18n.translate`, `I18n.l`, `I18n.localize`.
16
+ - **Expanded NAMESPACE_HELPERS in both scanners:** Added `useTranslate`, `useSpeak`, `withTranslation`.
17
+ - **Framework-aware report-model.js:** `scanSourceFiles()` accepts optional framework parameter, uses `getFrameworkPatterns()` for framework-specific key extraction.
18
+ - **Expanded CLI init framework list:** `checkI18nDependencies()` now scans 27+ npm package names instead of 7.
19
+ - **Sandbox test suite:** 98 new tests covering framework detection against real package.json, requirements.txt, Cargo.toml, go.mod, Gemfile fixtures. Coverage validation for all FRAMEWORK_PATTERNS, FRAMEWORK_SUGGESTIONS, FRAMEWORK_COMPATIBILITY, WRAPPER_SKIP_PATTERNS, and SCANNER_EXTENSIONS.
20
+
21
+ ### Changed
22
+
23
+ - **Version bumped to 4.7.0** for the comprehensive framework detection expansion.
24
+ - **detectFramework()** no longer returns `null` when only non-Node project files are present. Cascades through 5 detection phases: Node → Python → Rust → Go → Ruby.
25
+
26
+ ## [4.6.1] - 2026-07-05
27
+
28
+ ### Fixed
29
+
30
+ - **LANGUAGE_CONFIG missing 22+ languages:** `i18ntk-init.js` and `InitService.js` expanded from 21 to 61 languages with native names. All common ISO 639-1 codes now display proper names instead of "Unknown".
31
+ - **Init silently truncating defaultLanguages:** When `defaultLanguages` is an empty array or `null`, init no longer silently falls back to 5 languages.
32
+ - **Auto Translate exiting non-zero on success:** Residual untranslated values (placeholders, branded terms) no longer cause non-zero exit codes. Only real failures produce exit 1.
33
+ - **supportedExtensions silently overridden:** `i18ntk-init.js` no longer unconditionally overrides `supportedExtensions` from config with the format adapter's single extension.
34
+ - **allowedEnglishTerms in two places:** `validation-risk.js` now exports `resolveAllowedEnglishTerms()` which accepts both per-call options and project-level config terms as fallback sources.
35
+
36
+ ## [4.6.0] - 2026-07-04
9
37
 
10
38
  ### Added
11
39
 
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # i18ntk v4.6.0
1
+ # i18ntk v4.7.0
2
2
 
3
3
  A zero-dependency internationalization toolkit for setup, scanning, analysis, validation, usage tracking, translation completion, automatic JSON locale translation, reporting, and runtime translation loading.
4
4
 
@@ -9,7 +9,7 @@ A zero-dependency internationalization toolkit for setup, scanning, analysis, va
9
9
  [![node](https://img.shields.io/badge/node-%3E%3D16-339933)](https://nodejs.org)
10
10
  [![dependencies](https://img.shields.io/badge/dependencies-0-success)](https://www.npmjs.com/package/i18ntk)
11
11
  [![license](https://img.shields.io/badge/license-MIT-yellow.svg)](LICENSE)
12
- [![socket](https://socket.dev/api/badge/npm/package/i18ntk/4.6.0)](https://socket.dev/npm/package/i18ntk/overview/4.6.0)
12
+ [![socket](https://socket.dev/api/badge/npm/package/i18ntk/4.7.0)](https://socket.dev/npm/package/i18ntk/overview/4.7.0)
13
13
 
14
14
  [![i18ntk Workbench](https://img.shields.io/badge/VS_Code-Workbench-007ACC?logo=visualstudiocode&logoColor=white)](https://marketplace.visualstudio.com/items?itemName=VladNoskov.i18ntk-workbench)
15
15
  [![i18ntk Lens](https://img.shields.io/badge/VS_Code-Lens-007ACC?logo=visualstudiocode&logoColor=white)](https://marketplace.visualstudio.com/items?itemName=VladNoskov.i18ntk-lens)
@@ -27,15 +27,15 @@ npm install -g i18ntk
27
27
  npx i18ntk --help
28
28
  ```
29
29
 
30
- ## What's New in 4.6.0
30
+ ## What's New in 4.7.0
31
31
 
32
- - **25+ framework detections** — Rust, Remix, Gatsby, Astro, Qwik, SolidJS, Ember, React Native, Expo, Ionic added. All drawn from a single centralized detector.
33
- - **Rust support** — Cargo.toml detection and `.rs` file scanning with fluent/gettext-rs framework patterns.
34
- - **File extensions** — `.astro`, `.mdx`, `.mjs`, `.mts`, `.cjs`, `.cts`, `.rs` scanned across all tools.
35
- - **ICU/Fluent placeholder support** — Fluent `$variable` and ICU `{var, plural, ...}` patterns detected.
36
- - **JSX component detection** — `<Trans>`, `<FormattedMessage>`, `<Translate>` components recognized.
37
- - **Health score fix** — No more negative or misleadingly low scores.
38
- - **Centralized architecture** Framework data, extensions, excludes, and patterns live in one module consumed by all tools.
32
+ - **30+ framework detection patterns** — 13 new FRAMEWORK_PATTERNS: `i18ntk-runtime`, `nuxt`, `lingui`, `formatjs`, `ngx-translate`, `next-intl`, `svelte-i18n`, `solid-i18n`, `fastapi`, `ruby-on-rails`, `react-native-localize`, `ionic`. Each with framework-specific scan regexes for translation calls, JSX components, template directives, and pipes.
33
+ - **Non-Node project detection** — Python (`requirements.txt`, `pyproject.toml`, `setup.py`), Rust (`Cargo.toml`), Go (`go.mod`), Ruby (`Gemfile`) now detected when no `package.json` exists. Detects Django, Flask, FastAPI, Rails, and generic i18n.
34
+ - **20+ new WRAPPER_SKIP_PATTERNS** — Covers I18n.t(), useTranslate(), translateService.instant(), formatMessage(), bundle.get_message(), fluent!, ts! and more.
35
+ - **Framework-aware report generation** — `report-model.js` accepts optional framework parameter and uses framework-specific patterns for key extraction.
36
+ - **Expanded namespace helpers** — `useTranslate` (Qwik), `useSpeak` (Qwik), `withTranslation` (react-i18next) added.
37
+ - **Attribute key detection** — `i18nKey=`, `t-key=`, `data-i18n=` attributes detected in source scanning.
38
+ - **All frame works now have FRAMEWORK_COMPATIBILITY entries and FRAMEWORK_SUGGESTIONS** for consistent tooling.
39
39
 
40
40
  [Full changelog →](./CHANGELOG.md)
41
41
 
@@ -122,7 +122,7 @@ Example `.i18ntk-config`:
122
122
 
123
123
  ```json
124
124
  {
125
- "version": "4.6.0",
125
+ "version": "4.6.1",
126
126
  "sourceDir": "./locales",
127
127
  "i18nDir": "./locales",
128
128
  "sourceLanguage": "en",
@@ -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;
@@ -107,13 +138,15 @@ class I18nInitializer {
107
138
  };
108
139
 
109
140
  const i18nFrameworks = [
110
- 'react-i18next',
111
- 'vue-i18n',
112
- 'angular-i18n',
113
- 'i18next',
114
- 'next-i18next',
115
- 'svelte-i18n',
116
- '@nuxtjs/i18n'
141
+ 'react-i18next', 'vue-i18n', 'i18next', 'next-i18next',
142
+ 'svelte-i18n', '@nuxtjs/i18n', 'next-intl', 'remix-i18next',
143
+ 'i18next-remix', 'gatsby-plugin-react-i18next', 'gatsby-plugin-intl',
144
+ 'sveltekit-i18n', 'astro-i18next', '@astrojs/i18n',
145
+ 'qwik-speak', 'qwik-i18n', '@solid-primitives/i18n',
146
+ 'ember-intl', 'react-native-localize', 'expo-localization',
147
+ '@ngx-translate/core', '@ionic/angular',
148
+ 'formatjs', '@lingui/core', 'i18ntk-runtime', 'i18ntk/runtime',
149
+ 'angular-i18n', '@angular/localize'
117
150
  ];
118
151
 
119
152
  const installedFrameworks = i18nFrameworks.filter(framework => dependencies[framework]);
@@ -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,
@@ -18,29 +18,56 @@ const AdminAuth = require('../../../utils/admin-auth');
18
18
  const { parseConfirmation } = require('../../../utils/localized-confirm');
19
19
  const { normalizeReportFormat, writeReportFile } = require('../../../utils/report-writer');
20
20
 
21
- // Language configurations with native names
22
- const LANGUAGE_CONFIG = {
23
- 'en': { name: 'English', nativeName: 'English' },
24
- 'de': { name: 'German', nativeName: 'Deutsch' },
25
- 'es': { name: 'Spanish', nativeName: 'Espa\u00f1ol' },
26
- 'fr': { name: 'French', nativeName: 'Fran\u00e7ais' },
27
- 'ru': { name: 'Russian', nativeName: '\u0420\u0443\u0441\u0441\u043a\u0438\u0439' },
28
- 'it': { name: 'Italian', nativeName: 'Italiano' },
29
- 'ja': { name: 'Japanese', nativeName: '\u65e5\u672c\u8a9e' },
30
- 'ko': { name: 'Korean', nativeName: '\ud55c\uad6d\uc5b4' },
31
- 'zh': { name: 'Chinese', nativeName: '\u4e2d\u6587' },
32
- 'ar': { name: 'Arabic', nativeName: '\u0627\u0644\u0639\u0631\u0628\u064a\u0629' },
33
- 'hi': { name: 'Hindi', nativeName: '\u0939\u093f\u0928\u094d\u0926\u0940' },
34
- 'nl': { name: 'Dutch', nativeName: 'Nederlands' },
35
- 'sv': { name: 'Swedish', nativeName: 'Svenska' },
36
- 'da': { name: 'Danish', nativeName: 'Dansk' },
37
- 'no': { name: 'Norwegian', nativeName: 'Norsk' },
38
- 'fi': { name: 'Finnish', nativeName: 'Suomi' },
39
- 'pl': { name: 'Polish', nativeName: 'Polski' },
40
- 'cs': { name: 'Czech', nativeName: '\u010ce\u0161tina' },
41
- 'hu': { name: 'Hungarian', nativeName: 'Magyar' },
42
- 'tr': { name: 'Turkish', nativeName: 'T\u00fcrk\u00e7e' }
43
- };
21
+ // Language configurations with native names — comprehensive ISO 639-1 coverage
22
+ const LANGUAGE_CONFIG = {
23
+ 'af': { name: 'Afrikaans', nativeName: 'Afrikaans' }, 'sq': { name: 'Albanian', nativeName: 'Shqip' },
24
+ 'ar': { name: 'Arabic', nativeName: '\u0627\u0644\u0639\u0631\u0628\u064a\u0629' }, 'hy': { name: 'Armenian', nativeName: '\u0540\u0561\u0575\u0565\u0580\u0565\u0576' },
25
+ 'az': { name: 'Azerbaijani', nativeName: 'Az\u0259rbaycan' }, 'eu': { name: 'Basque', nativeName: 'Euskara' },
26
+ 'be': { name: 'Belarusian', nativeName: '\u0411\u0435\u043b\u0430\u0440\u0443\u0441\u043a\u0430\u044f' },
27
+ 'bn': { name: 'Bengali', nativeName: '\u09ac\u09be\u0982\u09b2\u09be' },
28
+ 'bs': { name: 'Bosnian', nativeName: 'Bosanski' }, 'bg': { name: 'Bulgarian', nativeName: '\u0411\u044a\u043b\u0433\u0430\u0440\u0441\u043a\u0438' },
29
+ 'ca': { name: 'Catalan', nativeName: 'Catal\u00e0' }, 'zh': { name: 'Chinese', nativeName: '\u4e2d\u6587' },
30
+ 'hr': { name: 'Croatian', nativeName: 'Hrvatski' }, 'cs': { name: 'Czech', nativeName: '\u010ce\u0161tina' },
31
+ 'da': { name: 'Danish', nativeName: 'Dansk' }, 'nl': { name: 'Dutch', nativeName: 'Nederlands' },
32
+ 'en': { name: 'English', nativeName: 'English' }, 'et': { name: 'Estonian', nativeName: 'Eesti' },
33
+ 'fi': { name: 'Finnish', nativeName: 'Suomi' }, 'fr': { name: 'French', nativeName: 'Fran\u00e7ais' },
34
+ 'ka': { name: 'Georgian', nativeName: '\u10e5\u10d0\u10e0\u10d7\u10e3\u10da\u10d8' },
35
+ 'de': { name: 'German', nativeName: 'Deutsch' }, 'el': { name: 'Greek', nativeName: '\u0395\u03bb\u03bb\u03b7\u03bd\u03b9\u03ba\u03ac' },
36
+ 'gu': { name: 'Gujarati', nativeName: '\u0a97\u0ac1\u0a9c\u0ab0\u0abe\u0aa4\u0ac0' },
37
+ 'he': { name: 'Hebrew', nativeName: '\u05e2\u05d1\u05e8\u05d9\u05ea' },
38
+ 'hi': { name: 'Hindi', nativeName: '\u0939\u093f\u0928\u094d\u0926\u0940' },
39
+ 'hu': { name: 'Hungarian', nativeName: 'Magyar' }, 'is': { name: 'Icelandic', nativeName: '\u00cdslenska' },
40
+ 'id': { name: 'Indonesian', nativeName: 'Bahasa Indonesia' },
41
+ 'ga': { name: 'Irish', nativeName: 'Gaeilge' }, 'it': { name: 'Italian', nativeName: 'Italiano' },
42
+ 'ja': { name: 'Japanese', nativeName: '\u65e5\u672c\u8a9e' },
43
+ 'kk': { name: 'Kazakh', nativeName: '\u049a\u0430\u0437\u0430\u049b' },
44
+ 'ko': { name: 'Korean', nativeName: '\ud55c\uad6d\uc5b4' },
45
+ 'lv': { name: 'Latvian', nativeName: 'Latvie\u0161u' }, 'lt': { name: 'Lithuanian', nativeName: 'Lietuvi\u0173' },
46
+ 'mk': { name: 'Macedonian', nativeName: '\u041c\u0430\u043a\u0435\u0434\u043e\u043d\u0441\u043a\u0438' },
47
+ 'ms': { name: 'Malay', nativeName: 'Bahasa Melayu' },
48
+ 'ml': { name: 'Malayalam', nativeName: '\u0d2e\u0d32\u0d2f\u0d3e\u0d33\u0d02' },
49
+ 'mr': { name: 'Marathi', nativeName: '\u092e\u0930\u093e\u0920\u0940' },
50
+ 'mn': { name: 'Mongolian', nativeName: '\u041c\u043e\u043d\u0433\u043e\u043b' },
51
+ 'no': { name: 'Norwegian', nativeName: 'Norsk' },
52
+ 'fa': { name: 'Persian', nativeName: '\u0641\u0627\u0631\u0633\u06cc' },
53
+ 'pl': { name: 'Polish', nativeName: 'Polski' }, 'pt': { name: 'Portuguese', nativeName: 'Portugu\u00eas' },
54
+ 'pa': { name: 'Punjabi', nativeName: '\u0a2a\u0a70\u0a1c\u0a3e\u0a2c\u0a40' },
55
+ 'ro': { name: 'Romanian', nativeName: 'Rom\u00e2n\u0103' },
56
+ 'ru': { name: 'Russian', nativeName: '\u0420\u0443\u0441\u0441\u043a\u0438\u0439' },
57
+ 'sr': { name: 'Serbian', nativeName: '\u0421\u0440\u043f\u0441\u043a\u0438' },
58
+ 'sk': { name: 'Slovak', nativeName: 'Sloven\u010dina' },
59
+ 'sl': { name: 'Slovenian', nativeName: 'Sloven\u0161\u010dina' },
60
+ 'es': { name: 'Spanish', nativeName: 'Espa\u00f1ol' },
61
+ 'sw': { name: 'Swahili', nativeName: 'Kiswahili' }, 'sv': { name: 'Swedish', nativeName: 'Svenska' },
62
+ 'tl': { name: 'Filipino', nativeName: 'Filipino' },
63
+ 'ta': { name: 'Tamil', nativeName: '\u0ba4\u0bae\u0bbf\u0bb4\u0bcd' },
64
+ 'te': { name: 'Telugu', nativeName: '\u0c24\u0c46\u0c32\u0c41\u0c17\u0c41' },
65
+ 'th': { name: 'Thai', nativeName: '\u0e44\u0e17\u0e22' },
66
+ 'tr': { name: 'Turkish', nativeName: 'T\u00fcrk\u00e7e' },
67
+ 'uk': { name: 'Ukrainian', nativeName: '\u0423\u043a\u0440\u0430\u0457\u043d\u0441\u044c\u043a\u0430' },
68
+ 'ur': { name: 'Urdu', nativeName: '\u0627\u0631\u062f\u0648' },
69
+ 'vi': { name: 'Vietnamese', nativeName: 'Ti\u1ebfng Vi\u1ec7t' },
70
+ };
44
71
 
45
72
  class InitService {
46
73
  constructor(config = {}) {
@@ -56,7 +83,9 @@ class InitService {
56
83
  };
57
84
 
58
85
  this.format = getFormatAdapter(this.config.format);
59
- this.config.supportedExtensions = [this.format.extension];
86
+ if (!this.config.supportedExtensions || !this.config.supportedExtensions.length) {
87
+ this.config.supportedExtensions = [this.format.extension];
88
+ }
60
89
  this.detectedFramework = detectFramework(process.cwd());
61
90
  if (this.detectedFramework && !this.config.translationPatterns) {
62
91
  this.config.translationPatterns = this.detectedFramework.patterns;
@@ -68,7 +97,9 @@ class InitService {
68
97
  : path.join(this.sourceDir, this.config.sourceLanguage);
69
98
 
70
99
  // Ensure defaultLanguages is properly initialized from config
71
- this.config.defaultLanguages = this.config.defaultLanguages || ['en', 'de', 'es', 'fr', 'ru'];
100
+ if (!this.config.defaultLanguages || !Array.isArray(this.config.defaultLanguages) || this.config.defaultLanguages.length === 0) {
101
+ this.config.defaultLanguages = ['en', 'de', 'es', 'fr', 'ru'];
102
+ }
72
103
  }
73
104
 
74
105
  // Check i18n dependencies
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "i18ntk",
3
- "version": "4.6.0",
3
+ "version": "4.7.0",
4
4
  "description": "i18n Tool Kit - Zero-dependency internationalization toolkit for setup, scanning, analysis, validation, auto translation, fixing, reporting, and runtime translation loading.",
5
5
  "readmeFilename": "README.md",
6
6
  "keywords": [
@@ -179,27 +179,28 @@
179
179
  },
180
180
  "preferGlobal": true,
181
181
  "versionInfo": {
182
- "version": "4.6.0",
183
- "releaseDate": "07/04/2026",
184
- "lastUpdated": "07/04/2026",
182
+ "version": "4.7.0",
183
+ "releaseDate": "07/07/2026",
184
+ "lastUpdated": "07/07/2026",
185
185
  "maintainer": "Vlad Noskov",
186
186
  "changelog": "./CHANGELOG.md",
187
187
  "documentation": "./README.md",
188
188
  "apiReference": "./docs/api/API_REFERENCE.md",
189
189
  "majorChanges": [
190
- "FRAMEWORK DETECTION: Added 10 new framework detections Rust (fluent, gettext-rs), Remix, Gatsby, Astro, Qwik, Solid, Ember, React Native, Expo, Ionic. Framework detection now unified across all code paths.",
191
- "RUST SUPPORT: Cargo.toml detection added to FrameworkDetectionService; .rs file extension added to all scanners and source walkers.",
192
- "FILE EXTENSIONS: Added .astro, .mdx, .mjs, .mts, .cjs, .cts, .rs to all file scan lists across CLI and VS Code extensions. Astro components, ESM modules, and Rust source files are now scanned for translation keys.",
193
- "ACTIVATION EVENTS: Added 14 new framework-specific activation triggers (next.config, astro.config, remix.config, svelte.config, nuxt.config, gatsby-config, Cargo.toml) for faster VS Code extension startup.",
194
- "ICU/FLUENT PLACEHOLDER SUPPORT: Added Fluent $variable and ICU MessageFormat {var, plural, ...} patterns to placeholder detection and comparison.",
195
- "REACT JSX COMPONENT DETECTION: Added detection of <Trans i18nKey>, <FormattedMessage id>, <FormattedMessage defaultMessage>, <t message>, and <Translate id> JSX components in both VS Code extensions.",
196
- "CONFIGURATION DEFAULTS: Updated exclude defaults and activation events with framework-specific directories (.nuxt, .output, .astro, .svelte-kit, .cache, __generated__, target).",
197
- "HEALTH SCORE: Fixed health score calculation to prevent negative scores. Penalty is now capped and uses a linear decay curve instead of unbounded subtraction.",
198
- "DOCUMENTATION: Updated README, CHANGELOG, and package metadata for 4.6.0. All supportedFrameworks now reflect actual detection capabilities.",
199
- "COMPREHENSIVE TESTING: All 128+ tests pass across CLI (33), Workbench (65), and Lens (32) packages."
190
+ "BUG FIX: LANGUAGE_CONFIG expanded from 21 to 61 languages with comprehensive ISO 639-1 coverage. All common languages now display their native names in init output instead of 'Unknown'.",
191
+ "BUG FIX: defaultLanguages no longer silently truncates to 5 when the config value is an empty array or null. A validation guard preserves the caller's configured list.",
192
+ "BUG FIX: Auto Translate now exits 0 on success even when residual untranslated values exist (placeholders, branded terms, short labels). Only real failures (0 translated with >0 total) now produce non-zero exit codes.",
193
+ "BUG FIX: supportedExtensions is no longer unconditionally overridden by the format adapter during init. Users who configure extensions in .i18ntk-config keep them.",
194
+ "BUG FIX: allowedEnglishTerms now unifies between validation-risk.js and project config. resolveAllowedEnglishTerms() accepts both inline options and project-level .i18ntk-config terms as fallback sources.",
195
+ "FRAMEWORK DETECTION: Added 10 new framework detections Rust, Remix, Gatsby, Astro, Qwik, SolidJS, Ember, React Native, Expo, Ionic. Centralized into framework-detector.js as single source of truth.",
196
+ "FILE EXTENSIONS: Added .astro, .mdx, .mjs, .mts, .cjs, .cts, .rs to all file scan lists via centralized SOURCE_EXTENSIONS and SCANNER_EXTENSIONS.",
197
+ "ICU/FLUENT PLACEHOLDER SUPPORT: Fluent $variable and ICU MessageFormat patterns added to placeholder detection and comparison.",
198
+ "JSX COMPONENT DETECTION: <Trans>, <FormattedMessage>, <Translate> JSX components detected in both VS Code extensions.",
199
+ "COMPREHENSIVE TESTING: All 384+ tests pass across CLI, Workbench, and Lens packages.",
200
+ "FRAMEWORK DETECTION v2: Added 13 additional FRAMEWORK_PATTERNS (i18ntk-runtime, nuxt, formatjs, lingui, ngx-translate, next-intl, svelte-i18n, solid-i18n, fastapi, ruby-on-rails, react-native-localize, ionic). Expanded detectFramework() to check Python/Rust/Go/Ruby project files. Added 20+ WRAPPER_SKIP_PATTERNS. Added 8 missing FRAMEWORK_COMPATIBILITY versions. Now supports 30+ frameworks across 15+ languages."
200
201
  ],
201
202
  "breakingChanges": [],
202
- "nextVersion": "4.6.1",
203
+ "nextVersion": "4.7.1",
203
204
  "supportedNodeVersions": ">=16.0.0",
204
205
  "supportedFrameworks": {
205
206
  "react-i18next": ">=11.0.0",
@@ -228,18 +229,18 @@
228
229
  "fluent-rs": ">=0.16.0",
229
230
  "gettext-rs": ">=0.7.0"
230
231
  },
231
- "supportPolicy": "Versions earlier than 4.6.0 may be unstable or insecure in CI automation. Upgrade to 4.6.0 or newer.",
232
+ "supportPolicy": "Versions earlier than 4.7.0 may be unstable or insecure in CI automation. Upgrade to 4.7.0 or newer.",
232
233
  "deprecations": [
233
234
  "4.3.0",
234
235
  "4.3.1",
235
236
  "4.3.2",
236
237
  "4.3.3"
237
238
  ],
238
- "deprecationMessage": "i18ntk 4.3.x and earlier have known security vulnerabilities (path traversal, JSON DoS). Upgrade to i18ntk@4.6.0 or newer: npm install -g i18ntk@latest",
239
+ "deprecationMessage": "i18ntk 4.3.x and earlier have known security vulnerabilities (path traversal, JSON DoS). Upgrade to i18ntk@4.7.0 or newer: npm install -g i18ntk@latest",
239
240
  "securityAdvisories": [
240
241
  "GHSA-i18ntk-4.3.x-path-traversal: Backup command accepted arbitrary paths without validation (fixed in 4.4.1)",
241
242
  "GHSA-i18ntk-4.3.x-json-dos: Deeply nested JSON files could cause denial of service (fixed in 4.4.1)"
242
243
  ]
243
244
  },
244
- "readme": "# i18ntk v4.6.0\n\nA zero-dependency internationalization toolkit for setup, scanning, analysis, validation, usage tracking, translation completion, automatic JSON locale translation, reporting, and runtime translation loading.\n\n![i18ntk Logo](https://raw.githubusercontent.com/vladnoskv/i18ntk/main/docs/screenshots/i18ntk-logo-public.PNG)\n\n[![npm version](https://img.shields.io/npm/v/i18ntk.svg?color=brightgreen)](https://www.npmjs.com/package/i18ntk)\n[![npm downloads](https://img.shields.io/npm/dt/i18ntk.svg)](https://www.npmjs.com/package/i18ntk)\n[![node](https://img.shields.io/badge/node-%3E%3D16-339933)](https://nodejs.org)\n[![dependencies](https://img.shields.io/badge/dependencies-0-success)](https://www.npmjs.com/package/i18ntk)\n[![license](https://img.shields.io/badge/license-MIT-yellow.svg)](LICENSE)\n[![socket](https://socket.dev/api/badge/npm/package/i18ntk/4.6.0)](https://socket.dev/npm/package/i18ntk/overview/4.6.0)\n\n[![i18ntk Workbench](https://img.shields.io/badge/VS_Code-Workbench-007ACC?logo=visualstudiocode&logoColor=white)](https://marketplace.visualstudio.com/items?itemName=VladNoskov.i18ntk-workbench)\n[![i18ntk Lens](https://img.shields.io/badge/VS_Code-Lens-007ACC?logo=visualstudiocode&logoColor=white)](https://marketplace.visualstudio.com/items?itemName=VladNoskov.i18ntk-lens)\n\n## Ecosystem\n\n- **i18ntk** — CLI toolkit and runtime (this package)\n- **i18ntk Workbench** — VS Code dashboard, reports, and key management\n- **i18ntk Lens** — inline hovers, CodeLens, and diagnostics\n\n## Install\n\n```bash\nnpm install -g i18ntk\nnpx i18ntk --help\n```\n\n## What's New in 4.6.0\n\n- **25+ framework detections** — Rust, Remix, Gatsby, Astro, Qwik, SolidJS, Ember, React Native, Expo, Ionic added. All drawn from a single centralized detector.\n- **Rust support** — Cargo.toml detection and `.rs` file scanning with fluent/gettext-rs framework patterns.\n- **File extensions** — `.astro`, `.mdx`, `.mjs`, `.mts`, `.cjs`, `.cts`, `.rs` scanned across all tools.\n- **ICU/Fluent placeholder support** — Fluent `$variable` and ICU `{var, plural, ...}` patterns detected.\n- **JSX component detection** — `<Trans>`, `<FormattedMessage>`, `<Translate>` components recognized.\n- **Health score fix** — No more negative or misleadingly low scores.\n- **Centralized architecture** — Framework data, extensions, excludes, and patterns live in one module consumed by all tools.\n\n[Full changelog →](./CHANGELOG.md)\n\n## Quick Start\n\n```bash\ni18ntk # interactive menu\ni18ntk --command=analyze # coverage report\ni18ntk --command=validate # quality checks\ni18ntk --command=usage # key usage tracking\ni18ntk report --json --out ./reports # full report\ni18ntk --command=complete # fill missing keys\ni18ntk --command=translate # auto-translate\ni18ntk --command=summary # status overview\n```\n\nSee [docs/getting-started.md](./docs/getting-started.md) for the full onboarding guide.\n\n## Command Reference\n\n| Command | Purpose | Output |\n| ----------- | ------------------------------------------ | ----------------------------- |\n| `i18ntk` | Interactive management menu | — |\n| `init` | Setup locale folders and target files | Locale JSON, `.i18ntk-config` |\n| `analyze` | Translation coverage comparison | Reports |\n| `validate` | Structure, quality, and risk validation | Summary report |\n| `usage` | Map keys to source, find dead/missing keys | Usage report |\n| `report` | Stable schema report (JSON/MD/HTML) | stdout or file output |\n| `scanner` | Detect hardcoded text in source files | Scanner report |\n| `complete` | Fill missing keys in target files | Target locale JSON |\n| `translate` | Auto-translate via provider AI | Target locale JSON |\n| `sizing` | Expansion risk and layout analysis | Sizing report |\n| `summary` | Project translation status overview | Console output |\n| `fixer` | Fix placeholders and markers | Locale JSON |\n| `backup` | Create/verify/restore locale backups | Backup archives |\n\nEach is available as `i18ntk --command=<name>` or standalone `i18ntk-<name>`.\n\n## Common Options\n\n```\n--code-dir <path> Source code directory\n--locales-dir <path> Locale files directory\n--output-dir <path> Report output directory\n--source-locale <code> Source language code (e.g. en)\n--framework <name> Override framework detection\n--no-prompt Skip interactive prompts\n--help Show help\n```\n\n## Auto Translate\n\n```bash\ni18ntk-translate locales/en/common.json de\ni18ntk-translate locales/en/common.json fr --dry-run --preserve-placeholders\n```\n\n**Providers:** Google (default), DeepL, LibreTranslate\n\n```bash\nexport DEEPL_API_KEY=\"your-key\"\ni18ntk-translate locales/en/common.json de --provider deepl --no-confirm\n```\n\n**Placeholder-aware translation** detects `{name}`, `{{count}}`, `%s`, `:id`, `${value}`, `$t(key)`, and ICU pattern syntax. The default mode is `--only-missing` — existing translations are preserved.\n\nProtected terms and keys via `i18ntk-auto-translate.json`:\n\n```json\n{\n \"version\": 1,\n \"terms\": [\"BrandName\", \"PRODUCT_CODE\"],\n \"keys\": [\"app.brandName\", \"product.*.symbol\"],\n \"values\": [\"BrandName Ltd\"],\n \"patterns\": [\"[A-Z]{2,}-\\\\d+\"]\n}\n```\n\n[Auto Translate guide →](./docs/auto-translate.md)\n\n## Configuration\n\nExample `.i18ntk-config`:\n\n```json\n{\n \"version\": \"4.6.0\",\n \"sourceDir\": \"./locales\",\n \"i18nDir\": \"./locales\",\n \"sourceLanguage\": \"en\",\n \"defaultLanguages\": [\"en\", \"de\", \"es\", \"fr\", \"ru\"],\n \"keyStyle\": \"dot.notation\",\n \"englishContentThresholdPercent\": 10,\n \"allowedEnglishTerms\": [\"BrandName\"],\n \"autoTranslate\": {\n \"placeholderMode\": \"preserve\",\n \"concurrency\": 12,\n \"onlyMissingOrEnglish\": true\n },\n \"extensions\": {\n \"workbench\": { \"localeDirectory\": \"./locales\", \"sourceLocale\": \"en\" },\n \"lens\": { \"localeDirectory\": \"./locales\", \"sourceLocale\": \"en\", \"keyFormats\": [\"dot\", \"snake\"] }\n }\n}\n```\n\n[Configuration reference →](./docs/api/CONFIGURATION.md)\n\n## Scanner\n\nDetects hardcoded text in 12+ languages with language-specific character ranges and stopword filtering. Framework-specific patterns for React, Vue, Angular, Svelte, Astro, Django, Flask, Python, Rust, Go, and more.\n\n```bash\ni18ntk-scanner --code-dir ./src --source-locale de\ni18ntk-scanner --code-dir ./src --source-locale ja --output-report\n```\n\n## Usage Analysis\n\nTracks key references, detects dead keys with confidence scores, resolves dynamic patterns (templates, arrays, object maps), and recommends namespace alignment.\n\n```bash\ni18ntk-usage --code-dir ./src --locales-dir ./locales --cleanup --dry-run-delete\n```\n\n## Runtime\n\n```js\nconst runtime = require('i18ntk/runtime');\nconst i18n = runtime.initRuntime({\n baseDir: './locales',\n language: 'en',\n fallbackLanguage: 'en',\n});\n\nconsole.log(i18n.t('common.hello'));\ni18n.setLanguage('fr');\nconsole.log(i18n.getAvailableLanguages());\n```\n\n**Lazy loading** reduces memory on large locale folders:\n\n```js\nconst i18n = runtime.initRuntime({ baseDir: './locales', language: 'en', lazy: true });\n```\n\n**Per-call language overrides:**\n\n```js\ni18n.t('common.hello', {}, { language: 'de' });\n```\n\n**Batch translation:**\n\n```js\ni18n.translateBatch(['menu.home', 'menu.settings']);\n```\n\nProduction guidance:\n\n- Use the instance from `initRuntime()` — not module-level `runtime.t()` — in multi-tenant apps\n- Use `lazy: true` for large folders; `preload: true` for small sets\n- Call `refresh(language)` after deploying changed locale files\n- `i18ntk/runtime/enhanced` remains available for async/encryption compatibility\n\n[Runtime guide →](./docs/runtime.md)\n\n## Watch\n\n```js\nconst watchLocales = require('i18ntk/utils/watch-locales');\nconst watcher = watchLocales('./locales');\n\nwatcher.on('change', (filePath) => console.log('changed:', filePath));\nwatcher.on('add', (filePath) => console.log('added:', filePath));\nwatcher.stop();\n```\n\nFeatures: 300ms debounce, SHA-256 hash tracking, 50-directory cap. The callback form `watchLocales('./locales', onChange)` is still supported.\n\n## Documentation\n\n- [Getting Started](./docs/getting-started.md)\n- [Configuration](./docs/api/CONFIGURATION.md)\n- [API Reference](./docs/api/API_REFERENCE.md)\n- [Runtime API](./docs/runtime.md)\n- [Auto Translate](./docs/auto-translate.md)\n- [Scanner Guide](./docs/scanner-guide.md)\n- [Environment Variables](./docs/environment-variables.md)\n\n## Security\n\n- No API key required for default Auto Translate\n- Do not store secrets in locale files, `.i18ntk-config`, or protection files\n- Report issues via [SECURITY.md](./SECURITY.md)\n\n## Related\n\n| Tool | Purpose |\n| ---------------- | ------------------------------------------- |\n| i18ntk Workbench | VS Code localization health dashboard |\n| i18ntk Lens | Inline hovers, CodeLens, and diagnostics |\n| PublishGuard | Pre-publish safety scanner for npm packages |\n| ContextKit | AI coding context manager |\n\n## License\n\nSee [LICENSE](./LICENSE).\n"
245
+ "readme": "# i18ntk v4.7.0\n\nA zero-dependency internationalization toolkit for setup, scanning, analysis, validation, usage tracking, translation completion, automatic JSON locale translation, reporting, and runtime translation loading.\n\n![i18ntk Logo](https://raw.githubusercontent.com/vladnoskv/i18ntk/main/docs/screenshots/i18ntk-logo-public.PNG)\n\n[![npm version](https://img.shields.io/npm/v/i18ntk.svg?color=brightgreen)](https://www.npmjs.com/package/i18ntk)\n[![npm downloads](https://img.shields.io/npm/dt/i18ntk.svg)](https://www.npmjs.com/package/i18ntk)\n[![node](https://img.shields.io/badge/node-%3E%3D16-339933)](https://nodejs.org)\n[![dependencies](https://img.shields.io/badge/dependencies-0-success)](https://www.npmjs.com/package/i18ntk)\n[![license](https://img.shields.io/badge/license-MIT-yellow.svg)](LICENSE)\n[![socket](https://socket.dev/api/badge/npm/package/i18ntk/4.7.0)](https://socket.dev/npm/package/i18ntk/overview/4.7.0)\n\n[![i18ntk Workbench](https://img.shields.io/badge/VS_Code-Workbench-007ACC?logo=visualstudiocode&logoColor=white)](https://marketplace.visualstudio.com/items?itemName=VladNoskov.i18ntk-workbench)\n[![i18ntk Lens](https://img.shields.io/badge/VS_Code-Lens-007ACC?logo=visualstudiocode&logoColor=white)](https://marketplace.visualstudio.com/items?itemName=VladNoskov.i18ntk-lens)\n\n## Ecosystem\n\n- **i18ntk** — CLI toolkit and runtime (this package)\n- **i18ntk Workbench** — VS Code dashboard, reports, and key management\n- **i18ntk Lens** — inline hovers, CodeLens, and diagnostics\n\n## Install\n\n```bash\nnpm install -g i18ntk\nnpx i18ntk --help\n```\n\n## What's New in 4.7.0\n\n- **30+ framework detection patterns** — 13 new FRAMEWORK_PATTERNS: `i18ntk-runtime`, `nuxt`, `lingui`, `formatjs`, `ngx-translate`, `next-intl`, `svelte-i18n`, `solid-i18n`, `fastapi`, `ruby-on-rails`, `react-native-localize`, `ionic`. Each with framework-specific scan regexes for translation calls, JSX components, template directives, and pipes.\n- **Non-Node project detection** — Python (`requirements.txt`, `pyproject.toml`, `setup.py`), Rust (`Cargo.toml`), Go (`go.mod`), Ruby (`Gemfile`) now detected when no `package.json` exists. Detects Django, Flask, FastAPI, Rails, and generic i18n.\n- **20+ new WRAPPER_SKIP_PATTERNS** — Covers I18n.t(), useTranslate(), translateService.instant(), formatMessage(), bundle.get_message(), fluent!, ts! and more.\n- **Framework-aware report generation** — `report-model.js` accepts optional framework parameter and uses framework-specific patterns for key extraction.\n- **Expanded namespace helpers** — `useTranslate` (Qwik), `useSpeak` (Qwik), `withTranslation` (react-i18next) added.\n- **Attribute key detection** — `i18nKey=`, `t-key=`, `data-i18n=` attributes detected in source scanning.\n- **All frame works now have FRAMEWORK_COMPATIBILITY entries and FRAMEWORK_SUGGESTIONS** for consistent tooling.\n\n[Full changelog →](./CHANGELOG.md)\n\n## Quick Start\n\n```bash\ni18ntk # interactive menu\ni18ntk --command=analyze # coverage report\ni18ntk --command=validate # quality checks\ni18ntk --command=usage # key usage tracking\ni18ntk report --json --out ./reports # full report\ni18ntk --command=complete # fill missing keys\ni18ntk --command=translate # auto-translate\ni18ntk --command=summary # status overview\n```\n\nSee [docs/getting-started.md](./docs/getting-started.md) for the full onboarding guide.\n\n## Command Reference\n\n| Command | Purpose | Output |\n| ----------- | ------------------------------------------ | ----------------------------- |\n| `i18ntk` | Interactive management menu | — |\n| `init` | Setup locale folders and target files | Locale JSON, `.i18ntk-config` |\n| `analyze` | Translation coverage comparison | Reports |\n| `validate` | Structure, quality, and risk validation | Summary report |\n| `usage` | Map keys to source, find dead/missing keys | Usage report |\n| `report` | Stable schema report (JSON/MD/HTML) | stdout or file output |\n| `scanner` | Detect hardcoded text in source files | Scanner report |\n| `complete` | Fill missing keys in target files | Target locale JSON |\n| `translate` | Auto-translate via provider AI | Target locale JSON |\n| `sizing` | Expansion risk and layout analysis | Sizing report |\n| `summary` | Project translation status overview | Console output |\n| `fixer` | Fix placeholders and markers | Locale JSON |\n| `backup` | Create/verify/restore locale backups | Backup archives |\n\nEach is available as `i18ntk --command=<name>` or standalone `i18ntk-<name>`.\n\n## Common Options\n\n```\n--code-dir <path> Source code directory\n--locales-dir <path> Locale files directory\n--output-dir <path> Report output directory\n--source-locale <code> Source language code (e.g. en)\n--framework <name> Override framework detection\n--no-prompt Skip interactive prompts\n--help Show help\n```\n\n## Auto Translate\n\n```bash\ni18ntk-translate locales/en/common.json de\ni18ntk-translate locales/en/common.json fr --dry-run --preserve-placeholders\n```\n\n**Providers:** Google (default), DeepL, LibreTranslate\n\n```bash\nexport DEEPL_API_KEY=\"your-key\"\ni18ntk-translate locales/en/common.json de --provider deepl --no-confirm\n```\n\n**Placeholder-aware translation** detects `{name}`, `{{count}}`, `%s`, `:id`, `${value}`, `$t(key)`, and ICU pattern syntax. The default mode is `--only-missing` — existing translations are preserved.\n\nProtected terms and keys via `i18ntk-auto-translate.json`:\n\n```json\n{\n \"version\": 1,\n \"terms\": [\"BrandName\", \"PRODUCT_CODE\"],\n \"keys\": [\"app.brandName\", \"product.*.symbol\"],\n \"values\": [\"BrandName Ltd\"],\n \"patterns\": [\"[A-Z]{2,}-\\\\d+\"]\n}\n```\n\n[Auto Translate guide →](./docs/auto-translate.md)\n\n## Configuration\n\nExample `.i18ntk-config`:\n\n```json\n{\n \"version\": \"4.6.1\",\n \"sourceDir\": \"./locales\",\n \"i18nDir\": \"./locales\",\n \"sourceLanguage\": \"en\",\n \"defaultLanguages\": [\"en\", \"de\", \"es\", \"fr\", \"ru\"],\n \"keyStyle\": \"dot.notation\",\n \"englishContentThresholdPercent\": 10,\n \"allowedEnglishTerms\": [\"BrandName\"],\n \"autoTranslate\": {\n \"placeholderMode\": \"preserve\",\n \"concurrency\": 12,\n \"onlyMissingOrEnglish\": true\n },\n \"extensions\": {\n \"workbench\": { \"localeDirectory\": \"./locales\", \"sourceLocale\": \"en\" },\n \"lens\": { \"localeDirectory\": \"./locales\", \"sourceLocale\": \"en\", \"keyFormats\": [\"dot\", \"snake\"] }\n }\n}\n```\n\n[Configuration reference →](./docs/api/CONFIGURATION.md)\n\n## Scanner\n\nDetects hardcoded text in 12+ languages with language-specific character ranges and stopword filtering. Framework-specific patterns for React, Vue, Angular, Svelte, Astro, Django, Flask, Python, Rust, Go, and more.\n\n```bash\ni18ntk-scanner --code-dir ./src --source-locale de\ni18ntk-scanner --code-dir ./src --source-locale ja --output-report\n```\n\n## Usage Analysis\n\nTracks key references, detects dead keys with confidence scores, resolves dynamic patterns (templates, arrays, object maps), and recommends namespace alignment.\n\n```bash\ni18ntk-usage --code-dir ./src --locales-dir ./locales --cleanup --dry-run-delete\n```\n\n## Runtime\n\n```js\nconst runtime = require('i18ntk/runtime');\nconst i18n = runtime.initRuntime({\n baseDir: './locales',\n language: 'en',\n fallbackLanguage: 'en',\n});\n\nconsole.log(i18n.t('common.hello'));\ni18n.setLanguage('fr');\nconsole.log(i18n.getAvailableLanguages());\n```\n\n**Lazy loading** reduces memory on large locale folders:\n\n```js\nconst i18n = runtime.initRuntime({ baseDir: './locales', language: 'en', lazy: true });\n```\n\n**Per-call language overrides:**\n\n```js\ni18n.t('common.hello', {}, { language: 'de' });\n```\n\n**Batch translation:**\n\n```js\ni18n.translateBatch(['menu.home', 'menu.settings']);\n```\n\nProduction guidance:\n\n- Use the instance from `initRuntime()` — not module-level `runtime.t()` — in multi-tenant apps\n- Use `lazy: true` for large folders; `preload: true` for small sets\n- Call `refresh(language)` after deploying changed locale files\n- `i18ntk/runtime/enhanced` remains available for async/encryption compatibility\n\n[Runtime guide →](./docs/runtime.md)\n\n## Watch\n\n```js\nconst watchLocales = require('i18ntk/utils/watch-locales');\nconst watcher = watchLocales('./locales');\n\nwatcher.on('change', (filePath) => console.log('changed:', filePath));\nwatcher.on('add', (filePath) => console.log('added:', filePath));\nwatcher.stop();\n```\n\nFeatures: 300ms debounce, SHA-256 hash tracking, 50-directory cap. The callback form `watchLocales('./locales', onChange)` is still supported.\n\n## Documentation\n\n- [Getting Started](./docs/getting-started.md)\n- [Configuration](./docs/api/CONFIGURATION.md)\n- [API Reference](./docs/api/API_REFERENCE.md)\n- [Runtime API](./docs/runtime.md)\n- [Auto Translate](./docs/auto-translate.md)\n- [Scanner Guide](./docs/scanner-guide.md)\n- [Environment Variables](./docs/environment-variables.md)\n\n## Security\n\n- No API key required for default Auto Translate\n- Do not store secrets in locale files, `.i18ntk-config`, or protection files\n- Report issues via [SECURITY.md](./SECURITY.md)\n\n## Related\n\n| Tool | Purpose |\n| ---------------- | ------------------------------------------- |\n| i18ntk Workbench | VS Code localization health dashboard |\n| i18ntk Lens | Inline hovers, CodeLens, and diagnostics |\n| PublishGuard | Pre-publish safety scanner for npm packages |\n| ContextKit | AI coding context manager |\n\n## License\n\nSee [LICENSE](./LICENSE).\n"
245
246
  }
@@ -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,
@@ -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) || [];
@@ -34,24 +34,32 @@ const ENGLISH_WORDS = new Set([
34
34
  'warning', 'when', 'with', 'without'
35
35
  ]);
36
36
 
37
- const DEFAULT_ALLOWED_ENGLISH_TERMS = new Set([
38
- 'api',
39
- ]);
37
+ const DEFAULT_ALLOWED_ENGLISH_TERMS = new Set(['api']);
38
+
39
+ function resolveAllowedEnglishTerms(options, projectConfig) {
40
+ const terms = new Set(DEFAULT_ALLOWED_ENGLISH_TERMS);
41
+ const sources = [
42
+ options && options.allowedEnglishTerms,
43
+ projectConfig && projectConfig.allowedEnglishTerms
44
+ ];
45
+ for (const source of sources) {
46
+ if (Array.isArray(source)) {
47
+ source.forEach(term => {
48
+ if (typeof term === 'string' && term.trim()) {
49
+ terms.add(term.trim().toLowerCase());
50
+ }
51
+ });
52
+ }
53
+ }
54
+ return terms;
55
+ }
40
56
 
41
57
  function normalizeLanguage(language) {
42
58
  return String(language || '').toLowerCase().split(/[-_]/)[0];
43
59
  }
44
60
 
45
61
  function toAllowedTermSet(terms) {
46
- const allowed = new Set(DEFAULT_ALLOWED_ENGLISH_TERMS);
47
- if (Array.isArray(terms)) {
48
- terms.forEach(term => {
49
- if (typeof term === 'string' && term.trim()) {
50
- allowed.add(term.trim().toLowerCase());
51
- }
52
- });
53
- }
54
- return allowed;
62
+ return resolveAllowedEnglishTerms({ allowedEnglishTerms: terms });
55
63
  }
56
64
 
57
65
  function stripNonLanguageTokens(value) {
@@ -171,5 +179,6 @@ module.exports = {
171
179
  DEFAULT_ENGLISH_THRESHOLD_PERCENT,
172
180
  analyzeEnglishContent,
173
181
  detectTranslationContentRisks,
174
- hasSecretLikeValue
182
+ hasSecretLikeValue,
183
+ resolveAllowedEnglishTerms
175
184
  };