zlocalz
Version:
ZLocalz - TUI Locale Guardian for Flutter ARB l10n/i18n validation and translation with AI-powered fixes
438 lines ⢠18 kB
JavaScript
;
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