feat: add Russian localization and independent STT languages
CI / Typecheck, tests, lint (push) Canceled after 0s
CI / Standalone (mac-arm64) (push) Canceled after 0s
CI / Standalone (linux-x64) (push) Canceled after 0s
CI / Standalone (windows-x64) (push) Canceled after 0s

This commit is contained in:
2026-10-25 17:18:44 +03:00
parent b277ee3e0f
commit a50ca4b867
93 changed files with 3261 additions and 904 deletions
+16 -12
View File
@@ -1,4 +1,6 @@
"use client";
import { useI18n } from "@/components/i18n";
import { useContext } from "react";
import { CheckIcon, SettingsIcon, XIcon } from "lucide-react";
@@ -27,12 +29,13 @@ const KIND_LABEL: Record<JobKind, string> = {
};
export function DispatchCard({ toolCallId, kind }: { toolCallId: string; kind: JobKind }) {
const { t } = useI18n();
const jobs = useContext(JobsContext);
const job = jobs.find((j) => j.toolCallId === toolCallId) ?? null;
const label = KIND_LABEL[kind];
if (!job || job.status === "running") {
return <DispatchActivityCard job={job} label={label} />;
return <DispatchActivityCard job={job} label={t(label)} />;
}
// BYOK (owner, 2026-09-22): a job refused for missing models is INFO,
// not scary-error — gray glyph + "needs model setup" + the deep-link
@@ -41,10 +44,10 @@ export function DispatchCard({ toolCallId, kind }: { toolCallId: string; kind: J
// whose key is gone reads as the key being needed — the deep-link
// lands on the SetupDialog's first-key stage on its own.
if (isModelsRefusal(job.error)) {
return <DispatchRefusedCard label={label} line="needs model setup" button="Set up models" />;
return <DispatchRefusedCard label={t(label)} line={t("needs model setup")} button={t("Set up models")} />;
}
if (isProviderKeyRefusal(job.error)) {
return <DispatchRefusedCard label={label} line="needs a provider key" button="Add a key" />;
return <DispatchRefusedCard label={t(label)} line={t("needs a provider key")} button={t("Add a key")} />;
}
const failed = job.status === "error";
return (
@@ -53,14 +56,15 @@ export function DispatchCard({ toolCallId, kind }: { toolCallId: string; kind: J
data-testid="dispatch-settled"
>
{failed ? <XIcon className="size-3.5 text-red-500" /> : <CheckIcon className="size-3.5" />}
<span className="font-medium uppercase tracking-wide">{label}</span>
<span>{failed ? "failed" : "delivered"}</span>
<span className="font-medium uppercase tracking-wide">{t(label)}</span>
<span>{failed ? t("failed") : t("delivered")}</span>
</div>
);
}
/** The refused card: quiet info tone — nothing red, nothing "failed". */
function DispatchRefusedCard({ label, line, button }: { label: string; line: string; button: string }) {
const { t } = useI18n();
const ctx = useContext(ChatActionsContext);
return (
<div
@@ -68,7 +72,7 @@ function DispatchRefusedCard({ label, line, button }: { label: string; line: str
data-testid="dispatch-refused"
>
<SettingsIcon className="size-3.5" />
<span className="font-medium uppercase tracking-wide">{label}</span>
<span className="font-medium uppercase tracking-wide">{t(label)}</span>
<span>{line}</span>
{ctx && (
<button
@@ -85,6 +89,7 @@ function DispatchRefusedCard({ label, line, button }: { label: string; line: str
}
function DispatchActivityCard({ job, label }: { job: JobRecord | null; label: string }) {
const { t } = useI18n();
// Elapsed from createdAt — jobs appear here only via the jobs feed (post-mount),
// so no SSR/hydration mismatch.
const elapsed =
@@ -99,19 +104,18 @@ function DispatchActivityCard({ job, label }: { job: JobRecord | null; label: st
the Braille spinner replaces the pulsing dot — one indicator
everywhere. This is the ONLY dispatch-card change; the card's
shape is untouched. */}
<BrailleSpinner label={`${label} running`} />
{label}
{elapsed !== null && <span className="font-normal">· {elapsed}s</span>}
<BrailleSpinner label={t("{v0} running", { v0: label })} />
{t(label)}
{elapsed !== null && <span className="font-normal">· {t("{v0}s", { v0: elapsed })}</span>}
</div>
{job?.activity && (
<div className="mt-1 truncate text-sm text-muted-foreground" data-testid="dispatch-activity-line">
{job.activity}
{t(job.activity)}
</div>
)}
{!job && (
<div className="mt-1 text-sm text-muted-foreground" data-testid="dispatch-pending">
Dispatched…
</div>
{t("Dispatched…")}</div>
)}
</div>
);