UNPKG

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