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
JavaScript
/**
* 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;