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
+28 -28
View File
@@ -1,4 +1,6 @@
"use client";
import { useI18n } from "@/components/i18n";
/**
* The first-run setup flow (model-slots wave, 2026-09-20, owner design
* "A — composer CTA + send interception"): chats cannot start until the
@@ -147,22 +149,22 @@ export function SetupKeysBody({
/** The MenuShell anchor — wired by the wrapper (its triggerRef). */
menuAnchor: React.RefObject<HTMLDivElement | null>;
}) {
const { t } = useI18n();
return (
<div className="flex flex-col gap-3 px-6 py-5" data-testid="setup-stage-keys">
<p className="text-xs text-muted-foreground">
Choose your AI provider and paste its API key. You’ll pick your models next.
</p>
{t("Choose your AI provider and paste its API key. You’ll pick your models next.")}</p>
<div className="flex flex-col gap-2">
<div ref={menuAnchor} className="relative max-w-96">
<button
type="button"
aria-label="Provider"
aria-label={t("Provider")}
data-testid="setup-key-provider"
aria-expanded={menuOpen}
onClick={onToggleMenu}
className="flex h-7 w-full items-center gap-1 rounded-sm border border-border bg-background px-1.5 font-ui text-xs text-foreground transition-colors hover:border-muted-foreground/50"
>
<span className="min-w-0 flex-1 truncate text-left">{provider ? provider.label : "Pick a provider…"}</span>
<span className="min-w-0 flex-1 truncate text-left">{provider ? provider.label : t("Pick a provider…")}</span>
<span aria-hidden className="text-muted-foreground">
▾
</span>
@@ -188,8 +190,8 @@ export function SetupKeysBody({
onKeyDown={(e) => {
if (e.key === "Enter" && value.trim()) onSave();
}}
placeholder="Paste key…"
aria-label={`${provider.label} key`}
placeholder={t("Paste key…")}
aria-label={t("{v0} key", { v0: provider.label })}
data-testid="setup-key-input"
disabled={saving}
autoFocus
@@ -202,17 +204,17 @@ export function SetupKeysBody({
data-testid="setup-key-save"
className="shrink-0 rounded-sm border border-border bg-background px-3 py-1.5 font-ui text-xs text-foreground transition-colors hover:border-muted-foreground/50 disabled:cursor-not-allowed disabled:opacity-40"
>
{saving ? "Saving…" : "Save key"}
{saving ? t("Saving…") : t("Save key")}
</button>
</div>
)}
{error && (
<p role="alert" data-testid="setup-key-error" className="text-xs text-red-400">
{error}
{t(error)}
</p>
)}
</div>
<div className="mt-2 border-t border-border/60 pt-4"><h3 className="mb-3 font-ui text-xs text-muted-foreground">Subscriptions</h3><SubscriptionConnections setup /></div>
<div className="mt-2 border-t border-border/60 pt-4"><h3 className="mb-3 font-ui text-xs text-muted-foreground">{t("Subscriptions")}</h3><SubscriptionConnections setup /></div>
</div>
);
}
@@ -228,6 +230,7 @@ export function SetupDialog({
onClose: () => void;
onDone: (models: ModelsSettings) => void;
}) {
const { t } = useI18n();
const backdropDismiss = useBackdropDismiss(onClose);
const [body, setBody] = useState<ModelsBody | null>(null);
const [failed, setFailed] = useState(false);
@@ -407,17 +410,17 @@ export function SetupDialog({
<div
role="dialog"
aria-modal="true"
aria-label="Set up your models"
aria-label={t("Set up your models")}
data-testid="setup-dialog"
className="settings-dialog-pop flex max-h-[85vh] w-[640px] max-w-[90vw] flex-col overflow-hidden rounded-md border border-border bg-card shadow-xl"
onClick={(e) => e.stopPropagation()}
>
<header className="flex h-15 shrink-0 items-center gap-3 border-b border-border px-5">
<h2 className="font-title text-2xl tracking-tight">Set up your models</h2>
<Hint label="Close" binding="escape" side="left">
<h2 className="font-title text-2xl tracking-tight">{t("Set up your models")}</h2>
<Hint label={t("Close")} binding="escape" side="left">
<button
type="button"
aria-label="Close setup"
aria-label={t("Close setup")}
data-testid="setup-close"
onClick={onClose}
className="ml-auto text-muted-foreground transition-colors hover:text-foreground"
@@ -431,15 +434,14 @@ export function SetupDialog({
<div className="flex flex-col px-6 py-5">
{failed ? (
<p className="text-sm text-foreground">
Model catalog could not be loaded. Check the connection and retry.
</p>
{t("Model catalog could not be loaded. Check the connection and retry.")}</p>
) : (
<SetupKeysBody
providers={keyProviders}
provider={keyProvider}
value={keyValue}
saving={keySaving}
error={keyError}
error={t(keyError)}
menuOpen={keyMenuOpen}
menuAnchor={keyTriggerRef}
onToggleMenu={() => setKeyMenuOpen((v) => !v)}
@@ -456,8 +458,7 @@ export function SetupDialog({
) : (
<div className="flex flex-col gap-4 overflow-y-auto px-6 py-5" data-testid="setup-stage-pickers">
<p className="text-xs text-muted-foreground">
Pick the model for each tier. All three are required before chats can start.
</p>
{t("Pick the model for each tier. All three are required before chats can start.")}</p>
{SLOT_ORDER.map((slot) => {
const entry = picked[slot];
const level = levels[slot];
@@ -467,29 +468,28 @@ export function SetupDialog({
return (
<div key={slot} data-testid={`setup-slot-${slot}`} className="flex flex-col gap-1.5">
<span className="flex items-center gap-1.5">
<span className="font-ui text-sm text-foreground">{SLOT_META[slot].label}</span>
<span className="font-ui text-sm text-foreground">{t(SLOT_META[slot].label)}</span>
{!body?.slots?.[slot]?.model && !picked[slot] && (
<span
data-testid={`setup-needed-${slot}`}
className="rounded-sm border border-border px-1 py-0.5 text-[10px] font-normal tracking-wide text-muted-foreground"
>
needed
</span>
{t("needed")}</span>
)}
</span>
<span className="text-xs text-muted-foreground/70">{SLOT_META[slot].roster}</span>
<span className="text-xs text-muted-foreground/70">{t(SLOT_META[slot].roster)}</span>
{slot !== "strong" && <><ChatGPTBackgroundAccess provider={entry?.provider} /><NativeSubscriptionBackgroundAccess provider={entry?.provider} /></>}
<div className="flex items-center gap-2">
<div className="relative flex-1">
<button
type="button"
aria-label={`${SLOT_META[slot].label} model`}
aria-label={t("{v0} model", { v0: SLOT_META[slot].label })}
data-testid={`setup-trigger-${slot}`}
aria-expanded={openMenu?.slot === slot && openMenu.kind === "model"}
onClick={() => setOpenMenu(openMenu?.slot === slot && openMenu.kind === "model" ? null : { slot, kind: "model" })}
className="flex h-7 min-w-40 w-full items-center gap-1 rounded-sm border border-border bg-background px-1.5 font-ui text-xs text-foreground transition-colors hover:border-muted-foreground/50"
>
<span className="min-w-0 flex-1 truncate">{entry ? prettyModel(entry.id) : "Pick a model…"}</span>
<span className="min-w-0 flex-1 truncate">{entry ? prettyModel(entry.id) : t("Pick a model…")}</span>
<span aria-hidden className="text-muted-foreground">
▾
</span>
@@ -527,7 +527,7 @@ export function SetupDialog({
}));
setOpenMenu(null);
}}
footer={catalog && catalog.length === 0 ? "No image-capable models. Add a provider key with access to a vision model." : undefined}
footer={catalog && catalog.length === 0 ? t("No image-capable models. Add a provider key with access to a vision model.") : undefined}
/>
</MenuShell>
)}
@@ -537,7 +537,7 @@ export function SetupDialog({
<div className="relative">
<button
type="button"
aria-label={`${SLOT_META[slot].label} thinking`}
aria-label={t("{v0} thinking", { v0: SLOT_META[slot].label })}
data-testid={`setup-thinking-${slot}`}
aria-expanded={openMenu?.slot === slot && openMenu.kind === "thinking"}
disabled={!entry}
@@ -582,9 +582,9 @@ export function SetupDialog({
onClick={() => void save()}
className="rounded-sm border border-border bg-background px-3 py-1.5 font-ui text-xs text-foreground transition-colors hover:border-muted-foreground/50 disabled:cursor-not-allowed disabled:opacity-40"
>
{saving ? "Saving…" : "Save models"}
{saving ? t("Saving…") : t("Save models")}
</button>
{error && <p className="text-xs text-red-400">{error}</p>}
{error && <p className="text-xs text-red-400">{t(error)}</p>}
</div>
</div>
)}