zlocalz
Version:
ZLocalz - TUI Locale Guardian for Flutter ARB l10n/i18n validation and translation with AI-powered fixes
466 lines ⢠21.1 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 });
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