UNPKG

i18ntk

Version:

i18n Tool Kit - Zero-dependency internationalization toolkit for setup, scanning, analysis, validation, auto translation, fixing, reporting, and runtime translation loading.

380 lines (316 loc) • 11.9 kB
#!/usr/bin/env node /** * i18ntk Python Command * Specialized command for Python i18n management * * Usage: i18ntk-py [options] */ const fs = require('fs'); const path = require('path'); const SecurityUtils = require('../utils/security'); const { getConfig, saveConfig } = require('../utils/config-helper'); const I18nHelper = require('../utils/i18n-helper'); const SetupEnforcer = require('../utils/setup-enforcer'); const { program } = require('../utils/mini-commander'); (async () => { try { await SetupEnforcer.checkSetupCompleteAsync(); } catch (error) { console.error('Setup check failed:', error.message); process.exit(1); } })(); class I18ntkPythonCommand { constructor() { this.config = null; this.sourceDir = './locales'; this.pythonPatterns = [ /_(?:gettext)?\s*\(\s*["'`]([^"'`]+)["'`]\s*\)/g, /gettext\s*\(\s*["'`]([^"'`]+)["'`]\s*\)/g, /gettext_lazy\s*\(\s*["'`]([^"'`]+)["'`]\s*\)/g, /ngettext\s*\(\s*["'`]([^"'`]+)["'`]\s*,/g, /lazy_gettext\s*\(\s*["'`]([^"'`]+)["'`]\s*\)/g, /ugettext\s*\(\s*["'`]([^"'`]+)["'`]\s*\)/g ]; } async init() { console.log('šŸ”§ Initializing i18ntk Python command...'); program .name('i18ntk-py') .description('i18ntk specialized for Python applications') .version('1.10.1') .option('-s, --source-dir <dir>', 'Source directory to scan', './') .option('-l, --locales-dir <dir>', 'Locales directory', './locales') .option('--framework <type>', 'Python framework type', 'auto') .option('--dry-run', 'Show what would be done without making changes') .option('--debug', 'Enable debug output') .option('--extract-only', 'Only extract translations, don\'t analyze') .option('--django', 'Force Django mode') .option('--flask', 'Force Flask mode') .option('--generic', 'Force generic Python mode') .parse(); this.options = program.opts(); this.sourceDir = path.resolve(this.options.sourceDir); this.localesDir = path.resolve(this.options.localesDir); await this.validateSourceDir(); await this.loadConfig(); } async validateSourceDir() { if (!SecurityUtils.safeExistsSync(this.sourceDir, path.dirname(this.sourceDir))) { console.error(`āŒ Source directory not found: ${this.sourceDir}`); process.exit(1); } const stats = fs.statSync(this.sourceDir); if (!stats.isDirectory()) { console.error(`āŒ Source path is not a directory: ${this.sourceDir}`); process.exit(1); } } async loadConfig() { try { this.config = await getConfig(); this.config.python = this.config.python || {}; } catch (error) { console.warn('āš ļø Could not load config, using defaults'); this.config = { python: {} }; } } async detectFramework() { if (this.options.django) return 'django'; if (this.options.flask) return 'flask'; if (this.options.generic) return 'generic'; console.log('šŸ” Detecting Python framework...'); // Check for Django const djangoIndicators = [ 'manage.py', 'settings.py', 'requirements.txt', 'django' ]; // Check for Flask const flaskIndicators = [ 'app.py', 'requirements.txt', 'flask' ]; let framework = 'generic'; try { // Check requirements.txt const requirementsPath = path.join(this.sourceDir, 'requirements.txt'); if (SecurityUtils.safeExistsSync(requirementsPath, this.sourceDir)) { const requirements = SecurityUtils.safeReadFileSync(requirementsPath, this.sourceDir, 'utf8'); if (requirements.includes('Django')) framework = 'django'; else if (requirements.includes('Flask')) framework = 'flask'; } // Check for Django files for (const indicator of djangoIndicators) { if (this.findFiles(indicator).length > 0) { framework = 'django'; break; } } // Check for Flask files for (const indicator of flaskIndicators) { if (this.findFiles(indicator).length > 0) { framework = 'flask'; break; } } } catch (error) { if (this.options.debug) { console.error('Debug: Framework detection error:', error.message); } } console.log(`āœ… Detected framework: ${framework}`); return framework; } findFiles(pattern) { const results = []; function scanDir(dir) { if (!SecurityUtils.safeExistsSync(dir, path.dirname(dir))) return; const items = fs.readdirSync(dir); for (const item of items) { const fullPath = path.join(dir, item); const stat = fs.statSync(fullPath); if (stat.isDirectory() && !item.startsWith('.') && item !== 'node_modules') { scanDir(fullPath); } else if (item.includes(pattern) || fullPath.includes(pattern)) { results.push(fullPath); } } } scanDir(this.sourceDir); return results; } async extractTranslations() { console.log('šŸ“¦ Extracting Python translations...'); const pythonFiles = this.findFiles('.py'); const translations = new Set(); for (const file of pythonFiles) { try { const content = SecurityUtils.safeReadFileSync(file, path.dirname(file), 'utf8'); for (const pattern of this.pythonPatterns) { let match; while ((match = pattern.exec(content)) !== null) { translations.add(match[1]); } } // Reset regex state for next file for (const pattern of this.pythonPatterns) { pattern.lastIndex = 0; } } catch (error) { if (this.options.debug) { console.error(`Error reading ${file}:`, error.message); } } } console.log(`šŸ“Š Found ${translations.size} unique translation keys`); return Array.from(translations); } async createLocaleStructure() { if (!SecurityUtils.safeExistsSync(this.localesDir, path.dirname(this.localesDir))) { if (this.options.dryRun) { console.log(`šŸ“ Would create directory: ${this.localesDir}`); return; } fs.mkdirSync(this.localesDir, { recursive: true }); console.log(`šŸ“ Created locales directory: ${this.localesDir}`); } const languages = ['en', 'es', 'fr', 'de', 'ja', 'ru', 'zh']; for (const lang of languages) { const langDir = path.join(this.localesDir, lang); if (!SecurityUtils.safeExistsSync(langDir, this.localesDir)) { if (this.options.dryRun) { console.log(`šŸ“ Would create directory: ${langDir}`); continue; } fs.mkdirSync(langDir, { recursive: true }); // Create basic translation files const commonFile = path.join(langDir, 'common.json'); const initialContent = { "python": { "welcome": `Welcome to Python (${lang})`, "framework_detected": "Framework detected: {framework}", "files_processed": "Processed {count} files" } }; SecurityUtils.safeWriteFileSync(commonFile, JSON.stringify(initialContent, null, 2), this.localesDir); } } } async analyzeFramework(framework) { console.log(`šŸ” Analyzing ${framework} project...`); const analysis = { framework, files: { total: 0, python: 0, templates: 0, config: 0 }, patterns: { gettext: 0, gettext_lazy: 0, ngettext: 0, django: 0, flask: 0 }, recommendations: [] }; // Count Python files const pythonFiles = this.findFiles('.py'); analysis.files.python = pythonFiles.length; analysis.files.total += pythonFiles.length; // Count template files const templateExtensions = ['.html', '.jinja', '.j2']; let templateFiles = []; for (const ext of templateExtensions) { templateFiles = templateFiles.concat(this.findFiles(ext)); } analysis.files.templates = templateFiles.length; analysis.files.total += templateFiles.length; // Analyze patterns for (const file of pythonFiles) { try { const content = SecurityUtils.safeReadFileSync(file, path.dirname(file), 'utf8'); if (content.includes('gettext(')) analysis.patterns.gettext++; if (content.includes('gettext_lazy(')) analysis.patterns.gettext_lazy++; if (content.includes('ngettext(')) analysis.patterns.ngettext++; if (content.includes('django')) analysis.patterns.django++; if (content.includes('flask')) analysis.patterns.flask++; } catch (error) { // Skip unreadable files } } // Generate recommendations if (framework === 'django' && analysis.patterns.gettext === 0) { analysis.recommendations.push('Consider adding Django gettext for i18n support'); } if (framework === 'flask' && analysis.patterns.gettext === 0) { analysis.recommendations.push('Consider adding Flask-Babel for i18n support'); } return analysis; } async generateReport(analysis, translations) { const report = { timestamp: new Date().toISOString(), framework: analysis.framework, summary: { totalFiles: analysis.files.total, pythonFiles: analysis.files.python, templateFiles: analysis.files.templates, translationKeys: translations.length }, patterns: analysis.patterns, recommendations: analysis.recommendations, files: { python: this.findFiles('.py'), templates: this.findFiles('.html').concat(this.findFiles('.jinja')).concat(this.findFiles('.j2')) }, translations: translations.sort() }; const reportPath = path.join(this.sourceDir, 'i18ntk-py-report.json'); if (this.options.dryRun) { console.log(`šŸ“Š Would create report: ${reportPath}`); console.log('šŸ“‹ Report contents:', JSON.stringify(report, null, 2)); } else { SecurityUtils.safeWriteFileSync(reportPath, JSON.stringify(report, null, 2), this.sourceDir); console.log(`šŸ“Š Report saved: ${reportPath}`); } return report; } async run() { try { console.log('šŸš€ i18ntk Python Command v1.10.1'); console.log('='.repeat(50)); await this.init(); const framework = await this.detectFramework(); const translations = await this.extractTranslations(); if (!this.options.extractOnly) { await this.createLocaleStructure(); const analysis = await this.analyzeFramework(framework); const report = await this.generateReport(analysis, translations); console.log('\nāœ… Analysis complete!'); console.log(`šŸ“Š Framework: ${framework}`); console.log(`šŸ“„ Python files: ${analysis.files.python}`); console.log(`šŸŽÆ Translation keys: ${translations.length}`); console.log(`šŸ“‹ Report: ${this.options.dryRun ? 'Not saved (dry-run)' : 'Saved to i18ntk-py-report.json'}`); } else { console.log(`šŸ“¦ Extracted ${translations.length} translation keys`); } } catch (error) { console.error('āŒ Error:', error.message); if (this.options.debug) { console.error(error.stack); } process.exit(1); } } } // Run if called directly if (require.main === module) { const cmd = new I18ntkPythonCommand(); cmd.run(); } module.exports = I18ntkPythonCommand;