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
JavaScript
"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