feat: add Russian localization and independent STT languages
This commit is contained in:
+28
-28
@@ -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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user