UNPKG

zlocalz

Version:

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

466 lines • 21.1 kB
"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 }); exports.SetupWizard = void 0; const inquirer_1 = __importDefault(require("inquirer")); const fs = __importStar(require("fs/promises")); const path = __importStar(require("path")); const chalk_1 = __importDefault(require("chalk")); class SetupWizard { cwd; constructor() { this.cwd = process.cwd(); } async run() { console.log(chalk_1.default.blue('šŸš€ Welcome to ZLocalz Setup Wizard!')); console.log(chalk_1.default.gray('Let\'s configure ZLocalz for your project.\n')); const detectedFiles = await this.detectExistingFiles(); const config = await this.collectBasicConfig(detectedFiles); const advancedConfig = await this.collectAdvancedConfig(config); const translationConfig = await this.collectTranslationConfig(); const finalConfig = { ...config, ...advancedConfig, ...translationConfig }; await this.createConfigFile(finalConfig); if (translationConfig.geminiApiKey) { await this.createEnvFile(translationConfig.geminiApiKey); } this.showSetupComplete(finalConfig); return finalConfig; } async detectExistingFiles() { const commonPaths = [ 'lib/l10n', 'assets/l10n', 'assets/locales', 'locales', 'i18n', 'translations' ]; const detectedFiles = []; const formats = new Set(); const detectedLocales = new Set(); let suggestedPath; let suggestedFormat; for (const searchPath of commonPaths) { const fullPath = path.join(this.cwd, searchPath); try { await fs.access(fullPath); const arbFiles = await this.findFiles(fullPath, '**/*.arb'); const jsonFiles = await this.findFiles(fullPath, '**/*.json'); const yamlFiles = await this.findFiles(fullPath, '**/*.{yml,yaml}'); const csvFiles = await this.findFiles(fullPath, '**/*.{csv,tsv}'); if (arbFiles.length > 0) { formats.add('arb'); detectedFiles.push(...arbFiles); for (const file of arbFiles) { const locale = this.extractLocaleFromFilename(file, 'arb'); if (locale) detectedLocales.add(locale); } if (!suggestedPath) { suggestedPath = searchPath; suggestedFormat = 'arb'; } } if (jsonFiles.length > 0) { formats.add('json'); detectedFiles.push(...jsonFiles); for (const file of jsonFiles) { const locale = this.extractLocaleFromFilename(file, 'json'); if (locale) detectedLocales.add(locale); } if (!suggestedPath) { suggestedPath = searchPath; suggestedFormat = 'json'; } } if (yamlFiles.length > 0) { formats.add('yaml'); detectedFiles.push(...yamlFiles); for (const file of yamlFiles) { const locale = this.extractLocaleFromFilename(file, 'yaml'); if (locale) detectedLocales.add(locale); } if (!suggestedPath) { suggestedPath = searchPath; suggestedFormat = 'yaml'; } } if (csvFiles.length > 0) { formats.add('csv'); detectedFiles.push(...csvFiles); if (!suggestedPath) { suggestedPath = searchPath; suggestedFormat = 'csv'; } } } catch (error) { } } const localesArray = Array.from(detectedLocales).sort(); const suggestedSourceLocale = this.determineMostLikelySource(localesArray); const suggestedTargetLocales = localesArray.filter(l => l !== suggestedSourceLocale); return { formats: Array.from(formats), paths: [...new Set(detectedFiles.map(f => path.dirname(path.relative(this.cwd, f))))], suggestedPath: suggestedPath || 'lib/l10n', suggestedFormat: suggestedFormat || 'arb', detectedLocales: localesArray, suggestedSourceLocale: suggestedSourceLocale || 'en', suggestedTargetLocales: suggestedTargetLocales }; } async findFiles(basePath, pattern) { try { const glob = (await Promise.resolve().then(() => __importStar(require('fast-glob')))).default; return await glob(pattern, { cwd: basePath, absolute: true, ignore: ['**/node_modules/**', '**/build/**'] }); } catch (error) { return []; } } extractLocaleFromFilename(filePath, format) { const filename = path.basename(filePath); switch (format) { case 'arb': const arbMatch = filename.match(/[_-]([a-z]{2,3}([_-][A-Z]{2})?)\.arb$/i); return arbMatch ? arbMatch[1].toLowerCase().replace('-', '_') : null; case 'json': const jsonMatch = filename.match(/^([a-z]{2,3}([_-][A-Z]{2})?)\.json$/i); return jsonMatch ? jsonMatch[1].toLowerCase().replace('-', '_') : null; case 'yaml': const yamlMatch = filename.match(/^([a-z]{2,3}([_-][A-Z]{2})?)\.(yml|yaml)$/i); return yamlMatch ? yamlMatch[1].toLowerCase().replace('-', '_') : null; default: return null; } } determineMostLikelySource(locales) { const commonSourceLocales = ['en', 'en_US', 'en_GB']; for (const sourceCandidate of commonSourceLocales) { if (locales.includes(sourceCandidate)) { return sourceCandidate; } } return locales.length > 0 ? locales[0] : null; } async collectBasicConfig(detectedFiles) { if (detectedFiles.formats.length > 0) { console.log(chalk_1.default.green(`āœ… Found existing localization files!`)); console.log(chalk_1.default.gray(` Formats: ${detectedFiles.formats.join(', ')}`)); console.log(chalk_1.default.gray(` Paths: ${detectedFiles.paths.join(', ')}`)); if (detectedFiles.detectedLocales && detectedFiles.detectedLocales.length > 0) { console.log(chalk_1.default.gray(` Detected locales: ${detectedFiles.detectedLocales.join(', ')}\n`)); } else { console.log(''); } } const questions = [ { type: 'input', name: 'flutterLocalesPath', message: 'Path to your localization files:', default: detectedFiles.suggestedPath, validate: async (input) => { const fullPath = path.join(this.cwd, input); try { await fs.access(fullPath); return true; } catch { return `Directory "${input}" does not exist. Should I create it? (y/n)`; } } }, { type: 'list', name: 'fileFormat', message: 'File format:', choices: [ { name: 'Auto-detect (recommended)', value: 'auto' }, { name: 'ARB (Flutter Application Resource Bundle)', value: 'arb' }, { name: 'JSON (JavaScript Object Notation)', value: 'json' }, { name: 'YAML (YAML Ain\'t Markup Language)', value: 'yaml' }, { name: 'CSV (Comma-Separated Values)', value: 'csv' }, { name: 'TSV (Tab-Separated Values)', value: 'tsv' } ], default: detectedFiles.suggestedFormat === 'arb' ? 1 : detectedFiles.suggestedFormat === 'json' ? 2 : detectedFiles.suggestedFormat === 'yaml' ? 3 : detectedFiles.suggestedFormat === 'csv' ? 4 : 0 }, { type: 'input', name: 'sourceLocale', message: 'Source locale (e.g., en, en_US):', default: detectedFiles.suggestedSourceLocale || 'en', validate: (input) => { const inputStr = String(input || ''); if (!inputStr.trim()) return 'Source locale is required'; if (!/^[a-z]{2,3}([_-][A-Z]{2})?$/.test(inputStr)) { return 'Please enter a valid locale code (e.g., en, es, en_US, pt_BR)'; } return true; } }, { type: 'input', name: 'targetLocales', message: 'Target locales (comma-separated, e.g., es,fr,de):', default: detectedFiles.suggestedTargetLocales && detectedFiles.suggestedTargetLocales.length > 0 ? detectedFiles.suggestedTargetLocales.join(',') : (detectedFiles.formats.length > 0 ? 'es,fr' : ''), validate: (input) => { const inputStr = String(input || ''); if (!inputStr.trim()) return 'At least one target locale is required'; const locales = inputStr.split(',').map(l => l.trim()); for (const locale of locales) { if (!/^[a-z]{2,3}([_-][A-Z]{2})?$/.test(locale)) { return `Invalid locale: ${locale}. Use format like: es,fr,de or en_US,pt_BR`; } } return true; }, filter: (input) => String(input || '').split(',').map(l => l.trim()).filter(Boolean) } ]; return await inquirer_1.default.prompt(questions); } async collectAdvancedConfig(basicConfig) { console.log(chalk_1.default.blue('\nāš™ļø Advanced Configuration')); const questions = [ { type: 'confirm', name: 'doAutoFix', message: 'Enable automatic fixes for common issues?', default: true }, { type: 'list', name: 'preferOrder', message: 'Key ordering preference:', choices: [ { name: 'Mirror source locale order', value: 'mirror-source' }, { name: 'Alphabetical order', value: 'alphabetical' } ], default: 'mirror-source' }, { type: 'confirm', name: 'autoUpdate', message: 'Enable automatic updates?', default: true } ]; if (basicConfig.fileFormat === 'csv' || basicConfig.fileFormat === 'tsv') { questions.push({ type: 'input', name: 'csvKeyColumn', message: 'CSV key column name:', default: 'key' }, { type: 'input', name: 'csvDelimiter', message: 'CSV delimiter:', default: basicConfig.fileFormat === 'tsv' ? '\\t' : ',', validate: (input) => String(input || '').length > 0 ? true : 'Delimiter cannot be empty' }); } const answers = await inquirer_1.default.prompt(questions); if (answers.csvKeyColumn || answers.csvDelimiter) { answers.csvOptions = { keyColumn: answers.csvKeyColumn, delimiter: answers.csvDelimiter === '\\t' ? '\t' : answers.csvDelimiter, valueColumns: basicConfig.targetLocales.reduce((acc, locale) => { acc[locale] = locale; return acc; }, { [basicConfig.sourceLocale]: basicConfig.sourceLocale }) }; delete answers.csvKeyColumn; delete answers.csvDelimiter; } return answers; } async collectTranslationConfig() { console.log(chalk_1.default.blue('\nšŸ¤– AI Translation Setup (Optional)')); console.log(chalk_1.default.gray('Enable AI-powered translation of missing keys using Google Gemini.\n')); const questions = [ { type: 'confirm', name: 'translateMissing', message: 'Enable AI translation for missing keys?', default: false }, { type: 'input', name: 'geminiApiKey', message: 'Google Gemini API key (will be saved to .env):', when: (answers) => answers.translateMissing, validate: (input) => { const inputStr = String(input || ''); if (!inputStr.trim()) return 'API key is required for translation'; if (inputStr.length < 20) return 'API key seems too short'; return true; } }, { type: 'list', name: 'geminiModel', message: 'Gemini model:', when: (answers) => answers.translateMissing, choices: [ { name: 'gemini-2.5-pro (recommended)', value: 'gemini-2.5-pro' }, { name: 'gemini-1.5-flash (faster)', value: 'gemini-1.5-flash' }, { name: 'gemini-pro', value: 'gemini-pro' } ], default: 'gemini-2.5-pro' }, { type: 'input', name: 'styleGuidelines', message: 'Style guidelines for translation (optional):', when: (answers) => answers.translateMissing, default: 'Concise UI text, sentence case, no trailing punctuation for buttons' } ]; return await inquirer_1.default.prompt(questions); } async createConfigFile(config) { const configFile = path.join(this.cwd, 'zlocalz.config.json'); const { geminiApiKey, ...configWithoutKey } = config; const configContent = JSON.stringify(configWithoutKey, null, 2); try { await fs.writeFile(configFile, configContent); console.log(chalk_1.default.green(`\nāœ… Created configuration file: ${configFile}`)); } catch (error) { console.log(chalk_1.default.red(`\nāŒ Failed to create config file: ${error}`)); throw error; } } async createEnvFile(apiKey) { const envFile = path.join(this.cwd, '.env'); const envContent = `# ZLocalz Configuration\nGEMINI_API_KEY=${apiKey}\n`; try { let existingContent = ''; try { existingContent = await fs.readFile(envFile, 'utf-8'); } catch (error) { } if (existingContent && !existingContent.includes('GEMINI_API_KEY')) { await fs.writeFile(envFile, existingContent + '\n' + envContent); console.log(chalk_1.default.green(`āœ… Added GEMINI_API_KEY to existing .env file`)); } else if (!existingContent) { await fs.writeFile(envFile, envContent); console.log(chalk_1.default.green(`āœ… Created .env file with API key`)); } else { console.log(chalk_1.default.yellow(`āš ļø GEMINI_API_KEY already exists in .env`)); } await this.addToGitignore('.env'); } catch (error) { console.log(chalk_1.default.red(`āŒ Failed to create .env file: ${error}`)); throw error; } } async addToGitignore(entry) { const gitignorePath = path.join(this.cwd, '.gitignore'); try { let gitignoreContent = ''; try { gitignoreContent = await fs.readFile(gitignorePath, 'utf-8'); } catch (error) { } if (!gitignoreContent.includes(entry)) { const newContent = gitignoreContent ? gitignoreContent + '\n' + entry + '\n' : entry + '\n'; await fs.writeFile(gitignorePath, newContent); console.log(chalk_1.default.green(`āœ… Added ${entry} to .gitignore`)); } } catch (error) { console.log(chalk_1.default.yellow(`āš ļø Could not update .gitignore: ${error}`)); } } showSetupComplete(config) { console.log(chalk_1.default.green('\nšŸŽ‰ Setup Complete!')); console.log(chalk_1.default.gray('─'.repeat(50))); console.log(chalk_1.default.blue('\nšŸ“‹ Configuration Summary:')); console.log(chalk_1.default.gray(` Path: ${config.flutterLocalesPath}`)); console.log(chalk_1.default.gray(` Format: ${config.fileFormat}`)); console.log(chalk_1.default.gray(` Source: ${config.sourceLocale}`)); console.log(chalk_1.default.gray(` Targets: ${config.targetLocales.join(', ')}`)); console.log(chalk_1.default.gray(` Auto-fix: ${config.doAutoFix ? 'enabled' : 'disabled'}`)); console.log(chalk_1.default.gray(` Auto-update: ${config.autoUpdate ? 'enabled' : 'disabled'}`)); console.log(chalk_1.default.gray(` Translation: ${config.translateMissing ? 'enabled' : 'disabled'}`)); console.log(chalk_1.default.blue('\nšŸš€ Next Steps:')); console.log(chalk_1.default.white(' 1. ') + chalk_1.default.gray('ZLocalz will now launch the TUI interface')); console.log(chalk_1.default.white(' 2. ') + chalk_1.default.gray('Use the interface to validate and fix your localization files')); console.log(chalk_1.default.white(' 3. ') + chalk_1.default.gray('Press ? in the TUI for help and keyboard shortcuts')); if (config.translateMissing) { console.log(chalk_1.default.white(' 4. ') + chalk_1.default.gray('Use the translate feature (t key) for missing translations')); } console.log(chalk_1.default.blue('\nšŸ’” Pro Tips:')); console.log(chalk_1.default.gray(' • Run "zlocalz scan" anytime to launch the TUI')); console.log(chalk_1.default.gray(' • Use "zlocalz --help" to see all available commands')); console.log(chalk_1.default.gray(' • Edit zlocalz.config.json to modify settings')); console.log(chalk_1.default.green('\n Starting ZLocalz TUI in 3 seconds...')); } } exports.SetupWizard = SetupWizard; //# sourceMappingURL=setup-wizard.js.map