UNPKG

react-native-i18n-auto

Version:

Auto i18n tool for React Native with full TypeScript support

335 lines (286 loc) 11.5 kB
#!/usr/bin/env node const { Command } = require('commander'); const path = require('path'); const babel = require('@babel/core'); const fs = require('fs'); const glob = require('glob'); const program = new Command(); // Define supported language codes const SUPPORTED_LANGUAGE_CODES = { ar: 'Arabic', zh: 'Chinese', en: 'English', fr: 'French', de: 'German', hi: 'Hindi', id: 'Indonesian', it: 'Italian', ja: 'Japanese', ko: 'Korean', ms: 'Malay', pt: 'Portuguese', ru: 'Russian', es: 'Spanish', th: 'Thai', vi: 'Vietnamese', }; // Function to create default configuration function createDefaultConfig() { const configPath = path.join(process.cwd(), 'i18n-auto.config.json'); // Skip if config file already exists if (fs.existsSync(configPath)) { return; } const defaultConfig = { defaultLanguage: 'ko', supportedLanguages: ['ko', 'en', 'ja'], localeDir: 'locales', sourceDir: ['app', 'src'], ignore: [ 'node_modules/**', 'dist/**', 'build/**', '.git/**', 'tests/**', '**/*.test.*', '**/*.spec.*', ], }; try { // Create config file fs.writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2), 'utf8'); console.log('✅ Created default config file: i18n-auto.config.json'); // Create locales directory const localeDir = path.join(process.cwd(), defaultConfig.localeDir); if (!fs.existsSync(localeDir)) { fs.mkdirSync(localeDir, { recursive: true }); console.log(`✅ Created ${defaultConfig.localeDir} directory`); // Create default language files defaultConfig.supportedLanguages.forEach((lang) => { const langFile = path.join(localeDir, `${lang}.json`); fs.writeFileSync(langFile, '{}', 'utf8'); console.log(`✅ Created empty translation file: ${lang}.json`); }); } } catch (error) { console.error('❌ Failed to create config file:', error); process.exit(1); } } // Function to convert text to key function parseToKey(text) { return ( text .trim() // Convert camelCase to underscore (e.g., userName -> user_name) .replace(/([a-z])([A-Z])/g, '$1_$2') // Convert spaces to underscores .replace(/\s+/g, '_') // Remove special characters (keep only alphanumeric and underscore) .replace(/[^A-Za-z0-9_]/g, '') // Convert to uppercase .toUpperCase() ); } // Function to load configuration function loadConfig() { const configPath = path.join(process.cwd(), 'i18n-auto.config.json'); // Create default config if config file doesn't exist if (!fs.existsSync(configPath)) { createDefaultConfig(); } try { const config = JSON.parse(fs.readFileSync(configPath, 'utf8')); // Handle sourceDir as array if (typeof config.sourceDir === 'string') { config.sourceDir = [config.sourceDir]; } else if (!config.sourceDir) { config.sourceDir = ['.']; } else if (!Array.isArray(config.sourceDir)) { console.error('❌ sourceDir must be a string or an array of strings'); process.exit(1); } config.ignore = config.ignore || []; // Default ignore patterns const defaultIgnore = ['node_modules/**', 'dist/**', 'build/**', '.git/**']; config.ignore = [...defaultIgnore, ...config.ignore]; // Validate language codes if (!config.defaultLanguage || !SUPPORTED_LANGUAGE_CODES[config.defaultLanguage]) { console.error(`❌ Invalid default language code: ${config.defaultLanguage}`); console.error('Please use one of the supported language codes.'); process.exit(1); } if ( !config.supportedLanguages || !Array.isArray(config.supportedLanguages) || config.supportedLanguages.length === 0 ) { console.error('❌ supportedLanguages must be a non-empty array'); process.exit(1); } const invalidLanguages = config.supportedLanguages.filter( (lang) => !SUPPORTED_LANGUAGE_CODES[lang], ); if (invalidLanguages.length > 0) { console.error(`❌ Invalid language codes found: ${invalidLanguages.join(', ')}`); console.error('Please use the supported language codes:'); Object.entries(SUPPORTED_LANGUAGE_CODES).forEach(([code, name]) => { console.error(` ${code}: ${name}`); }); process.exit(1); } if (!config.supportedLanguages.includes(config.defaultLanguage)) { console.error( `❌ Default language '${config.defaultLanguage}' must be included in supportedLanguages`, ); process.exit(1); } return config; } catch (error) { console.error('❌ Failed to parse configuration file:', error.message); process.exit(1); } } // Function to manage translations function manageTranslations(config, extractedTexts) { console.log('\n📝 Managing translations...'); const localeDir = path.join(process.cwd(), config.localeDir || 'locales'); console.log(`📁 Locale directory: ${localeDir}`); // Create locale directory if (!fs.existsSync(localeDir)) { console.log('📂 Creating locale directory...'); fs.mkdirSync(localeDir, { recursive: true }); } // Convert texts to key-value pairs const translations = {}; extractedTexts.forEach((text) => { const key = parseToKey(text); translations[key] = text; // Keep original text as value }); console.log(`\n🔍 Found ${Object.keys(translations).length} texts to translate:`); Object.entries(translations).forEach(([key, text]) => { console.log(` - ${key}: "${text}"`); }); // Process translation files for each language config.supportedLanguages.forEach((lang) => { const filePath = path.join(localeDir, `${lang}.json`); console.log(`\n💾 Processing ${lang}.json`); let existing = {}; // Load existing translation file if it exists if (fs.existsSync(filePath)) { console.log(` 📖 Loading existing translations...`); existing = JSON.parse(fs.readFileSync(filePath, 'utf8')); } // Add new texts let newKeysCount = 0; Object.entries(translations).forEach(([key, text]) => { if (!existing[key]) { // Keep original text for default language, empty string for others existing[key] = lang === config.defaultLanguage ? text : ''; newKeysCount++; } }); // Save in sorted order const sorted = {}; Object.keys(existing) .sort() .forEach((key) => { sorted[key] = existing[key]; }); fs.writeFileSync(filePath, JSON.stringify(sorted, null, 2)); console.log(` ✅ Added ${newKeysCount} new keys`); console.log(` 📝 Total ${Object.keys(sorted).length} translations`); }); } program .version('1.0.0') .description('React Native Automatic i18n Transformation CLI') .command('run') .description('Transform JSX text components to i18n.t() calls') .action(() => { console.log('🚀 Starting i18n transformation...'); try { // Load configuration const config = loadConfig(); console.log('✅ Configuration loaded'); // Set to store extracted texts const extractedTexts = new Set(); // Get the plugin path const pluginPath = path.resolve( __dirname, '../dist/babel-plugin/babel-plugin-i18n-auto.js', ); if (!fs.existsSync(pluginPath)) { console.error('❌ Babel plugin not found at:', pluginPath); process.exit(1); } // Get all files with the specified extensions const sourceDirectories = config.sourceDir.map((dir) => path.resolve(process.cwd(), dir), ); console.log('\n📂 Searching in:'); sourceDirectories.forEach((dir) => { console.log(` - ${path.relative(process.cwd(), dir)}`); }); console.log('🚫 Ignored patterns:', config.ignore.join(', ')); const files = sourceDirectories.reduce((allFiles, sourceDir) => { const dirFiles = glob.sync('**/*.{js,jsx,ts,tsx}', { cwd: sourceDir, ignore: config.ignore, absolute: true, }); return [...allFiles, ...dirFiles]; }, []); if (files.length === 0) { console.log('⚠️ No files found to transform'); return; } console.log(`\n📝 Found ${files.length} files to transform:`); files.forEach((file) => { console.log(` - ${path.relative(process.cwd(), file)}`); }); files.forEach((file) => { // File is already absolute path, no need to add process.cwd() const source = fs.readFileSync(file, 'utf8'); // Transform the file with minimal configuration const result = babel.transformSync(source, { filename: file, // Use absolute path here plugins: [[pluginPath, { extractedTexts }]], // Pass extractedTexts to plugin parserOpts: { plugins: ['jsx', 'typescript'], }, configFile: false, babelrc: false, retainLines: true, compact: false, generatorOpts: { retainLines: true, compact: false, minified: false, comments: true, jsescOption: { minimal: true, }, }, }); if (result) { // Create backup of original file fs.writeFileSync(`${file}.backup`, source); // Overwrite the original file with transformed code fs.writeFileSync(file, result.code); console.log(`✅ Transformed: ${file}`); } }); // Create/update translation files with extracted texts manageTranslations(config, Array.from(extractedTexts)); console.log('✨ All files transformed successfully!'); console.log('💾 Original files backed up with .backup extension'); console.log( `📦 Translation files updated in ${config.localeDir || 'locales'} directory`, ); } catch (error) { console.error('❌ Error during transformation:', error); process.exit(1); } }); program.parse(process.argv);