react-native-i18n-auto
Version:
Auto i18n tool for React Native with full TypeScript support
335 lines (286 loc) • 11.5 kB
JavaScript
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);