From 3f5c57634bf84f795ba3f8d3681b22c13c8239f9 Mon Sep 17 00:00:00 2001 From: argenis de la rosa Date: Mon, 9 Mar 2026 13:50:21 -0400 Subject: [PATCH] feat(web): replace native language selects with custom dropdown and fix RTL text alignment This commit modernizes the language selector UI across the ZeroClaw web dashboard by replacing native setCode(e.target.value)} - placeholder="6-digit code" - className="w-full px-4 py-3 bg-gray-800 border border-gray-700 rounded-lg text-white text-center text-2xl tracking-widest focus:outline-none focus:border-blue-500 mb-4" + placeholder={translate('auth.code_placeholder')} + className="w-full rounded-xl border border-[#29509c] bg-[#071228]/90 px-4 py-3 text-center text-2xl tracking-[0.35em] text-white focus:border-[#4f83ff] focus:outline-none mb-4" maxLength={6} autoFocus /> {error && ( -

{error}

+

{error}

)} @@ -81,11 +113,38 @@ function PairingDialog({ onPair }: { onPair: (code: string) => Promise }) function AppContent() { const { isAuthenticated, loading, pair, logout } = useAuth(); - const [locale, setLocaleState] = useState('tr'); + const [locale, setLocaleState] = useState(() => { + const initialLocale = (() => { + if (typeof window === 'undefined') { + return 'en'; + } - const setAppLocale = (newLocale: string) => { + const saved = window.localStorage.getItem(LOCALE_STORAGE_KEY); + if (saved) { + return coerceLocale(saved); + } + + return coerceLocale(window.navigator.language); + })(); + + setLocale(initialLocale); + if (typeof document !== 'undefined') { + applyLocaleToDocument(initialLocale, document); + } + return initialLocale; + }); + + useEffect(() => { + setLocale(locale); + if (typeof window !== 'undefined') { + window.localStorage.setItem(LOCALE_STORAGE_KEY, locale); + applyLocaleToDocument(locale, document); + } + }, [locale]); + + const setAppLocale = (newLocale: Locale) => { + setLocale(newLocale); setLocaleState(newLocale); - setLocale(newLocale as Locale); }; // Listen for 401 events to force logout @@ -99,19 +158,22 @@ function AppContent() { if (loading) { return ( -
-

Connecting...

+
+
+
+

{tLocale('common.connecting', locale)}

+
); } if (!isAuthenticated) { - return ; + return ; } return ( - + }> } /> } /> diff --git a/web/src/components/controls/LanguageSelector.tsx b/web/src/components/controls/LanguageSelector.tsx new file mode 100644 index 000000000..2c60ab3b5 --- /dev/null +++ b/web/src/components/controls/LanguageSelector.tsx @@ -0,0 +1,129 @@ +import { Check, ChevronDown } from 'lucide-react'; +import { useEffect, useRef, useState } from 'react'; +import { + getLanguageOption, + getLanguageOptionLabel, + getLocaleDirection, + LANGUAGE_OPTIONS, + type Locale, +} from '@/lib/i18n'; + +interface LanguageSelectorProps { + locale: Locale; + onChange: (locale: Locale) => void; + ariaLabel: string; + title?: string; + align?: 'left' | 'right'; + buttonClassName?: string; + menuClassName?: string; +} + +export default function LanguageSelector({ + locale, + onChange, + ariaLabel, + title, + align = 'right', + buttonClassName = '', + menuClassName = '', +}: LanguageSelectorProps) { + const [open, setOpen] = useState(false); + const containerRef = useRef(null); + const activeLanguage = getLanguageOption(locale); + const localeDirection = getLocaleDirection(locale); + + useEffect(() => { + const handlePointerDown = (event: MouseEvent) => { + if (!containerRef.current?.contains(event.target as Node)) { + setOpen(false); + } + }; + + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + setOpen(false); + } + }; + + window.addEventListener('mousedown', handlePointerDown); + window.addEventListener('keydown', handleKeyDown); + return () => { + window.removeEventListener('mousedown', handlePointerDown); + window.removeEventListener('keydown', handleKeyDown); + }; + }, []); + + const alignmentClass = align === 'left' ? 'left-0' : 'right-0'; + + return ( +
+ + + {open ? ( +
+
+ {LANGUAGE_OPTIONS.map((option) => { + const selected = option.value === locale; + return ( + + ); + })} +
+
+ ) : null} +
+ ); +} diff --git a/web/src/components/layout/Header.tsx b/web/src/components/layout/Header.tsx index 7e26ba6b5..05ce96dd2 100644 --- a/web/src/components/layout/Header.tsx +++ b/web/src/components/layout/Header.tsx @@ -1,6 +1,7 @@ import { useLocation } from 'react-router-dom'; -import { LogOut } from 'lucide-react'; -import { t } from '@/lib/i18n'; +import { LogOut, Menu } from 'lucide-react'; +import { t, tLocale } from '@/lib/i18n'; +import LanguageSelector from '@/components/controls/LanguageSelector'; import { useLocaleContext } from '@/App'; import { useAuth } from '@/hooks/useAuth'; @@ -17,42 +18,59 @@ const routeTitles: Record = { '/doctor': 'nav.doctor', }; -export default function Header() { +interface HeaderProps { + onToggleSidebar: () => void; +} + +export default function Header({ onToggleSidebar }: HeaderProps) { const location = useLocation(); const { logout } = useAuth(); const { locale, setAppLocale } = useLocaleContext(); const titleKey = routeTitles[location.pathname] ?? 'nav.dashboard'; - const pageTitle = t(titleKey); - - const toggleLanguage = () => { - setAppLocale(locale === 'en' ? 'tr' : 'en'); - }; + const pageTitle = tLocale(titleKey, locale); return ( -
- {/* Page title */} -

{pageTitle}

+
+
- {/* Right-side controls */} -
- {/* Language switcher */} +
- {/* Logout */} +
+

+ {pageTitle} +

+

+ {tLocale('header.dashboard_tagline', locale)} +

+
+
+ +
+ +
diff --git a/web/src/components/layout/Layout.tsx b/web/src/components/layout/Layout.tsx index b31f127b4..94f010342 100644 --- a/web/src/components/layout/Layout.tsx +++ b/web/src/components/layout/Layout.tsx @@ -1,19 +1,47 @@ import { Outlet } from 'react-router-dom'; +import { useState } from 'react'; import Sidebar from '@/components/layout/Sidebar'; import Header from '@/components/layout/Header'; +const SIDEBAR_COLLAPSED_KEY = 'zeroclaw:sidebar-collapsed'; + export default function Layout() { + const [sidebarOpen, setSidebarOpen] = useState(false); + const [sidebarCollapsed, setSidebarCollapsed] = useState(() => { + if (typeof window === 'undefined') { + return false; + } + return window.localStorage.getItem(SIDEBAR_COLLAPSED_KEY) === '1'; + }); + + const toggleSidebarCollapsed = () => { + setSidebarCollapsed((prev) => { + const next = !prev; + if (typeof window !== 'undefined') { + window.localStorage.setItem(SIDEBAR_COLLAPSED_KEY, next ? '1' : '0'); + } + return next; + }); + }; + return ( -
- {/* Fixed sidebar */} - +
+ setSidebarOpen(false)} + onToggleCollapse={toggleSidebarCollapsed} + /> - {/* Main area offset by sidebar width (240px / w-60) */} -
-
+
+
setSidebarOpen((open) => !open)} /> - {/* Page content */} -
+
diff --git a/web/src/components/layout/Sidebar.tsx b/web/src/components/layout/Sidebar.tsx index e378229d4..57740c80a 100644 --- a/web/src/components/layout/Sidebar.tsx +++ b/web/src/components/layout/Sidebar.tsx @@ -1,5 +1,7 @@ +import { useEffect, useState } from 'react'; import { NavLink } from 'react-router-dom'; import { + ChevronsLeftRightEllipsis, LayoutDashboard, MessageSquare, Wrench, @@ -10,9 +12,12 @@ import { DollarSign, Activity, Stethoscope, + X, } from 'lucide-react'; import { t } from '@/lib/i18n'; +const COLLAPSE_BUTTON_DELAY_MS = 1000; + const navItems = [ { to: '/', icon: LayoutDashboard, labelKey: 'nav.dashboard' }, { to: '/agent', icon: MessageSquare, labelKey: 'nav.agent' }, @@ -26,40 +31,125 @@ const navItems = [ { to: '/doctor', icon: Stethoscope, labelKey: 'nav.doctor' }, ]; -export default function Sidebar() { - return ( - +export default function Sidebar({ + isOpen, + isCollapsed, + onClose, + onToggleCollapse, +}: SidebarProps) { + const [showCollapseButton, setShowCollapseButton] = useState(false); + + useEffect(() => { + const id = setTimeout(() => setShowCollapseButton(true), COLLAPSE_BUTTON_DELAY_MS); + return () => clearTimeout(id); + }, []); + + return ( + <> + + )} + +
+
+ + + +
+

{t('sidebar.gateway_dashboard')}

+

+ {isCollapsed ? 'UI' : t('sidebar.runtime_mode')} +

+
+ + ); } diff --git a/web/src/index.css b/web/src/index.css index 66e881a91..5208cf0ec 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -1,89 +1,598 @@ @import "tailwindcss"; -/* - * ZeroClaw Dark Theme - * Dark-mode by default with gray cards and blue/green accents. - */ - @theme { - --color-bg-primary: #0a0a0f; - --color-bg-secondary: #12121a; - --color-bg-card: #1a1a2e; - --color-bg-card-hover: #22223a; - --color-bg-input: #14141f; - - --color-border-default: #2a2a3e; - --color-border-subtle: #1e1e30; - - --color-accent-blue: #3b82f6; - --color-accent-blue-hover: #2563eb; - --color-accent-green: #10b981; - --color-accent-green-hover: #059669; - - --color-text-primary: #e2e8f0; - --color-text-secondary: #94a3b8; - --color-text-muted: #64748b; - - --color-status-success: #10b981; - --color-status-warning: #f59e0b; - --color-status-error: #ef4444; - --color-status-info: #3b82f6; + --color-electric-50: #eaf3ff; + --color-electric-100: #d8e8ff; + --color-electric-300: #87b8ff; + --color-electric-500: #2f8fff; + --color-electric-700: #0f57dd; + --color-electric-900: #031126; } -/* Base styles */ html { color-scheme: dark; } body { - background-color: var(--color-bg-primary); - color: var(--color-text-primary); + margin: 0; + min-height: 100dvh; + color: #edf4ff; + background: #020813; font-family: - "Inter", - ui-sans-serif, - system-ui, - -apple-system, + "Sora", + "Manrope", + "Avenir Next", + "Segoe UI", sans-serif; -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; + text-rendering: geometricPrecision; + overflow-x: hidden; } #root { - min-height: 100vh; + min-height: 100dvh; +} + +.app-shell { + position: relative; + isolation: isolate; + background: + radial-gradient(circle at 8% 5%, rgba(47, 143, 255, 0.22), transparent 35%), + radial-gradient(circle at 92% 14%, rgba(0, 209, 255, 0.16), transparent 32%), + linear-gradient(175deg, #020816 0%, #03091b 46%, #040e24 100%); +} + +.app-shell::before, +.app-shell::after { + content: ""; + position: fixed; + inset: 0; + pointer-events: none; + z-index: -1; +} + +.app-shell::before { + background-image: + linear-gradient(rgba(76, 118, 194, 0.1) 1px, transparent 1px), + linear-gradient(90deg, rgba(76, 118, 194, 0.1) 1px, transparent 1px); + background-size: 34px 34px; + mask-image: radial-gradient(circle at 50% 36%, black 22%, transparent 80%); + opacity: 0.35; +} + +.app-shell::after { + background: + radial-gradient(circle at 16% 86%, rgba(42, 128, 255, 0.34), transparent 43%), + radial-gradient(circle at 84% 22%, rgba(0, 212, 255, 0.2), transparent 38%), + radial-gradient(circle at 52% 122%, rgba(40, 118, 255, 0.3), transparent 56%); + filter: blur(4px); + animation: appGlowDrift 28s ease-in-out infinite; +} + +.glass-header { + position: relative; + backdrop-filter: blur(16px); + background: linear-gradient(160deg, rgba(6, 19, 45, 0.85), rgba(5, 14, 33, 0.9)); + box-shadow: + 0 18px 32px -28px rgba(68, 145, 255, 0.95), + inset 0 1px 0 rgba(140, 183, 255, 0.14); +} + +.glass-header::after { + content: ""; + position: absolute; + inset: -1px; + border-radius: inherit; + background: linear-gradient(105deg, transparent 10%, rgba(79, 155, 255, 0.24), transparent 70%); + transform: translateX(-70%); + animation: topGlowSweep 7s ease-in-out infinite; + pointer-events: none; +} + +.hero-panel { + position: relative; + overflow: hidden; + border-radius: 1.25rem; + border: 1px solid #21438c; + padding: 1.15rem 1.2rem; + background: + radial-gradient(circle at 0% 0%, rgba(56, 143, 255, 0.24), transparent 40%), + linear-gradient(146deg, rgba(8, 26, 64, 0.95), rgba(4, 13, 34, 0.92)); + box-shadow: + inset 0 1px 0 rgba(130, 174, 255, 0.16), + 0 22px 50px -38px rgba(64, 145, 255, 0.94); +} + +.hero-panel::after { + content: ""; + position: absolute; + inset: 0; + background: linear-gradient(118deg, transparent, rgba(128, 184, 255, 0.12), transparent 70%); + transform: translateX(-62%); + animation: heroSweep 5.8s ease-in-out infinite; + pointer-events: none; +} + +.hero-panel::before { + content: ""; + position: absolute; + width: 19rem; + height: 19rem; + right: -6rem; + top: -10rem; + border-radius: 999px; + background: radial-gradient(circle at center, rgba(63, 167, 255, 0.42), transparent 70%); + filter: blur(12px); + animation: heroGlowPulse 4.8s ease-in-out infinite; + pointer-events: none; +} + +.status-pill { + display: inline-flex; + align-items: center; + gap: 0.35rem; + border-radius: 999px; + border: 1px solid #2d58ac; + background: rgba(6, 29, 78, 0.68); + color: #c2d9ff; + padding: 0.35rem 0.65rem; + font-size: 0.68rem; + text-transform: uppercase; + letter-spacing: 0.12em; + box-shadow: 0 0 22px -16px rgba(83, 153, 255, 0.95); +} + +.electric-brand-mark { + display: flex; + align-items: center; + justify-content: center; + background: + radial-gradient(circle at 22% 18%, rgba(94, 200, 255, 0.35), rgba(23, 119, 255, 0.2) 52%, rgba(10, 72, 181, 0.28) 100%); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.24), + 0 10px 25px -12px rgba(41, 130, 255, 0.95); +} + +.electric-card { + position: relative; + overflow: hidden; + border-radius: 1rem; + border: 1px solid #1a3670; + background: + linear-gradient(165deg, rgba(8, 24, 60, 0.95), rgba(4, 14, 34, 0.96)); + box-shadow: + inset 0 1px 0 rgba(129, 174, 255, 0.12), + 0 25px 45px -36px rgba(47, 140, 255, 0.95), + 0 0 0 1px rgba(63, 141, 255, 0.2), + 0 0 26px -17px rgba(76, 176, 255, 0.82); +} + +.electric-card::after { + content: ""; + position: absolute; + left: -20%; + right: -20%; + bottom: -65%; + height: 72%; + border-radius: 50%; + background: radial-gradient(circle, rgba(59, 148, 255, 0.25), transparent 72%); + filter: blur(16px); + opacity: 0.45; + pointer-events: none; + animation: cardGlowPulse 5.2s ease-in-out infinite; +} + +.electric-icon { + display: flex; + align-items: center; + justify-content: center; + color: #9bc3ff; + background: + radial-gradient(circle at 35% 22%, rgba(123, 198, 255, 0.38), rgba(29, 92, 214, 0.32) 66%, rgba(12, 44, 102, 0.48) 100%); + border: 1px solid rgba(86, 143, 255, 0.45); +} + +.metric-head { + display: inline-flex; + align-items: center; + gap: 0.4rem; + border-radius: 999px; + border: 1px solid #244991; + background: rgba(6, 22, 54, 0.74); + color: #91b8fb; + font-size: 0.66rem; + letter-spacing: 0.11em; + text-transform: uppercase; + padding: 0.3rem 0.55rem; +} + +.metric-value { + color: #ffffff; + font-size: clamp(1.15rem, 1.8vw, 1.45rem); + font-weight: 620; + letter-spacing: 0.02em; +} + +.metric-sub { + color: #89aee8; + font-size: 0.78rem; +} + +.metric-pill { + border-radius: 0.85rem; + border: 1px solid #1d3c77; + background: rgba(5, 17, 44, 0.86); + padding: 0.6rem 0.72rem; + box-shadow: + 0 0 0 1px rgba(62, 137, 255, 0.16), + 0 16px 30px -24px rgba(47, 140, 255, 0.86), + 0 0 18px -15px rgba(73, 176, 255, 0.78); +} + +.metric-pill span { + display: block; + font-size: 0.68rem; + text-transform: uppercase; + letter-spacing: 0.1em; + color: #85a9e1; +} + +.metric-pill strong { + display: block; + margin-top: 0.2rem; + font-size: 0.93rem; + color: #f5f9ff; +} + +.electric-progress { + background: + linear-gradient(90deg, #1f76ff 0%, #2f97ff 60%, #48cdff 100%); + box-shadow: 0 0 18px -7px rgba(62, 166, 255, 0.95); +} + +.pairing-shell { + position: relative; + isolation: isolate; + min-height: 100dvh; + background: + radial-gradient(circle at 20% 5%, rgba(64, 141, 255, 0.24), transparent 35%), + radial-gradient(circle at 75% 92%, rgba(0, 193, 255, 0.13), transparent 35%), + linear-gradient(155deg, #020816 0%, #030c20 58%, #030915 100%); + overflow: hidden; +} + +.pairing-shell::after { + content: ""; + position: absolute; + inset: -20%; + background: + radial-gradient(circle at 10% 20%, rgba(84, 173, 255, 0.25), transparent 60%), + radial-gradient(circle at 85% 80%, rgba(0, 204, 255, 0.22), transparent 60%); + filter: blur(12px); + opacity: 0.7; + animation: pairingSpotlightSweep 18s ease-in-out infinite; + pointer-events: none; + z-index: -1; +} + +.pairing-card { + position: relative; + overflow: hidden; + border: 1px solid #2956a8; + background: + linear-gradient(155deg, rgba(9, 27, 68, 0.9), rgba(4, 15, 35, 0.94)); + box-shadow: + inset 0 1px 0 rgba(146, 190, 255, 0.16), + 0 30px 60px -44px rgba(47, 141, 255, 0.98), + 0 0 0 1px rgba(67, 150, 255, 0.2), + 0 0 28px -18px rgba(76, 184, 255, 0.82); +} + +.pairing-card::before { + content: ""; + position: absolute; + inset: -1px; + border-radius: inherit; + background: linear-gradient(135deg, transparent 10%, rgba(102, 186, 255, 0.5), transparent 80%); + mix-blend-mode: screen; + opacity: 0.0; + transform: translateX(-65%); + animation: pairingCardSweep 7.5s ease-in-out infinite; + pointer-events: none; +} + +.pairing-brand { + background-image: linear-gradient(120deg, #5bc0ff 0%, #f9e775 28%, #5bc0ff 56%, #f9e775 100%); + background-size: 260% 260%; + background-clip: text; + -webkit-background-clip: text; + color: transparent; + text-shadow: + 0 0 14px rgba(96, 189, 255, 0.8), + 0 0 32px rgba(46, 138, 255, 0.9), + 0 0 42px rgba(252, 238, 147, 0.85); + letter-spacing: 0.18em; + animation: pairingElectricCharge 5.4s ease-in-out infinite; +} + +:is(div, section, article)[class*="bg-gray-900"][class*="rounded-xl"][class*="border"], +:is(div, section, article)[class*="bg-gray-900"][class*="rounded-lg"][class*="border"], +:is(div, section, article)[class*="bg-gray-950"][class*="rounded-lg"][class*="border"] { + box-shadow: + 0 0 0 1px rgba(67, 144, 255, 0.14), + 0 22px 40px -32px rgba(45, 134, 255, 0.86), + 0 0 22px -16px rgba(73, 180, 255, 0.75); +} + +.electric-button { + border: 1px solid #4a89ff; + background: linear-gradient(126deg, #125bdf 0%, #1f88ff 55%, #17b4ff 100%); + box-shadow: 0 18px 30px -20px rgba(47, 141, 255, 0.9); + transition: transform 180ms ease, filter 180ms ease, box-shadow 180ms ease; +} + +.electric-button:hover { + transform: translateY(-1px); + filter: brightness(1.05); + box-shadow: 0 20px 34px -19px rgba(56, 154, 255, 0.95); +} + +.electric-loader { + border: 3px solid rgba(89, 146, 255, 0.22); + border-top-color: #51abff; + box-shadow: 0 0 20px -12px rgba(66, 157, 255, 1); + animation: spin 1s linear infinite; +} + +.motion-rise { + animation: riseIn 580ms ease both; +} + +.motion-delay-1 { + animation-delay: 70ms; +} + +.motion-delay-2 { + animation-delay: 130ms; +} + +.motion-delay-3 { + animation-delay: 190ms; +} + +.motion-delay-4 { + animation-delay: 250ms; +} + +* { + scrollbar-width: thin; + scrollbar-color: #244787 #081126; } -/* Scrollbar styling */ ::-webkit-scrollbar { width: 8px; height: 8px; } ::-webkit-scrollbar-track { - background: var(--color-bg-secondary); + background: #081126; } ::-webkit-scrollbar-thumb { - background: var(--color-border-default); - border-radius: 4px; + background: #244787; + border-radius: 999px; } ::-webkit-scrollbar-thumb:hover { - background: var(--color-text-muted); + background: #3160b6; } -/* Card utility */ -.card { - background-color: var(--color-bg-card); - border: 1px solid var(--color-border-default); - border-radius: 0.75rem; -} - -.card:hover { - background-color: var(--color-bg-card-hover); -} - -/* Focus ring utility */ *:focus-visible { - outline: 2px solid var(--color-accent-blue); + outline: 2px solid #4ea4ff; outline-offset: 2px; } + +@keyframes riseIn { + from { + opacity: 0; + transform: translateY(14px) scale(0.985); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +@keyframes heroSweep { + 0%, + 100% { + transform: translateX(-68%); + opacity: 0; + } + 30% { + opacity: 0.65; + } + 60% { + transform: translateX(58%); + opacity: 0; + } +} + +@keyframes heroGlowPulse { + 0%, + 100% { + opacity: 0.38; + transform: scale(0.94); + } + 50% { + opacity: 0.72; + transform: scale(1.08); + } +} + +@keyframes cardGlowPulse { + 0%, + 100% { + opacity: 0.28; + transform: translateY(0) scale(0.96); + } + 50% { + opacity: 0.55; + transform: translateY(-2%) scale(1.04); + } +} + +@keyframes pairingElectricCharge { + 0%, + 100% { + background-position: 0% 50%; + text-shadow: + 0 0 14px rgba(86, 177, 255, 0.5), + 0 0 32px rgba(36, 124, 255, 0.7), + 0 0 38px rgba(252, 238, 147, 0.6); + transform: translateY(0) scale(1); + } + 35% { + background-position: 80% 50%; + text-shadow: + 0 0 26px rgba(138, 218, 255, 1), + 0 0 52px rgba(56, 176, 255, 1), + 0 0 60px rgba(252, 238, 147, 1); + transform: translateY(-1px) scale(1.06); + } + 60% { + background-position: 50% 50%; + text-shadow: + 0 0 18px rgba(86, 177, 255, 0.7), + 0 0 36px rgba(36, 124, 255, 0.8), + 0 0 44px rgba(252, 238, 147, 0.7); + transform: translateY(0) scale(1.02); + } +} + +@keyframes pairingSpotlightSweep { + 0% { + transform: translate3d(-12%, 8%, 0) scale(1); + opacity: 0.45; + } + 30% { + transform: translate3d(10%, -4%, 0) scale(1.06); + opacity: 0.7; + } + 55% { + transform: translate3d(16%, 10%, 0) scale(1.1); + opacity: 0.6; + } + 80% { + transform: translate3d(-8%, -6%, 0) scale(1.04); + opacity: 0.5; + } + 100% { + transform: translate3d(-12%, 8%, 0) scale(1); + opacity: 0.45; + } +} + +@keyframes pairingCardSweep { + 0%, + 100% { + transform: translateX(-70%); + opacity: 0; + } + 25% { + opacity: 0.55; + } + 50% { + transform: translateX(55%); + opacity: 0; + } +} + +@keyframes topGlowSweep { + 0%, + 100% { + transform: translateX(-78%); + opacity: 0; + } + 30% { + opacity: 0.55; + } + 58% { + transform: translateX(58%); + opacity: 0; + } +} + +@keyframes appGlowDrift { + 0% { + opacity: 0.3; + transform: translate3d(-3%, 1.8%, 0) scale(1); + } + 25% { + opacity: 0.5; + transform: translate3d(2.6%, -1.2%, 0) scale(1.04); + } + 50% { + opacity: 0.56; + transform: translate3d(4.4%, -3.4%, 0) scale(1.09); + } + 75% { + opacity: 0.44; + transform: translate3d(-1.8%, -2.1%, 0) scale(1.05); + } + 100% { + opacity: 0.34; + transform: translate3d(-3.6%, 2.6%, 0) scale(1.01); + } +} + +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (max-width: 768px) { + .hero-panel { + padding: 0.95rem 0.95rem; + } + + .status-pill { + padding: 0.28rem 0.52rem; + font-size: 0.61rem; + letter-spacing: 0.1em; + } + + .metric-value { + font-size: 1.08rem; + } + + .metric-sub { + font-size: 0.74rem; + } + + .electric-card { + border-radius: 0.9rem; + } +} + +@media (prefers-reduced-motion: reduce) { + .hero-panel::after, + .hero-panel::before, + .glass-header::after, + .electric-card::after, + .app-shell::after, + .pairing-shell::after, + .pairing-card::before, + .motion-rise, + .electric-loader { + animation: none !important; + } + + .electric-button { + transition: none !important; + } +} diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts index 181462b9b..af81fd109 100644 --- a/web/src/lib/api.ts +++ b/web/src/lib/api.ts @@ -72,6 +72,21 @@ function unwrapField(value: T | Record, key: string): T { return value as T; } +function normalizeMemoryCategory(value: unknown): string { + if (typeof value === 'string') { + return value; + } + + if (value && typeof value === 'object') { + const custom = (value as { custom?: unknown }).custom; + if (typeof custom === 'string' && custom.trim().length > 0) { + return custom; + } + } + + return 'unknown'; +} + // --------------------------------------------------------------------------- // Pairing // --------------------------------------------------------------------------- @@ -208,7 +223,11 @@ export function getMemory( if (category) params.set('category', category); const qs = params.toString(); return apiFetch(`/api/memory${qs ? `?${qs}` : ''}`).then( - (data) => unwrapField(data, 'entries'), + (data) => + unwrapField(data, 'entries').map((entry) => ({ + ...entry, + category: normalizeMemoryCategory((entry as { category: unknown }).category), + })), ); } diff --git a/web/src/lib/i18n.test.ts b/web/src/lib/i18n.test.ts new file mode 100644 index 000000000..d760fd3ea --- /dev/null +++ b/web/src/lib/i18n.test.ts @@ -0,0 +1,64 @@ +import { describe, expect, it } from 'vitest'; +import { + applyLocaleToDocument, + coerceLocale, + getLanguageOption, + getLanguageOptionLabel, + getLocaleDirection, + LANGUAGE_OPTIONS, + LANGUAGE_SWITCH_ORDER, +} from './i18n'; + +describe('language metadata', () => { + it('keeps language options aligned with switch order', () => { + expect(LANGUAGE_OPTIONS.map((option) => option.value)).toEqual(LANGUAGE_SWITCH_ORDER); + expect(new Set(LANGUAGE_OPTIONS.map((option) => option.value)).size).toBe(LANGUAGE_OPTIONS.length); + }); + + it('provides a flag-backed label for every locale', () => { + for (const option of LANGUAGE_OPTIONS) { + expect(getLanguageOption(option.value)).toEqual(option); + expect(getLanguageOptionLabel(option)).toBe(option.label); + expect(option.flag.length).toBeGreaterThan(0); + } + }); +}); + +describe('coerceLocale', () => { + it('normalizes browser locale variants to supported locales', () => { + expect(coerceLocale('ar-SA')).toBe('ar'); + expect(coerceLocale('he-IL')).toBe('he'); + expect(coerceLocale('iw-IL')).toBe('he'); + expect(coerceLocale('pt-BR')).toBe('pt'); + expect(coerceLocale('no-NO')).toBe('nb'); + expect(coerceLocale('zh-Hans')).toBe('zh-CN'); + expect(coerceLocale(undefined)).toBe('en'); + }); +}); + +describe('locale direction', () => { + it('returns rtl only for rtl languages', () => { + expect(getLocaleDirection('ar')).toBe('rtl'); + expect(getLocaleDirection('he')).toBe('rtl'); + expect(getLocaleDirection('ur')).toBe('rtl'); + expect(getLocaleDirection('en')).toBe('ltr'); + expect(getLocaleDirection('ja')).toBe('ltr'); + }); + + it('applies lang and dir to a document-like target', () => { + const target = { + documentElement: { lang: '', dir: '' }, + body: { dir: '' }, + }; + + applyLocaleToDocument('ar', target); + expect(target.documentElement.lang).toBe('ar'); + expect(target.documentElement.dir).toBe('rtl'); + expect(target.body.dir).toBe('rtl'); + + applyLocaleToDocument('fr', target); + expect(target.documentElement.lang).toBe('fr'); + expect(target.documentElement.dir).toBe('ltr'); + expect(target.body.dir).toBe('ltr'); + }); +}); diff --git a/web/src/lib/i18n.ts b/web/src/lib/i18n.ts index eac6ad02b..3a3d2fe04 100644 --- a/web/src/lib/i18n.ts +++ b/web/src/lib/i18n.ts @@ -1,383 +1,1623 @@ import { useState, useEffect } from 'react'; import { getStatus } from './api'; -// --------------------------------------------------------------------------- -// Translation dictionaries -// --------------------------------------------------------------------------- +export type Locale = + | 'en' + | 'zh-CN' + | 'ja' + | 'ko' + | 'vi' + | 'tl' + | 'es' + | 'pt' + | 'it' + | 'de' + | 'fr' + | 'ar' + | 'hi' + | 'ru' + | 'bn' + | 'he' + | 'pl' + | 'cs' + | 'nl' + | 'tr' + | 'uk' + | 'id' + | 'th' + | 'ur' + | 'ro' + | 'sv' + | 'el' + | 'hu' + | 'fi' + | 'da' + | 'nb'; -export type Locale = 'en' | 'tr'; +export const LANGUAGE_SWITCH_ORDER: ReadonlyArray = [ + 'en', + 'zh-CN', + 'ja', + 'ko', + 'vi', + 'tl', + 'es', + 'pt', + 'it', + 'de', + 'fr', + 'ar', + 'hi', + 'ru', + 'bn', + 'he', + 'pl', + 'cs', + 'nl', + 'tr', + 'uk', + 'id', + 'th', + 'ur', + 'ro', + 'sv', + 'el', + 'hu', + 'fi', + 'da', + 'nb', +]; -const translations: Record> = { - en: { - // Navigation - 'nav.dashboard': 'Dashboard', - 'nav.agent': 'Agent', - 'nav.tools': 'Tools', - 'nav.cron': 'Scheduled Jobs', - 'nav.integrations': 'Integrations', - 'nav.memory': 'Memory', - 'nav.config': 'Configuration', - 'nav.cost': 'Cost Tracker', - 'nav.logs': 'Logs', - 'nav.doctor': 'Doctor', +export type LocaleDirection = 'ltr' | 'rtl'; - // Dashboard - 'dashboard.title': 'Dashboard', - 'dashboard.provider': 'Provider', - 'dashboard.model': 'Model', - 'dashboard.uptime': 'Uptime', - 'dashboard.temperature': 'Temperature', - 'dashboard.gateway_port': 'Gateway Port', - 'dashboard.locale': 'Locale', - 'dashboard.memory_backend': 'Memory Backend', - 'dashboard.paired': 'Paired', - 'dashboard.channels': 'Channels', - 'dashboard.health': 'Health', - 'dashboard.status': 'Status', - 'dashboard.overview': 'Overview', - 'dashboard.system_info': 'System Information', - 'dashboard.quick_actions': 'Quick Actions', +export interface LanguageOption { + value: Locale; + label: string; + flag: string; + direction: LocaleDirection; +} - // Agent / Chat - 'agent.title': 'Agent Chat', - 'agent.send': 'Send', - 'agent.placeholder': 'Type a message...', - 'agent.connecting': 'Connecting...', - 'agent.connected': 'Connected', - 'agent.disconnected': 'Disconnected', - 'agent.reconnecting': 'Reconnecting...', - 'agent.thinking': 'Thinking...', - 'agent.tool_call': 'Tool Call', - 'agent.tool_result': 'Tool Result', +export const LANGUAGE_OPTIONS: ReadonlyArray = [ + { value: 'en', label: 'English', flag: '🇺🇸', direction: 'ltr' }, + { value: 'zh-CN', label: '简体中文', flag: '🇨🇳', direction: 'ltr' }, + { value: 'ja', label: '日本語', flag: '🇯🇵', direction: 'ltr' }, + { value: 'ko', label: '한국어', flag: '🇰🇷', direction: 'ltr' }, + { value: 'vi', label: 'Tiếng Việt', flag: '🇻🇳', direction: 'ltr' }, + { value: 'tl', label: 'Tagalog', flag: '🇵🇭', direction: 'ltr' }, + { value: 'es', label: 'Español', flag: '🇪🇸', direction: 'ltr' }, + { value: 'pt', label: 'Português', flag: '🇵🇹', direction: 'ltr' }, + { value: 'it', label: 'Italiano', flag: '🇮🇹', direction: 'ltr' }, + { value: 'de', label: 'Deutsch', flag: '🇩🇪', direction: 'ltr' }, + { value: 'fr', label: 'Français', flag: '🇫🇷', direction: 'ltr' }, + { value: 'ar', label: 'العربية', flag: '🇸🇦', direction: 'rtl' }, + { value: 'hi', label: 'हिन्दी', flag: '🇮🇳', direction: 'ltr' }, + { value: 'ru', label: 'Русский', flag: '🇷🇺', direction: 'ltr' }, + { value: 'bn', label: 'বাংলা', flag: '🇧🇩', direction: 'ltr' }, + { value: 'he', label: 'עברית', flag: '🇮🇱', direction: 'rtl' }, + { value: 'pl', label: 'Polski', flag: '🇵🇱', direction: 'ltr' }, + { value: 'cs', label: 'Čeština', flag: '🇨🇿', direction: 'ltr' }, + { value: 'nl', label: 'Nederlands', flag: '🇳🇱', direction: 'ltr' }, + { value: 'tr', label: 'Türkçe', flag: '🇹🇷', direction: 'ltr' }, + { value: 'uk', label: 'Українська', flag: '🇺🇦', direction: 'ltr' }, + { value: 'id', label: 'Bahasa Indonesia', flag: '🇮🇩', direction: 'ltr' }, + { value: 'th', label: 'ไทย', flag: '🇹🇭', direction: 'ltr' }, + { value: 'ur', label: 'اردو', flag: '🇵🇰', direction: 'rtl' }, + { value: 'ro', label: 'Română', flag: '🇷🇴', direction: 'ltr' }, + { value: 'sv', label: 'Svenska', flag: '🇸🇪', direction: 'ltr' }, + { value: 'el', label: 'Ελληνικά', flag: '🇬🇷', direction: 'ltr' }, + { value: 'hu', label: 'Magyar', flag: '🇭🇺', direction: 'ltr' }, + { value: 'fi', label: 'Suomi', flag: '🇫🇮', direction: 'ltr' }, + { value: 'da', label: 'Dansk', flag: '🇩🇰', direction: 'ltr' }, + { value: 'nb', label: 'Norsk Bokmål', flag: '🇳🇴', direction: 'ltr' }, +]; - // Tools - 'tools.title': 'Available Tools', - 'tools.name': 'Name', - 'tools.description': 'Description', - 'tools.parameters': 'Parameters', - 'tools.search': 'Search tools...', - 'tools.empty': 'No tools available.', - 'tools.count': 'Total tools', +const RTL_LOCALES = new Set(['ar', 'he', 'ur']); - // Cron - 'cron.title': 'Scheduled Jobs', - 'cron.add': 'Add Job', - 'cron.delete': 'Delete', - 'cron.enable': 'Enable', - 'cron.disable': 'Disable', - 'cron.name': 'Name', - 'cron.command': 'Command', - 'cron.schedule': 'Schedule', - 'cron.next_run': 'Next Run', - 'cron.last_run': 'Last Run', - 'cron.last_status': 'Last Status', - 'cron.enabled': 'Enabled', - 'cron.empty': 'No scheduled jobs.', - 'cron.confirm_delete': 'Are you sure you want to delete this job?', +export function getLocaleDirection(locale: Locale): LocaleDirection { + return RTL_LOCALES.has(locale) ? 'rtl' : 'ltr'; +} - // Integrations - 'integrations.title': 'Integrations', - 'integrations.available': 'Available', - 'integrations.active': 'Active', - 'integrations.coming_soon': 'Coming Soon', - 'integrations.category': 'Category', - 'integrations.status': 'Status', - 'integrations.search': 'Search integrations...', - 'integrations.empty': 'No integrations found.', - 'integrations.activate': 'Activate', - 'integrations.deactivate': 'Deactivate', +export function getLanguageOption(locale: Locale): LanguageOption { + const matched = LANGUAGE_OPTIONS.find((option) => option.value === locale); + if (matched) { + return matched; + } - // Memory - 'memory.title': 'Memory Store', - 'memory.search': 'Search memory...', - 'memory.add': 'Store Memory', - 'memory.delete': 'Delete', - 'memory.key': 'Key', - 'memory.content': 'Content', - 'memory.category': 'Category', - 'memory.timestamp': 'Timestamp', - 'memory.session': 'Session', - 'memory.score': 'Score', - 'memory.empty': 'No memory entries found.', - 'memory.confirm_delete': 'Are you sure you want to delete this memory entry?', - 'memory.all_categories': 'All Categories', + const fallback = LANGUAGE_OPTIONS.find((option) => option.value === 'en'); + if (!fallback) { + throw new Error('English locale metadata is missing.'); + } - // Config - 'config.title': 'Configuration', - 'config.save': 'Save', - 'config.reset': 'Reset', - 'config.saved': 'Configuration saved successfully.', - 'config.error': 'Failed to save configuration.', - 'config.loading': 'Loading configuration...', - 'config.editor_placeholder': 'TOML configuration...', + return fallback; +} - // Cost - 'cost.title': 'Cost Tracker', - 'cost.session': 'Session Cost', - 'cost.daily': 'Daily Cost', - 'cost.monthly': 'Monthly Cost', - 'cost.total_tokens': 'Total Tokens', - 'cost.request_count': 'Requests', - 'cost.by_model': 'Cost by Model', - 'cost.model': 'Model', - 'cost.tokens': 'Tokens', - 'cost.requests': 'Requests', - 'cost.usd': 'Cost (USD)', +export function getLanguageOptionLabel(option: LanguageOption): string { + return option.label; +} - // Logs - 'logs.title': 'Live Logs', - 'logs.clear': 'Clear', - 'logs.pause': 'Pause', - 'logs.resume': 'Resume', - 'logs.filter': 'Filter logs...', - 'logs.empty': 'No log entries.', - 'logs.connected': 'Connected to event stream.', - 'logs.disconnected': 'Disconnected from event stream.', +export interface LocaleDocumentTarget { + documentElement?: { lang?: string; dir?: string }; + body?: { dir?: string } | null; +} - // Doctor - 'doctor.title': 'System Diagnostics', - 'doctor.run': 'Run Diagnostics', - 'doctor.running': 'Running diagnostics...', - 'doctor.ok': 'OK', - 'doctor.warn': 'Warning', - 'doctor.error': 'Error', - 'doctor.severity': 'Severity', - 'doctor.category': 'Category', - 'doctor.message': 'Message', - 'doctor.empty': 'No diagnostics have been run yet.', - 'doctor.summary': 'Diagnostic Summary', +export function applyLocaleToDocument(locale: Locale, target: LocaleDocumentTarget): void { + const direction = getLocaleDirection(locale); - // Auth / Pairing - 'auth.pair': 'Pair Device', - 'auth.pairing_code': 'Pairing Code', - 'auth.pair_button': 'Pair', - 'auth.logout': 'Logout', - 'auth.pairing_success': 'Pairing successful!', - 'auth.pairing_failed': 'Pairing failed. Please try again.', - 'auth.enter_code': 'Enter your pairing code to connect to the agent.', + if (target.documentElement) { + target.documentElement.lang = locale; + target.documentElement.dir = direction; + } - // Common - 'common.loading': 'Loading...', - 'common.error': 'An error occurred.', - 'common.retry': 'Retry', - 'common.cancel': 'Cancel', - 'common.confirm': 'Confirm', - 'common.save': 'Save', - 'common.delete': 'Delete', - 'common.edit': 'Edit', - 'common.close': 'Close', - 'common.yes': 'Yes', - 'common.no': 'No', - 'common.search': 'Search...', - 'common.no_data': 'No data available.', - 'common.refresh': 'Refresh', - 'common.back': 'Back', - 'common.actions': 'Actions', - 'common.name': 'Name', - 'common.description': 'Description', - 'common.status': 'Status', - 'common.created': 'Created', - 'common.updated': 'Updated', + if (target.body) { + target.body.dir = direction; + } +} - // Health - 'health.title': 'System Health', - 'health.component': 'Component', - 'health.status': 'Status', - 'health.last_ok': 'Last OK', - 'health.last_error': 'Last Error', - 'health.restart_count': 'Restarts', - 'health.pid': 'Process ID', - 'health.uptime': 'Uptime', - 'health.updated_at': 'Last Updated', - }, - - tr: { - // Navigation - 'nav.dashboard': 'Kontrol Paneli', - 'nav.agent': 'Ajan', - 'nav.tools': 'Araclar', - 'nav.cron': 'Zamanlanmis Gorevler', - 'nav.integrations': 'Entegrasyonlar', - 'nav.memory': 'Hafiza', - 'nav.config': 'Yapilandirma', - 'nav.cost': 'Maliyet Takibi', - 'nav.logs': 'Kayitlar', - 'nav.doctor': 'Doktor', - - // Dashboard - 'dashboard.title': 'Kontrol Paneli', - 'dashboard.provider': 'Saglayici', - 'dashboard.model': 'Model', - 'dashboard.uptime': 'Calisma Suresi', - 'dashboard.temperature': 'Sicaklik', - 'dashboard.gateway_port': 'Gecit Portu', - 'dashboard.locale': 'Yerel Ayar', - 'dashboard.memory_backend': 'Hafiza Motoru', - 'dashboard.paired': 'Eslestirilmis', - 'dashboard.channels': 'Kanallar', - 'dashboard.health': 'Saglik', - 'dashboard.status': 'Durum', - 'dashboard.overview': 'Genel Bakis', - 'dashboard.system_info': 'Sistem Bilgisi', - 'dashboard.quick_actions': 'Hizli Islemler', - - // Agent / Chat - 'agent.title': 'Ajan Sohbet', - 'agent.send': 'Gonder', - 'agent.placeholder': 'Bir mesaj yazin...', - 'agent.connecting': 'Baglaniyor...', - 'agent.connected': 'Bagli', - 'agent.disconnected': 'Baglanti Kesildi', - 'agent.reconnecting': 'Yeniden Baglaniyor...', - 'agent.thinking': 'Dusunuyor...', - 'agent.tool_call': 'Arac Cagrisi', - 'agent.tool_result': 'Arac Sonucu', - - // Tools - 'tools.title': 'Mevcut Araclar', - 'tools.name': 'Ad', - 'tools.description': 'Aciklama', - 'tools.parameters': 'Parametreler', - 'tools.search': 'Arac ara...', - 'tools.empty': 'Mevcut arac yok.', - 'tools.count': 'Toplam arac', - - // Cron - 'cron.title': 'Zamanlanmis Gorevler', - 'cron.add': 'Gorev Ekle', - 'cron.delete': 'Sil', - 'cron.enable': 'Etkinlestir', - 'cron.disable': 'Devre Disi Birak', - 'cron.name': 'Ad', - 'cron.command': 'Komut', - 'cron.schedule': 'Zamanlama', - 'cron.next_run': 'Sonraki Calistirma', - 'cron.last_run': 'Son Calistirma', - 'cron.last_status': 'Son Durum', - 'cron.enabled': 'Etkin', - 'cron.empty': 'Zamanlanmis gorev yok.', - 'cron.confirm_delete': 'Bu gorevi silmek istediginizden emin misiniz?', - - // Integrations - 'integrations.title': 'Entegrasyonlar', - 'integrations.available': 'Mevcut', - 'integrations.active': 'Aktif', - 'integrations.coming_soon': 'Yakinda', - 'integrations.category': 'Kategori', - 'integrations.status': 'Durum', - 'integrations.search': 'Entegrasyon ara...', - 'integrations.empty': 'Entegrasyon bulunamadi.', - 'integrations.activate': 'Etkinlestir', - 'integrations.deactivate': 'Devre Disi Birak', - - // Memory - 'memory.title': 'Hafiza Deposu', - 'memory.search': 'Hafizada ara...', - 'memory.add': 'Hafiza Kaydet', - 'memory.delete': 'Sil', - 'memory.key': 'Anahtar', - 'memory.content': 'Icerik', - 'memory.category': 'Kategori', - 'memory.timestamp': 'Zaman Damgasi', - 'memory.session': 'Oturum', - 'memory.score': 'Skor', - 'memory.empty': 'Hafiza kaydi bulunamadi.', - 'memory.confirm_delete': 'Bu hafiza kaydini silmek istediginizden emin misiniz?', - 'memory.all_categories': 'Tum Kategoriler', - - // Config - 'config.title': 'Yapilandirma', - 'config.save': 'Kaydet', - 'config.reset': 'Sifirla', - 'config.saved': 'Yapilandirma basariyla kaydedildi.', - 'config.error': 'Yapilandirma kaydedilemedi.', - 'config.loading': 'Yapilandirma yukleniyor...', - 'config.editor_placeholder': 'TOML yapilandirmasi...', - - // Cost - 'cost.title': 'Maliyet Takibi', - 'cost.session': 'Oturum Maliyeti', - 'cost.daily': 'Gunluk Maliyet', - 'cost.monthly': 'Aylik Maliyet', - 'cost.total_tokens': 'Toplam Token', - 'cost.request_count': 'Istekler', - 'cost.by_model': 'Modele Gore Maliyet', - 'cost.model': 'Model', - 'cost.tokens': 'Token', - 'cost.requests': 'Istekler', - 'cost.usd': 'Maliyet (USD)', - - // Logs - 'logs.title': 'Canli Kayitlar', - 'logs.clear': 'Temizle', - 'logs.pause': 'Duraklat', - 'logs.resume': 'Devam Et', - 'logs.filter': 'Kayitlari filtrele...', - 'logs.empty': 'Kayit girisi yok.', - 'logs.connected': 'Olay akisina baglandi.', - 'logs.disconnected': 'Olay akisi baglantisi kesildi.', - - // Doctor - 'doctor.title': 'Sistem Teshisleri', - 'doctor.run': 'Teshis Calistir', - 'doctor.running': 'Teshisler calistiriliyor...', - 'doctor.ok': 'Tamam', - 'doctor.warn': 'Uyari', - 'doctor.error': 'Hata', - 'doctor.severity': 'Ciddiyet', - 'doctor.category': 'Kategori', - 'doctor.message': 'Mesaj', - 'doctor.empty': 'Henuz teshis calistirilmadi.', - 'doctor.summary': 'Teshis Ozeti', - - // Auth / Pairing - 'auth.pair': 'Cihaz Esle', - 'auth.pairing_code': 'Eslestirme Kodu', - 'auth.pair_button': 'Esle', - 'auth.logout': 'Cikis Yap', - 'auth.pairing_success': 'Eslestirme basarili!', - 'auth.pairing_failed': 'Eslestirme basarisiz. Lutfen tekrar deneyin.', - 'auth.enter_code': 'Ajana baglanmak icin eslestirme kodunuzu girin.', - - // Common - 'common.loading': 'Yukleniyor...', - 'common.error': 'Bir hata olustu.', - 'common.retry': 'Tekrar Dene', - 'common.cancel': 'Iptal', - 'common.confirm': 'Onayla', - 'common.save': 'Kaydet', - 'common.delete': 'Sil', - 'common.edit': 'Duzenle', - 'common.close': 'Kapat', - 'common.yes': 'Evet', - 'common.no': 'Hayir', - 'common.search': 'Ara...', - 'common.no_data': 'Veri mevcut degil.', - 'common.refresh': 'Yenile', - 'common.back': 'Geri', - 'common.actions': 'Islemler', - 'common.name': 'Ad', - 'common.description': 'Aciklama', - 'common.status': 'Durum', - 'common.created': 'Olusturulma', - 'common.updated': 'Guncellenme', - - // Health - 'health.title': 'Sistem Sagligi', - 'health.component': 'Bilesen', - 'health.status': 'Durum', - 'health.last_ok': 'Son Basarili', - 'health.last_error': 'Son Hata', - 'health.restart_count': 'Yeniden Baslatmalar', - 'health.pid': 'Islem Kimligi', - 'health.uptime': 'Calisma Suresi', - 'health.updated_at': 'Son Guncelleme', - }, +export const LANGUAGE_BUTTON_LABELS: Record = { + en: 'EN', + 'zh-CN': '中文', + ja: '日本語', + ko: 'KO', + vi: 'VI', + tl: 'TL', + es: 'ES', + pt: 'PT', + it: 'IT', + de: 'DE', + fr: 'FR', + ar: 'AR', + hi: 'HI', + ru: 'RU', + bn: 'BN', + he: 'HE', + pl: 'PL', + cs: 'CS', + nl: 'NL', + tr: 'TR', + uk: 'UK', + id: 'ID', + th: 'TH', + ur: 'UR', + ro: 'RO', + sv: 'SV', + el: 'EL', + hu: 'HU', + fi: 'FI', + da: 'DA', + nb: 'NB', }; -// --------------------------------------------------------------------------- -// Current locale state -// --------------------------------------------------------------------------- +const en = { + 'nav.dashboard': 'Dashboard', + 'nav.agent': 'Agent', + 'nav.tools': 'Tools', + 'nav.cron': 'Scheduled Jobs', + 'nav.integrations': 'Integrations', + 'nav.memory': 'Memory', + 'nav.config': 'Configuration', + 'nav.cost': 'Cost Tracker', + 'nav.logs': 'Logs', + 'nav.doctor': 'Doctor', + + 'dashboard.title': 'Dashboard', + 'dashboard.provider': 'Provider', + 'dashboard.model': 'Model', + 'dashboard.uptime': 'Uptime', + 'dashboard.temperature': 'Temperature', + 'dashboard.gateway_port': 'Gateway Port', + 'dashboard.locale': 'Locale', + 'dashboard.memory_backend': 'Memory Backend', + 'dashboard.paired': 'Paired', + 'dashboard.channels': 'Channels', + 'dashboard.health': 'Health', + 'dashboard.status': 'Status', + 'dashboard.overview': 'Overview', + 'dashboard.system_info': 'System Information', + 'dashboard.quick_actions': 'Quick Actions', + 'dashboard.load_failed': 'Dashboard load failed', + 'dashboard.load_unknown_error': 'Unknown dashboard load error', + 'dashboard.hero_eyebrow': 'ZeroClaw Command Deck', + 'dashboard.hero_title': 'Electric Runtime Dashboard', + 'dashboard.hero_subtitle': 'Real-time telemetry, cost pulse, and operations status in a single collapsible surface.', + 'dashboard.live_gateway': 'Live Gateway', + 'dashboard.unpaired': 'Unpaired', + 'dashboard.provider_model': 'Provider / Model', + 'dashboard.since_last_restart': 'Since last restart', + 'dashboard.pairing_active': 'Pairing active', + 'dashboard.no_paired_devices': 'No paired devices', + 'dashboard.cost_pulse': 'Cost Pulse', + 'dashboard.cost_subtitle': 'Session, daily, and monthly runtime spend', + 'dashboard.session': 'Session', + 'dashboard.daily': 'Daily', + 'dashboard.monthly': 'Monthly', + 'dashboard.channel_activity': 'Channel Activity', + 'dashboard.channel_subtitle': 'Live integrations and route connectivity', + 'dashboard.no_channels': 'No channels configured.', + 'dashboard.active': 'Active', + 'dashboard.inactive': 'Inactive', + 'dashboard.component_health': 'Component Health', + 'dashboard.component_subtitle': 'Runtime heartbeat and restart awareness', + 'dashboard.no_component_health': 'No component health is currently available.', + 'dashboard.restarts': 'Restarts', + 'dashboard.unknown_provider': 'Unknown', + + 'agent.title': 'Agent Chat', + 'agent.send': 'Send', + 'agent.placeholder': 'Type a message...', + 'agent.connecting': 'Connecting...', + 'agent.connected': 'Connected', + 'agent.disconnected': 'Disconnected', + 'agent.reconnecting': 'Reconnecting...', + 'agent.thinking': 'Thinking...', + 'agent.tool_call': 'Tool Call', + 'agent.tool_result': 'Tool Result', + 'agent.connection_error': 'Connection error. Attempting to reconnect...', + 'agent.failed_send': 'Failed to send message. Please try again.', + 'agent.empty_title': 'ZeroClaw Agent', + 'agent.empty_subtitle': 'Send a message to start the conversation', + 'agent.unknown_error': 'Unknown error', + + 'tools.title': 'Available Tools', + 'tools.name': 'Name', + 'tools.description': 'Description', + 'tools.parameters': 'Parameters', + 'tools.search': 'Search tools...', + 'tools.empty': 'No tools available.', + 'tools.count': 'Total tools', + 'tools.agent_tools': 'Agent Tools', + 'tools.cli_tools': 'CLI Tools', + 'tools.no_search_results': 'No tools match your search.', + 'tools.parameter_schema': 'Parameter Schema', + 'tools.path': 'Path', + 'tools.version': 'Version', + 'tools.load_failed': 'Failed to load tools', + + 'cron.title': 'Scheduled Jobs', + 'cron.add': 'Add Job', + 'cron.delete': 'Delete', + 'cron.enable': 'Enable', + 'cron.disable': 'Disable', + 'cron.name': 'Name', + 'cron.command': 'Command', + 'cron.schedule': 'Schedule', + 'cron.next_run': 'Next Run', + 'cron.last_run': 'Last Run', + 'cron.last_status': 'Last Status', + 'cron.enabled': 'Enabled', + 'cron.empty': 'No scheduled jobs.', + 'cron.confirm_delete': 'Are you sure you want to delete this job?', + 'cron.scheduled_tasks': 'Scheduled Tasks', + 'cron.add_cron_job': 'Add Cron Job', + 'cron.name_optional': 'Name (optional)', + 'cron.schedule_required_command_required': 'Schedule and command are required.', + 'cron.adding': 'Adding...', + 'cron.no_tasks_configured': 'No scheduled tasks configured.', + 'cron.load_failed': 'Failed to load cron jobs', + 'cron.failed_add': 'Failed to add job', + 'cron.failed_delete': 'Failed to delete job', + 'cron.delete_prompt': 'Delete?', + 'cron.id': 'ID', + 'cron.disabled': 'Disabled', + + 'integrations.title': 'Integrations', + 'integrations.available': 'Available', + 'integrations.active': 'Active', + 'integrations.coming_soon': 'Coming Soon', + 'integrations.category': 'Category', + 'integrations.status': 'Status', + 'integrations.search': 'Search integrations...', + 'integrations.empty': 'No integrations found.', + 'integrations.activate': 'Activate', + 'integrations.deactivate': 'Deactivate', + 'integrations.load_failed': 'Failed to load integrations', + 'integrations.all': 'all', + + 'memory.title': 'Memory Store', + 'memory.search': 'Search memory...', + 'memory.add': 'Store Memory', + 'memory.delete': 'Delete', + 'memory.key': 'Key', + 'memory.content': 'Content', + 'memory.category': 'Category', + 'memory.timestamp': 'Timestamp', + 'memory.session': 'Session', + 'memory.score': 'Score', + 'memory.empty': 'No memory entries found.', + 'memory.confirm_delete': 'Are you sure you want to delete this memory entry?', + 'memory.all_categories': 'All Categories', + 'memory.add_memory': 'Add Memory', + 'memory.search_entries': 'Search memory entries...', + 'memory.load_failed': 'Failed to load memory', + 'memory.key_content_required': 'Key and content are required.', + 'memory.failed_store': 'Failed to store memory', + 'memory.failed_delete': 'Failed to delete memory', + 'memory.category_optional': 'Category (optional)', + 'memory.key_placeholder': 'e.g. user_preferences', + 'memory.content_placeholder': 'Memory content...', + 'memory.category_placeholder': 'e.g. preferences, context, facts', + 'memory.search_button': 'Search', + 'memory.saving': 'Saving...', + 'memory.delete_prompt': 'Delete?', + + 'config.title': 'Configuration', + 'config.save': 'Save', + 'config.reset': 'Reset', + 'config.saved': 'Configuration saved successfully.', + 'config.error': 'Failed to save configuration.', + 'config.loading': 'Loading configuration...', + 'config.editor_placeholder': 'TOML configuration...', + 'config.saving': 'Saving...', + 'config.masked_title': 'Sensitive fields are masked', + 'config.masked_description': 'API keys, tokens, and passwords are hidden for security. To update a masked field, replace the entire masked value with your new value.', + 'config.toml_configuration': 'TOML Configuration', + 'config.lines': 'lines', + + 'cost.title': 'Cost Tracker', + 'cost.session': 'Session Cost', + 'cost.daily': 'Daily Cost', + 'cost.monthly': 'Monthly Cost', + 'cost.total_tokens': 'Total Tokens', + 'cost.request_count': 'Requests', + 'cost.by_model': 'Cost by Model', + 'cost.model': 'Model', + 'cost.tokens': 'Tokens', + 'cost.requests': 'Requests', + 'cost.usd': 'Cost (USD)', + 'cost.total_requests': 'Total Requests', + 'cost.token_statistics': 'Token Statistics', + 'cost.avg_tokens_per_request': 'Avg Tokens / Request', + 'cost.cost_per_1k_tokens': 'Cost per 1K Tokens', + 'cost.model_breakdown': 'Model Breakdown', + 'cost.no_model_data': 'No model data available.', + 'cost.share': 'Share', + 'cost.load_failed': 'Failed to load cost data', + + 'logs.title': 'Live Logs', + 'logs.clear': 'Clear', + 'logs.pause': 'Pause', + 'logs.resume': 'Resume', + 'logs.filter': 'Filter logs...', + 'logs.empty': 'No log entries.', + 'logs.connected': 'Connected to event stream.', + 'logs.disconnected': 'Disconnected from event stream.', + 'logs.events': 'events', + 'logs.jump_to_bottom': 'Jump to bottom', + 'logs.filter_label': 'Filter:', + 'logs.paused_stream': 'Log streaming is paused.', + 'logs.waiting_for_events': 'Waiting for events...', + + 'doctor.title': 'System Diagnostics', + 'doctor.run': 'Run Diagnostics', + 'doctor.running': 'Running diagnostics...', + 'doctor.ok': 'OK', + 'doctor.warn': 'Warning', + 'doctor.error': 'Error', + 'doctor.severity': 'Severity', + 'doctor.category': 'Category', + 'doctor.message': 'Message', + 'doctor.empty': 'No diagnostics have been run yet.', + 'doctor.summary': 'Diagnostic Summary', + 'doctor.running_short': 'Running...', + 'doctor.running_hint': 'This may take a few seconds.', + 'doctor.issues_found': 'Issues Found', + 'doctor.warnings': 'Warnings', + 'doctor.all_clear': 'All Clear', + 'doctor.instructions': 'Click "Run Diagnostics" to check your ZeroClaw installation.', + + 'auth.pair': 'Pair Device', + 'auth.pairing_code': 'Pairing Code', + 'auth.pair_button': 'Pair', + 'auth.logout': 'Logout', + 'auth.pairing_success': 'Pairing successful!', + 'auth.pairing_failed': 'Pairing failed. Please try again.', + 'auth.enter_code': 'Enter the one-time pairing code from your terminal', + 'auth.code_placeholder': '6-digit code', + 'auth.pairing_progress': 'Pairing...', + + 'common.loading': 'Loading...', + 'common.error': 'An error occurred.', + 'common.retry': 'Retry', + 'common.cancel': 'Cancel', + 'common.confirm': 'Confirm', + 'common.save': 'Save', + 'common.delete': 'Delete', + 'common.edit': 'Edit', + 'common.close': 'Close', + 'common.yes': 'Yes', + 'common.no': 'No', + 'common.search': 'Search...', + 'common.no_data': 'No data available.', + 'common.refresh': 'Refresh', + 'common.back': 'Back', + 'common.actions': 'Actions', + 'common.name': 'Name', + 'common.description': 'Description', + 'common.status': 'Status', + 'common.created': 'Created', + 'common.updated': 'Updated', + 'common.languages': 'Languages', + 'common.select_language': 'Select language', + 'common.connecting': 'Connecting...', + + 'health.title': 'System Health', + 'health.component': 'Component', + 'health.status': 'Status', + 'health.last_ok': 'Last OK', + 'health.last_error': 'Last Error', + 'health.restart_count': 'Restart Count', + 'health.pid': 'Process ID', + 'health.uptime': 'Uptime', + 'health.updated_at': 'Updated At', + + 'header.dashboard_tagline': 'ZeroClaw dashboard', + 'sidebar.gateway_dashboard': 'Gateway + Dashboard', + 'sidebar.runtime_mode': 'Runtime Mode', + 'navigation.open': 'Open navigation', + 'navigation.close': 'Close navigation', + 'navigation.expand': 'Expand navigation', + 'navigation.collapse': 'Collapse navigation', +} satisfies Record; + +const tr = { + ...en, + 'nav.dashboard': 'Kontrol Paneli', + 'nav.agent': 'Ajan', + 'nav.tools': 'Araçlar', + 'nav.cron': 'Zamanlanmış Görevler', + 'nav.integrations': 'Entegrasyonlar', + 'nav.memory': 'Hafıza', + 'nav.config': 'Yapılandırma', + 'nav.cost': 'Maliyet Takibi', + 'nav.logs': 'Kayıtlar', + 'nav.doctor': 'Doktor', + 'agent.title': 'Ajan Sohbeti', + 'agent.send': 'Gönder', + 'agent.placeholder': 'Bir mesaj yazın...', + 'agent.connecting': 'Bağlanıyor...', + 'agent.connected': 'Bağlı', + 'agent.disconnected': 'Bağlantı Kesildi', + 'agent.reconnecting': 'Yeniden bağlanıyor...', + 'agent.thinking': 'Düşünüyor...', + 'agent.tool_call': 'Araç Çağrısı', + 'agent.tool_result': 'Araç Sonucu', + 'agent.connection_error': 'Bağlantı hatası. Yeniden bağlanmaya çalışılıyor...', + 'agent.failed_send': 'Mesaj gönderilemedi. Lütfen tekrar deneyin.', + 'agent.empty_title': 'ZeroClaw Ajanı', + 'agent.empty_subtitle': 'Konuşmayı başlatmak için bir mesaj gönderin', + 'dashboard.title': 'Kontrol Paneli', + 'dashboard.provider': 'Sağlayıcı', + 'dashboard.model': 'Model', + 'dashboard.uptime': 'Çalışma Süresi', + 'dashboard.temperature': 'Sıcaklık', + 'dashboard.gateway_port': 'Ağ Geçidi Portu', + 'dashboard.locale': 'Yerel Ayar', + 'dashboard.memory_backend': 'Hafıza Arka Ucu', + 'dashboard.hero_eyebrow': 'ZeroClaw Komuta Güvertesi', + 'dashboard.hero_title': 'Elektrik Çalışma Zamanı Paneli', + 'dashboard.hero_subtitle': 'Gerçek zamanlı telemetri, maliyet akışı ve operasyon durumunu tek bir daraltılabilir yüzeyde görün.', + 'dashboard.live_gateway': 'Canlı Ağ Geçidi', + 'dashboard.unpaired': 'Eşleşmemiş', + 'dashboard.provider_model': 'Sağlayıcı / Model', + 'dashboard.since_last_restart': 'Son yeniden başlatmadan beri', + 'dashboard.pairing_active': 'Eşleştirme etkin', + 'dashboard.no_paired_devices': 'Eşleşmiş cihaz yok', + 'dashboard.cost_pulse': 'Maliyet Nabzı', + 'dashboard.cost_subtitle': 'Oturum, günlük ve aylık çalışma zamanı harcaması', + 'dashboard.session': 'Oturum', + 'dashboard.daily': 'Günlük', + 'dashboard.monthly': 'Aylık', + 'dashboard.channel_activity': 'Kanal Etkinliği', + 'dashboard.channel_subtitle': 'Canlı entegrasyonlar ve rota bağlantısı', + 'dashboard.no_channels': 'Hiç kanal yapılandırılmamış.', + 'dashboard.active': 'Aktif', + 'dashboard.inactive': 'Pasif', + 'dashboard.component_health': 'Bileşen Sağlığı', + 'dashboard.component_subtitle': 'Çalışma zamanı nabzı ve yeniden başlatma farkındalığı', + 'dashboard.no_component_health': 'Şu anda bileşen sağlığı bilgisi yok.', + 'dashboard.restarts': 'Yeniden Başlatmalar', + 'tools.title': 'Mevcut Araçlar', + 'tools.search': 'Araç ara...', + 'tools.agent_tools': 'Ajan Araçları', + 'tools.cli_tools': 'CLI Araçları', + 'tools.no_search_results': 'Aramanızla eşleşen araç yok.', + 'tools.parameter_schema': 'Parametre Şeması', + 'tools.path': 'Yol', + 'tools.version': 'Sürüm', + 'cron.title': 'Zamanlanmış Görevler', + 'cron.add': 'Görev Ekle', + 'cron.scheduled_tasks': 'Zamanlanmış Görevler', + 'cron.add_cron_job': 'Cron Görevi Ekle', + 'cron.name_optional': 'Ad (isteğe bağlı)', + 'cron.schedule_required_command_required': 'Zamanlama ve komut gereklidir.', + 'cron.adding': 'Ekleniyor...', + 'cron.no_tasks_configured': 'Zamanlanmış görev yapılandırılmamış.', + 'cron.load_failed': 'Cron görevleri yüklenemedi', + 'cron.failed_add': 'Görev eklenemedi', + 'cron.failed_delete': 'Görev silinemedi', + 'cron.delete_prompt': 'Silinsin mi?', + 'cron.disabled': 'Devre Dışı', + 'integrations.title': 'Entegrasyonlar', + 'integrations.available': 'Mevcut', + 'integrations.active': 'Aktif', + 'integrations.coming_soon': 'Yakında', + 'integrations.empty': 'Entegrasyon bulunamadı.', + 'integrations.load_failed': 'Entegrasyonlar yüklenemedi', + 'memory.title': 'Hafıza Deposu', + 'memory.add_memory': 'Hafıza Ekle', + 'memory.search_entries': 'Hafıza girdilerinde ara...', + 'memory.all_categories': 'Tüm Kategoriler', + 'memory.search_button': 'Ara', + 'memory.load_failed': 'Hafıza yüklenemedi', + 'memory.key_content_required': 'Anahtar ve içerik gereklidir.', + 'memory.failed_store': 'Hafıza kaydedilemedi', + 'memory.failed_delete': 'Hafıza silinemedi', + 'memory.category_optional': 'Kategori (isteğe bağlı)', + 'memory.key_placeholder': 'örn. kullanici_tercihleri', + 'memory.content_placeholder': 'Hafıza içeriği...', + 'memory.category_placeholder': 'örn. tercihler, bağlam, gerçekler', + 'memory.saving': 'Kaydediliyor...', + 'memory.delete_prompt': 'Silinsin mi?', + 'config.title': 'Yapılandırma', + 'config.save': 'Kaydet', + 'config.saved': 'Yapılandırma başarıyla kaydedildi.', + 'config.saving': 'Kaydediliyor...', + 'config.masked_title': 'Hassas alanlar maskelendi', + 'config.masked_description': 'Güvenlik için API anahtarları, belirteçler ve parolalar gizlenir. Maskelenmiş bir alanı güncellemek için tüm maskeli değeri yeni değerinizle değiştirin.', + 'config.toml_configuration': 'TOML Yapılandırması', + 'config.lines': 'satır', + 'cost.title': 'Maliyet Takibi', + 'cost.session': 'Oturum Maliyeti', + 'cost.daily': 'Günlük Maliyet', + 'cost.monthly': 'Aylık Maliyet', + 'cost.total_requests': 'Toplam İstek', + 'cost.token_statistics': 'Belirteç İstatistikleri', + 'cost.avg_tokens_per_request': 'İstek Başına Ort. Belirteç', + 'cost.cost_per_1k_tokens': '1K Belirteç Başına Maliyet', + 'cost.model_breakdown': 'Model Dağılımı', + 'cost.no_model_data': 'Model verisi yok.', + 'cost.share': 'Pay', + 'cost.load_failed': 'Maliyet verisi yüklenemedi', + 'logs.title': 'Canlı Kayıtlar', + 'logs.pause': 'Duraklat', + 'logs.resume': 'Sürdür', + 'logs.events': 'olay', + 'logs.jump_to_bottom': 'Alta git', + 'logs.filter_label': 'Filtre:', + 'logs.paused_stream': 'Kayıt akışı duraklatıldı.', + 'logs.waiting_for_events': 'Olaylar bekleniyor...', + 'doctor.title': 'Sistem Teşhisleri', + 'doctor.run': 'Teşhisleri Çalıştır', + 'doctor.running': 'Teşhisler çalıştırılıyor...', + 'doctor.running_short': 'Çalışıyor...', + 'doctor.running_hint': 'Bu birkaç saniye sürebilir.', + 'doctor.warn': 'Uyarı', + 'doctor.issues_found': 'Sorunlar Bulundu', + 'doctor.warnings': 'Uyarılar', + 'doctor.all_clear': 'Temiz', + 'doctor.instructions': 'ZeroClaw kurulumunuzu kontrol etmek için "Teşhisleri Çalıştır" düğmesine tıklayın.', + 'auth.pair': 'Cihazı Eşle', + 'auth.pair_button': 'Eşle', + 'auth.logout': 'Çıkış Yap', + 'auth.enter_code': 'Terminalinizdeki tek kullanımlık eşleştirme kodunu girin', + 'auth.code_placeholder': '6 haneli kod', + 'auth.pairing_progress': 'Eşleştiriliyor...', + 'common.languages': 'Diller', + 'common.select_language': 'Dil seçin', + 'common.connecting': 'Bağlanıyor...', + 'header.dashboard_tagline': 'ZeroClaw paneli', + 'sidebar.gateway_dashboard': 'Ağ Geçidi + Panel', + 'sidebar.runtime_mode': 'Çalışma Modu', + 'navigation.open': 'Gezinmeyi aç', + 'navigation.close': 'Gezinmeyi kapat', + 'navigation.expand': 'Gezinmeyi genişlet', + 'navigation.collapse': 'Gezinmeyi daralt', +}; + +const zhCn = { + ...en, + 'nav.dashboard': '仪表盘', + 'nav.agent': '代理', + 'nav.tools': '工具', + 'nav.cron': '定时任务', + 'nav.integrations': '集成', + 'nav.memory': '记忆', + 'nav.config': '配置', + 'nav.cost': '成本跟踪', + 'nav.logs': '日志', + 'nav.doctor': '诊断', + 'dashboard.hero_title': '电光运行仪表盘', + 'dashboard.live_gateway': '在线网关', + 'dashboard.unpaired': '未配对', + 'agent.title': '代理聊天', + 'agent.placeholder': '输入消息…', + 'agent.connecting': '正在连接…', + 'agent.connected': '已连接', + 'agent.disconnected': '已断开', + 'tools.search': '搜索工具…', + 'tools.agent_tools': '代理工具', + 'tools.cli_tools': 'CLI 工具', + 'cron.add': '添加任务', + 'cron.scheduled_tasks': '定时任务', + 'integrations.title': '集成', + 'memory.add_memory': '添加记忆', + 'memory.search_entries': '搜索记忆条目…', + 'config.save': '保存', + 'config.saving': '正在保存…', + 'cost.session': '会话成本', + 'cost.daily': '每日成本', + 'cost.monthly': '每月成本', + 'logs.title': '实时日志', + 'logs.pause': '暂停', + 'logs.resume': '继续', + 'doctor.title': '系统诊断', + 'doctor.run': '运行诊断', + 'doctor.running_short': '运行中…', + 'auth.pair_button': '配对', + 'auth.enter_code': '输入终端中的一次性配对码', + 'auth.code_placeholder': '6 位代码', + 'auth.pairing_progress': '正在配对…', + 'auth.logout': '退出', + 'common.languages': '语言', + 'common.select_language': '选择语言', + 'header.dashboard_tagline': 'ZeroClaw 仪表盘', +}; + +const ja = { + ...en, + 'nav.dashboard': 'ダッシュボード', + 'nav.agent': 'エージェント', + 'nav.tools': 'ツール', + 'nav.cron': 'スケジュール', + 'nav.integrations': '連携', + 'nav.memory': 'メモリ', + 'nav.config': '設定', + 'nav.cost': 'コスト', + 'nav.logs': 'ログ', + 'nav.doctor': '診断', + 'dashboard.hero_title': 'エレクトリック・ランタイム・ダッシュボード', + 'dashboard.live_gateway': 'ライブゲートウェイ', + 'dashboard.unpaired': '未ペア', + 'agent.title': 'エージェントチャット', + 'agent.placeholder': 'メッセージを入力…', + 'agent.connecting': '接続中…', + 'agent.connected': '接続済み', + 'agent.disconnected': '切断済み', + 'tools.search': 'ツールを検索…', + 'tools.agent_tools': 'エージェントツール', + 'tools.cli_tools': 'CLI ツール', + 'cron.add': 'ジョブを追加', + 'cron.scheduled_tasks': 'スケジュールされたジョブ', + 'integrations.title': '連携', + 'memory.add_memory': 'メモリを追加', + 'memory.search_entries': 'メモリエントリを検索…', + 'config.save': '保存', + 'config.saving': '保存中…', + 'cost.session': 'セッションコスト', + 'cost.daily': '日次コスト', + 'cost.monthly': '月次コスト', + 'logs.title': 'ライブログ', + 'logs.pause': '一時停止', + 'logs.resume': '再開', + 'doctor.title': 'システム診断', + 'doctor.run': '診断を実行', + 'doctor.running_short': '実行中…', + 'auth.pair_button': 'ペアリング', + 'auth.enter_code': '端末のワンタイムペアリングコードを入力してください', + 'auth.code_placeholder': '6桁のコード', + 'auth.pairing_progress': 'ペアリング中…', + 'auth.logout': 'ログアウト', + 'common.languages': '言語', + 'common.select_language': '言語を選択', + 'header.dashboard_tagline': 'ZeroClaw ダッシュボード', +}; + +const ru = { + ...en, + 'nav.dashboard': 'Панель', + 'nav.agent': 'Агент', + 'nav.tools': 'Инструменты', + 'nav.cron': 'Задания', + 'nav.integrations': 'Интеграции', + 'nav.memory': 'Память', + 'nav.config': 'Конфигурация', + 'nav.cost': 'Расходы', + 'nav.logs': 'Логи', + 'nav.doctor': 'Диагностика', + 'dashboard.hero_title': 'Панель электрического рантайма', + 'dashboard.live_gateway': 'Живой шлюз', + 'dashboard.unpaired': 'Не сопряжено', + 'agent.title': 'Чат агента', + 'agent.placeholder': 'Введите сообщение…', + 'agent.connecting': 'Подключение…', + 'agent.connected': 'Подключено', + 'agent.disconnected': 'Отключено', + 'tools.search': 'Поиск инструментов…', + 'tools.agent_tools': 'Инструменты агента', + 'tools.cli_tools': 'CLI-инструменты', + 'cron.add': 'Добавить задачу', + 'cron.scheduled_tasks': 'Запланированные задания', + 'integrations.title': 'Интеграции', + 'memory.add_memory': 'Добавить память', + 'memory.search_entries': 'Искать записи памяти…', + 'config.save': 'Сохранить', + 'config.saving': 'Сохранение…', + 'cost.session': 'Стоимость сессии', + 'cost.daily': 'Стоимость за день', + 'cost.monthly': 'Стоимость за месяц', + 'logs.title': 'Живые логи', + 'logs.pause': 'Пауза', + 'logs.resume': 'Продолжить', + 'doctor.title': 'Диагностика системы', + 'doctor.run': 'Запустить диагностику', + 'doctor.running_short': 'Выполняется…', + 'auth.pair_button': 'Сопрячь', + 'auth.enter_code': 'Введите одноразовый код сопряжения из терминала', + 'auth.code_placeholder': '6-значный код', + 'auth.pairing_progress': 'Сопряжение…', + 'auth.logout': 'Выйти', + 'common.languages': 'Языки', + 'common.select_language': 'Выберите язык', + 'header.dashboard_tagline': 'Панель ZeroClaw', +}; + +const fr = { + ...en, + 'nav.dashboard': 'Tableau de bord', + 'nav.agent': 'Agent', + 'nav.tools': 'Outils', + 'nav.cron': 'Tâches planifiées', + 'nav.integrations': 'Intégrations', + 'nav.memory': 'Mémoire', + 'nav.config': 'Configuration', + 'nav.cost': 'Coûts', + 'nav.logs': 'Journaux', + 'nav.doctor': 'Diagnostic', + 'dashboard.hero_title': 'Tableau de bord runtime électrique', + 'dashboard.live_gateway': 'Passerelle active', + 'dashboard.unpaired': 'Non appairé', + 'agent.title': 'Chat agent', + 'agent.placeholder': 'Saisissez un message…', + 'agent.connecting': 'Connexion…', + 'agent.connected': 'Connecté', + 'agent.disconnected': 'Déconnecté', + 'tools.search': 'Rechercher des outils…', + 'tools.agent_tools': 'Outils agent', + 'tools.cli_tools': 'Outils CLI', + 'cron.add': 'Ajouter une tâche', + 'cron.scheduled_tasks': 'Tâches planifiées', + 'integrations.title': 'Intégrations', + 'memory.add_memory': 'Ajouter une mémoire', + 'memory.search_entries': 'Rechercher dans la mémoire…', + 'config.save': 'Enregistrer', + 'config.saving': 'Enregistrement…', + 'cost.session': 'Coût de session', + 'cost.daily': 'Coût journalier', + 'cost.monthly': 'Coût mensuel', + 'logs.title': 'Journaux en direct', + 'logs.pause': 'Pause', + 'logs.resume': 'Reprendre', + 'doctor.title': 'Diagnostic système', + 'doctor.run': 'Lancer le diagnostic', + 'doctor.running_short': 'Exécution…', + 'auth.pair_button': 'Associer', + 'auth.enter_code': 'Entrez le code d’appairage à usage unique affiché dans le terminal', + 'auth.code_placeholder': 'Code à 6 chiffres', + 'auth.pairing_progress': 'Appairage…', + 'auth.logout': 'Déconnexion', + 'common.languages': 'Langues', + 'common.select_language': 'Choisir la langue', + 'header.dashboard_tagline': 'Tableau de bord ZeroClaw', +}; + +const vi = { + ...en, + 'nav.dashboard': 'Bảng điều khiển', + 'nav.agent': 'Tác tử', + 'nav.tools': 'Công cụ', + 'nav.cron': 'Lịch tác vụ', + 'nav.integrations': 'Tích hợp', + 'nav.memory': 'Bộ nhớ', + 'nav.config': 'Cấu hình', + 'nav.cost': 'Chi phí', + 'nav.logs': 'Nhật ký', + 'nav.doctor': 'Chẩn đoán', + 'dashboard.hero_title': 'Bảng điều khiển runtime điện xanh', + 'dashboard.live_gateway': 'Cổng hoạt động', + 'dashboard.unpaired': 'Chưa ghép đôi', + 'agent.title': 'Trò chuyện với tác tử', + 'agent.placeholder': 'Nhập tin nhắn…', + 'agent.connecting': 'Đang kết nối…', + 'agent.connected': 'Đã kết nối', + 'agent.disconnected': 'Đã ngắt kết nối', + 'tools.search': 'Tìm công cụ…', + 'tools.agent_tools': 'Công cụ tác tử', + 'tools.cli_tools': 'Công cụ CLI', + 'cron.add': 'Thêm tác vụ', + 'cron.scheduled_tasks': 'Tác vụ đã lên lịch', + 'integrations.title': 'Tích hợp', + 'memory.add_memory': 'Thêm bộ nhớ', + 'memory.search_entries': 'Tìm trong bộ nhớ…', + 'config.save': 'Lưu', + 'config.saving': 'Đang lưu…', + 'cost.session': 'Chi phí phiên', + 'cost.daily': 'Chi phí ngày', + 'cost.monthly': 'Chi phí tháng', + 'logs.title': 'Nhật ký trực tiếp', + 'logs.pause': 'Tạm dừng', + 'logs.resume': 'Tiếp tục', + 'doctor.title': 'Chẩn đoán hệ thống', + 'doctor.run': 'Chạy chẩn đoán', + 'doctor.running_short': 'Đang chạy…', + 'auth.pair_button': 'Ghép đôi', + 'auth.enter_code': 'Nhập mã ghép đôi một lần từ terminal', + 'auth.code_placeholder': 'Mã 6 chữ số', + 'auth.pairing_progress': 'Đang ghép đôi…', + 'auth.logout': 'Đăng xuất', + 'common.languages': 'Ngôn ngữ', + 'common.select_language': 'Chọn ngôn ngữ', + 'header.dashboard_tagline': 'Bảng điều khiển ZeroClaw', +}; + + +const createLocale = (overrides: Record) => ({ + ...en, + ...overrides, +}); + +const ko = createLocale({ + 'nav.dashboard': '대시보드', + 'nav.agent': '에이전트', + 'nav.tools': '도구', + 'nav.cron': '예약 작업', + 'nav.integrations': '통합', + 'nav.memory': '메모리', + 'nav.config': '설정', + 'nav.cost': '비용 추적', + 'nav.logs': '로그', + 'nav.doctor': '진단', + 'dashboard.hero_title': '전기 런타임 대시보드', + 'agent.placeholder': '메시지를 입력하세요…', + 'tools.search': '도구 검색…', + 'cron.add': '작업 추가', + 'memory.add_memory': '메모리 추가', + 'config.save': '저장', + 'cost.token_statistics': '토큰 통계', + 'logs.title': '실시간 로그', + 'doctor.title': '시스템 진단', + 'auth.pair_button': '페어링', + 'auth.enter_code': '터미널에 표시된 일회용 페어링 코드를 입력하세요', + 'auth.code_placeholder': '6자리 코드', + 'auth.pairing_progress': '페어링 중…', + 'auth.logout': '로그아웃', + 'common.languages': '언어', + 'common.select_language': '언어 선택', + 'header.dashboard_tagline': 'ZeroClaw 대시보드', +}); + +const tl = createLocale({ + 'nav.dashboard': 'Dashboard', + 'nav.agent': 'Ahente', + 'nav.tools': 'Mga Tool', + 'nav.cron': 'Naka-iskedyul na Trabaho', + 'nav.integrations': 'Mga Integrasyon', + 'nav.memory': 'Alaala', + 'nav.config': 'Konpigurasyon', + 'nav.cost': 'Pagsubaybay sa Gastos', + 'nav.logs': 'Mga Log', + 'nav.doctor': 'Diyagnostiko', + 'dashboard.hero_title': 'Elektrikong Dashboard ng Runtime', + 'agent.placeholder': 'Mag-type ng mensahe…', + 'tools.search': 'Maghanap ng tool…', + 'cron.add': 'Magdagdag ng gawain', + 'memory.add_memory': 'Magdagdag ng alaala', + 'config.save': 'I-save', + 'cost.token_statistics': 'Estadistika ng Token', + 'logs.title': 'Mga Live Log', + 'doctor.title': 'Diyagnostiko ng System', + 'auth.pair_button': 'Ipares', + 'auth.enter_code': 'Ilagay ang isang beses na pairing code mula sa terminal', + 'auth.code_placeholder': '6-digit na code', + 'auth.pairing_progress': 'Pinapares…', + 'auth.logout': 'Mag-logout', + 'common.languages': 'Mga Wika', + 'common.select_language': 'Piliin ang wika', + 'header.dashboard_tagline': 'Dashboard ng ZeroClaw', +}); + +const es = createLocale({ + 'nav.dashboard': 'Panel', + 'nav.agent': 'Agente', + 'nav.tools': 'Herramientas', + 'nav.cron': 'Tareas programadas', + 'nav.integrations': 'Integraciones', + 'nav.memory': 'Memoria', + 'nav.config': 'Configuración', + 'nav.cost': 'Costos', + 'nav.logs': 'Registros', + 'nav.doctor': 'Diagnóstico', + 'dashboard.hero_title': 'Panel eléctrico del runtime', + 'agent.placeholder': 'Escribe un mensaje…', + 'tools.search': 'Buscar herramientas…', + 'cron.add': 'Agregar tarea', + 'memory.add_memory': 'Agregar memoria', + 'config.save': 'Guardar', + 'cost.token_statistics': 'Estadísticas de tokens', + 'logs.title': 'Registros en vivo', + 'doctor.title': 'Diagnóstico del sistema', + 'auth.pair_button': 'Vincular', + 'auth.enter_code': 'Introduce el código de vinculación de un solo uso del terminal', + 'auth.code_placeholder': 'Código de 6 dígitos', + 'auth.pairing_progress': 'Vinculando…', + 'auth.logout': 'Cerrar sesión', + 'common.languages': 'Idiomas', + 'common.select_language': 'Elegir idioma', + 'header.dashboard_tagline': 'Panel de ZeroClaw', +}); + +const pt = createLocale({ + 'nav.dashboard': 'Painel', + 'nav.agent': 'Agente', + 'nav.tools': 'Ferramentas', + 'nav.cron': 'Tarefas agendadas', + 'nav.integrations': 'Integrações', + 'nav.memory': 'Memória', + 'nav.config': 'Configuração', + 'nav.cost': 'Custos', + 'nav.logs': 'Logs', + 'nav.doctor': 'Diagnóstico', + 'dashboard.hero_title': 'Painel elétrico do runtime', + 'agent.placeholder': 'Digite uma mensagem…', + 'tools.search': 'Buscar ferramentas…', + 'cron.add': 'Adicionar tarefa', + 'memory.add_memory': 'Adicionar memória', + 'config.save': 'Salvar', + 'cost.token_statistics': 'Estatísticas de tokens', + 'logs.title': 'Logs ao vivo', + 'doctor.title': 'Diagnóstico do sistema', + 'auth.pair_button': 'Parear', + 'auth.enter_code': 'Digite o código único de pareamento mostrado no terminal', + 'auth.code_placeholder': 'Código de 6 dígitos', + 'auth.pairing_progress': 'Pareando…', + 'auth.logout': 'Sair', + 'common.languages': 'Idiomas', + 'common.select_language': 'Escolher idioma', + 'header.dashboard_tagline': 'Painel do ZeroClaw', +}); + +const it = createLocale({ + 'nav.dashboard': 'Dashboard', + 'nav.agent': 'Agente', + 'nav.tools': 'Strumenti', + 'nav.cron': 'Attività pianificate', + 'nav.integrations': 'Integrazioni', + 'nav.memory': 'Memoria', + 'nav.config': 'Configurazione', + 'nav.cost': 'Costi', + 'nav.logs': 'Log', + 'nav.doctor': 'Diagnostica', + 'dashboard.hero_title': 'Dashboard runtime elettrica', + 'agent.placeholder': 'Scrivi un messaggio…', + 'tools.search': 'Cerca strumenti…', + 'cron.add': 'Aggiungi attività', + 'memory.add_memory': 'Aggiungi memoria', + 'config.save': 'Salva', + 'cost.token_statistics': 'Statistiche token', + 'logs.title': 'Log in tempo reale', + 'doctor.title': 'Diagnostica di sistema', + 'auth.pair_button': 'Associa', + 'auth.enter_code': 'Inserisci il codice di associazione monouso dal terminale', + 'auth.code_placeholder': 'Codice a 6 cifre', + 'auth.pairing_progress': 'Associazione…', + 'auth.logout': 'Disconnetti', + 'common.languages': 'Lingue', + 'common.select_language': 'Scegli lingua', + 'header.dashboard_tagline': 'Dashboard di ZeroClaw', +}); + +const de = createLocale({ + 'nav.dashboard': 'Dashboard', + 'nav.agent': 'Agent', + 'nav.tools': 'Werkzeuge', + 'nav.cron': 'Geplante Aufgaben', + 'nav.integrations': 'Integrationen', + 'nav.memory': 'Speicher', + 'nav.config': 'Konfiguration', + 'nav.cost': 'Kosten', + 'nav.logs': 'Protokolle', + 'nav.doctor': 'Diagnose', + 'dashboard.hero_title': 'Elektrisches Runtime-Dashboard', + 'agent.placeholder': 'Nachricht eingeben…', + 'tools.search': 'Werkzeuge suchen…', + 'cron.add': 'Aufgabe hinzufügen', + 'memory.add_memory': 'Speicher hinzufügen', + 'config.save': 'Speichern', + 'cost.token_statistics': 'Token-Statistiken', + 'logs.title': 'Live-Protokolle', + 'doctor.title': 'Systemdiagnose', + 'auth.pair_button': 'Koppeln', + 'auth.enter_code': 'Geben Sie den einmaligen Kopplungscode aus dem Terminal ein', + 'auth.code_placeholder': '6-stelliger Code', + 'auth.pairing_progress': 'Kopplung…', + 'auth.logout': 'Abmelden', + 'common.languages': 'Sprachen', + 'common.select_language': 'Sprache auswählen', + 'header.dashboard_tagline': 'ZeroClaw-Dashboard', +}); + +const ar = createLocale({ + 'nav.dashboard': 'لوحة التحكم', + 'nav.agent': 'الوكيل', + 'nav.tools': 'الأدوات', + 'nav.cron': 'المهام المجدولة', + 'nav.integrations': 'التكاملات', + 'nav.memory': 'الذاكرة', + 'nav.config': 'الإعدادات', + 'nav.cost': 'تتبع التكلفة', + 'nav.logs': 'السجلات', + 'nav.doctor': 'التشخيص', + 'dashboard.hero_title': 'لوحة تشغيل كهربائية', + 'agent.placeholder': 'اكتب رسالة…', + 'tools.search': 'ابحث في الأدوات…', + 'cron.add': 'إضافة مهمة', + 'memory.add_memory': 'إضافة ذاكرة', + 'config.save': 'حفظ', + 'cost.token_statistics': 'إحصاءات الرموز', + 'logs.title': 'السجلات المباشرة', + 'doctor.title': 'تشخيص النظام', + 'auth.pair_button': 'اقتران', + 'auth.enter_code': 'أدخل رمز الاقتران لمرة واحدة من الطرفية', + 'auth.code_placeholder': 'رمز من 6 أرقام', + 'auth.pairing_progress': 'جارٍ الاقتران…', + 'auth.logout': 'تسجيل الخروج', + 'common.languages': 'اللغات', + 'common.select_language': 'اختر اللغة', + 'header.dashboard_tagline': 'لوحة ZeroClaw', +}); + +const hi = createLocale({ + 'nav.dashboard': 'डैशबोर्ड', + 'nav.agent': 'एजेंट', + 'nav.tools': 'टूल्स', + 'nav.cron': 'निर्धारित कार्य', + 'nav.integrations': 'इंटीग्रेशन', + 'nav.memory': 'मेमोरी', + 'nav.config': 'कॉन्फ़िगरेशन', + 'nav.cost': 'लागत ट्रैकर', + 'nav.logs': 'लॉग्स', + 'nav.doctor': 'जाँच', + 'dashboard.hero_title': 'इलेक्ट्रिक रनटाइम डैशबोर्ड', + 'agent.placeholder': 'संदेश लिखें…', + 'tools.search': 'टूल खोजें…', + 'cron.add': 'कार्य जोड़ें', + 'memory.add_memory': 'मेमोरी जोड़ें', + 'config.save': 'सहेजें', + 'cost.token_statistics': 'टोकन आँकड़े', + 'logs.title': 'लाइव लॉग्स', + 'doctor.title': 'सिस्टम जाँच', + 'auth.pair_button': 'पेयर करें', + 'auth.enter_code': 'टर्मिनल से एक-बार वाला पेयरिंग कोड दर्ज करें', + 'auth.code_placeholder': '6-अंकों का कोड', + 'auth.pairing_progress': 'पेयर किया जा रहा है…', + 'auth.logout': 'लॉग आउट', + 'common.languages': 'भाषाएँ', + 'common.select_language': 'भाषा चुनें', + 'header.dashboard_tagline': 'ZeroClaw डैशबोर्ड', +}); + +const bn = createLocale({ + 'nav.dashboard': 'ড্যাশবোর্ড', + 'nav.agent': 'এজেন্ট', + 'nav.tools': 'টুলস', + 'nav.cron': 'নির্ধারিত কাজ', + 'nav.integrations': 'ইন্টিগ্রেশন', + 'nav.memory': 'মেমরি', + 'nav.config': 'কনফিগারেশন', + 'nav.cost': 'খরচ ট্র্যাকার', + 'nav.logs': 'লগ', + 'nav.doctor': 'ডায়াগনস্টিক', + 'dashboard.hero_title': 'ইলেকট্রিক রানটাইম ড্যাশবোর্ড', + 'agent.placeholder': 'একটি বার্তা লিখুন…', + 'tools.search': 'টুল খুঁজুন…', + 'cron.add': 'কাজ যোগ করুন', + 'memory.add_memory': 'মেমরি যোগ করুন', + 'config.save': 'সংরক্ষণ করুন', + 'cost.token_statistics': 'টোকেন পরিসংখ্যান', + 'logs.title': 'লাইভ লগ', + 'doctor.title': 'সিস্টেম ডায়াগনস্টিক', + 'auth.pair_button': 'পেয়ার করুন', + 'auth.enter_code': 'টার্মিনাল থেকে একবারের পেয়ারিং কোড লিখুন', + 'auth.code_placeholder': '৬-সংখ্যার কোড', + 'auth.pairing_progress': 'পেয়ার করা হচ্ছে…', + 'auth.logout': 'লগ আউট', + 'common.languages': 'ভাষাসমূহ', + 'common.select_language': 'ভাষা বেছে নিন', + 'header.dashboard_tagline': 'ZeroClaw ড্যাশবোর্ড', +}); + +const he = createLocale({ + 'nav.dashboard': 'לוח מחוונים', + 'nav.agent': 'סוכן', + 'nav.tools': 'כלים', + 'nav.cron': 'משימות מתוזמנות', + 'nav.integrations': 'אינטגרציות', + 'nav.memory': 'זיכרון', + 'nav.config': 'תצורה', + 'nav.cost': 'מעקב עלויות', + 'nav.logs': 'יומנים', + 'nav.doctor': 'אבחון', + 'dashboard.hero_title': 'לוח מחוונים חשמלי של זמן הריצה', + 'agent.placeholder': 'הקלד הודעה…', + 'tools.search': 'חפש כלים…', + 'cron.add': 'הוסף משימה', + 'memory.add_memory': 'הוסף זיכרון', + 'config.save': 'שמור', + 'cost.token_statistics': 'סטטיסטיקות אסימונים', + 'logs.title': 'יומנים חיים', + 'doctor.title': 'אבחון מערכת', + 'auth.pair_button': 'התאמה', + 'auth.enter_code': 'הזן את קוד ההתאמה החד-פעמי מהמסוף', + 'auth.code_placeholder': 'קוד בן 6 ספרות', + 'auth.pairing_progress': 'מתבצעת התאמה…', + 'auth.logout': 'התנתק', + 'common.languages': 'שפות', + 'common.select_language': 'בחר שפה', + 'header.dashboard_tagline': 'לוח המחוונים של ZeroClaw', +}); + +const pl = createLocale({ + 'nav.dashboard': 'Pulpit', + 'nav.agent': 'Agent', + 'nav.tools': 'Narzędzia', + 'nav.cron': 'Zaplanowane zadania', + 'nav.integrations': 'Integracje', + 'nav.memory': 'Pamięć', + 'nav.config': 'Konfiguracja', + 'nav.cost': 'Koszty', + 'nav.logs': 'Logi', + 'nav.doctor': 'Diagnostyka', + 'dashboard.hero_title': 'Elektryczny pulpit runtime', + 'agent.placeholder': 'Wpisz wiadomość…', + 'tools.search': 'Szukaj narzędzi…', + 'cron.add': 'Dodaj zadanie', + 'memory.add_memory': 'Dodaj pamięć', + 'config.save': 'Zapisz', + 'cost.token_statistics': 'Statystyki tokenów', + 'logs.title': 'Logi na żywo', + 'doctor.title': 'Diagnostyka systemu', + 'auth.pair_button': 'Sparuj', + 'auth.enter_code': 'Wprowadź jednorazowy kod parowania z terminala', + 'auth.code_placeholder': '6-cyfrowy kod', + 'auth.pairing_progress': 'Parowanie…', + 'auth.logout': 'Wyloguj', + 'common.languages': 'Języki', + 'common.select_language': 'Wybierz język', + 'header.dashboard_tagline': 'Pulpit ZeroClaw', +}); + +const cs = createLocale({ + 'nav.dashboard': 'Nástěnka', + 'nav.agent': 'Agent', + 'nav.tools': 'Nástroje', + 'nav.cron': 'Plánované úlohy', + 'nav.integrations': 'Integrace', + 'nav.memory': 'Paměť', + 'nav.config': 'Konfigurace', + 'nav.cost': 'Náklady', + 'nav.logs': 'Logy', + 'nav.doctor': 'Diagnostika', + 'dashboard.hero_title': 'Elektrický runtime panel', + 'agent.placeholder': 'Napište zprávu…', + 'tools.search': 'Hledat nástroje…', + 'cron.add': 'Přidat úlohu', + 'memory.add_memory': 'Přidat paměť', + 'config.save': 'Uložit', + 'cost.token_statistics': 'Statistiky tokenů', + 'logs.title': 'Živé logy', + 'doctor.title': 'Diagnostika systému', + 'auth.pair_button': 'Spárovat', + 'auth.enter_code': 'Zadejte jednorázový párovací kód z terminálu', + 'auth.code_placeholder': '6místný kód', + 'auth.pairing_progress': 'Párování…', + 'auth.logout': 'Odhlásit se', + 'common.languages': 'Jazyky', + 'common.select_language': 'Vyberte jazyk', + 'header.dashboard_tagline': 'Panel ZeroClaw', +}); + +const nl = createLocale({ + 'nav.dashboard': 'Dashboard', + 'nav.agent': 'Agent', + 'nav.tools': 'Tools', + 'nav.cron': 'Geplande taken', + 'nav.integrations': 'Integraties', + 'nav.memory': 'Geheugen', + 'nav.config': 'Configuratie', + 'nav.cost': 'Kosten', + 'nav.logs': 'Logs', + 'nav.doctor': 'Diagnose', + 'dashboard.hero_title': 'Elektrisch runtime-dashboard', + 'agent.placeholder': 'Typ een bericht…', + 'tools.search': 'Tools zoeken…', + 'cron.add': 'Taak toevoegen', + 'memory.add_memory': 'Geheugen toevoegen', + 'config.save': 'Opslaan', + 'cost.token_statistics': 'Tokenstatistieken', + 'logs.title': 'Live-logs', + 'doctor.title': 'Systeemdiagnose', + 'auth.pair_button': 'Koppelen', + 'auth.enter_code': 'Voer de eenmalige koppelcode uit de terminal in', + 'auth.code_placeholder': '6-cijferige code', + 'auth.pairing_progress': 'Koppelen…', + 'auth.logout': 'Afmelden', + 'common.languages': 'Talen', + 'common.select_language': 'Kies taal', + 'header.dashboard_tagline': 'ZeroClaw-dashboard', +}); + +const uk = createLocale({ + 'nav.dashboard': 'Панель', + 'nav.agent': 'Агент', + 'nav.tools': 'Інструменти', + 'nav.cron': 'Заплановані завдання', + 'nav.integrations': 'Інтеграції', + 'nav.memory': 'Пам’ять', + 'nav.config': 'Конфігурація', + 'nav.cost': 'Витрати', + 'nav.logs': 'Журнали', + 'nav.doctor': 'Діагностика', + 'dashboard.hero_title': 'Електрична панель runtime', + 'agent.placeholder': 'Введіть повідомлення…', + 'tools.search': 'Пошук інструментів…', + 'cron.add': 'Додати завдання', + 'memory.add_memory': 'Додати пам’ять', + 'config.save': 'Зберегти', + 'cost.token_statistics': 'Статистика токенів', + 'logs.title': 'Живі журнали', + 'doctor.title': 'Діагностика системи', + 'auth.pair_button': 'З’єднати', + 'auth.enter_code': 'Введіть одноразовий код з’єднання з термінала', + 'auth.code_placeholder': '6-значний код', + 'auth.pairing_progress': 'З’єднання…', + 'auth.logout': 'Вийти', + 'common.languages': 'Мови', + 'common.select_language': 'Оберіть мову', + 'header.dashboard_tagline': 'Панель ZeroClaw', +}); + +const id = createLocale({ + 'nav.dashboard': 'Dasbor', + 'nav.agent': 'Agen', + 'nav.tools': 'Alat', + 'nav.cron': 'Tugas terjadwal', + 'nav.integrations': 'Integrasi', + 'nav.memory': 'Memori', + 'nav.config': 'Konfigurasi', + 'nav.cost': 'Biaya', + 'nav.logs': 'Log', + 'nav.doctor': 'Diagnosis', + 'dashboard.hero_title': 'Dasbor runtime elektrik', + 'agent.placeholder': 'Tulis pesan…', + 'tools.search': 'Cari alat…', + 'cron.add': 'Tambah tugas', + 'memory.add_memory': 'Tambah memori', + 'config.save': 'Simpan', + 'cost.token_statistics': 'Statistik token', + 'logs.title': 'Log langsung', + 'doctor.title': 'Diagnosis sistem', + 'auth.pair_button': 'Pasangkan', + 'auth.enter_code': 'Masukkan kode pairing sekali pakai dari terminal', + 'auth.code_placeholder': 'Kode 6 digit', + 'auth.pairing_progress': 'Sedang memasangkan…', + 'auth.logout': 'Keluar', + 'common.languages': 'Bahasa', + 'common.select_language': 'Pilih bahasa', + 'header.dashboard_tagline': 'Dasbor ZeroClaw', +}); + +const th = createLocale({ + 'nav.dashboard': 'แดชบอร์ด', + 'nav.agent': 'เอเจนต์', + 'nav.tools': 'เครื่องมือ', + 'nav.cron': 'งานที่ตั้งเวลา', + 'nav.integrations': 'การเชื่อมต่อ', + 'nav.memory': 'หน่วยความจำ', + 'nav.config': 'การกำหนดค่า', + 'nav.cost': 'ต้นทุน', + 'nav.logs': 'บันทึก', + 'nav.doctor': 'วินิจฉัย', + 'dashboard.hero_title': 'แดชบอร์ดรันไทม์ไฟฟ้า', + 'agent.placeholder': 'พิมพ์ข้อความ…', + 'tools.search': 'ค้นหาเครื่องมือ…', + 'cron.add': 'เพิ่มงาน', + 'memory.add_memory': 'เพิ่มหน่วยความจำ', + 'config.save': 'บันทึก', + 'cost.token_statistics': 'สถิติโทเค็น', + 'logs.title': 'บันทึกสด', + 'doctor.title': 'วินิจฉัยระบบ', + 'auth.pair_button': 'จับคู่', + 'auth.enter_code': 'ป้อนรหัสจับคู่แบบใช้ครั้งเดียวจากเทอร์มินัล', + 'auth.code_placeholder': 'รหัส 6 หลัก', + 'auth.pairing_progress': 'กำลังจับคู่…', + 'auth.logout': 'ออกจากระบบ', + 'common.languages': 'ภาษา', + 'common.select_language': 'เลือกภาษา', + 'header.dashboard_tagline': 'แดชบอร์ด ZeroClaw', +}); + +const ur = createLocale({ + 'nav.dashboard': 'ڈیش بورڈ', + 'nav.agent': 'ایجنٹ', + 'nav.tools': 'ٹولز', + 'nav.cron': 'شیڈول شدہ کام', + 'nav.integrations': 'انضمامات', + 'nav.memory': 'میموری', + 'nav.config': 'ترتیبات', + 'nav.cost': 'لاگت', + 'nav.logs': 'لاگز', + 'nav.doctor': 'تشخیص', + 'dashboard.hero_title': 'الیکٹرک رن ٹائم ڈیش بورڈ', + 'agent.placeholder': 'پیغام لکھیں…', + 'tools.search': 'ٹولز تلاش کریں…', + 'cron.add': 'کام شامل کریں', + 'memory.add_memory': 'میموری شامل کریں', + 'config.save': 'محفوظ کریں', + 'cost.token_statistics': 'ٹوکن کے اعدادوشمار', + 'logs.title': 'لائیو لاگز', + 'doctor.title': 'سسٹم تشخیص', + 'auth.pair_button': 'جوڑیں', + 'auth.enter_code': 'ٹرمینل سے ایک بار استعمال ہونے والا پیئرنگ کوڈ درج کریں', + 'auth.code_placeholder': '6 ہندسوں کا کوڈ', + 'auth.pairing_progress': 'جوڑا جا رہا ہے…', + 'auth.logout': 'لاگ آؤٹ', + 'common.languages': 'زبانیں', + 'common.select_language': 'زبان منتخب کریں', + 'header.dashboard_tagline': 'ZeroClaw ڈیش بورڈ', +}); + +const ro = createLocale({ + 'nav.dashboard': 'Tablou de bord', + 'nav.agent': 'Agent', + 'nav.tools': 'Unelte', + 'nav.cron': 'Sarcini programate', + 'nav.integrations': 'Integrări', + 'nav.memory': 'Memorie', + 'nav.config': 'Configurație', + 'nav.cost': 'Costuri', + 'nav.logs': 'Jurnale', + 'nav.doctor': 'Diagnostic', + 'dashboard.hero_title': 'Tablou de bord runtime electric', + 'agent.placeholder': 'Scrie un mesaj…', + 'tools.search': 'Caută unelte…', + 'cron.add': 'Adaugă sarcină', + 'memory.add_memory': 'Adaugă memorie', + 'config.save': 'Salvează', + 'cost.token_statistics': 'Statistici tokenuri', + 'logs.title': 'Jurnale live', + 'doctor.title': 'Diagnostic sistem', + 'auth.pair_button': 'Asociază', + 'auth.enter_code': 'Introdu codul unic de asociere din terminal', + 'auth.code_placeholder': 'Cod din 6 cifre', + 'auth.pairing_progress': 'Asociere…', + 'auth.logout': 'Deconectare', + 'common.languages': 'Limbi', + 'common.select_language': 'Alege limba', + 'header.dashboard_tagline': 'Tabloul ZeroClaw', +}); + +const sv = createLocale({ + 'nav.dashboard': 'Instrumentpanel', + 'nav.agent': 'Agent', + 'nav.tools': 'Verktyg', + 'nav.cron': 'Schemalagda jobb', + 'nav.integrations': 'Integrationer', + 'nav.memory': 'Minne', + 'nav.config': 'Konfiguration', + 'nav.cost': 'Kostnader', + 'nav.logs': 'Loggar', + 'nav.doctor': 'Diagnostik', + 'dashboard.hero_title': 'Elektrisk runtimepanel', + 'agent.placeholder': 'Skriv ett meddelande…', + 'tools.search': 'Sök verktyg…', + 'cron.add': 'Lägg till jobb', + 'memory.add_memory': 'Lägg till minne', + 'config.save': 'Spara', + 'cost.token_statistics': 'Tokenstatistik', + 'logs.title': 'Live-loggar', + 'doctor.title': 'Systemdiagnostik', + 'auth.pair_button': 'Para', + 'auth.enter_code': 'Ange engångskoden från terminalen', + 'auth.code_placeholder': '6-siffrig kod', + 'auth.pairing_progress': 'Parar…', + 'auth.logout': 'Logga ut', + 'common.languages': 'Språk', + 'common.select_language': 'Välj språk', + 'header.dashboard_tagline': 'ZeroClaw-panel', +}); + +const el = createLocale({ + 'nav.dashboard': 'Πίνακας ελέγχου', + 'nav.agent': 'Πράκτορας', + 'nav.tools': 'Εργαλεία', + 'nav.cron': 'Προγραμματισμένες εργασίες', + 'nav.integrations': 'Ενσωματώσεις', + 'nav.memory': 'Μνήμη', + 'nav.config': 'Ρυθμίσεις', + 'nav.cost': 'Κόστος', + 'nav.logs': 'Αρχεία καταγραφής', + 'nav.doctor': 'Διάγνωση', + 'dashboard.hero_title': 'Ηλεκτρικός πίνακας runtime', + 'agent.placeholder': 'Πληκτρολογήστε μήνυμα…', + 'tools.search': 'Αναζήτηση εργαλείων…', + 'cron.add': 'Προσθήκη εργασίας', + 'memory.add_memory': 'Προσθήκη μνήμης', + 'config.save': 'Αποθήκευση', + 'cost.token_statistics': 'Στατιστικά token', + 'logs.title': 'Ζωντανά αρχεία καταγραφής', + 'doctor.title': 'Διάγνωση συστήματος', + 'auth.pair_button': 'Σύζευξη', + 'auth.enter_code': 'Εισαγάγετε τον εφάπαξ κωδικό σύζευξης από το terminal', + 'auth.code_placeholder': '6ψήφιος κωδικός', + 'auth.pairing_progress': 'Σύζευξη…', + 'auth.logout': 'Αποσύνδεση', + 'common.languages': 'Γλώσσες', + 'common.select_language': 'Επιλέξτε γλώσσα', + 'header.dashboard_tagline': 'Πίνακας ZeroClaw', +}); + +const hu = createLocale({ + 'nav.dashboard': 'Irányítópult', + 'nav.agent': 'Ügynök', + 'nav.tools': 'Eszközök', + 'nav.cron': 'Ütemezett feladatok', + 'nav.integrations': 'Integrációk', + 'nav.memory': 'Memória', + 'nav.config': 'Konfiguráció', + 'nav.cost': 'Költségek', + 'nav.logs': 'Naplók', + 'nav.doctor': 'Diagnosztika', + 'dashboard.hero_title': 'Elektromos runtime irányítópult', + 'agent.placeholder': 'Írjon üzenetet…', + 'tools.search': 'Eszközök keresése…', + 'cron.add': 'Feladat hozzáadása', + 'memory.add_memory': 'Memória hozzáadása', + 'config.save': 'Mentés', + 'cost.token_statistics': 'Tokenstatisztika', + 'logs.title': 'Élő naplók', + 'doctor.title': 'Rendszerdiagnosztika', + 'auth.pair_button': 'Párosítás', + 'auth.enter_code': 'Adja meg a terminál egyszer használatos párosítási kódját', + 'auth.code_placeholder': '6 számjegyű kód', + 'auth.pairing_progress': 'Párosítás…', + 'auth.logout': 'Kijelentkezés', + 'common.languages': 'Nyelvek', + 'common.select_language': 'Nyelv kiválasztása', + 'header.dashboard_tagline': 'ZeroClaw irányítópult', +}); + +const fi = createLocale({ + 'nav.dashboard': 'Hallintapaneeli', + 'nav.agent': 'Agentti', + 'nav.tools': 'Työkalut', + 'nav.cron': 'Ajastetut tehtävät', + 'nav.integrations': 'Integraatiot', + 'nav.memory': 'Muisti', + 'nav.config': 'Asetukset', + 'nav.cost': 'Kustannukset', + 'nav.logs': 'Lokit', + 'nav.doctor': 'Diagnostiikka', + 'dashboard.hero_title': 'Sähköinen runtime-hallintapaneeli', + 'agent.placeholder': 'Kirjoita viesti…', + 'tools.search': 'Etsi työkaluja…', + 'cron.add': 'Lisää tehtävä', + 'memory.add_memory': 'Lisää muisti', + 'config.save': 'Tallenna', + 'cost.token_statistics': 'Token-tilastot', + 'logs.title': 'Live-lokit', + 'doctor.title': 'Järjestelmädiagnostiikka', + 'auth.pair_button': 'Yhdistä', + 'auth.enter_code': 'Syötä terminaalin kertakäyttöinen parituskoodi', + 'auth.code_placeholder': '6-numeroinen koodi', + 'auth.pairing_progress': 'Yhdistetään…', + 'auth.logout': 'Kirjaudu ulos', + 'common.languages': 'Kielet', + 'common.select_language': 'Valitse kieli', + 'header.dashboard_tagline': 'ZeroClaw-hallintapaneeli', +}); + +const da = createLocale({ + 'nav.dashboard': 'Kontrolpanel', + 'nav.agent': 'Agent', + 'nav.tools': 'Værktøjer', + 'nav.cron': 'Planlagte job', + 'nav.integrations': 'Integrationer', + 'nav.memory': 'Hukommelse', + 'nav.config': 'Konfiguration', + 'nav.cost': 'Omkostninger', + 'nav.logs': 'Logge', + 'nav.doctor': 'Diagnostik', + 'dashboard.hero_title': 'Elektrisk runtime-kontrolpanel', + 'agent.placeholder': 'Skriv en besked…', + 'tools.search': 'Søg værktøjer…', + 'cron.add': 'Tilføj job', + 'memory.add_memory': 'Tilføj hukommelse', + 'config.save': 'Gem', + 'cost.token_statistics': 'Tokenstatistik', + 'logs.title': 'Live-logge', + 'doctor.title': 'Systemdiagnostik', + 'auth.pair_button': 'Par', + 'auth.enter_code': 'Indtast engangskoden fra terminalen', + 'auth.code_placeholder': '6-cifret kode', + 'auth.pairing_progress': 'Parrer…', + 'auth.logout': 'Log ud', + 'common.languages': 'Sprog', + 'common.select_language': 'Vælg sprog', + 'header.dashboard_tagline': 'ZeroClaw-kontrolpanel', +}); + +const nb = createLocale({ + 'nav.dashboard': 'Kontrollpanel', + 'nav.agent': 'Agent', + 'nav.tools': 'Verktøy', + 'nav.cron': 'Planlagte jobber', + 'nav.integrations': 'Integrasjoner', + 'nav.memory': 'Minne', + 'nav.config': 'Konfigurasjon', + 'nav.cost': 'Kostnader', + 'nav.logs': 'Logger', + 'nav.doctor': 'Diagnostikk', + 'dashboard.hero_title': 'Elektrisk runtime-kontrollpanel', + 'agent.placeholder': 'Skriv en melding…', + 'tools.search': 'Søk etter verktøy…', + 'cron.add': 'Legg til jobb', + 'memory.add_memory': 'Legg til minne', + 'config.save': 'Lagre', + 'cost.token_statistics': 'Tokenstatistikk', + 'logs.title': 'Live-logger', + 'doctor.title': 'Systemdiagnostikk', + 'auth.pair_button': 'Koble til', + 'auth.enter_code': 'Skriv inn engangskoden fra terminalen', + 'auth.code_placeholder': '6-sifret kode', + 'auth.pairing_progress': 'Kobler til…', + 'auth.logout': 'Logg ut', + 'common.languages': 'Språk', + 'common.select_language': 'Velg språk', + 'header.dashboard_tagline': 'ZeroClaw-kontrollpanel', +}); + + +const translations: Record> = { + en, + 'zh-CN': zhCn, + ja, + ko, + vi, + tl, + es, + pt, + it, + de, + fr, + ar, + hi, + ru, + bn, + he, + pl, + cs, + nl, + tr, + uk, + id, + th, + ur, + ro, + sv, + el, + hu, + fi, + da, + nb, +}; let currentLocale: Locale = 'en'; @@ -389,34 +1629,50 @@ export function setLocale(locale: Locale): void { currentLocale = locale; } -// --------------------------------------------------------------------------- -// Translation function -// --------------------------------------------------------------------------- +export function coerceLocale(locale: string | undefined): Locale { + if (!locale) return 'en'; + const normalized = locale.toLowerCase(); + if (normalized.startsWith('zh')) return 'zh-CN'; + if (normalized.startsWith('ja')) return 'ja'; + if (normalized.startsWith('ko')) return 'ko'; + if (normalized.startsWith('vi')) return 'vi'; + if (normalized.startsWith('tl')) return 'tl'; + if (normalized.startsWith('es')) return 'es'; + if (normalized.startsWith('pt')) return 'pt'; + if (normalized.startsWith('it')) return 'it'; + if (normalized.startsWith('de')) return 'de'; + if (normalized.startsWith('fr')) return 'fr'; + if (normalized.startsWith('ar')) return 'ar'; + if (normalized.startsWith('hi')) return 'hi'; + if (normalized.startsWith('ru')) return 'ru'; + if (normalized.startsWith('bn')) return 'bn'; + if (normalized.startsWith('iw') || normalized.startsWith('he')) return 'he'; + if (normalized.startsWith('pl')) return 'pl'; + if (normalized.startsWith('cs')) return 'cs'; + if (normalized.startsWith('nl')) return 'nl'; + if (normalized.startsWith('tr')) return 'tr'; + if (normalized.startsWith('uk')) return 'uk'; + if (normalized.startsWith('id')) return 'id'; + if (normalized.startsWith('th')) return 'th'; + if (normalized.startsWith('ur')) return 'ur'; + if (normalized.startsWith('ro')) return 'ro'; + if (normalized.startsWith('sv')) return 'sv'; + if (normalized.startsWith('el')) return 'el'; + if (normalized.startsWith('hu')) return 'hu'; + if (normalized.startsWith('fi')) return 'fi'; + if (normalized.startsWith('da')) return 'da'; + if (normalized.startsWith('nb') || normalized.startsWith('no')) return 'nb'; + return 'en'; +} -/** - * Translate a key using the current locale. Returns the key itself if no - * translation is found. - */ export function t(key: string): string { return translations[currentLocale]?.[key] ?? translations.en[key] ?? key; } -/** - * Get the translation for a specific locale. Falls back to English, then to the - * raw key. - */ export function tLocale(key: string, locale: Locale): string { return translations[locale]?.[key] ?? translations.en[key] ?? key; } -// --------------------------------------------------------------------------- -// React hook -// --------------------------------------------------------------------------- - -/** - * React hook that fetches the locale from /api/status on mount and keeps the - * i18n module in sync. Returns the current locale and a `t` helper bound to it. - */ export function useLocale(): { locale: Locale; t: (key: string) => string } { const [locale, setLocaleState] = useState(currentLocale); @@ -426,9 +1682,7 @@ export function useLocale(): { locale: Locale; t: (key: string) => string } { getStatus() .then((status) => { if (cancelled) return; - const detected = status.locale?.toLowerCase().startsWith('tr') - ? 'tr' - : 'en'; + const detected = coerceLocale(status.locale); setLocale(detected); setLocaleState(detected); }) diff --git a/web/src/main.tsx b/web/src/main.tsx index 990523b67..2e607b55c 100644 --- a/web/src/main.tsx +++ b/web/src/main.tsx @@ -6,8 +6,8 @@ import './index.css'; ReactDOM.createRoot(document.getElementById('root')!).render( - {/* Vite base '/_app/' scopes static asset URLs only; app routes stay rooted at '/' for SPA fallback. */} - + {/* Match React Router paths to Vite's public base so in-app links resolve under /_app/. */} + diff --git a/web/src/pages/AgentChat.tsx b/web/src/pages/AgentChat.tsx index 1ddfa822c..9038351b5 100644 --- a/web/src/pages/AgentChat.tsx +++ b/web/src/pages/AgentChat.tsx @@ -2,6 +2,7 @@ import { useState, useEffect, useRef } from 'react'; import { Send, Bot, User, AlertCircle } from 'lucide-react'; import type { WsMessage } from '@/types/api'; import { WebSocketClient } from '@/lib/ws'; +import { t } from '@/lib/i18n'; interface ChatMessage { id: string; @@ -35,7 +36,7 @@ export default function AgentChat() { }; ws.onError = () => { - setError('Connection error. Attempting to reconnect...'); + setError(t('agent.connection_error')); }; ws.onMessage = (msg: WsMessage) => { @@ -67,37 +68,37 @@ export default function AgentChat() { case 'tool_call': setMessages((prev) => [ ...prev, - { - id: crypto.randomUUID(), - role: 'agent', - content: `[Tool Call] ${msg.name ?? 'unknown'}(${JSON.stringify(msg.args ?? {})})`, - timestamp: new Date(), - }, - ]); + { + id: crypto.randomUUID(), + role: 'agent', + content: `[${t('agent.tool_call')}] ${msg.name ?? 'unknown'}(${JSON.stringify(msg.args ?? {})})`, + timestamp: new Date(), + }, + ]); break; case 'tool_result': setMessages((prev) => [ ...prev, - { - id: crypto.randomUUID(), - role: 'agent', - content: `[Tool Result] ${msg.output ?? ''}`, - timestamp: new Date(), - }, - ]); + { + id: crypto.randomUUID(), + role: 'agent', + content: `[${t('agent.tool_result')}] ${msg.output ?? ''}`, + timestamp: new Date(), + }, + ]); break; case 'error': setMessages((prev) => [ ...prev, - { - id: crypto.randomUUID(), - role: 'agent', - content: `[Error] ${msg.message ?? 'Unknown error'}`, - timestamp: new Date(), - }, - ]); + { + id: crypto.randomUUID(), + role: 'agent', + content: `[${t('doctor.error')}] ${msg.message ?? t('agent.unknown_error')}`, + timestamp: new Date(), + }, + ]); setTyping(false); pendingContentRef.current = ''; break; @@ -135,7 +136,7 @@ export default function AgentChat() { setTyping(true); pendingContentRef.current = ''; } catch { - setError('Failed to send message. Please try again.'); + setError(t('agent.failed_send')); } setInput(''); @@ -164,8 +165,8 @@ export default function AgentChat() { {messages.length === 0 && (
-

ZeroClaw Agent

-

Send a message to start the conversation

+

{t('agent.empty_title')}

+

{t('agent.empty_subtitle')}

)} @@ -219,7 +220,7 @@ export default function AgentChat() {
-

Typing...

+

{t('agent.thinking')}

)} @@ -234,10 +235,11 @@ export default function AgentChat() { setInput(e.target.value)} onKeyDown={handleKeyDown} - placeholder={connected ? 'Type a message...' : 'Connecting...'} + placeholder={connected ? t('agent.placeholder') : t('agent.connecting')} disabled={!connected} className="w-full bg-gray-800 border border-gray-700 rounded-xl px-4 py-3 text-sm text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:opacity-50" /> @@ -257,7 +259,7 @@ export default function AgentChat() { }`} /> - {connected ? 'Connected' : 'Disconnected'} + {connected ? t('agent.connected') : t('agent.disconnected')} diff --git a/web/src/pages/Config.tsx b/web/src/pages/Config.tsx index 17a4868d2..1b60468b4 100644 --- a/web/src/pages/Config.tsx +++ b/web/src/pages/Config.tsx @@ -7,6 +7,7 @@ import { ShieldAlert, } from 'lucide-react'; import { getConfig, putConfig } from '@/lib/api'; +import { t } from '@/lib/i18n'; export default function Config() { const [config, setConfig] = useState(''); @@ -31,9 +32,9 @@ export default function Config() { setSuccess(null); try { await putConfig(config); - setSuccess('Configuration saved successfully.'); + setSuccess(t('config.saved')); } catch (err: unknown) { - setError(err instanceof Error ? err.message : 'Failed to save configuration'); + setError(err instanceof Error ? err.message : t('config.error')); } finally { setSaving(false); } @@ -60,7 +61,7 @@ export default function Config() {
-

Configuration

+

{t('config.title')}

@@ -77,11 +78,10 @@ export default function Config() {

- Sensitive fields are masked + {t('config.masked_title')}

- API keys, tokens, and passwords are hidden for security. To update a - masked field, replace the entire masked value with your new value. + {t('config.masked_description')}

@@ -106,10 +106,10 @@ export default function Config() {
- TOML Configuration + {t('config.toml_configuration')} - {config.split('\n').length} lines + {config.split('\n').length} {t('config.lines')}