UNPKG

i18ntk

Version:

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

463 lines (392 loc) 14.8 kB
#!/usr/bin/env node /** * i18ntk-php.js - PHP Language I18n Management Command * * Supports: * - Laravel i18n * - Symfony translations * - WordPress i18n * - Standard PHP gettext * - PHP array translations * - JSON translations */ 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 PhpI18nManager { constructor() { this.supportedPatterns = [ '__(', '_e(', '_n(', '_x(', '_ex(', '_nx(', 'trans(', 'trans_choice(', 'Lang::get(', 'Lang::choice(', '$this->trans(', 'Yii::t(', '$this->t(', 'dgettext(', 'dngettext(', 'dcgettext(', 'ngettext(', 'gettext(' ]; this.fileExtensions = ['.php', '.blade.php', '.twig', '.phtml', '.module', '.theme']; this.resourceFormats = ['.php', '.json', '.yml', '.yaml', '.po', '.mo']; } async detectFramework(sourceDir) { // Check for Laravel const composerJson = path.join(sourceDir, 'composer.json'); if (SecurityUtils.safeExistsSync(composerJson, sourceDir)) { const content = SecurityUtils.safeReadFileSync(composerJson, sourceDir, 'utf8'); const composer = SecurityUtils.safeParseJSON(content); if (composer.require && composer.require['laravel/framework']) { return 'laravel'; } if (composer.require && composer.require['symfony/framework-bundle']) { return 'symfony'; } if (composer.require && composer.require['yiisoft/yii2']) { return 'yii2'; } if (composer.require && composer.require['wordpress/wordpress']) { return 'wordpress'; } } // Check for WordPress const wpConfig = path.join(sourceDir, 'wp-config.php'); if (SecurityUtils.safeExistsSync(wpConfig, sourceDir)) { return 'wordpress'; } // Check for standard PHP const phpFiles = this.findFiles(sourceDir, '.php'); if (phpFiles.length > 0) { return 'standard-php'; } return 'generic'; } async extractTranslations(sourceDir, options = {}) { const framework = await this.detectFramework(sourceDir); const translations = new Set(); // Process PHP files const phpFiles = [ ...this.findFiles(sourceDir, '.php'), ...this.findFiles(sourceDir, '.blade.php'), ...this.findFiles(sourceDir, '.twig'), ...this.findFiles(sourceDir, '.phtml') ]; for (const file of phpFiles) { const content = SecurityUtils.safeReadFileSync(file, path.dirname(file), 'utf8'); // Extract PHP i18n patterns const patterns = [ /__\(\s*["']([^"']+)["']/g, /_e\(\s*["']([^"']+)["']/g, /_n\(\s*["']([^"']+)["']/g, /_x\(\s*["']([^"']+)["']/g, /trans\(\s*["']([^"']+)["']/g, /trans_choice\(\s*["']([^"']+)["']/g, /Lang::get\(\s*["']([^"']+)["']/g, /Lang::choice\(\s*["']([^"']+)["']/g, /Yii::t\(\s*["']([^"']+)["']/g, /gettext\(\s*["']([^"']+)["']/g, /dgettext\(\s*["']([^"']+)["']/g, /ngettext\(\s*["']([^"']+)["']/g, /dngettext\(\s*["']([^"']+)["']/g ]; for (const pattern of patterns) { let match; while ((match = pattern.exec(content)) !== null) { translations.add(match[1]); } } // Blade template patterns const bladePatterns = [ /@lang\(['"]([^'"]+)['"]\)/g, /@choice\(['"]([^'"]+)['"]\)/g, /{{\s*__\(['"]([^'"]+)['"]\)\s*}}/g, /{{\s*trans\(['"]([^'"]+)['"]\)\s*}}/g ]; for (const pattern of bladePatterns) { let match; while ((match = pattern.exec(content)) !== null) { translations.add(match[1]); } } } // Process translation files const translationFiles = [ ...this.findFiles(sourceDir, '.php'), ...this.findFiles(sourceDir, '.json'), ...this.findFiles(sourceDir, '.yml'), ...this.findFiles(sourceDir, '.yaml') ]; for (const file of translationFiles) { if (file.includes('lang') || file.includes('translations') || file.includes('i18n')) { try { const content = SecurityUtils.safeReadFileSync(file, path.dirname(file), 'utf8'); if (file.endsWith('.php')) { // PHP array translations const arrayPattern = /'([^']+)'\s*=>/g; let match; while ((match = arrayPattern.exec(content)) !== null) { translations.add(match[1]); } } else if (file.endsWith('.json')) { // JSON translations const data = SecurityUtils.safeParseJSON(content); const keys = this.extractKeysFromObject(data); keys.forEach(key => translations.add(key)); } else if (file.endsWith('.yml') || file.endsWith('.yaml')) { // YAML translations (basic parsing) const lines = content.split('\n'); for (const line of lines) { const trimmed = line.trim(); if (trimmed && !trimmed.startsWith('#') && trimmed.includes(':')) { const key = trimmed.split(':')[0].trim(); if (!key.startsWith(' ')) { translations.add(key); } } } } } catch (error) { // Skip files that can't be parsed } } } return { framework, translations: Array.from(translations), files: phpFiles.length + translationFiles.length, patterns: this.supportedPatterns }; } extractKeysFromObject(obj, prefix = '') { const keys = []; for (const [key, value] of Object.entries(obj)) { const fullKey = prefix ? `${prefix}.${key}` : key; keys.push(fullKey); if (typeof value === 'object' && value !== null && !Array.isArray(value)) { keys.push(...this.extractKeysFromObject(value, fullKey)); } } return keys; } async createLocaleStructure(outputDir, languages = ['en'], framework = 'standard-php') { const localesDir = path.join(outputDir, 'locales'); for (const lang of languages) { const langDir = path.join(localesDir, lang); fs.mkdirSync(langDir, { recursive: true }); if (framework === 'laravel') { // Laravel structure SecurityUtils.safeWriteFileSync(path.join(langDir, 'messages.php'), `<?php // Laravel i18n for ${lang} return [ 'hello' => 'Hello, World!', 'items' => [ 'one' => ':count item', 'other' => ':count items' ], 'welcome' => 'Welcome to our application' ]; `, path.dirname(langDir)); SecurityUtils.safeWriteFileSync(path.join(langDir, 'validation.php'), `<?php // Laravel validation messages for ${lang} return [ 'required' => 'The :attribute field is required.', 'email' => 'The :attribute must be a valid email address.', 'unique' => 'The :attribute has already been taken.' ]; `, path.dirname(localesDir)); } else if (framework === 'symfony') { // Symfony structure SecurityUtils.safeWriteFileSync(path.join(langDir, 'messages.yml'), `# Symfony i18n for ${lang} hello: "Hello, World!" items: one: "%count% item" other: "%count% items" welcome: "Welcome to our application" `, path.dirname(localesDir)); SecurityUtils.safeWriteFileSync(path.join(langDir, 'validators.yml'), `# Symfony validation for ${lang} required: "The {{ field }} field is required." email: "The {{ field }} must be a valid email address." unique: "The {{ field }} has already been taken." `, path.dirname(localesDir)); } else { // Standard PHP SecurityUtils.safeWriteFileSync(path.join(langDir, 'messages.php'), `<?php // PHP i18n for ${lang} return [ 'hello' => 'Hello, World!', 'items' => [ 'one' => '{0} item', 'other' => '{0} items' ], 'welcome' => 'Welcome to our application' ]; `, path.dirname(langDir)); SecurityUtils.safeWriteFileSync(path.join(langDir, 'messages.json'), JSON.stringify({ hello: "Hello, World!", items: { one: "{0} item", other: "{0} items" }, welcome: "Welcome to our application" }, null, 2), path.dirname(langDir)); } } return localesDir; } findFiles(dir, extension) { const files = []; function traverse(currentDir) { if (!SecurityUtils.safeExistsSync(currentDir, path.dirname(currentDir))) return; const items = fs.readdirSync(currentDir); for (const item of items) { const fullPath = path.join(currentDir, item); try { const stat = fs.statSync(fullPath); if (stat.isDirectory() && !item.startsWith('.') && !['node_modules', 'vendor', 'cache', 'storage'].includes(item)) { traverse(fullPath); } else if (stat.isFile() && path.extname(item) === extension) { files.push(fullPath); } } catch (error) { // Skip files we can't access } } } traverse(dir); return files; } async generateReport(results, outputDir) { const report = { timestamp: new Date().toISOString(), framework: results.framework, totalTranslations: results.translations.length, translations: results.translations, filesProcessed: results.files, patterns: results.patterns, recommendations: this.getRecommendations(results) }; const reportPath = path.join(outputDir, 'i18ntk-php-report.json'); SecurityUtils.safeWriteFileSync(reportPath, JSON.stringify(report, null, 2), outputDir); return reportPath; } getRecommendations(results) { const recommendations = []; if (results.translations.length === 0) { recommendations.push('No translations found. Check your PHP i18n patterns'); } if (results.files === 0) { recommendations.push('No PHP files found in source directory'); } if (results.framework === 'generic') { recommendations.push('Consider using Laravel or Symfony for better i18n support'); } if (results.framework === 'wordpress') { recommendations.push('Use WordPress i18n functions (__(), _e(), _n()) for consistency'); } return recommendations; } } // CLI Implementation program .name('i18ntk-php') .description('PHP language i18n management tool') .version('1.10.1'); program .command('init') .description('Initialize PHP i18n structure') .option('-s, --source-dir <dir>', 'Source directory', './') .option('-o, --output-dir <dir>', 'Output directory', './i18ntk-reports') .option('-l, --languages <langs>', 'Languages (comma-separated)', 'en') .option('-f, --framework <framework>', 'Framework (laravel|symfony|wordpress|standard)', 'standard-php') .action(async (options) => { try { const manager = new PhpI18nManager(); const languages = options.languages.split(','); const localesDir = await manager.createLocaleStructure(options.outputDir, languages, options.framework); console.log(`✅ PHP i18n structure initialized in: ${localesDir}`); console.log(`📊 Languages: ${languages.join(', ')}`); console.log(`🎯 Framework: ${options.framework}`); } catch (error) { console.error('❌ Error initializing PHP i18n:', error.message); process.exit(1); } }); program .command('analyze') .description('Analyze PHP i18n usage') .option('-s, --source-dir <dir>', 'Source directory', './') .option('-o, --output-dir <dir>', 'Output directory', './i18ntk-reports') .option('--dry-run', 'Preview without making changes') .action(async (options) => { try { SecurityUtils.validatePath(options.sourceDir); const manager = new PhpI18nManager(); const results = await manager.extractTranslations(options.sourceDir); console.log(`🔍 Framework detected: ${results.framework}`); console.log(`📊 Files processed: ${results.files}`); console.log(`📝 Translations found: ${results.translations.length}`); if (!options.dryRun) { const reportPath = await manager.generateReport(results, options.outputDir); console.log(`📄 Report saved: ${reportPath}`); } } catch (error) { console.error('❌ Error analyzing PHP i18n:', error.message); process.exit(1); } }); program .command('extract') .description('Extract PHP translations') .option('-s, --source-dir <dir>', 'Source directory', './') .option('-o, --output-dir <dir>', 'Output directory', './locales') .option('-l, --languages <langs>', 'Languages (comma-separated)', 'en') .option('-f, --framework <framework>', 'Framework (laravel|symfony|wordpress|standard)', 'standard-php') .action(async (options) => { try { SecurityUtils.validatePath(options.sourceDir); const manager = new PhpI18nManager(); const results = await manager.extractTranslations(options.sourceDir); await manager.createLocaleStructure(options.outputDir, options.languages.split(','), options.framework); console.log(`✅ Extracted ${results.translations.length} translations`); console.log(`📁 Locale structure created in: ${options.outputDir}`); console.log(`🎯 Framework: ${options.framework}`); } catch (error) { console.error('❌ Error extracting PHP translations:', error.message); process.exit(1); } }); // Handle uncaught errors process.on('uncaughtException', (error) => { console.error('❌ Uncaught exception:', error.message); process.exit(1); }); process.on('unhandledRejection', (reason) => { console.error('❌ Unhandled rejection:', reason); process.exit(1); }); // Export for programmatic use module.exports = { PhpI18nManager }; if (require.main === module) { program.parse(); }