UNPKG

zlocalz

Version:

ZLocalz - TUI Locale Guardian for Flutter ARB l10n/i18n validation and translation with AI-powered fixes

438 lines • 18 kB
#!/usr/bin/env node "use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const commander_1 = require("commander"); const fs = __importStar(require("fs/promises")); const path = __importStar(require("path")); const chalk_1 = __importDefault(require("chalk")); const ora_1 = __importDefault(require("ora")); const child_process_1 = require("child_process"); const universal_parser_1 = require("./core/universal-parser"); const validator_1 = require("./core/validator"); const fixer_1 = require("./core/fixer"); const translator_1 = require("./core/translator"); const report_generator_1 = require("./core/report-generator"); const app_1 = require("./tui/app"); const updater_1 = require("./utils/updater"); const setup_wizard_1 = require("./utils/setup-wizard"); const zod_1 = require("zod"); const ConfigSchema = zod_1.z.object({ flutterLocalesPath: zod_1.z.string(), sourceLocale: zod_1.z.string(), targetLocales: zod_1.z.array(zod_1.z.string()), fileFormat: zod_1.z.enum(['arb', 'json', 'yaml', 'yml', 'csv', 'tsv', 'auto']).default('auto'), filePattern: zod_1.z.string().optional(), doAutoFix: zod_1.z.boolean().default(false), translateMissing: zod_1.z.boolean().default(false), geminiModel: zod_1.z.string().optional(), geminiApiKey: zod_1.z.string().optional(), styleGuidelines: zod_1.z.string().optional(), domainGlossary: zod_1.z.record(zod_1.z.string()).optional(), doNotTranslate: zod_1.z.array(zod_1.z.string()).optional(), preferOrder: zod_1.z.enum(['mirror-source', 'alphabetical']).optional(), csvOptions: zod_1.z.object({ delimiter: zod_1.z.string().optional(), keyColumn: zod_1.z.string().optional(), valueColumns: zod_1.z.record(zod_1.z.string()).optional() }).optional(), autoUpdate: zod_1.z.boolean().default(true) }); const program = new commander_1.Command(); let updater = new updater_1.ZLocalzUpdater(true); program .name('zlocalz') .description('ZLocalz - Universal TUI Locale Guardian for Flutter l10n/i18n validation and translation') .version('1.0.7') .addHelpText('afterAll', `\n${chalk_1.default.yellow('āš ļø Beta:')} ZLocalz is currently in beta and may have issues.\n` + `If you encounter problems, please open an issue via ${chalk_1.default.cyan('zlocalz issue --new')} or at ${chalk_1.default.cyan('https://github.com/bllfoad/zlocalz/issues')}\n`) .hook('preAction', async (thisCommand) => { await updater.showWelcomeMessage(); if (thisCommand.name() !== 'update') { updater.checkForUpdates().catch(() => { }); } }); program .command('scan') .description('Scan and validate ARB files') .option('-c, --config <path>', 'Path to config file', './zlocalz.config.json') .option('-p, --path <path>', 'Flutter locales path') .option('-s, --source <locale>', 'Source locale') .option('-t, --targets <locales...>', 'Target locales') .option('-f, --format <format>', 'File format (arb, json, yaml, csv, tsv, auto)', 'auto') .option('--pattern <pattern>', 'Custom file pattern') .option('--auto-fix', 'Auto-fix issues') .option('--translate', 'Translate missing keys') .option('--no-tui', 'Disable TUI interface') .action(async (options) => { try { const config = await loadConfig(options); if (!options.tui) { await runCLI(config); } else { await launchTUI(config); } } catch (error) { console.error(chalk_1.default.red('Error:'), error); process.exit(1); } }); program .command('fix') .description('Auto-fix validation issues') .option('-c, --config <path>', 'Path to config file', './zlocalz.config.json') .action(async (options) => { try { const config = await loadConfig({ ...options, autoFix: true }); await runCLI(config); } catch (error) { console.error(chalk_1.default.red('Error:'), error); process.exit(1); } }); program .command('translate') .description('Translate missing keys') .option('-c, --config <path>', 'Path to config file', './zlocalz.config.json') .option('-k, --key <key>', 'Gemini API key') .action(async (options) => { try { const config = await loadConfig({ ...options, translateMissing: true, geminiApiKey: options.key || process.env.GEMINI_API_KEY }); await runCLI(config); } catch (error) { console.error(chalk_1.default.red('Error:'), error); process.exit(1); } }); program .command('update') .description('Update ZLocalz to the latest version') .option('--check', 'Only check for updates, don\'t install') .action(async (options) => { try { if (options.check) { await updater.checkForUpdates(); await updater.showReleaseNotes(); } else { const success = await updater.performAutoUpdate(false); process.exit(success ? 0 : 1); } } catch (error) { console.error(chalk_1.default.red('Update failed:'), error); process.exit(1); } }); program .command('issue') .description('Open GitHub issues page or create a new issue') .option('--new', 'Open the new issue form') .action((options) => { const base = 'https://github.com/bllfoad/zlocalz/issues'; const url = options.new ? `${base}/new/choose` : base; openUrlInBrowser(url); }); if (process.argv.length === 2) { checkForConfigAndRun().catch((error) => { console.error(chalk_1.default.red('Error:'), error); process.exit(1); }); } else { program.parse(); } async function checkForConfigAndRun() { try { await updater.checkForUpdates(); await updater.showWelcomeMessage(); if (updater) { updater.performAutoUpdate(true).catch(() => { }); } } catch (error) { } const configPath = path.resolve('./zlocalz.config.json'); try { await fs.access(configPath); const config = await loadConfig({ config: configPath }); await launchTUI(config); } catch (error) { await runSetupWizard(); } } async function runSetupWizard() { const wizard = new setup_wizard_1.SetupWizard(); console.log(chalk_1.default.blue('\nšŸ”§ ZLocalz Setup')); console.log(chalk_1.default.gray('No configuration found. Let\'s set up ZLocalz for your project.')); try { const config = await wizard.run(); await new Promise(resolve => setTimeout(resolve, 3000)); await launchTUI(config); } catch (error) { if (error instanceof Error && error.message.includes('User force closed')) { console.log(chalk_1.default.yellow('\nšŸ‘‹ Setup cancelled. Run `zlocalz` again to continue setup.')); process.exit(0); } throw error; } } async function loadConfig(options) { let config = {}; if (options.config) { try { const configPath = path.resolve(options.config); const configContent = await fs.readFile(configPath, 'utf-8'); config = JSON.parse(configContent); } catch (error) { if (options.config !== './zlocalz.config.json') { throw new Error(`Failed to load config file: ${error}`); } } } if (options.path) config.flutterLocalesPath = options.path; if (options.source) config.sourceLocale = options.source; if (options.targets) config.targetLocales = options.targets; if (options.format) config.fileFormat = options.format; if (options.pattern) config.filePattern = options.pattern; if (options.autoFix !== undefined) config.doAutoFix = options.autoFix; if (options.translateMissing !== undefined) config.translateMissing = options.translateMissing; if (options.geminiApiKey) config.geminiApiKey = options.geminiApiKey; config.geminiApiKey = config.geminiApiKey || process.env.GEMINI_API_KEY; return ConfigSchema.parse(config); } async function launchTUI(config) { updater = new updater_1.ZLocalzUpdater(config.autoUpdate !== false); const spinner = (0, ora_1.default)('Loading locale files...').start(); try { const { sourceFile, targetFiles } = await loadLocaleFiles(config); spinner.succeed('Locale files loaded'); const app = new app_1.LocalzApp(config); await app.initialize(sourceFile, Array.from(targetFiles.values())); const validator = new validator_1.Validator(sourceFile, Array.from(targetFiles.values())); const reports = validator.validate(); const issues = new Map(); for (const [locale, report] of reports) { issues.set(locale, report.issues); } app.updateIssues(issues); setupAppHandlers(app, config, sourceFile, targetFiles); app.render(); } catch (error) { spinner.fail('Failed to load ARB files'); throw error; } } async function runCLI(config) { updater = new updater_1.ZLocalzUpdater(config.autoUpdate !== false); const spinner = (0, ora_1.default)('Scanning locale files...').start(); try { const { sourceFile, targetFiles } = await loadLocaleFiles(config); const formats = [...new Set([sourceFile.format, ...Array.from(targetFiles.values()).map(f => f.format)])]; spinner.succeed(`Found ${targetFiles.size + 1} files (${formats.join(', ')})`); const validator = new validator_1.Validator(sourceFile, Array.from(targetFiles.values())); const reports = validator.validate(); displayValidationResults(reports); const originalFiles = new Map(Array.from(targetFiles.entries()).map(([k, v]) => [k, structuredClone(v)])); const appliedFixes = new Map(); const translations = []; if (config.doAutoFix) { spinner.start('Applying auto-fixes...'); const fixer = new fixer_1.Fixer(config, sourceFile); for (const [locale, file] of targetFiles) { const report = reports.get(locale); if (report && report.issues.length > 0) { const { fixedFile, appliedFixes: fixes } = await fixer.autoFix(file, report.issues); targetFiles.set(locale, fixedFile); appliedFixes.set(locale, fixes); } } spinner.succeed(`Applied ${Array.from(appliedFixes.values()).flat().length} fixes`); } if (config.translateMissing && config.geminiApiKey) { spinner.start('Translating missing keys...'); const translator = new translator_1.Translator(config, config.geminiApiKey); for (const [locale, file] of targetFiles) { const report = reports.get(locale); if (report) { const missingKeys = report.issues .filter(i => i.type === 'missing') .map(i => i.key); if (missingKeys.length > 0) { const localeTranslations = await translator.translateMissing(sourceFile, file, locale, missingKeys); translations.push(...localeTranslations); } } } spinner.succeed(`Translated ${translations.length} keys`); } spinner.start('Generating report...'); const report = await report_generator_1.ReportGenerator.generateReport(config, sourceFile, targetFiles, reports, appliedFixes, translations, originalFiles); await fs.writeFile('zlocalz-report.json', JSON.stringify(report, null, 2)); spinner.succeed('Report saved to zlocalz-report.json'); if (config.doAutoFix || translations.length > 0) { spinner.start('Writing updated files...'); const parser = new universal_parser_1.UniversalParser(config); for (const file of targetFiles.values()) { await parser.writeFile(file); } spinner.succeed('Files updated'); } } catch (error) { spinner.fail('Operation failed'); throw error; } } async function loadLocaleFiles(config) { const parser = new universal_parser_1.UniversalParser(config); const files = await parser.discoverFiles(config.flutterLocalesPath); if (files.length === 0) { const formats = config.fileFormat === 'auto' ? 'supported formats' : config.fileFormat; throw new Error(`No ${formats} files found in ${config.flutterLocalesPath}`); } const allLocaleFiles = []; for (const file of files) { const format = universal_parser_1.UniversalParser.detectFormat(file); if (format === 'csv' || format === 'tsv') { const multiLocaleFiles = await parser.parseAllLocalesFromFile(file); allLocaleFiles.push(...multiLocaleFiles); } else { const singleFile = await parser.parseFile(file); allLocaleFiles.push(singleFile); } } const sourceFile = allLocaleFiles.find(f => f.locale === config.sourceLocale); if (!sourceFile) { const availableLocales = allLocaleFiles.map(f => f.locale).join(', '); throw new Error(`Source locale '${config.sourceLocale}' not found. Available locales: ${availableLocales}`); } const targetFiles = new Map(); for (const file of allLocaleFiles) { if (config.targetLocales.includes(file.locale)) { targetFiles.set(file.locale, file); } } return { sourceFile, targetFiles }; } function displayValidationResults(reports) { console.log('\n' + chalk_1.default.bold('Validation Results:')); for (const [locale, report] of reports) { const { stats } = report; const hasIssues = Object.values(stats).some((v) => typeof v === 'number' && v > 0 && v !== stats.totalKeys); console.log(`\n${chalk_1.default.cyan(locale)}:`); if (!hasIssues) { console.log(chalk_1.default.green(' āœ“ No issues found')); } else { if (stats.missingKeys > 0) { console.log(chalk_1.default.red(` āœ— Missing keys: ${stats.missingKeys}`)); } if (stats.extraKeys > 0) { console.log(chalk_1.default.yellow(` ⚠ Extra keys: ${stats.extraKeys}`)); } if (stats.duplicates > 0) { console.log(chalk_1.default.yellow(` ⚠ Duplicates: ${stats.duplicates}`)); } if (stats.icuErrors > 0) { console.log(chalk_1.default.red(` āœ— ICU errors: ${stats.icuErrors}`)); } if (stats.placeholderMismatches > 0) { console.log(chalk_1.default.red(` āœ— Placeholder mismatches: ${stats.placeholderMismatches}`)); } if (stats.formattingWarnings > 0) { console.log(chalk_1.default.yellow(` ⚠ Formatting warnings: ${stats.formattingWarnings}`)); } } } } function setupAppHandlers(app, config, sourceFile, targetFiles) { app.on('execute-command', async (command) => { switch (command) { case 'save': const parser = new universal_parser_1.UniversalParser(config); for (const file of targetFiles.values()) { await parser.writeFile(file); } break; case 'validate': const validator = new validator_1.Validator(sourceFile, Array.from(targetFiles.values())); const reports = validator.validate(); const issues = new Map(); for (const [locale, report] of reports) { issues.set(locale, report.issues); } app.updateIssues(issues); break; } }); } function openUrlInBrowser(url) { const platform = process.platform; if (platform === 'win32') { (0, child_process_1.spawn)('cmd', ['/c', 'start', '', url], { detached: true, stdio: 'ignore' }); } else if (platform === 'darwin') { (0, child_process_1.spawn)('open', [url], { detached: true, stdio: 'ignore' }); } else { (0, child_process_1.spawn)('xdg-open', [url], { detached: true, stdio: 'ignore' }); } } //# sourceMappingURL=cli.js.map