UNPKG

zlocalz

Version:

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

704 lines (694 loc) • 22.8 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; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.LocalzApp = void 0; const blessed = __importStar(require("blessed")); const events_1 = require("events"); class LocalzApp extends events_1.EventEmitter { screen; state; initialized = false; updating = false; header; footer; mainContainer; filesPane; keysPane; detailsPane; filesList; keysList; detailsText; loadingScreen; helpModal; errorModal; constructor(config) { super(); this.state = { config, targetFiles: new Map(), issues: new Map(), selectedKeys: new Set(), unsavedChanges: false, isLoading: true, focusedPane: 'files' }; try { this.initializeScreen(); this.createUI(); this.setupEventHandlers(); this.initialized = true; } catch (error) { this.handleFatalError(error); } } initializeScreen() { this.screen = blessed.screen({ smartCSR: true, fullUnicode: true, title: 'ZLocalz - Universal Locale Guardian', dockBorders: false, warnings: false }); this.screen.on('error', (err) => { this.handleFatalError(err); }); this.screen.on('resize', () => { this.handleResize(); }); process.on('SIGINT', () => this.handleExit()); process.on('SIGTERM', () => this.handleExit()); process.on('uncaughtException', (err) => this.handleFatalError(err)); } createUI() { this.createLayout(); this.createComponents(); this.setupKeyBindings(); this.showLoading(); } createLayout() { this.header = blessed.box({ parent: this.screen, top: 0, left: 0, width: '100%', height: 1, content: ' ZLocalz - Loading...', style: { fg: 'white', bg: 'blue' }, tags: true }); this.mainContainer = blessed.box({ parent: this.screen, top: 1, left: 0, width: '100%', height: '100%-2', style: { bg: 'black' } }); this.footer = blessed.box({ parent: this.screen, bottom: 0, left: 0, width: '100%', height: 1, content: ' Press ? for help | q to quit', style: { fg: 'white', bg: 'blue' }, tags: true }); } createComponents() { this.createMainPanes(); this.createOverlays(); } createMainPanes() { this.filesPane = blessed.box({ parent: this.mainContainer, label: ' šŸ“ Locales ', top: 0, left: 0, width: '25%', height: '100%', border: { type: 'line' }, style: { border: { fg: 'gray' }, label: { fg: 'white', bold: true } }, tags: true }); this.filesList = blessed.list({ parent: this.filesPane, top: 0, left: 0, width: '100%-2', height: '100%-2', keys: true, mouse: true, style: { selected: { bg: 'blue', fg: 'white' }, item: { fg: 'white' } }, scrollbar: { ch: 'ā–ˆ', style: { bg: 'gray' } } }); this.keysPane = blessed.box({ parent: this.mainContainer, label: ' šŸ”‘ Translation Keys ', top: 0, left: '25%', width: '45%', height: '100%', border: { type: 'line' }, style: { border: { fg: 'gray' }, label: { fg: 'white', bold: true } }, tags: true }); this.keysList = blessed.list({ parent: this.keysPane, top: 0, left: 0, width: '100%-2', height: '100%-2', keys: true, mouse: true, style: { selected: { bg: 'green', fg: 'white' }, item: { fg: 'white' } }, scrollbar: { ch: 'ā–ˆ', style: { bg: 'gray' } } }); this.detailsPane = blessed.box({ parent: this.mainContainer, label: ' šŸ” Details & Issues ', top: 0, left: '70%', width: '30%', height: '100%', border: { type: 'line' }, style: { border: { fg: 'gray' }, label: { fg: 'white', bold: true } }, tags: true }); this.detailsText = blessed.text({ parent: this.detailsPane, top: 0, left: 0, width: '100%-2', height: '100%-2', content: 'Select a key to view details', style: { fg: 'white' }, tags: true, wrap: true, scrollable: true, alwaysScroll: true, scrollbar: { ch: 'ā–ˆ', style: { bg: 'gray' } } }); } createOverlays() { this.loadingScreen = blessed.box({ parent: this.screen, top: 'center', left: 'center', width: 50, height: 10, border: { type: 'line' }, style: { border: { fg: 'cyan' }, bg: 'black' }, content: `{center}{bold}šŸš€ ZLocalz Loading...{/bold} {cyan-fg}Initializing locale files...{/cyan-fg} Please wait...{/center}`, tags: true, hidden: false }); this.helpModal = blessed.box({ parent: this.screen, top: 'center', left: 'center', width: '80%', height: '80%', border: { type: 'line' }, style: { border: { fg: 'cyan' }, bg: 'black' }, label: ' šŸ“– ZLocalz Help ', content: this.getHelpContent(), tags: true, scrollable: true, alwaysScroll: true, keys: true, vi: true, mouse: true, hidden: true }); this.errorModal = blessed.box({ parent: this.screen, top: 'center', left: 'center', width: '70%', height: '60%', border: { type: 'line' }, style: { border: { fg: 'red' }, bg: 'black' }, label: ' āŒ Error ', content: '', tags: true, scrollable: true, alwaysScroll: true, keys: true, mouse: true, hidden: true }); } setupEventHandlers() { this.filesList.on('select item', (_item, index) => { this.selectLocale(index); }); this.keysList.on('select item', (_item, index) => { this.selectKey(index); }); } setupKeyBindings() { this.screen.key(['q', 'C-c'], () => this.handleExit()); this.screen.key(['?', 'h', 'F1'], () => this.showHelp()); this.screen.key(['escape'], () => this.hideModals()); this.screen.key(['tab'], () => this.cycleFocus()); this.screen.key(['S-tab'], () => this.cycleFocus(true)); this.screen.key(['r'], () => this.refresh()); this.screen.key(['s'], () => this.save()); this.screen.key(['space'], () => this.toggleSelection()); this.screen.key(['1'], () => this.setFilter('missing')); this.screen.key(['2'], () => this.setFilter('extra')); this.screen.key(['3'], () => this.setFilter('icuError')); this.screen.key(['0'], () => this.clearFilter()); } selectLocale(index) { if (this.state.isLoading) return; try { const locales = Array.from(this.state.targetFiles.keys()).sort(); if (index >= 0 && index < locales.length) { this.state.currentLocale = locales[index]; this.state.currentKey = undefined; this.updateKeysList(); this.updateDetails(); this.updateHeader(); } } catch (error) { this.showError('Failed to select locale', error); } } selectKey(index) { if (this.state.isLoading || !this.state.currentLocale) return; try { const keys = this.getFilteredKeys(); if (index >= 0 && index < keys.length) { this.state.currentKey = keys[index]; this.updateDetails(); } } catch (error) { this.showError('Failed to select key', error); } } cycleFocus(reverse = false) { if (this.state.isLoading) return; const panes = ['files', 'keys', 'details']; const currentIndex = panes.indexOf(this.state.focusedPane); const nextIndex = reverse ? (currentIndex - 1 + panes.length) % panes.length : (currentIndex + 1) % panes.length; this.state.focusedPane = panes[nextIndex]; this.updateFocus(); } updateFocus() { if (this.updating) return; try { this.updating = true; if (this.filesPane?.style?.border) this.filesPane.style.border.fg = 'gray'; if (this.keysPane?.style?.border) this.keysPane.style.border.fg = 'gray'; if (this.detailsPane?.style?.border) this.detailsPane.style.border.fg = 'gray'; switch (this.state.focusedPane) { case 'files': if (this.filesPane?.style?.border) this.filesPane.style.border.fg = 'cyan'; if (this.filesList && typeof this.filesList.focus === 'function') { this.screen.focusPush(this.filesList); } this.updateFooter('Tab: Next pane | Enter: Select | ?: Help | q: Quit'); break; case 'keys': if (this.keysPane?.style?.border) this.keysPane.style.border.fg = 'cyan'; if (this.keysList && typeof this.keysList.focus === 'function') { this.screen.focusPush(this.keysList); } this.updateFooter('Space: Select | 1-3: Filter | 0: Clear filter | r: Refresh | s: Save'); break; case 'details': if (this.detailsPane?.style?.border) this.detailsPane.style.border.fg = 'cyan'; if (this.detailsText && typeof this.detailsText.focus === 'function') { this.screen.focusPush(this.detailsText); } this.updateFooter('↑↓: Scroll | Tab: Next pane | ESC: Back'); break; } if (this.screen && typeof this.screen.render === 'function') { this.screen.render(); } } catch (error) { console.error('Focus update error:', error); } finally { this.updating = false; } } toggleSelection() { if (!this.state.currentKey || this.state.isLoading) return; if (this.state.selectedKeys.has(this.state.currentKey)) { this.state.selectedKeys.delete(this.state.currentKey); } else { this.state.selectedKeys.add(this.state.currentKey); } this.updateKeysList(); } setFilter(filterType) { this.state.filter = this.state.filter === filterType ? undefined : filterType; this.updateKeysList(); this.updateHeader(); } clearFilter() { this.state.filter = undefined; this.updateKeysList(); this.updateHeader(); } refresh() { if (this.state.isLoading) return; this.emit('refresh'); } save() { if (this.state.isLoading) return; this.emit('save'); this.state.unsavedChanges = false; this.updateHeader(); } updateHeader() { const locale = this.state.currentLocale || 'No locale'; const issueCount = this.getIssueCount(); const changes = this.state.unsavedChanges ? ' •' : ''; const filter = this.state.filter ? ` | Filter: ${this.state.filter}` : ''; this.header.setContent(` ZLocalz | Locale: ${locale} | Issues: ${issueCount}${filter}${changes}`); this.screen.render(); } updateFooter(content) { this.footer.setContent(` ${content}`); this.screen.render(); } updateFilesList() { try { const locales = Array.from(this.state.targetFiles.keys()).sort(); const items = locales.map(locale => { const issues = this.state.issues.get(locale)?.length || 0; const indicator = issues > 0 ? ` (${issues} issues)` : ' āœ“'; return `šŸ“„ ${locale}${indicator}`; }); this.filesList.setItems(items); if (locales.length > 0 && !this.state.currentLocale) { this.state.currentLocale = locales[0]; this.filesList.select(0); this.updateKeysList(); } this.screen.render(); } catch (error) { this.showError('Failed to update files list', error); } } updateKeysList() { if (!this.state.currentLocale) return; try { const keys = this.getFilteredKeys(); const items = keys.map(key => { const selected = this.state.selectedKeys.has(key) ? 'āœ“ ' : ' '; const issues = this.getKeyIssues(key); const indicator = issues.length > 0 ? ' āš ļø' : ''; return `${selected}${key}${indicator}`; }); this.keysList.setItems(items); this.screen.render(); } catch (error) { this.showError('Failed to update keys list', error); } } updateDetails() { try { if (!this.state.currentKey || !this.state.currentLocale) { this.detailsText.setContent('Select a key to view details'); this.screen.render(); return; } const key = this.state.currentKey; const locale = this.state.currentLocale; const value = this.getKeyValue(key, locale); const issues = this.getKeyIssues(key); let content = `{bold}Key:{/bold} ${key}\n`; content += `{bold}Locale:{/bold} ${locale}\n`; content += `{bold}Value:{/bold} ${value || '{red-fg}(missing){/red-fg}'}\n\n`; if (issues.length > 0) { content += `{bold}Issues:{/bold}\n`; issues.forEach(issue => { const severity = issue.severity === 'error' ? '{red-fg}ERROR{/red-fg}' : '{yellow-fg}WARN{/yellow-fg}'; content += `• ${severity}: ${issue.message}\n`; }); } else { content += `{green-fg}āœ… No issues found{/green-fg}`; } this.detailsText.setContent(content); this.screen.render(); } catch (error) { this.showError('Failed to update details', error); } } getFilteredKeys() { if (!this.state.currentLocale) return []; const file = this.state.targetFiles.get(this.state.currentLocale); if (!file) return []; let keys = Object.keys(file.entries); if (this.state.filter) { const issues = this.state.issues.get(this.state.currentLocale) || []; const filteredIssues = issues.filter(issue => issue.type === this.state.filter); keys = keys.filter(key => filteredIssues.some(issue => issue.key === key)); } return keys.sort(); } getKeyIssues(key) { if (!this.state.currentLocale) return []; const issues = this.state.issues.get(this.state.currentLocale) || []; return issues.filter(issue => issue.key === key); } getKeyValue(key, locale) { const file = this.state.targetFiles.get(locale); return file?.entries[key]?.value; } getIssueCount() { if (!this.state.currentLocale) return 0; const issues = this.state.issues.get(this.state.currentLocale) || []; return this.state.filter ? issues.filter(issue => issue.type === this.state.filter).length : issues.length; } showLoading() { this.loadingScreen.show(); this.screen.render(); } hideLoading() { this.state.isLoading = false; this.loadingScreen.hide(); this.updateFocus(); this.screen.render(); } showHelp() { this.helpModal.show(); this.helpModal.focus(); this.screen.render(); } hideModals() { this.helpModal.hide(); this.errorModal.hide(); this.updateFocus(); } showError(title, error) { const errorMessage = error instanceof Error ? error.message : String(error); const content = `{center}{bold}${title}{/bold}{/center} {red-fg}${errorMessage}{/red-fg} {center}Press ESC to close{/center}`; this.errorModal.setContent(content); this.errorModal.show(); this.errorModal.focus(); this.screen.render(); } handleResize() { try { this.screen.render(); } catch (error) { } } handleExit() { if (this.state.unsavedChanges) { } try { this.screen.destroy(); } catch (error) { } process.exit(0); } handleFatalError(error) { const errorMessage = error instanceof Error ? error.message : String(error); try { if (this.screen) { this.screen.destroy(); } } catch (e) { } console.error('\nāŒ ZLocalz TUI Fatal Error:'); console.error(errorMessage); console.error('\nšŸ’” Try running with --no-tui flag for command-line mode'); process.exit(1); } getHelpContent() { return `{center}{bold}šŸš€ ZLocalz - Universal Locale Guardian{/bold}{/center} {bold}NAVIGATION{/bold} • Tab / Shift+Tab Cycle between panes • ↑↓ / j k Navigate lists • Enter Select item • ESC Close modals / go back • q / Ctrl+C Quit application {bold}ACTIONS{/bold} • Space Select/deselect key • r Refresh data • s Save changes • ? Show this help {bold}FILTERS{/bold} • 1 Show missing keys only • 2 Show extra keys only • 3 Show ICU errors only • 0 Clear all filters {bold}VISUAL INDICATORS{/bold} • āœ“ Selected key • āš ļø Key has issues • • Unsaved changes • (N issues) Issue count per locale {bold}PANES{/bold} • Left: Locale files with issue counts • Middle: Translation keys with status • Right: Selected key details and issues {center}Press ESC to close help{/center}`; } async initialize(sourceFile, targetFiles) { try { this.state.sourceFile = sourceFile; this.state.targetFiles = new Map(targetFiles.map(file => [file.locale, file])); this.updateFilesList(); this.updateKeysList(); this.updateDetails(); this.updateHeader(); this.hideLoading(); this.emit('ready'); } catch (error) { this.handleFatalError(error); } } updateIssues(issues) { try { this.state.issues = issues; this.updateFilesList(); this.updateKeysList(); this.updateDetails(); this.updateHeader(); } catch (error) { this.showError('Failed to update issues', error); } } render() { if (!this.initialized) return; try { this.screen.render(); } catch (error) { this.handleFatalError(error); } } destroy() { try { if (this.screen) { this.screen.destroy(); } } catch (error) { } } } exports.LocalzApp = LocalzApp; //# sourceMappingURL=app.js.map