web: i18n support — English, 简体中文, 繁體中文, Italiano

Lightweight i18n without react-i18next: a LocaleProvider context +
useLocale() hook with {{var}} interpolation, auto-detect from
navigator.language, persisted to localStorage.

98 translation keys across 4 locale files (en/zh-CN/zh-TW/it).
All user-visible strings in App/Brain/Profiling/ErrorBoundary are
now t() calls. Language switcher added to the sidebar footer.

Build clean (tsc + vite), 18 tests pass.
This commit is contained in:
ZacharyZcR
2026-07-19 04:22:46 +08:00
parent 420a0720c3
commit e486574442
11 changed files with 675 additions and 119 deletions
+64 -57
View File
@@ -9,6 +9,7 @@ import {
Database,
Feather,
Gauge,
Globe,
HardDrive,
KeyRound,
Layers,
@@ -34,6 +35,7 @@ import { Brain } from "./Brain"
import { Profiling } from "./Profiling"
import { persistPublicSettings, stored } from "@/lib/storage"
import { cn } from "@/lib/utils"
import { useLocale } from "./i18n"
const message = (role: ChatMessage["role"], content: string): ChatMessage => {
let id: string
@@ -42,15 +44,12 @@ const message = (role: ChatMessage["role"], content: string): ChatMessage => {
}
export default function App() {
// When the page is served by the engine itself (coli web), same-origin is the
// right default: no CORS, no manual endpoint editing. The Vite dev server
// (port 5173) keeps the classic default.
const { t, locale, setLocale, locales } = useLocale()
const servedByEngine = typeof window !== "undefined" && window.location.port !== "5173" && window.location.protocol.startsWith("http")
const defaultBase = servedByEngine ? `${window.location.origin}/v1` : "http://127.0.0.1:8000/v1"
const [baseUrl, setBaseUrl] = useState(() => {
const saved = stored(localStorage, "colibri.baseUrl", defaultBase)
// migrate: a stored FACTORY default pointing at another origin would trip CORS
// when the page is engine-served — upgrade it to same-origin once.
if (servedByEngine && saved === "http://127.0.0.1:8000/v1" && defaultBase !== saved) return defaultBase
return saved
})
@@ -120,7 +119,7 @@ export default function App() {
const result = await getHealth(baseUrl, apiKey)
if (!disposed) { setHealth(result); setHealthError("") }
} catch (cause) {
if (!disposed) setHealthError(cause instanceof Error ? cause.message : "Runtime metrics unavailable")
if (!disposed) setHealthError(cause instanceof Error ? cause.message : "status.runtimeUnavailable")
}
}
const timer = window.setInterval(() => void poll(), 5000)
@@ -155,13 +154,13 @@ export default function App() {
} catch (cause) {
if (!controller.signal.aborted) {
setHealth(null)
setHealthError(cause instanceof Error ? cause.message : "Runtime metrics unavailable")
setHealthError(cause instanceof Error ? cause.message : "status.runtimeUnavailable")
}
}
} catch (cause) {
if (controller.signal.aborted) return
setConnected(false)
setError(cause instanceof Error ? cause.message : "Could not reach the server.")
setError(cause instanceof Error ? cause.message : "status.serverError")
} finally {
if (probeRef.current === controller) { probeRef.current = null; setConnecting(false) }
}
@@ -227,7 +226,7 @@ export default function App() {
if (controller.signal.aborted) {
updateMessages((current) => current.filter((item) => item.id !== assistant.id || item.content))
} else {
setError(cause instanceof Error ? cause.message : "Generation failed.")
setError(cause instanceof Error ? cause.message : "status.generationFailed")
updateMessages((current) => current.filter((item) => item.id !== assistant.id || item.content))
}
} finally {
@@ -241,22 +240,22 @@ export default function App() {
<aside className="sidebar">
<div className="brand-row">
<div className="brand-mark"><Feather className="size-5" /></div>
<div><h1>colibrì</h1><p>local giant, tiny footprint</p></div>
<div><h1>colibrì</h1><p>{t("brand.tagline")}</p></div>
</div>
<section className="side-section">
<div className="section-title"><Link2 className="size-3.5" /> Connection</div>
<label>API endpoint<Input value={baseUrl} onChange={(event) => setBaseUrl(event.target.value)} /></label>
<label>API key<div className="relative"><KeyRound className="field-icon" /><Input className="pl-9" type="password" value={apiKey} placeholder="optional" onChange={(event) => setApiKey(event.target.value)} /></div><span className="field-help">Kept in memory only · sent to this endpoint</span></label>
<div className="section-title"><Link2 className="size-3.5" /> {t("sidebar.connection")}</div>
<label>{t("sidebar.endpoint")}<Input value={baseUrl} onChange={(event) => setBaseUrl(event.target.value)} /></label>
<label>{t("sidebar.apiKey")}<div className="relative"><KeyRound className="field-icon" /><Input className="pl-9" type="password" value={apiKey} placeholder={t("sidebar.apiKeyPlaceholder")} onChange={(event) => setApiKey(event.target.value)} /></div><span className="field-help">{t("sidebar.apiKeyHelp")}</span></label>
<Button type="button" variant="secondary" onClick={connect} disabled={connecting}>
{connecting ? <LoaderCircle className="size-4 animate-spin" /> : <RefreshCw className="size-4" />}
Probe server
{t("sidebar.probe")}
</Button>
<div className={cn("connection-state", connected && "connected")} aria-live="polite"><span />{connected ? "Engine reachable" : "Not connected"}</div>
<div className={cn("connection-state", connected && "connected")} aria-live="polite"><span />{connected ? t("status.connected") : t("status.notConnected")}</div>
</section>
<section className="side-section runtime-section" aria-live="polite">
<div className="section-title"><Activity className="size-3.5" /> Runtime</div>
<div className="section-title"><Activity className="size-3.5" /> {t("sidebar.runtime")}</div>
{health?.hwinfo ? <div className="hw-panel">
{health.hwinfo.cpu ? <div className="hw-row"><Cpu className="size-3.5" /><span>{health.hwinfo.cpu}</span></div> : null}
{health.hwinfo.gpus > 0 ? <div className="hw-row"><MonitorDot className="size-3.5" /><span>{health.hwinfo.gpus}× GPU<small>{health.hwinfo.vram_total_gb.toFixed(0)} GB VRAM</small></span></div> : null}
@@ -265,66 +264,74 @@ export default function App() {
</div> : null}
{health?.scheduler ? <>
<div className="runtime-grid">
<div><span>Active</span><strong>{active}<small> / {capacity}</small></strong></div>
<div><span>Queued</span><strong>{health.scheduler.queued}<small> / {health.scheduler.max_queue}</small></strong></div>
<div><span>Completed</span><strong>{health.scheduler.completed}</strong></div>
<div><span>Failures</span><strong>{failures}</strong></div>
<div><span>{t("dashboard.active")}</span><strong>{active}<small> / {capacity}</small></strong></div>
<div><span>{t("dashboard.queued")}</span><strong>{health.scheduler.queued}<small> / {health.scheduler.max_queue}</small></strong></div>
<div><span>{t("dashboard.completed")}</span><strong>{health.scheduler.completed}</strong></div>
<div><span>{t("dashboard.failures")}</span><strong>{failures}</strong></div>
</div>
{health.tiers ? (() => {
const t = health.tiers
const total = Math.max(t.vram + t.ram + t.disk, 1)
const ti = health.tiers
const total = Math.max(ti.vram + ti.ram + ti.disk, 1)
return <div className="tier-panel">
<div className="tier-bar" role="img" aria-label={`Experts: ${t.vram} VRAM, ${t.ram} RAM, ${t.disk} disk`}>
<span className="tier-vram" style={{ width: `${(100 * t.vram) / total}%` }} />
<span className="tier-ram" style={{ width: `${(100 * t.ram) / total}%` }} />
<span className="tier-disk" style={{ width: `${(100 * t.disk) / total}%` }} />
<div className="tier-bar" role="img" aria-label={t("tier.ariaLabel", { vram: ti.vram, ram: ti.ram, disk: ti.disk })}>
<span className="tier-vram" style={{ width: `${(100 * ti.vram) / total}%` }} />
<span className="tier-ram" style={{ width: `${(100 * ti.ram) / total}%` }} />
<span className="tier-disk" style={{ width: `${(100 * ti.disk) / total}%` }} />
</div>
<div className="tier-legend">
<span><i className="tier-vram" />VRAM <strong>{t.vram.toLocaleString()}</strong><small>{t.vram_gb.toFixed(1)} GB</small></span>
<span><i className="tier-ram" />RAM <strong>{t.ram.toLocaleString()}</strong><small>{t.ram_gb.toFixed(1)} GB</small></span>
<span><i className="tier-disk" />Disk <strong>{t.disk.toLocaleString()}</strong></span>
<span><i className="tier-vram" />{t("tier.vram")} <strong>{ti.vram.toLocaleString()}</strong><small>{ti.vram_gb.toFixed(1)} GB</small></span>
<span><i className="tier-ram" />{t("tier.ram")} <strong>{ti.ram.toLocaleString()}</strong><small>{ti.ram_gb.toFixed(1)} GB</small></span>
<span><i className="tier-disk" />{t("tier.disk")} <strong>{ti.disk.toLocaleString()}</strong></span>
</div>
</div>
})() : null}
{totalTokens.prompt + totalTokens.completion > 0 ? <div className="session-stats">
<span><Database className="size-3" /> Session: <strong>{totalTokens.prompt.toLocaleString()}</strong> prompt + <strong>{totalTokens.completion.toLocaleString()}</strong> completion</span>
<span><Database className="size-3" /> {t("dashboard.session")} <strong>{totalTokens.prompt.toLocaleString()}</strong> {t("dashboard.prompt")} + <strong>{totalTokens.completion.toLocaleString()}</strong> {t("dashboard.completion")}</span>
</div> : null}
<div className="runtime-foot"><span className="runtime-dot" /> Scheduler online <code>{kvSlots} KV</code></div>
</> : <p className="runtime-unavailable">{connected ? (healthError || "Runtime metrics unavailable") : "Probe the server to inspect runtime state."}</p>}
<div className="runtime-foot"><span className="runtime-dot" /> {t("sidebar.schedulerOnline")} <code>{kvSlots} KV</code></div>
</> : <p className="runtime-unavailable">{connected ? (healthError ? t(healthError) : t("status.runtimeUnavailable")) : t("sidebar.runtimeProbe")}</p>}
</section>
<section className="side-section">
<div className="section-title"><SlidersHorizontal className="size-3.5" /> Inference</div>
<label>Model<select value={model} onChange={(event) => setModel(event.target.value)}>{models.length ? models.map((id) => <option key={id}>{id}</option>) : <option>{model}</option>}</select></label>
{health?.kv_slots && health.kv_slots > 1 ? <label>KV session<select value={cacheSlot} onChange={(event) => setCacheSlot(Number(event.target.value))} disabled={loading}>
{Array.from({ length: kvSlots }, (_, slot) => <option key={slot} value={slot}>Session {slot + 1}</option>)}
</select><span className="field-help">Isolated context · conversation follows the selected slot</span></label> : null}
<label><span className="label-line"><span>Temperature</span><code>{temperature.toFixed(1)}</code></span><input className="range" type="range" min="0" max="2" step="0.1" value={temperature} onChange={(event) => setTemperature(Number(event.target.value))} /></label>
<label>Max output tokens<Input type="number" min={1} max={4096} value={maxTokens} onChange={(event) => { const value = Number(event.target.value); if (Number.isFinite(value)) setMaxTokens(Math.min(4096, Math.max(1, Math.round(value)))) }} /></label>
<div className="section-title"><SlidersHorizontal className="size-3.5" /> {t("sidebar.inference")}</div>
<label>{t("sidebar.model")}<select value={model} onChange={(event) => setModel(event.target.value)}>{models.length ? models.map((id) => <option key={id}>{id}</option>) : <option>{model}</option>}</select></label>
{health?.kv_slots && health.kv_slots > 1 ? <label>{t("sidebar.kvSession")}<select value={cacheSlot} onChange={(event) => setCacheSlot(Number(event.target.value))} disabled={loading}>
{Array.from({ length: kvSlots }, (_, slot) => <option key={slot} value={slot}>{t("sidebar.sessionLabel", { slot: slot + 1 })}</option>)}
</select><span className="field-help">{t("sidebar.kvSessionHelp")}</span></label> : null}
<label><span className="label-line"><span>{t("sidebar.temperature")}</span><code>{temperature.toFixed(1)}</code></span><input className="range" type="range" min="0" max="2" step="0.1" value={temperature} onChange={(event) => setTemperature(Number(event.target.value))} /></label>
<label>{t("sidebar.maxTokens")}<Input type="number" min={1} max={4096} value={maxTokens} onChange={(event) => { const value = Number(event.target.value); if (Number.isFinite(value)) setMaxTokens(Math.min(4096, Math.max(1, Math.round(value)))) }} /></label>
<button type="button" className={cn("toggle-row", thinking && "active")} aria-pressed={thinking} onClick={() => setThinking((value) => !value)}>
<span><BrainCircuit className="size-4" /> Reasoning</span><i><b /></i>
<span><BrainCircuit className="size-4" /> {t("sidebar.reasoning")}</span><i><b /></i>
</button>
</section>
<div className="sidebar-foot"><Cpu className="size-3.5" /><span>OpenAI-compatible transport</span></div>
<div className="sidebar-foot">
<div><Cpu className="size-3.5" /><span>{t("sidebar.transport")}</span></div>
<div className="locale-switcher">
<Globe className="size-3.5" />
<select value={locale} onChange={(e) => setLocale(e.target.value)}>
{locales.map((l) => <option key={l.code} value={l.code}>{l.label}</option>)}
</select>
</div>
</div>
</aside>
<main className="chat-panel">
<header className="topbar">
<div><span className="eyebrow">ACTIVE MODEL</span><strong>{model}</strong></div>
<div><span className="eyebrow">{t("topbar.activeModel")}</span><strong>{model}</strong></div>
<div className="view-tabs">
<button className={view === "chat" ? "active" : ""} onClick={() => setView("chat")}><MessageSquareText className="size-3.5" /> Chat</button>
<button className={view === "brain" ? "active" : ""} onClick={() => setView("brain")}><BrainCircuit className="size-3.5" /> Brain</button>
<button className={view === "profiling" ? "active" : ""} onClick={() => setView("profiling")}><Gauge className="size-3.5" /> Profiling</button>
<button className={view === "chat" ? "active" : ""} onClick={() => setView("chat")}><MessageSquareText className="size-3.5" /> {t("nav.chat")}</button>
<button className={view === "brain" ? "active" : ""} onClick={() => setView("brain")}><BrainCircuit className="size-3.5" /> {t("nav.brain")}</button>
<button className={view === "profiling" ? "active" : ""} onClick={() => setView("profiling")}><Gauge className="size-3.5" /> {t("nav.profiling")}</button>
</div>
<div className="top-actions">
{loading && tokenCount > 0 ? <Badge className="badge-live"><Zap className="size-3 flash" /> {tokenCount} tokens</Badge> : null}
{!loading && tokPerSec != null ? <Badge className="badge-speed"><Gauge className="size-3" /> {tokPerSec.toFixed(1)} tok/s</Badge> : null}
{loading && tokenCount > 0 ? <Badge className="badge-live"><Zap className="size-3 flash" /> {t("topbar.tokens", { n: tokenCount })}</Badge> : null}
{!loading && tokPerSec != null ? <Badge className="badge-speed"><Gauge className="size-3" /> {t("topbar.tokPerSec", { n: tokPerSec.toFixed(1) })}</Badge> : null}
{!loading && ttft != null ? <Badge><Timer className="size-3" /> TTFT {(ttft/1000).toFixed(1)}s</Badge> : null}
{!loading && lastRun?.usage ? <Badge><Layers className="size-3" /> {lastRun.usage.prompt_tokens}{lastRun.usage.completion_tokens}</Badge> : null}
{lastRun?.queueWaitMs != null ? <Badge><Clock className="size-3" /> queue {Math.round(lastRun.queueWaitMs)}ms</Badge> : null}
<Badge><MonitorDot className="size-3" /> slot {cacheSlot + 1}</Badge>
<Button variant="ghost" size="sm" onClick={() => { updateMessages([]); setTokPerSec(null); setTtft(null); setTokenCount(0); setTotalTokens({prompt:0,completion:0}) }} disabled={!messages.length || loading}><Trash2 className="size-3.5" /> Clear</Button>
<Badge><MonitorDot className="size-3" /> {t("topbar.slot", { n: cacheSlot + 1 })}</Badge>
<Button variant="ghost" size="sm" onClick={() => { updateMessages([]); setTokPerSec(null); setTtft(null); setTokenCount(0); setTotalTokens({prompt:0,completion:0}) }} disabled={!messages.length || loading}><Trash2 className="size-3.5" /> {t("topbar.clear")}</Button>
</div>
</header>
@@ -335,11 +342,11 @@ export default function App() {
{!messages.length ? (
<div className="empty-state">
<div className="orb"><Feather /></div>
<span className="eyebrow">COLIBRÌ ENGINE</span>
<h2>Ask the giant.<br /><em>Keep the machine yours.</em></h2>
<p>Connect to a local colibrì server and stream responses directly from your hardware. Nothing leaves the endpoint you choose.</p>
<span className="eyebrow">{t("hero.title")}</span>
<h2>{t("hero.subtitle")}<br /><em>{t("hero.tagline")}</em></h2>
<p>{t("hero.description")}</p>
<div className="suggestions">
{["Explain how expert routing works", "Write a small C benchmark", "Compare RAM and VRAM caching"].map((item) => <button key={item} onClick={() => setDraft(item)}>{item}<ArrowUp className="size-3.5 rotate-45" /></button>)}
{[t("prompts.routing"), t("prompts.benchmark"), t("prompts.caching")].map((item) => <button key={item} onClick={() => setDraft(item)}>{item}<ArrowUp className="size-3.5 rotate-45" /></button>)}
</div>
</div>
) : (
@@ -347,7 +354,7 @@ export default function App() {
{messages.map((item) => (
<article key={item.id} className={cn("message", item.role)}>
<div className="avatar">{item.role === "user" ? "Y" : <Feather className="size-4" />}</div>
<div><div className="message-meta">{item.role === "user" ? "You" : "colibrì"}</div><div className="message-body">{item.content || <span className="typing" aria-label="Generating"><i /><i /><i /></span>}</div></div>
<div><div className="message-meta">{item.role === "user" ? t("chat.you") : t("chat.colibri")}</div><div className="message-body">{item.content || <span className="typing" aria-label="Generating"><i /><i /><i /></span>}</div></div>
</article>
))}
<div ref={bottomRef} />
@@ -356,10 +363,10 @@ export default function App() {
</div>
<div className="composer-wrap">
{error && <div className="error-banner" role="alert">{error}</div>}
{error && <div className="error-banner" role="alert">{t(error)}</div>}
<div className="composer">
<Textarea value={draft} onChange={(event) => setDraft(event.target.value)} placeholder="Message colibrì…" onKeyDown={(event) => { if (event.key === "Enter" && !event.shiftKey && !event.nativeEvent.isComposing) { event.preventDefault(); void send() } }} />
<div className="composer-foot"><span><MessageSquareText className="size-3.5" /> Enter to send · Shift+Enter for newline</span>{loading ? <Button variant="destructive" size="icon" aria-label="Stop generation" onClick={() => abortRef.current?.abort()}><CircleStop className="size-4" /></Button> : <Button size="icon" aria-label="Send message" disabled={!canSend} onClick={() => void send()}><ArrowUp className="size-4" /></Button>}</div>
<Textarea value={draft} onChange={(event) => setDraft(event.target.value)} placeholder={t("chat.placeholder")} onKeyDown={(event) => { if (event.key === "Enter" && !event.shiftKey && !event.nativeEvent.isComposing) { event.preventDefault(); void send() } }} />
<div className="composer-foot"><span><MessageSquareText className="size-3.5" /> {t("chat.inputHint")}</span>{loading ? <Button variant="destructive" size="icon" aria-label={t("chat.stop")} onClick={() => abortRef.current?.abort()}><CircleStop className="size-4" /></Button> : <Button size="icon" aria-label={t("chat.send")} disabled={!canSend} onClick={() => void send()}><ArrowUp className="size-4" /></Button>}</div>
</div>
</div>
</>}