i18ntk
Version:
i18n Tool Kit - Zero-dependency internationalization toolkit for setup, scanning, analysis, validation, auto translation, fixing, reporting, and runtime translation loading.
586 lines (486 loc) • 20.4 kB
JavaScript
/**
* Interactive Locale Optimizer Script
*
* Provides an enhanced interactive experience for selecting UI locales
* during package initialization. Integrates with i18ntk init process.
*
* Usage:
* node utils/locale-optimizer.js --interactive
* node utils/locale-optimizer.js --list
* node utils/locale-optimizer.js --keep en,es,de
* node utils/locale-optimizer.js --restore
*/
const fs = require('fs');
const path = require('path');
const SecurityUtils = require('./security');
const cliHelper = require('./cli-helper');
const JsonOutput = require('./json-output');
const { getGlobalReadline } = require('./cli');
class LocaleOptimizer {
constructor() {
this.uiLocalesDir = path.join(__dirname, '..', 'resources', 'i18n', 'ui-locales');
this.allLocales = ['en', 'de', 'es', 'fr', 'it', 'pt', 'nl', 'pl', 'sv', 'uk', 'cs', 'tr', 'ru', 'ja', 'ko', 'zh', 'ar', 'hi', 'th', 'vi', 'he', 'el', 'hu'];
this.backupDir = path.join(__dirname, '..', 'backups', 'locales');
this.rl = getGlobalReadline();
}
/**
* List all available locales and their sizes with enhanced UI
*/
listLocales(options = {}) {
const { json = false, indent = 2, sortKeys = false } = options;
if (json) {
const jsonOutput = new JsonOutput('locale-optimizer', '1.10.1');
const available = this.getAvailableLocales();
let totalSize = 0;
const locales = available.map(locale => {
const filePath = path.join(this.uiLocalesDir, `${locale}.json`);
const stats = fs.statSync(filePath);
const sizeKB = parseFloat((stats.size / 1024).toFixed(1));
totalSize += stats.size;
return {
locale,
name: this.getDisplayName(locale),
size: sizeKB,
status: locale === 'en' ? 'required' : 'optional',
path: filePath
};
});
const missing = this.allLocales.filter(l => !available.includes(l));
jsonOutput.setStatus('ok', 'Locale analysis completed');
jsonOutput.setStats({
totalLocales: available.length,
totalSize: parseFloat((totalSize / 1024).toFixed(1)),
potentialSavings: parseFloat((totalSize/1024 - this.getLocaleSize('en')).toFixed(1)),
missingLocales: missing.length
});
jsonOutput.addMetadata({
locales,
missing: missing.map(locale => ({
locale,
name: this.getDisplayName(locale)
}))
});
console.log(JSON.stringify(jsonOutput.output(), null, indent));
return;
}
console.log('\n📊 UI Locale Package Analysis');
console.log('â•'.repeat(40));
const available = this.getAvailableLocales();
let totalSize = 0;
if (available.length === 0) {
console.log(' ⌠No locale files found');
return;
}
console.log(' Language Size Status');
console.log(' ─────────────────────────');
available.forEach(locale => {
const filePath = path.join(this.uiLocalesDir, `${locale}.json`);
const stats = fs.statSync(filePath);
const sizeKB = (stats.size / 1024).toFixed(1);
totalSize += stats.size;
const displayName = this.getDisplayName(locale);
const status = locale === 'en' ? 'Required' : 'Optional';
console.log(` ${locale.toUpperCase()}${' '.repeat(9-locale.length)}${sizeKB.padStart(5)}KB ${status}`);
});
const missing = this.allLocales.filter(l => !available.includes(l));
if (missing.length > 0) {
console.log(`\n âš ï¸ Missing: ${missing.join(', ').toUpperCase()}`);
}
console.log(`\n📦 Total package size: ${(totalSize / 1024).toFixed(1)}KB`);
console.log(`💡 Potential savings: ${(totalSize/1024 - this.getLocaleSize('en')).toFixed(1)}KB (English only)`);
console.log(` Run --interactive to optimize now`);
}
/**
* Get display name for locale
*/
getDisplayName(locale) {
const names = {
'en': 'English',
'es': 'Spanish',
'fr': 'French',
'de': 'German',
'ja': 'Japanese',
'ru': 'Russian',
'zh': 'Chinese'
};
return names[locale] || locale.toUpperCase();
}
/**
* Keep only specified locales, backup others with warnings
*/
keepLocales(localesToKeep) {
const keepList = Array.isArray(localesToKeep) ? localesToKeep : localesToKeep.split(',').map(l => l.trim().toLowerCase());
console.log('🎯 Optimizing package size...');
console.log(` Keeping: ${keepList.join(', ').toUpperCase()}`);
// Create backup directory
if (!SecurityUtils.safeExistsSync(this.backupDir)) {
fs.mkdirSync(this.backupDir, { recursive: true });
}
let removedCount = 0;
let savedSpace = 0;
const removedLocales = [];
this.allLocales.forEach(locale => {
const filePath = path.join(this.uiLocalesDir, `${locale}.json`);
const backupPath = path.join(this.backupDir, `${locale}.json`);
if (!keepList.includes(locale) && SecurityUtils.safeExistsSync(filePath)) {
// Backup the file
fs.copyFileSync(filePath, backupPath);
// Remove from ui-locales
const stats = fs.statSync(filePath);
savedSpace += stats.size;
fs.unlinkSync(filePath);
removedCount++;
removedLocales.push(locale);
console.log(` ✅ Removed ${locale.toUpperCase()} (backed up)`);
}
});
if (removedCount > 0) {
console.log(`\nâš ï¸ WARNING: Removing locales may break UI functionality`);
console.log(` If issues occur, restore with: node utils/locale-optimizer.js --restore`);
console.log(` Or reinstall the package: npm install -g i18ntk`);
// Create warning file
const warningPath = path.join(this.backupDir, 'REMOVED_LOCALES.txt');
SecurityUtils.safeWriteFileSync(warningPath, `Removed locales: ${removedLocales.join(',')}\nRestore with: node utils/locale-optimizer.js --restore`);
}
const savedKB = (savedSpace / 1024).toFixed(1);
console.log(`\n🎉 Optimization complete!`);
console.log(` Removed: ${removedCount} locales`);
console.log(` Saved: ${savedKB}KB`);
console.log(` Backup location: ${this.backupDir}`);
}
/**
* Restore locales from backup
*/
restoreLocales() {
console.log('🔄 Restoring locales from backup...');
if (!SecurityUtils.safeExistsSync(this.backupDir)) {
console.log(' ⌠No backup directory found');
return;
}
const backupFiles = fs.readdirSync(this.backupDir).filter(f => f.endsWith('.json'));
if (backupFiles.length === 0) {
console.log(' ⌠No backup files found');
return;
}
let restoredCount = 0;
backupFiles.forEach(file => {
const backupPath = path.join(this.backupDir, file);
const restorePath = path.join(this.uiLocalesDir, file);
fs.copyFileSync(backupPath, restorePath);
restoredCount++;
console.log(` ✅ Restored ${file.replace('.json', '').toUpperCase()}`);
});
console.log(`\n🎉 Restoration complete!`);
console.log(` Restored: ${restoredCount} locales`);
}
/**
* Dry run mode - show optimization scenarios without making changes
*/
dryRun() {
console.log('🔠LOCALE OPTIMIZER - DRY RUN MODE');
console.log('â•'.repeat(50));
const available = this.getAvailableLocales();
const totalSize = this.getTotalSize();
console.log(`📊 Current state:`);
console.log(` Total locales: ${available.length}`);
console.log(` Total package size: ${totalSize.toFixed(1)}KB`);
console.log(` Individual locale sizes:`);
available.forEach(locale => {
const size = this.getLocaleSize(locale);
const status = locale === 'en' ? 'REQUIRED' : 'Optional';
console.log(` - ${locale.toUpperCase()}: ${size.toFixed(1)}KB (${status})`);
});
console.log('\n🎯 Optimization scenarios:');
// Scenario 1: English only
const enSize = this.getLocaleSize('en');
console.log(` Scenario 1: Keep only English (minimal)`);
console.log(` - Size: ${enSize.toFixed(1)}KB`);
console.log(` - Savings: ${(totalSize - enSize).toFixed(1)}KB (${(((totalSize - enSize) / totalSize) * 100).toFixed(1)}%)`);
// Scenario 2: English + Spanish
const esSize = this.getLocaleSize('es');
const enEsSize = enSize + esSize;
console.log('\n Scenario 2: Keep English + Spanish');
console.log(` - Size: ${enEsSize.toFixed(1)}KB`);
console.log(` - Savings: ${(totalSize - enEsSize).toFixed(1)}KB (${(((totalSize - enEsSize) / totalSize) * 100).toFixed(1)}%)`);
// Scenario 3: English + Spanish + French
const frSize = this.getLocaleSize('fr');
const enEsFrSize = enSize + esSize + frSize;
console.log('\n Scenario 3: Keep English + Spanish + French');
console.log(` - Size: ${enEsFrSize.toFixed(1)}KB`);
console.log(` - Savings: ${(totalSize - enEsFrSize).toFixed(1)}KB (${(((totalSize - enEsFrSize) / totalSize) * 100).toFixed(1)}%)`);
console.log('\n💡 To run actual optimization:');
console.log(' node utils/locale-optimizer.js --interactive');
console.log('\n✅ Dry run complete - no files were modified');
}
/**
* Interactive locale selection with enhanced UI
*/
async interactiveSelect() {
console.log('\n🌠i18ntk Locale Optimizer');
console.log('â•'.repeat(50));
console.log('Select which UI locales to keep (English is always kept)');
console.log('This will reduce package size by removing unused language files\n');
const available = this.getAvailableLocales();
const selections = new Set(['en']); // Always keep English
console.log('📊 Available locales and sizes:');
available.forEach(locale => {
const size = this.getLocaleSize(locale);
const status = locale === 'en' ? '✅ (Required)' : '○';
console.log(` ${status} ${locale.toUpperCase()}: ${size}KB`);
});
console.log('\n💡 Type locale codes separated by commas (e.g., en,es,de)');
console.log(' Press Enter to keep all, or type "cancel" to abort\n');
const answer = await cliHelper.prompt('Select locales to keep: ');
if (answer.toLowerCase() === 'cancel') {
console.log('⌠Operation cancelled');
return false;
}
if (answer.trim() === '') {
console.log('✅ Keeping all locales');
return true;
}
const selected = answer.split(',').map(l => l.trim().toLowerCase());
const valid = selected.filter(l => available.includes(l));
if (valid.length === 0) {
console.log('âš ï¸ No valid locales selected, keeping all');
return true;
}
// Always include English
valid.push('en');
const unique = [...new Set(valid)];
console.log(`\n🎯 Selected: ${unique.join(', ').toUpperCase()}`);
this.showImpact(unique);
const confirm = await cliHelper.prompt('\nProceed? (y/N): ');
if (confirm.toLowerCase() === 'y' || confirm.toLowerCase() === 'yes') {
this.keepLocales(unique.join(','));
console.log('\n🎉 Package optimized successfully!');
console.log('💡 Use --restore to bring back removed locales');
} else {
console.log('⌠Operation cancelled');
}
return true;
}
/**
* Show size impact of selections
*/
showImpact(selectedLocales) {
const allSize = this.getTotalSize();
const selectedSize = selectedLocales.reduce((sum, locale) => {
return sum + this.getLocaleSize(locale);
}, 0);
const saved = allSize - selectedSize;
const percentage = ((saved / allSize) * 100).toFixed(1);
console.log(`\n📈 Impact Analysis:`);
console.log(` Current: ${allSize.toFixed(1)}KB`);
console.log(` New: ${selectedSize.toFixed(1)}KB`);
console.log(` Saved: ${saved.toFixed(1)}KB (${percentage}%)`);
}
/**
* Get available locales
*/
getAvailableLocales() {
return this.allLocales.filter(locale => {
const filePath = path.join(this.uiLocalesDir, `${locale}.json`);
return SecurityUtils.safeExistsSync(filePath);
});
}
/**
* Get locale file size
*/
getLocaleSize(locale) {
const filePath = path.join(this.uiLocalesDir, `${locale}.json`);
if (SecurityUtils.safeExistsSync(filePath)) {
return fs.statSync(filePath).size / 1024;
}
return 0;
}
/**
* Get total package size
*/
getTotalSize() {
return this.getAvailableLocales().reduce((sum, locale) => {
return sum + this.getLocaleSize(locale);
}, 0);
}
/**
* Get current active locales
*/
getActiveLocales() {
const activeLocales = [];
this.allLocales.forEach(locale => {
const filePath = path.join(this.uiLocalesDir, `${locale}.json`);
if (SecurityUtils.safeExistsSync(filePath)) {
activeLocales.push(locale);
}
});
return activeLocales;
}
/**
* Run the optimizer with specified options
*/
async run(options = {}) {
const { interactive = false, dryRun = false, list = false, keep = null, restore = false, json = false, indent = 2, sortKeys = false } = options;
if (restore) {
if (json) {
const jsonOutput = new JsonOutput('locale-optimizer', '1.10.1');
jsonOutput.setStatus('ok', 'Locales restored successfully');
console.log(JSON.stringify(jsonOutput.getOutput(sortKeys), null, indent));
} else {
this.restoreLocales();
}
return;
}
if (list) {
this.listLocales({ json, indent, sortKeys });
return;
}
if (keep) {
if (json) {
const jsonOutput = new JsonOutput('locale-optimizer', '1.10.1');
const keepList = Array.isArray(keep) ? keep : keep.split(',').map(l => l.trim().toLowerCase());
// Calculate impact
const allSize = this.getTotalSize();
const selectedSize = keepList.reduce((sum, locale) => sum + this.getLocaleSize(locale), 0);
const saved = allSize - selectedSize;
jsonOutput.setStatus('ok', 'Locales optimized successfully');
jsonOutput.setStats({
kept: keepList.length,
removed: this.allLocales.length - keepList.length,
originalSize: parseFloat(allSize.toFixed(1)),
newSize: parseFloat(selectedSize.toFixed(1)),
saved: parseFloat(saved.toFixed(1))
});
this.keepLocales(keep);
console.log(JSON.stringify(jsonOutput.getOutput(sortKeys), null, indent));
} else {
this.keepLocales(keep);
}
return;
}
if (dryRun) {
if (json) {
const jsonOutput = new JsonOutput('locale-optimizer', '1.10.1');
const available = this.getAvailableLocales();
const totalSize = this.getTotalSize();
const enSize = this.getLocaleSize('en');
jsonOutput.setStatus('ok', 'Dry run analysis completed');
jsonOutput.setStats({
totalLocales: available.length,
totalSize: parseFloat(totalSize.toFixed(1)),
potentialSavings: parseFloat((totalSize - enSize).toFixed(1))
});
jsonOutput.addMetadata({
scenarios: [
{
name: 'English only',
locales: ['en'],
size: parseFloat(enSize.toFixed(1)),
savings: parseFloat((totalSize - enSize).toFixed(1))
}
]
});
console.log(JSON.stringify(jsonOutput.output(), null, indent));
} else {
this.dryRun();
}
return;
}
if (interactive) {
if (json) {
const jsonOutput = new JsonOutput('locale-optimizer', '1.10.1');
jsonOutput.setStatus('error', 'Interactive mode not supported with JSON output');
console.log(JSON.stringify(jsonOutput.getOutput(sortKeys), null, indent));
return;
}
await this.interactiveSelect();
return;
}
// Default to list mode
this.listLocales({ json, indent, sortKeys });
}
}
// CLI Handler
async function main() {
const optimizer = new LocaleOptimizer();
const args = process.argv.slice(2);
// Parse arguments
const options = {
interactive: args.includes('--interactive'),
list: args.includes('--list'),
restore: args.includes('--restore'),
dryRun: args.includes('--dry-run'),
json: args.includes('--json')
};
// Extract JSON formatting options
const jsonArgs = {
indent: 2,
sortKeys: false,
newline: 'lf'
};
const indentArg = args.find(arg => arg.startsWith('--indent='));
if (indentArg) {
jsonArgs.indent = parseInt(indentArg.split('=')[1]) || 2;
}
if (args.includes('--sort-keys')) {
jsonArgs.sortKeys = true;
}
const newlineArg = args.find(arg => arg.startsWith('--newline='));
if (newlineArg) {
jsonArgs.newline = newlineArg.split('=')[1] || 'lf';
}
// Handle --keep with value extraction
const keepArg = args.find(arg => arg.startsWith('--keep='));
if (keepArg) {
options.keep = keepArg.split('=')[1];
}
// Handle --remove with value extraction (converted to keep)
const removeArg = args.find(arg => arg.startsWith('--remove='));
if (removeArg) {
const localesToRemove = removeArg.split('=')[1].split(',').map(x => x.trim());
const keepLocales = optimizer.allLocales.filter(l => !localesToRemove.includes(l));
options.keep = keepLocales.join(',');
}
// Handle --init
if (args.includes('--init')) {
console.log('\n📦 Package Size Optimization');
console.log('â•'.repeat(30));
optimizer.listLocales();
const answer = await cliHelper.prompt('\nOptimize package size now? (Y/n): ');
if (answer.toLowerCase() === 'n' || answer.toLowerCase() === 'no') {
console.log(' Skipping optimization (you can run later with --interactive)');
cliHelper.close();
return;
} else {
cliHelper.close();
options.interactive = true;
}
}
if (!options.interactive && !options.list && !options.keep && !options.restore && !options.dryRun && !args.includes('--init')) {
console.log('🌠i18ntk Locale Optimizer v2.0');
console.log('â•'.repeat(35));
console.log('Interactive package size optimization for UI locales');
console.log('');
console.log('Usage:');
console.log(' node utils/locale-optimizer.js --interactive 🎯 Interactive selection');
console.log(' node utils/locale-optimizer.js --list 📊 List all locales');
console.log(' node utils/locale-optimizer.js --keep en,es,de âš¡ Quick keep');
console.log(' node utils/locale-optimizer.js --restore 🔄 Restore all locales');
console.log(' node utils/locale-optimizer.js --init 🚀 Called during init');
console.log(' node utils/locale-optimizer.js --dry-run 🔠Simulation mode');
console.log('');
console.log('💡 Example: Keep only English and Spanish');
console.log(' node utils/locale-optimizer.js --keep en,es');
return;
}
await optimizer.run({
...options,
json: jsonArgs,
indent: jsonArgs.indent,
sortKeys: jsonArgs.sortKeys
});
}
if (require.main === module) {
main();
}
module.exports = LocaleOptimizer;