i18ntk
Version:
i18n Tool Kit - Zero-dependency internationalization toolkit for setup, scanning, analysis, validation, auto translation, fixing, reporting, and runtime translation loading.
916 lines (870 loc) • 32.9 kB
JavaScript
const path = require('path');
const SecurityUtils = require('./security');
// Framework compatibility information
const FRAMEWORK_COMPATIBILITY = {
'i18ntk-runtime': { minVersion: '1.0.0' },
'vue-i18n': { minVersion: '9.0.0' },
'react-i18next': { minVersion: '11.0.0' },
'i18next': { minVersion: '20.0.0' },
'lingui': { minVersion: '3.0.0' },
'formatjs': { minVersion: '5.0.0' },
'django': { minVersion: '3.0.0' },
'flask': { minVersion: '2.0.0' },
'fastapi': { minVersion: '0.65.0' },
'python': { minVersion: '3.6.0' },
'ruby': { minVersion: '2.7.0' },
'ruby-on-rails': { minVersion: '6.0.0' },
'go': { minVersion: '1.16.0' },
'rust': { minVersion: '1.65.0' },
'fluent': { minVersion: '0.16.0' },
'remix': { minVersion: '1.0.0' },
'gatsby': { minVersion: '4.0.0' },
'astro': { minVersion: '2.0.0' },
'qwik': { minVersion: '1.0.0' },
'nuxt': { minVersion: '7.0.0' },
'nuxt-i18n': { minVersion: '7.0.0' },
'next-intl': { minVersion: '2.0.0' },
'ngx-translate': { minVersion: '13.0.0' },
'svelte-i18n': { minVersion: '3.0.0' },
'solid-i18n': { minVersion: '1.0.0' },
'ember-intl': { minVersion: '5.0.0' },
'react-native-localize': { minVersion: '2.0.0' },
'ionic': { minVersion: '6.0.0' },
'expo': { minVersion: '48.0.0' },
'vanilla': { minVersion: '1.0.0' }
};
// Define framework detection in order of specificity
const FRAMEWORKS = {
// i18ntk-runtime is the framework-agnostic runtime for i18ntk
'i18ntk-runtime': {
name: 'i18ntk-runtime',
description: 'Lightweight i18n runtime for i18ntk',
deps: ['i18ntk/runtime', 'i18ntk-runtime'],
globs: ['src/**/*.{js,jsx,ts,tsx}'],
patterns: [
/\bi18n\.t\(\s*['\"`]([^'\"`]+)['\"`]\s*(?:,|\/\*|\*\/|\/\/|$)/g, // i18n.t('key')
/\bi18n\.translate\(\s*['\"`]([^'\"`]+)['\"`]\s*(?:,|\/\*|\*\/|\/\/|$)/g, // i18n.translate('key')
/\buseI18n\(\s*\)\s*\.\s*t\(\s*['\"`]([^'\"`]+)['\"`]\s*(?:,|\/\*|\*\/|\/\/|$)/g, // useI18n().t('key')
/\buseTranslation\(\s*['\"`][^'\"`]*['\"`]?\s*\)\s*\{\s*\w*\s*,\s*t\s*\}[^)]*\)\s*=>/g, // const { t } = useTranslation()
/\$\{\s*t\(\s*['\"`]([^'\"`]+)['\"`]\s*(?:,|\/\*|\*\/|\/\/|$)/g // ${t('key')} in template literals
],
configFile: 'i18n.js',
configFilePatterns: [
/i18n\.(js|ts)$/,
/i18ntk\.config\.(js|ts)$/,
/\.i18nrc(\.(js|json))?$/,
],
setupGuide: 'Refer to the official i18ntk documentation on GitHub for setup instructions.',
priority: 100, // Higher priority to detect before other frameworks
ignore: [
'node_modules/**',
'**/node_modules/**',
'**/dist/**',
'**/build/**',
'**/.git/**',
'**/__tests__/**',
'**/*.test.{js,jsx,ts,tsx}'
]
},
// Vue i18n has highest specificity due to its unique syntax
'vue-i18n': {
name: 'vue-i18n',
deps: ['vue-i18n', '@intlify/vite-plugin-vue-i18n'],
globs: ['src/**/*.{js,ts,vue}'],
patterns: [
/\$t\(['"`]([^'"`]+)['"`]\)/g, // $t('key')
/useI18n\([^)]*\)\s*\.\s*t\(['"`]([^'"`]+)['"`]\)/g, // useI18n().t('key')
/v-t=["']([^"']+)["']/g, // v-t="'key'"
/\{\{[^}]*\$t\(['"`]([^'"`]+)['"`]\)[^}]*\}\}/g // {{ $t('key') }} in templates
],
ignore: ['node_modules/**']
},
// React i18next has medium specificity
'react-i18next': {
name: 'React i18next',
description: 'Internationalization framework for React',
deps: ['i18next', 'react-i18next'],
globs: ['**/*.{js,jsx,ts,tsx}'],
regex: /\b(?:useTranslation|withTranslation|Trans|I18n|i18n\.t|t\(?=\s*[`'"])/,
configFile: 'i18n.js',
configFilePatterns: [/i18n\.(js|ts)$/, /i18next\.config\.(js|ts)$/],
setupGuide: 'Refer to the official react-i18next documentation for setup instructions.'
},
// Base i18next has lowest specificity
'i18next': {
name: 'i18next',
deps: ['i18next'],
globs: ['src/**/*.{js,ts}'],
patterns: [
/i18n\.t\(['"`]([^'"`]+)['"`]\)/g, // i18n.t('key')
/i18next\.t\(['"`]([^'"`]+)['"`]\)/g, // i18next.t('key')
/new i18next\(/g // new i18next()
],
ignore: ['node_modules/**']
},
lingui: {
deps: ['@lingui/core', '@lingui/react'],
globs: ['src/**/*.{js,jsx,ts,tsx}'],
patterns: [
/(?<![\w$.])t\s*\(['"`]([^'"`]+)['"`]\)/g,
/_\(['"`]([^'"`]+)['"`]\)/g
],
ignore: ['node_modules/**']
},
formatjs: {
deps: ['react-intl', '@formatjs/intl'],
globs: ['src/**/*.{js,jsx,ts,tsx}'],
patterns: [
/formatMessage\(\s*\{\s*id:\s*['"`]([^'"`]+)['"`]\)/g
],
ignore: ['node_modules/**']
},
django: {
deps: ['Django', 'django'],
globs: ['**/*.{py,html,txt}'],
patterns: [
/gettext\(['"`]([^'"`]+)['"`]\)/g,
/_\(['"`]([^'"`]+)['"`]\)/g,
/gettext_lazy\(['"`]([^'"`]+)['"`]\)/g,
/_lazy\(['"`]([^'"`]+)['"`]\)/g,
/\{\%\s*trans\s+['"`]([^'"`]+)['"`]\s*%\}/g,
/\{\%\s*blocktrans\s*%\}([^%]+)\{\%\s*endblocktrans\s*%\}/g
],
ignore: ['node_modules/**', '__pycache__/**', '*.pyc']
},
flask: {
deps: ['Flask', 'flask', 'flask-babel'],
globs: ['**/*.{py,html,txt}'],
patterns: [
/gettext\(['"`]([^'"`]+)['"`]\)/g,
/_\(['"`]([^'"`]+)['"`]\)/g,
/lazy_gettext\(['"`]([^'"`]+)['"`]\)/g,
/\{\{\s*gettext\(['"`]([^'"`]+)['"`]\)\s*\}\}/g,
/\{\{\s*_[('"`]([^'"`]+)['"`]\)\s*\}\}/g
],
ignore: [
'node_modules/**',
'__pycache__/**',
'*.pyc',
'*.pyo',
'*.pyd',
'venv/**',
'.venv/**',
'env/**',
'.env/**',
'dist/**',
'build/**',
'*.egg-info/**',
'*.so',
'*.dll',
'*.dylib',
'*.so.*',
'*.dylib.*'
]
},
python: {
name: 'python',
deps: ['gettext', 'babel', 'python-gettext'],
globs: ['**/*.{py,py3,pyw,txt}'],
patterns: [
/gettext\(['"`]([^'"`]+)['"`]\)/g,
/\(['"`]([^'"`]+)['"`]\)/g,
/gettext_lazy\(['"`]([^'"`]+)['"`]\)/g,
/_l\(['"`]([^'"`]+)['"`]\)/g,
/_n\(['"`]([^'"`]+)['"`]/g,
/pgettext\([^,]+,\s*['"`]([^'"`]+)['"`]\)/g,
/npgettext\([^,]+,\s*[^,]+,\s*['"`]([^'"`]+)['"`]/g
],
ignore: [
'node_modules/**',
'__pycache__/**',
'*.pyc',
'*.pyo',
'*.pyd',
'venv/**',
'.venv/**',
'env/**',
'.env/**',
'dist/**',
'build/**',
'*.egg-info/**',
'*.so',
'*.dll',
'*.dylib',
'*.so.*',
'*.dylib.*'
]
},
fastapi: {
name: 'fastapi',
deps: ['fastapi', 'python-i18n', 'fastapi-i18n'],
globs: ['**/*.{py,html,j2,jinja2}'],
patterns: [
/_\(['"`]([^'"`]+)['"`]\)/g,
/i18n\.t\(['"`]([^'"`]+)['"`]\)/g,
/i18n\.translate\(['"`]([^'"`]+)['"`]\)/g,
/gettext\(['"`]([^'"`]+)['"`]\)/g,
/\{\{\s*_\('["']([^"']+)["']\)\s*\}\}/g,
/\{\{\s*gettext\(['"`]([^'"`]+)['"`]\)\s*\}\}/g
],
ignore: [
'node_modules/**',
'__pycache__/**',
'*.pyc',
'*.pyo',
'*.pyd',
'venv/**',
'.venv/**',
'env/**',
'.env/**',
'dist/**',
'build/**',
'*.egg-info/**',
'*.so',
'*.dll',
'*.dylib',
'*.so.*',
'*.dylib.*',
'alembic/**',
'migrations/**'
]
},
'ruby-on-rails': {
name: 'ruby-on-rails',
deps: ['rails', 'i18n', 'rails-i18n'],
globs: [
'app/**/*.{rb,erb,haml,slim,html.erb,html.haml,html.slim,js.erb}',
'config/locales/*.{rb,yml,yaml}'
],
patterns: [
/(?<![\w$.])t\s*\(['"]([^'"]+)['"]/g,
/I18n\.t\(['"]([^'"]+)['"]/g,
/I18n\.translate\(['"]([^'"]+)['"]/g,
/l\(['"]([^'"]+)['"]/g,
/I18n\.l\(['"]([^'"]+)['"]/g,
/I18n\.localize\(['"]([^'"]+)['"]/g,
/<%= t\(['"]([^'"]+)['"]/g,
/<%= I18n\.t\(['"]([^'"]+)['"]/g
],
ignore: [
'node_modules/**',
'tmp/**',
'log/**',
'vendor/**',
'public/**',
'coverage/**',
'spec/tmp/**',
'test/tmp/**'
]
},
go: {
name: 'go',
deps: ['go-i18n', 'x-text'],
globs: ['**/*.go'],
patterns: [
/i18n\.NewMessage\([^,]+,\s*["`]([^"`]+)["`]/g,
/i18n\.NewLocalizer\([^)]+\)\.MustLocalize\([^,]+,\s*["`]([^"`]+)["`]/g,
/\.Get\([^,]+,\s*["`]([^"`]+)["`]/g,
/i18n\.Translate\([^,]+,\s*["`]([^"`]+)["`]/g
],
ignore: [
'**/*_test.go',
'vendor/**',
'**/testdata/**',
'**/mocks/**',
'**/mock_*/**',
'**/generated/**',
'**/pb/*.go',
'**/*.pb.go',
'**/*.pb.gw.go',
'**/wire_gen.go',
'**/bindata.go',
'**/doc.go',
'**/test_*.go',
'**/*_test.go',
'**/test/**',
'**/tests/**'
]
},
rust: {
name: 'rust',
deps: ['fluent', 'fluent-bundle', 'fluent-resmgr', 'fluent-rs', 'intl-memoizer', 'unic-langid'],
globs: ['**/*.rs'],
patterns: [
/\bLocalization::new\(\s*&\[\s*([^\]]+)\s*\],\s*&\[\s*([^\]]+)\s*\]\)/g,
/bundle\.get_message\(\s*["`]([^"`]+)["`]/g,
/\.get_message\s*\(\s*["`]([^"`]+)["`]/g,
/\bformat!\s*\(\s*["`]([^"`]+)["`]/g
],
ignore: [
'target/**',
'**/*_test.rs',
'**/test/**',
'**/tests/**',
'**/examples/**',
'**/benches/**',
'**/migrations/**'
]
},
'ngx-translate': {
name: 'Angular ngx-translate',
deps: ['@ngx-translate/core', '@ngx-translate/http-loader'],
patterns: [
/translate\s*=\s*["'`]([^"'`]+)["'`]/g,
/translateService\.instant\(\s*['"`]([^"'`]+)['"`]/g,
/translateService\.get\(\s*['"`]([^"'`]+)['"`]/g,
/\|\s*translate\s*[^}]*\}\}/g
]
},
'next-intl': {
name: 'next-intl (Next.js)',
deps: ['next-intl', 'next-i18next', '@next-intl/core'],
patterns: [
/\bt\(\s*['"`]([^'"`]+)['"`]/g,
/\buseTranslations\(\s*\)/g
]
},
'nuxt-i18n': {
name: 'Nuxt i18n',
deps: ['@nuxtjs/i18n', 'nuxt-i18n', '@intlify/nuxt3'],
patterns: [
/\$t\(\s*['"`]([^'"`]+)['"`]/g,
/\$tc\(\s*['"`]([^'"`]+)['"`]/g,
/\blocalePath\(\s*\)/g
]
},
'svelte-i18n': {
name: 'svelte-i18n',
deps: ['svelte-i18n', 'sveltekit-i18n', '@sveltekit-i18n/base'],
patterns: [
/\$_\(\s*['"`]([^'"`]+)['"`]/g,
/\$_\w*\(\s*['"`]([^'"`]+)['"`]/g,
/\bt\.get\(\s*['"`]([^'"`]+)['"`]/g
]
},
'solid-i18n': {
name: 'Solid i18n',
deps: ['@solid-primitives/i18n', 'i18next-solid'],
patterns: [
/\buseI18n\(\s*\)\s*\.\s*t\(\s*['"`]([^'"`]+)['"`]/g,
/\bt\(\s*['"`]([^'"`]+)['"`]/g
]
},
remix: {
name: 'Remix i18n',
deps: ['remix-i18next', 'i18next-remix', '@remix-run/react'],
globs: ['app/**/*.{js,jsx,ts,tsx}'],
patterns: [
/\buseTranslation\(\s*['"`][^'"`]*['"`]?\s*\)/g,
/\bt\(\s*['"`]([^'"`]+)['"`]/g,
/\bgetServerSideTranslations\(/g
]
},
gatsby: {
name: 'Gatsby i18n',
deps: ['gatsby-plugin-react-i18next', '@herob191/gatsby-plugin-react-i18next', 'gatsby-plugin-intl', 'gatsby-theme-i18n'],
globs: ['src/**/*.{js,jsx,ts,tsx}'],
patterns: [
/\bt\(\s*['"`]([^'"`]+)['"`]/g,
/\buseTranslation\(\s*['"`][^'"`]*['"`]?\s*\)/g,
/\bgraphql\s*`[^`]*locales[^`]*`/g
]
},
astro: {
name: 'Astro i18n',
deps: ['astro-i18next', '@astrojs/i18n', 'astro-i18n', '@inox-tools/astro-i18n'],
globs: ['src/**/*.{astro,js,jsx,ts,tsx}'],
patterns: [
/\bt\(\s*['"`]([^'"`]+)['"`]/g,
/\bAstro\.i18n\(\s*['"`]([^'"`]+)['"`]/g,
/\buseTranslations\(\s*['"`][^'"`]*['"`]?\s*\)/g
]
},
qwik: {
name: 'Qwik i18n',
deps: ['qwik-speak', 'qwik-i18n', '@qwikdev/i18n'],
globs: ['src/**/*.{tsx,ts,jsx,js}'],
patterns: [
/\buseTranslate\(\s*\)/g,
/\bt\(\s*['"`]([^'"`]+)['"`]/g,
/\buseSpeak\(\s*\)/g
]
},
'ember-intl': {
name: 'Ember Intl',
deps: ['ember-intl', '@ember-intl/formatjs'],
globs: ['app/**/*.{js,ts,hbs}'],
patterns: [
/\bintl\.t\(\s*['"`]([^'"`]+)['"`]/g,
/\{\{\s*t\s+['"`]([^'"`]+)['"`]\s*\}\}/g,
/\{\{t\s+['"`]([^'"`]+)['"`]\s*\}\}/g
]
},
'react-native-localize': {
name: 'React Native Localize',
deps: ['react-native-localize', 'i18n-js', 'expo-localization'],
globs: ['**/*.{js,jsx,ts,tsx}'],
patterns: [
/\bi18n\.t\(\s*['"`]([^'"`]+)['"`]/g,
/\bt\(\s*['"`]([^'"`]+)['"`]/g,
/\btranslate\(\s*['"`]([^'"`]+)['"`]/g
]
},
ionic: {
name: 'Ionic i18n',
deps: ['@ionic/angular', 'ionic-react', '@ionic/vue'],
globs: ['src/**/*.{ts,tsx,html}'],
patterns: [
/\btranslateService\.instant\(\s*['"`]([^'"`]+)['"`]/g,
/\btranslateService\.get\(\s*['"`]([^'"`]+)['"`]/g,
/\|\s*translate\s*[^}]*\}\}/g
]
}
};
// ============================================================
// CENTRALIZED SHARED CONSTANTS — single source of truth
// All scanner/validator/extension code imports from here.
// ============================================================
const SOURCE_EXTENSIONS = new Set([
'.js', '.jsx', '.ts', '.tsx', '.mjs', '.mts', '.cjs', '.cts',
'.vue', '.svelte', '.astro', '.mdx', '.html', '.rs',
'.py', '.pyx', '.pyi', '.go', '.rb', '.java', '.php', '.hbs'
]);
const SCANNER_EXTENSIONS = new Set([
'.js', '.jsx', '.ts', '.tsx', '.mjs', '.mts', '.cjs', '.cts',
'.vue', '.html', '.svelte', '.astro', '.mdx',
'.py', '.pyx', '.pyi', '.php', '.rb', '.go', '.rs', '.java'
]);
const EXCLUDE_DIRS = new Set([
'node_modules', '.git', '.hg', '.svn',
'.next', '.nuxt', '.output', '.astro', '.svelte-kit', '.cache', '__generated__',
'dist', 'build', 'coverage', 'out', 'target',
'vendor', 'tmp', 'temp', '.terraform'
]);
const SOURCE_DIRS = [
'src', 'app', 'lib', 'source', 'pages', 'routes', 'components',
'layouts', 'api', 'content', 'modules', 'views'
];
const FRAMEWORK_PATTERNS = {
'i18ntk-runtime': [
/>([^<{][^<>{]*[^}>])</g,
/<button[^>]*>([^<]{2,99})<\/button>/g,
/i18n\.t\(["']([^"']{2,99})["']\)/g,
/useI18n\(\)\.t\(["']([^"']{2,99})["']\)/g,
/<t\s[\s\S]*?\bmessage\s*=\s*(?:\{|)(['"`])([^'"`}]+)\1[^>]*>/g
],
react: [
/children:\s*["']([^"']{2,99})["']/g,
/dangerouslySetInnerHTML={{\s*__html:\s*["']([^"']{2,99})["']/g,
/>([^<{][^<>{]*[^}>])</g,
/<button[^>]*>([^<]{2,99})<\/button>/g,
/<span[^>]*>([^<]{2,99})<\/span>/g,
/<(?:FormattedMessage|Trans)[\s\S]*?\b(?:id|defaultMessage|i18nKey)\s*=\s*(?:\{|)(['"`])([^'"`}]+)\1[^>]*>/g
],
next: [
/children:\s*["']([^"']{2,99})["']/g,
/>([^<{][^<>{]*[^}>])</g,
/<button[^>]*>([^<]{2,99})<\/button>/g,
/<(?:FormattedMessage|Trans)[\s\S]*?\b(?:id|defaultMessage|i18nKey)\s*=\s*(?:\{|)(['"`])([^'"`}]+)\1[^>]*>/g
],
nuxt: [
/>([^<{][^<>{]*[^}>])</g,
/<button[^>]*>([^<]{2,99})<\/button>/g,
/\$t\(["']([^"']{2,99})["']\)/g,
/\$tc\(["']([^"']{2,99})["']\)/g,
/v-t=["']([^"']{2,99})["']/g,
/\blocalePath\(\s*\)/g
],
vue: [
/v-text=["']([^"']{2,99})["']/g,
/v-html=["']([^"']{2,99})["']/g,
/>([^<{][^<>{]*[^}>])</g,
/<button[^>]*>([^<]{2,99})<\/button>/g,
/<span[^>]*>([^<]{2,99})<\/span>/g,
/\$t\(["']([^"']{2,99})["']\)/g,
/v-t=["']([^"']{2,99})["']/g
],
angular: [
/\[innerHTML\]=["']([^"']{2,99})["']/g,
/\[textContent\]=["']([^"']{2,99})["']/g,
/>([^<{][^<>{]*[^}>])</g,
/<button[^>]*>([^<]{2,99})<\/button>/g,
/<span[^>]*>([^<]{2,99})<\/span>/g,
/i18n=["']([^"']{2,99})["']/g,
/\[attr\.title\]=["']([^"']{2,99})["']/g
],
svelte: [
/>([^<{][^<>{]*[^}>])</g,
/<button[^>]*>([^<]{2,99})<\/button>/g,
/\$_\(["']([^"']{2,99})["']\)/g,
/t\.set\(["']([^"']{2,99})["']/g,
/\{#if[^}]*\}([^<]{2,99})\{\/if\}/g
],
astro: [
/>([^<{][^<>{]*[^}>])</g,
/<button[^>]*>([^<]{2,99})<\/button>/g,
/t\(["']([^"']{2,99})["']\)/g
],
remix: [
/children:\s*["']([^"']{2,99})["']/g,
/>([^<{][^<>{]*[^}>])</g,
/<button[^>]*>([^<]{2,99})<\/button>/g,
/<(?:FormattedMessage|Trans)[\s\S]*?\b(?:id|defaultMessage|i18nKey)\s*=\s*(?:\{|)(['"`])([^'"`}]+)\1[^>]*>/g
],
qwik: [
/>([^<{][^<>{]*[^}>])</g,
/<button[^>]*>([^<]{2,99})<\/button>/g,
/t\(["']([^"']{2,99})["']\)/g,
/useTranslate\(\)/g
],
solid: [
/>([^<{][^<>{]*[^}>])</g,
/<button[^>]*>([^<]{2,99})<\/button>/g,
/t\(["']([^"']{2,99})["']\)/g,
/useI18n\(\)\.t\(["']([^"']{2,99})["']\)/g
],
ember: [
/>([^<{][^<>{]*[^}>])</g,
/\{\{t\s+["']([^"']{2,99})["']\s*\}\}/g,
/\{\{intl\.t\s+["']([^"']{2,99})["']\s*\}\}/g
],
gatsby: [
/children:\s*["']([^"']{2,99})["']/g,
/>([^<{][^<>{]*[^}>])</g,
/<(?:Trans)[\s\S]*?\bi18nKey\s*=\s*(?:\{|)(['"`])([^'"`}]+)\1[^>]*>/g
],
django: [
/\{\%\s*trans\s+["']([^"']{2,99})["']\s*%\}/g,
/\{\%\s*blocktrans\s*%\}([^%]{2,99})\{\%\s*endblocktrans\s*%\}/g,
/{{\s*_["']([^"']{2,99})["']\s*}}/g,
/{{\s*gettext\(["']([^"']{2,99})["']\)\s*}}/g
],
flask: [
/\{\{\s*_["']([^"']{2,99})["']\s*}}/g,
/\{\{\s*gettext\(["']([^"']{2,99})["']\)\s*}}/g,
/\{\{\s*lazy_gettext\(["']([^"']{2,99})["']\)\s*}}/g
],
python: [
/gettext\(["']([^"']{2,99})["']\)/g,
/_\(["']([^"']{2,99})["']\)/g,
/gettext_lazy\(["']([^"']{2,99})["']\)/g,
/lazy_gettext\(["']([^"']{2,99})["']\)/g
],
rust: [
/bundle\.get_message\(\s*["']([^"']{2,99})["']\)/g,
/\.get_message\s*\(\s*["']([^"']{2,99})["']\)/g,
/ts!\s*\(\s*["']([^"']{2,99})["']/g,
/fluent!\s*\(\s*["']([^"']{2,99})["']/g
],
go: [
/i18n\.Translate\([^,]+,\s*["']([^"']{2,99})["']\)/g,
/i18n\.NewMessage\([^,]+,\s*["']([^"']{2,99})["']\)/g,
/t\.Get\([^,]+,\s*["']([^"']{2,99})["']\)/g
],
lingui: [
/>([^<{][^<>{]*[^}>])</g,
/(?<![\w$.])t\s*\(\s*['"`]([^'"`]{2,99})['"`]/g,
/_\(\s*['"`]([^'"`]{2,99})['"`]/g,
/<Translate\s[\s\S]*?\bid\s*=\s*(?:\{|)(['"`])([^'"`}]+)\1[^>]*>/g
],
formatjs: [
/>([^<{][^<>{]*[^}>])</g,
/formatMessage\(\s*\{\s*id:\s*['"`]([^'"`]{2,99})['"`]/g,
/<FormattedMessage\s[\s\S]*?\bid\s*=\s*(?:\{|)(['"`])([^'"`}]+)\1[^>]*>/g
],
'ngx-translate': [
/>([^<{][^<>{]*[^}>])</g,
/\|\s*translate\s*[^}]*\}\}/g,
/translateService\.instant\(\s*['"`]([^'"`]{2,99})['"`]/g,
/translateService\.get\(\s*['"`]([^'"`]{2,99})['"`]/g,
/translate\s*=\s*["'`]([^"'`]{2,99})["'`]/g,
/\[innerHTML\]=["']([^"']{2,99})["']/g
],
'next-intl': [
/>([^<{][^<>{]*[^}>])</g,
/t\(\s*['"`]([^'"`]{2,99})['"`]/g,
/\buseTranslations\(\s*()\)/g
],
'svelte-i18n': [
/>([^<{][^<>{]*[^}>])</g,
/\$_\(\s*['"`]([^'"`]{2,99})['"`]/g,
/\$_\w*\(\s*['"`]([^'"`]{2,99})['"`]/g,
/\bt\.get\(\s*['"`]([^'"`]{2,99})['"`]/g
],
'solid-i18n': [
/>([^<{][^<>{]*[^}>])</g,
/\buseI18n\(\s*\)\s*\.\s*t\(\s*['"`]([^'"`]{2,99})['"`]/g,
/t\(\s*['"`]([^'"`]{2,99})['"`]/g,
/<Translate\s[\s\S]*?\bid\s*=\s*(?:\{|)(['"`])([^'"`}]+)\1[^>]*>/g
],
fastapi: [
/_\(\s*['"`]([^'"`]{2,99})['"`]/g,
/i18n\.t\(\s*['"`]([^'"`]{2,99})['"`]/g,
/\{\{\s*_['"]([^'"]{2,99})['"]\s*\}\}/g,
/\{\{\s*gettext\(['"`]([^'"`]{2,99})['"`]\)\s*\}\}/g
],
'ruby-on-rails': [
/I18n\.t\(\s*['"]([^'"]{2,99})['"]/g,
/I18n\.translate\(\s*['"]([^'"]{2,99})['"]/g,
/I18n\.l\(\s*['"]([^'"]{2,99})['"]/g,
/<%= t\(['"]([^'"]{2,99})['"]/g,
/<%= I18n\.t\(['"]([^'"]{2,99})['"]/g,
/(?<![\w$.])t\s*\(\s*['"]([^'"]{2,99})['"]/g
],
'react-native-localize': [
/>([^<{][^<>{]*[^}>])</g,
/\bi18n\.t\(\s*['"`]([^'"`]{2,99})['"`]/g,
/\bt\(\s*['"`]([^'"`]{2,99})['"`]/g,
/\btranslate\(\s*['"`]([^'"`]{2,99})['"`]/g
],
ionic: [
/>([^<{][^<>{]*[^}>])</g,
/\|\s*translate\s*[^}]*\}\}/g,
/translateService\.instant\(\s*['"`]([^'"`]{2,99})['"`]/g,
/translateService\.get\(\s*['"`]([^'"`]{2,99})['"`]/g
],
vanilla: [
/t\(["']([^"']{2,99})["']\)/g,
/i18n\.t\(["']([^"']{2,99})["']\)/g,
/translate\(["']([^"']{2,99})["']\)/g
]
};
const FRAMEWORK_SUGGESTIONS = {
react: { hook: 'const { t } = useTranslation();', usage: "{t('ui.KEY')}", component: "<Trans i18nKey=\"ui.KEY\">text</Trans>" },
next: { hook: 'const t = useTranslations();', usage: "{t('ui.KEY')}", component: "<Trans i18nKey=\"ui.KEY\">text</Trans>" },
vue: { directive: "{{ $t('ui.KEY') }}", method: "this.$t('ui.KEY')" },
angular: { pipe: "{{ 'text' | translate }}", service: "this.translateService.instant('ui.KEY')" },
svelte: { store: "$_(('ui.KEY'))", method: "t.set('ui.KEY', 'text')" },
astro: { import: "import { t } from 'astro-i18next';", usage: "{t('ui.KEY')}" },
remix: { hook: 'const { t } = useTranslation();', usage: "{t('ui.KEY')}", server: 'export const handle = i18next.handle;' },
qwik: { hook: 'const t = useTranslate();', usage: "{t('ui.KEY')}" },
solid: { hook: 'const [t] = useI18n();', usage: "{t('ui.KEY')}" },
ember: { template: "{{t 'ui.KEY'}}", helper: "this.intl.t('ui.KEY')" },
gatsby: { hook: 'const { t } = useTranslation();', usage: "{t('ui.KEY')}", plugin: "'gatsby-plugin-react-i18next'" },
nuxt: { directive: "{{ $t('ui.KEY') }}", method: "this.$t('ui.KEY')", component: "<NuxtLink :to=\"localePath('ui.KEY')\">" },
'i18ntk-runtime': { hook: 'const { t } = useI18n();', usage: "{t('ui.KEY')}", component: "<t message=\"ui.KEY\">text</t>" },
lingui: { hook: 'import { t } from \"@lingui/macro\";', usage: "{t('ui.KEY')}", component: "<Trans id=\"ui.KEY\">text</Trans>" },
formatjs: { hook: 'import { FormattedMessage, useIntl } from \"react-intl\";', usage: "intl.formatMessage({ id: 'ui.KEY' })", component: "<FormattedMessage id=\"ui.KEY\" />" },
'ngx-translate': { pipe: "{{ 'ui.KEY' | translate }}", service: "this.translateService.instant('ui.KEY')" },
'next-intl': { hook: 'const t = useTranslations();', usage: "{t('ui.KEY')}" },
'svelte-i18n': { store: "$_('ui.KEY')", method: "t.get('ui.KEY')" },
'solid-i18n': { hook: 'const [t] = useI18n();', usage: "{t('ui.KEY')}", component: "<Translate id=\"ui.KEY\" />" },
fastapi: { python: "from fastapi_i18n import _\n_('text')", template: "{{ _('text') }}" },
'ruby-on-rails': { helper: "t('ui.KEY')", method: "I18n.t('ui.KEY')", template: "<%= t('ui.KEY') %>" },
'react-native-localize': { hook: 'import { t } from \"i18n-js\";', usage: "{t('ui.KEY')}" },
ionic: { pipe: "{{ 'ui.KEY' | translate }}", service: "this.translateService.instant('ui.KEY')" },
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')" },
flask: { template: "{{ _('text') }}", python: "from flask_babel import gettext as _\n_('text')", lazy: "from flask_babel import lazy_gettext as _\n_('text')" },
python: { gettext: "import gettext\ngettext.gettext('text')", underscore: "from gettext import gettext as _\n_('text')", lazy: "from gettext import gettext_lazy as _\n_('text')" },
rust: { fluent: 'bundle.get_message("ui_KEY")', gettext: 'gettext("ui_KEY")' },
go: { translate: 'i18n.Translate("ui_KEY")', message: 'i18n.NewMessage("ui_KEY")' },
vanilla: { generic: "t('ui.KEY')" }
};
const WRAPPER_SKIP_PATTERNS = [
't(', 'tx(', 'i18n.t(', 'i18n.translate(', 'translate(',
'useI18n(', 'useTranslation(', '__', '__t', '$_(', '$t(', '_(',
'$tc(', 'gettext(', 'gettext_lazy(', 'lazy_gettext(', 'pgettext(', 'ngettext(',
'I18n.t(', 'I18n.translate(', 'I18n.l(', 'I18n.localize(',
't.get(', 'useTranslate(', 'useSpeak(', 'withTranslation(',
'formatMessage(', 'bundle.get_message(', 'i18n.NewMessage(',
'i18n.Translate(', '_l(', '_n(', 't.set(', 'fluent!', 'ts!',
'translateService.instant(', 'translateService.get('
];
function _keySnippet(text) {
return String(text || '').toLowerCase()
.replace(/[^a-z0-9\s]/g, '')
.replace(/\s+/g, '_')
.substring(0, 40);
}
function getFrameworkPatterns(framework) {
const base = [
/(?<![\w])["'`]([^"'`]{2,99})["'`]/g,
/`([^`]{2,99})`/g,
/>([^<]{2,99})</g,
/title=["']([^"']{2,99})["']/g,
/alt=["']([^"']{2,99})["']/g,
/placeholder=["']([^"']{2,99})["']/g
];
return [...base, ...(FRAMEWORK_PATTERNS[framework] || FRAMEWORK_PATTERNS.vanilla || [])];
}
function getFrameworkSuggestions(framework, text) {
const template = FRAMEWORK_SUGGESTIONS[framework] || FRAMEWORK_SUGGESTIONS.vanilla || {};
const result = {};
for (const [k, v] of Object.entries(template)) {
result[k] = v.replace(/ui\.KEY/g, `ui.${_keySnippet(text)}`).replace(/\btext\b/g, text);
}
return result;
}
function getSourceExtensions() { return [...SOURCE_EXTENSIONS]; }
function getExcludeDirs() { return [...EXCLUDE_DIRS]; }
function detectProjectFramework(projectRoot) {
const detected = detectFramework(projectRoot);
if (detected && detected.id) return detected.id;
return 'vanilla';
}
/**
* Detects the i18n framework being used in the project
* @param {string} projectRoot - Path to the project root
* @returns {Object|null} Detected framework info or null if none found
*/
function detectFramework(projectRoot) {
if (!projectRoot || typeof projectRoot !== 'string') {
throw new Error('Invalid project root path');
}
const detectedFrameworks = [];
// Phase 1: Check package.json for Node.js-based i18n frameworks
const packageJsonPath = path.join(projectRoot, 'package.json');
if (SecurityUtils.safeExistsSync(packageJsonPath, projectRoot)) {
try {
const packageJsonContent = SecurityUtils.safeReadFileSync(packageJsonPath, projectRoot, 'utf8');
if (packageJsonContent) {
const packageJson = SecurityUtils.safeParseJSON(packageJsonContent);
if (packageJson) {
const deps = {
...(packageJson.dependencies || {}),
...(packageJson.devDependencies || {}),
...(packageJson.peerDependencies || {})
};
const sortedFrameworks = Object.entries(FRAMEWORKS).sort((a, b) =>
(b[1].priority || 0) - (a[1].priority || 0)
);
for (const [id, framework] of sortedFrameworks) {
try {
const frameworkDeps = framework.deps || framework.dependencies || [];
const hasAnyDep = frameworkDeps.some(dep => dep in deps);
if (hasAnyDep) {
const mainDep = frameworkDeps[0];
const frameworkInfo = {
id, name: framework.name || id, description: framework.description,
confidence: id === 'i18ntk-runtime' ? 0.95 : 0.9,
version: deps[mainDep] || '', priority: framework.priority || 0
};
detectedFrameworks.push(frameworkInfo);
}
} catch (_) { /* skip */ }
}
}
}
} catch (_) { /* skip */ }
}
// Phase 2: Check Python project files (no package.json or no Node matches)
if (detectedFrameworks.length === 0) {
const pyProjectFiles = ['requirements.txt', 'setup.py', 'setup.cfg', 'pyproject.toml', 'Pipfile', 'Pipfile.lock'];
const hasPyProject = pyProjectFiles.some(f => SecurityUtils.safeExistsSync(path.join(projectRoot, f), projectRoot));
if (hasPyProject) {
const pyDeps = readPyProjectDeps(projectRoot);
const pyFrameworks = ['django', 'flask', 'fastapi', 'python'];
for (const id of pyFrameworks) {
const framework = FRAMEWORKS[id];
if (!framework) continue;
const frameworkDeps = framework.deps || [];
const hasAnyDep = frameworkDeps.some(dep => pyDeps.includes(dep.toLowerCase().replace(/[_-]/g, '')));
if (hasAnyDep) {
detectedFrameworks.push({
id, name: framework.name || id, description: framework.description,
confidence: 0.8, version: '', priority: framework.priority || 0
});
}
}
if (detectedFrameworks.length === 0 && hasPyProject) {
detectedFrameworks.push({
id: 'python', name: 'Python', description: 'Python project with gettext/i18n',
confidence: 0.5, version: '', priority: 0
});
}
}
}
// Phase 3: Check Rust project (Cargo.toml)
if (detectedFrameworks.length === 0) {
const cargoPath = path.join(projectRoot, 'Cargo.toml');
if (SecurityUtils.safeExistsSync(cargoPath, projectRoot)) {
try {
const cargoContent = SecurityUtils.safeReadFileSync(cargoPath, projectRoot, 'utf8');
if (cargoContent) {
const hasRustI18n = /fluent|rust-i18n|gettext/.test(cargoContent);
if (hasRustI18n || /^\[package\]/m.test(cargoContent)) {
detectedFrameworks.push({
id: 'rust', name: 'Rust', description: 'Rust project with i18n',
confidence: hasRustI18n ? 0.85 : 0.4, version: '', priority: 0
});
}
}
} catch (_) { /* skip */ }
}
}
// Phase 4: Check Go project (go.mod)
if (detectedFrameworks.length === 0) {
const goModPath = path.join(projectRoot, 'go.mod');
if (SecurityUtils.safeExistsSync(goModPath, projectRoot)) {
try {
const goModContent = SecurityUtils.safeReadFileSync(goModPath, projectRoot, 'utf8');
if (goModContent) {
const hasGoI18n = /go-i18n|x-text|i18n/.test(goModContent);
detectedFrameworks.push({
id: 'go', name: 'Go', description: 'Go project with i18n',
confidence: hasGoI18n ? 0.85 : 0.4, version: '', priority: 0
});
}
} catch (_) { /* skip */ }
}
}
// Phase 5: Check Ruby project (Gemfile)
if (detectedFrameworks.length === 0) {
const gemfilePaths = ['Gemfile', 'gems.rb'];
for (const gf of gemfilePaths) {
const gemfilePath = path.join(projectRoot, gf);
if (SecurityUtils.safeExistsSync(gemfilePath, projectRoot)) {
try {
const gemfileContent = SecurityUtils.safeReadFileSync(gemfilePath, projectRoot, 'utf8');
if (gemfileContent) {
const hasRails = /rails/.test(gemfileContent);
const hasI18n = /i18n/.test(gemfileContent);
if (hasRails) {
detectedFrameworks.push({
id: 'ruby-on-rails', name: 'Ruby on Rails', description: 'Rails project with i18n',
confidence: hasI18n ? 0.9 : 0.7, version: '', priority: 0
});
break;
}
if (hasI18n) {
detectedFrameworks.push({
id: 'ruby-on-rails', name: 'Ruby on Rails', description: 'Ruby project with i18n',
confidence: 0.6, version: '', priority: 0
});
break;
}
}
} catch (_) { /* skip */ }
}
}
}
// Return the framework with highest confidence, if any
if (detectedFrameworks.length > 0) {
return detectedFrameworks.sort((a, b) => {
const confidenceDiff = b.confidence - a.confidence;
if (confidenceDiff !== 0) return confidenceDiff;
return (b.priority || 0) - (a.priority || 0);
})[0];
}
return null;
}
function readPyProjectDeps(projectRoot) {
const deps = [];
try {
const reqPath = path.join(projectRoot, 'requirements.txt');
if (SecurityUtils.safeExistsSync(reqPath, projectRoot)) {
const content = SecurityUtils.safeReadFileSync(reqPath, projectRoot, 'utf8');
if (content) {
for (const line of content.split('\n')) {
const stripped = line.replace(/[<>=!~].*$/, '').trim();
if (stripped && !stripped.startsWith('#')) deps.push(stripped.toLowerCase().replace(/[_-]/g, ''));
}
}
}
} catch (_) { /* skip */ }
return deps;
}
module.exports = { detectFramework, FRAMEWORKS, FRAMEWORK_COMPATIBILITY,
SOURCE_EXTENSIONS, SCANNER_EXTENSIONS, EXCLUDE_DIRS, SOURCE_DIRS,
FRAMEWORK_PATTERNS, FRAMEWORK_SUGGESTIONS, WRAPPER_SKIP_PATTERNS,
getFrameworkPatterns, getFrameworkSuggestions, getSourceExtensions, getExcludeDirs, detectProjectFramework };