zlocalz
Version:
ZLocalz - TUI Locale Guardian for Flutter ARB l10n/i18n validation and translation with AI-powered fixes
369 lines (364 loc) • 19.9 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;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.LocalzApp = void 0;
exports.ZLocalzTUI = ZLocalzTUI;
const jsx_runtime_1 = require("react/jsx-runtime");
const react_1 = __importStar(require("react"));
const ink_1 = require("ink");
const events_1 = require("events");
function appReducer(state, action) {
switch (action.type) {
case 'SET_FILES':
return {
...state,
sourceFile: action.sourceFile,
targetFiles: new Map(action.targetFiles.map(f => [f.locale, f])),
currentLocale: action.targetFiles[0]?.locale,
isLoading: false,
view: 'main'
};
case 'SET_ISSUES':
return { ...state, issues: action.issues };
case 'SELECT_LOCALE':
return { ...state, currentLocale: action.locale, currentKey: undefined };
case 'SELECT_KEY':
return { ...state, currentKey: action.key };
case 'TOGGLE_KEY_SELECTION':
const newSelectedKeys = new Set(state.selectedKeys);
if (newSelectedKeys.has(action.key)) {
newSelectedKeys.delete(action.key);
}
else {
newSelectedKeys.add(action.key);
}
return { ...state, selectedKeys: newSelectedKeys };
case 'SET_FILTER':
return { ...state, filter: action.filter };
case 'SET_FOCUS':
return { ...state, focusedPane: action.pane };
case 'SET_VIEW':
return { ...state, view: action.view };
case 'SET_ERROR':
return { ...state, error: action.error, view: 'error' };
case 'CLEAR_ERROR':
return { ...state, error: undefined, view: 'main' };
case 'SET_LOADING':
return { ...state, isLoading: action.loading, view: action.loading ? 'loading' : 'main' };
case 'MARK_UNSAVED':
return { ...state, unsavedChanges: true };
case 'MARK_SAVED':
return { ...state, unsavedChanges: false };
default:
return state;
}
}
function Header({ state }) {
const locale = state.currentLocale || 'No locale';
const issueCount = state.currentLocale ? (state.issues.get(state.currentLocale)?.length || 0) : 0;
const changes = state.unsavedChanges ? ' •' : '';
const filter = state.filter ? ` | Filter: ${state.filter}` : '';
return ((0, jsx_runtime_1.jsx)(ink_1.Box, { borderStyle: "single", borderColor: "blue", padding: 0, children: (0, jsx_runtime_1.jsxs)(ink_1.Text, { bold: true, color: "white", children: ["\uD83D\uDE80 ZLocalz | Locale: ", (0, jsx_runtime_1.jsx)(ink_1.Text, { color: "yellow", children: locale }), " | Issues: ", (0, jsx_runtime_1.jsx)(ink_1.Text, { color: "red", children: issueCount }), filter, changes] }) }));
}
function FilesList({ state, dispatch, focused }) {
const locales = Array.from(state.targetFiles.keys()).sort();
const selectedIndex = state.currentLocale ? locales.indexOf(state.currentLocale) : 0;
return ((0, jsx_runtime_1.jsxs)(ink_1.Box, { borderStyle: "single", borderColor: focused ? "cyan" : "gray", flexDirection: "column", width: "25%", height: "100%", children: [(0, jsx_runtime_1.jsx)(ink_1.Text, { bold: true, color: "white", children: "\uD83D\uDCC1 Locales" }), locales.map((locale, index) => {
const issues = state.issues.get(locale)?.length || 0;
const indicator = issues > 0 ? ` (${issues})` : ' ✓';
const isSelected = index === selectedIndex;
return ((0, jsx_runtime_1.jsx)(ink_1.Box, { children: (0, jsx_runtime_1.jsxs)(ink_1.Text, { color: isSelected ? "black" : "white", backgroundColor: isSelected ? "blue" : undefined, children: ["\uD83D\uDCC4 ", locale, indicator] }) }, locale));
})] }));
}
function KeysList({ state, dispatch, focused }) {
const getFilteredKeys = () => {
if (!state.currentLocale)
return [];
const file = state.targetFiles.get(state.currentLocale);
if (!file)
return [];
let keys = Object.keys(file.entries);
if (state.filter) {
const issues = state.issues.get(state.currentLocale) || [];
const filteredIssues = issues.filter(issue => issue.type === state.filter);
keys = keys.filter(key => filteredIssues.some(issue => issue.key === key));
}
return keys.sort();
};
const keys = getFilteredKeys();
const selectedIndex = state.currentKey ? keys.indexOf(state.currentKey) : 0;
return ((0, jsx_runtime_1.jsxs)(ink_1.Box, { borderStyle: "single", borderColor: focused ? "cyan" : "gray", flexDirection: "column", width: "45%", height: "100%", children: [(0, jsx_runtime_1.jsx)(ink_1.Text, { bold: true, color: "white", children: "\uD83D\uDD11 Translation Keys" }), keys.slice(0, 20).map((key, index) => {
const isSelected = index === selectedIndex;
const isKeySelected = state.selectedKeys.has(key);
const issues = state.issues.get(state.currentLocale || '')?.filter(i => i.key === key) || [];
const hasIssues = issues.length > 0;
return ((0, jsx_runtime_1.jsx)(ink_1.Box, { children: (0, jsx_runtime_1.jsxs)(ink_1.Text, { color: isSelected ? "black" : "white", backgroundColor: isSelected ? "green" : undefined, children: [isKeySelected ? '✓ ' : ' ', key, hasIssues ? ' ⚠️' : ''] }) }, key));
}), keys.length > 20 && ((0, jsx_runtime_1.jsxs)(ink_1.Text, { color: "gray", children: ["... and ", keys.length - 20, " more keys"] }))] }));
}
function Details({ state, focused }) {
const getKeyValue = () => {
if (!state.currentKey || !state.currentLocale)
return undefined;
const file = state.targetFiles.get(state.currentLocale);
return file?.entries[state.currentKey]?.value;
};
const getKeyIssues = () => {
if (!state.currentLocale || !state.currentKey)
return [];
const issues = state.issues.get(state.currentLocale) || [];
return issues.filter(issue => issue.key === state.currentKey);
};
if (!state.currentKey || !state.currentLocale) {
return ((0, jsx_runtime_1.jsxs)(ink_1.Box, { borderStyle: "single", borderColor: focused ? "cyan" : "gray", flexDirection: "column", width: "30%", height: "100%", children: [(0, jsx_runtime_1.jsx)(ink_1.Text, { bold: true, color: "white", children: "\uD83D\uDD0D Details & Issues" }), (0, jsx_runtime_1.jsx)(ink_1.Text, { color: "gray", children: "Select a key to view details" })] }));
}
const value = getKeyValue();
const issues = getKeyIssues();
return ((0, jsx_runtime_1.jsxs)(ink_1.Box, { borderStyle: "single", borderColor: focused ? "cyan" : "gray", flexDirection: "column", width: "30%", height: "100%", children: [(0, jsx_runtime_1.jsx)(ink_1.Text, { bold: true, color: "white", children: "\uD83D\uDD0D Details & Issues" }), (0, jsx_runtime_1.jsxs)(ink_1.Box, { flexDirection: "column", marginTop: 1, children: [(0, jsx_runtime_1.jsxs)(ink_1.Text, { children: [(0, jsx_runtime_1.jsx)(ink_1.Text, { bold: true, children: "Key:" }), " ", state.currentKey] }), (0, jsx_runtime_1.jsxs)(ink_1.Text, { children: [(0, jsx_runtime_1.jsx)(ink_1.Text, { bold: true, children: "Locale:" }), " ", state.currentLocale] }), (0, jsx_runtime_1.jsxs)(ink_1.Text, { children: [(0, jsx_runtime_1.jsx)(ink_1.Text, { bold: true, children: "Value:" }), " ", value || (0, jsx_runtime_1.jsx)(ink_1.Text, { color: "red", children: "(missing)" })] }), issues.length > 0 ? ((0, jsx_runtime_1.jsxs)(ink_1.Box, { flexDirection: "column", marginTop: 1, children: [(0, jsx_runtime_1.jsx)(ink_1.Text, { bold: true, children: "Issues:" }), issues.map((issue, index) => ((0, jsx_runtime_1.jsxs)(ink_1.Text, { color: issue.severity === 'error' ? 'red' : 'yellow', children: ["\u2022 ", issue.severity.toUpperCase(), ": ", issue.message] }, index)))] })) : ((0, jsx_runtime_1.jsx)(ink_1.Text, { color: "green", marginTop: 1, children: "\u2705 No issues found" }))] })] }));
}
function Footer({ state }) {
const getShortcuts = () => {
switch (state.focusedPane) {
case 'files':
return 'Tab: Next pane | Enter: Select | ?: Help | q: Quit';
case 'keys':
return 'Space: Select | 1-3: Filter | 0: Clear | r: Refresh | s: Save | Tab: Next';
case 'details':
return 'Tab: Next pane | ↑↓: Scroll | ESC: Back';
default:
return '?: Help | q: Quit';
}
};
return ((0, jsx_runtime_1.jsx)(ink_1.Box, { borderStyle: "single", borderColor: "blue", padding: 0, children: (0, jsx_runtime_1.jsx)(ink_1.Text, { color: "white", children: getShortcuts() }) }));
}
function LoadingView() {
return ((0, jsx_runtime_1.jsxs)(ink_1.Box, { flexDirection: "column", alignItems: "center", justifyContent: "center", height: "100%", children: [(0, jsx_runtime_1.jsx)(ink_1.Text, { bold: true, color: "cyan", children: "\uD83D\uDE80 ZLocalz Loading..." }), (0, jsx_runtime_1.jsx)(ink_1.Text, { color: "cyan", children: "Initializing locale files..." }), (0, jsx_runtime_1.jsx)(ink_1.Text, { children: "Please wait..." })] }));
}
function ErrorView({ error, onClose }) {
return ((0, jsx_runtime_1.jsx)(ink_1.Box, { flexDirection: "column", alignItems: "center", justifyContent: "center", height: "100%", children: (0, jsx_runtime_1.jsx)(ink_1.Box, { borderStyle: "single", borderColor: "red", padding: 2, width: "70%", children: (0, jsx_runtime_1.jsxs)(ink_1.Box, { flexDirection: "column", children: [(0, jsx_runtime_1.jsx)(ink_1.Text, { bold: true, color: "red", children: "\u274C Error" }), (0, jsx_runtime_1.jsx)(ink_1.Text, { color: "white", wrap: "wrap", children: error }), (0, jsx_runtime_1.jsx)(ink_1.Text, { color: "gray", marginTop: 1, children: "Press ESC to close" })] }) }) }));
}
function HelpView({ onClose }) {
const helpContent = `🚀 ZLocalz - Universal Locale Guardian
NAVIGATION
• Tab / Shift+Tab Cycle between panes
• ↑↓ / j k Navigate lists
• Enter Select item
• ESC Close help / go back
• q / Ctrl+C Quit application
ACTIONS
• Space Select/deselect key
• r Refresh data
• s Save changes
FILTERS
• 1 Show missing keys only
• 2 Show extra keys only
• 3 Show ICU errors only
• 0 Clear all filters
VISUAL INDICATORS
• ✓ Selected key
• ⚠️ Key has issues
• • Unsaved changes
• (N) Issue count per locale
Press ESC to close help`;
return ((0, jsx_runtime_1.jsx)(ink_1.Box, { flexDirection: "column", alignItems: "center", justifyContent: "center", height: "100%", children: (0, jsx_runtime_1.jsx)(ink_1.Box, { borderStyle: "single", borderColor: "cyan", padding: 2, width: "80%", height: "80%", children: (0, jsx_runtime_1.jsxs)(ink_1.Box, { flexDirection: "column", children: [(0, jsx_runtime_1.jsx)(ink_1.Text, { bold: true, color: "cyan", children: "\uD83D\uDCD6 ZLocalz Help" }), (0, jsx_runtime_1.jsx)(ink_1.Text, { color: "white", wrap: "wrap", children: helpContent })] }) }) }));
}
function ZLocalzTUI({ config, emitter }) {
const { exit } = (0, ink_1.useApp)();
const [state, dispatch] = react_1.default.useReducer(appReducer, {
config,
targetFiles: new Map(),
issues: new Map(),
selectedKeys: new Set(),
unsavedChanges: false,
focusedPane: 'files',
view: 'loading',
isLoading: true
});
(0, ink_1.useInput)((input, key) => {
if (key.ctrl && input === 'c') {
exit();
return;
}
if (input === 'q' && state.view === 'main') {
exit();
return;
}
if (key.escape) {
if (state.view === 'help' || state.view === 'error') {
dispatch({ type: 'SET_VIEW', view: 'main' });
}
return;
}
if (input === '?' || input === 'h' || key.f1) {
dispatch({ type: 'SET_VIEW', view: 'help' });
return;
}
if (state.view !== 'main')
return;
if (key.tab) {
const panes = ['files', 'keys', 'details'];
const currentIndex = panes.indexOf(state.focusedPane);
const nextIndex = key.shift
? (currentIndex - 1 + panes.length) % panes.length
: (currentIndex + 1) % panes.length;
dispatch({ type: 'SET_FOCUS', pane: panes[nextIndex] });
return;
}
if (state.focusedPane === 'files') {
if (key.upArrow || input === 'k') {
const locales = Array.from(state.targetFiles.keys()).sort();
const currentIndex = state.currentLocale ? locales.indexOf(state.currentLocale) : 0;
const newIndex = Math.max(0, currentIndex - 1);
if (locales[newIndex]) {
dispatch({ type: 'SELECT_LOCALE', locale: locales[newIndex] });
}
}
else if (key.downArrow || input === 'j') {
const locales = Array.from(state.targetFiles.keys()).sort();
const currentIndex = state.currentLocale ? locales.indexOf(state.currentLocale) : 0;
const newIndex = Math.min(locales.length - 1, currentIndex + 1);
if (locales[newIndex]) {
dispatch({ type: 'SELECT_LOCALE', locale: locales[newIndex] });
}
}
}
else if (state.focusedPane === 'keys') {
if (input === ' ' && state.currentKey) {
dispatch({ type: 'TOGGLE_KEY_SELECTION', key: state.currentKey });
}
if (key.upArrow || input === 'k') {
const keys = getFilteredKeys(state);
const currentIndex = state.currentKey ? keys.indexOf(state.currentKey) : 0;
const newIndex = Math.max(0, currentIndex - 1);
if (keys[newIndex]) {
dispatch({ type: 'SELECT_KEY', key: keys[newIndex] });
}
}
else if (key.downArrow || input === 'j') {
const keys = getFilteredKeys(state);
const currentIndex = state.currentKey ? keys.indexOf(state.currentKey) : 0;
const newIndex = Math.min(keys.length - 1, currentIndex + 1);
if (keys[newIndex]) {
dispatch({ type: 'SELECT_KEY', key: keys[newIndex] });
}
}
}
if (input === 'r') {
emitter.emit('refresh');
}
else if (input === 's') {
emitter.emit('save');
dispatch({ type: 'MARK_SAVED' });
}
if (input === '1') {
dispatch({ type: 'SET_FILTER', filter: state.filter === 'missing' ? undefined : 'missing' });
}
else if (input === '2') {
dispatch({ type: 'SET_FILTER', filter: state.filter === 'extra' ? undefined : 'extra' });
}
else if (input === '3') {
dispatch({ type: 'SET_FILTER', filter: state.filter === 'icuError' ? undefined : 'icuError' });
}
else if (input === '0') {
dispatch({ type: 'SET_FILTER', filter: undefined });
}
});
(0, react_1.useEffect)(() => {
const handleFilesReady = (sourceFile, targetFiles) => {
dispatch({ type: 'SET_FILES', sourceFile, targetFiles });
};
const handleIssuesUpdate = (issues) => {
dispatch({ type: 'SET_ISSUES', issues });
};
const handleError = (error) => {
dispatch({ type: 'SET_ERROR', error });
};
emitter.on('files-ready', handleFilesReady);
emitter.on('issues-updated', handleIssuesUpdate);
emitter.on('error', handleError);
return () => {
emitter.off('files-ready', handleFilesReady);
emitter.off('issues-updated', handleIssuesUpdate);
emitter.off('error', handleError);
};
}, [emitter]);
if (state.view === 'loading') {
return (0, jsx_runtime_1.jsx)(LoadingView, {});
}
if (state.view === 'error') {
return (0, jsx_runtime_1.jsx)(ErrorView, { error: state.error || 'Unknown error', onClose: () => dispatch({ type: 'CLEAR_ERROR' }) });
}
if (state.view === 'help') {
return (0, jsx_runtime_1.jsx)(HelpView, { onClose: () => dispatch({ type: 'SET_VIEW', view: 'main' }) });
}
return ((0, jsx_runtime_1.jsxs)(ink_1.Box, { flexDirection: "column", height: "100%", children: [(0, jsx_runtime_1.jsx)(Header, { state: state }), (0, jsx_runtime_1.jsxs)(ink_1.Box, { flexGrow: 1, flexDirection: "row", children: [(0, jsx_runtime_1.jsx)(FilesList, { state: state, dispatch: dispatch, focused: state.focusedPane === 'files' }), (0, jsx_runtime_1.jsx)(KeysList, { state: state, dispatch: dispatch, focused: state.focusedPane === 'keys' }), (0, jsx_runtime_1.jsx)(Details, { state: state, focused: state.focusedPane === 'details' })] }), (0, jsx_runtime_1.jsx)(Footer, { state: state })] }));
}
function getFilteredKeys(state) {
if (!state.currentLocale)
return [];
const file = state.targetFiles.get(state.currentLocale);
if (!file)
return [];
let keys = Object.keys(file.entries);
if (state.filter) {
const issues = state.issues.get(state.currentLocale) || [];
const filteredIssues = issues.filter(issue => issue.type === state.filter);
keys = keys.filter(key => filteredIssues.some(issue => issue.key === key));
}
return keys.sort();
}
class LocalzApp extends events_1.EventEmitter {
instance;
config;
constructor(config) {
super();
this.config = config;
}
async initialize(sourceFile, targetFiles) {
this.instance = (0, ink_1.render)((0, jsx_runtime_1.jsx)(ZLocalzTUI, { config: this.config, emitter: this }));
this.emit('files-ready', sourceFile, targetFiles);
}
updateIssues(issues) {
this.emit('issues-updated', issues);
}
render() {
}
destroy() {
if (this.instance) {
this.instance.unmount();
}
}
}
exports.LocalzApp = LocalzApp;
//# sourceMappingURL=app-ink.js.map