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 -27
View File
@@ -1,4 +1,7 @@
"use client";
import { translator, type Translator } from "@/src/core";
import { useI18n } from "@/components/i18n";
/**
* The Keys section (wave-5 polish, 2026-09-21 — owner design): a MINIMAL
* key-value table. Provider left, masked key right, no description hints
@@ -98,11 +101,11 @@ export function classifyKeys(
/** The minimal status line, e.g. "…ab12 · 64 chars" (" · env" marks an
* environment-owned value — the row itself is read-only then). Pure. */
export function keyStatusLine(status: AuthKeyStatus): string {
if (!status.present || !status.masked) return "not set";
export function keyStatusLine(status: AuthKeyStatus, t: Translator = translator("en")): string {
if (!status.present || !status.masked) return t("not set");
const tail = status.masked.replace(/ \((\d+) chars\)$/, "");
const chars = status.masked.match(/\((\d+) chars\)$/)?.[1] ?? "?";
return `${tail} · ${chars}${status.source === "env" ? " · env" : ""}`;
return `${tail} · ${chars}${status.source === "env" ? t(" · env") : ""}`;
}
/** One table row: label left, tiny docs link + status right. Writable
@@ -132,6 +135,7 @@ function KeyTableRow({
onCancel: () => void;
testid: string;
}) {
const { t } = useI18n();
const envOwned = status.source === "env";
const [value, setValue] = useState("");
const [error, setError] = useState<string | null>(null);
@@ -163,8 +167,8 @@ function KeyTableRow({
onKeyDown={(e) => {
if (e.key === "Enter") void save();
}}
placeholder="Paste key…"
aria-label={`${label} key`}
placeholder={t("Paste key…")}
aria-label={t("{v0} key", { v0: label })}
data-testid={`${testid}-input`}
disabled={busy}
autoFocus
@@ -177,8 +181,7 @@ function KeyTableRow({
data-testid={`${testid}-save`}
className="shrink-0 text-xs text-foreground underline-offset-2 hover:underline disabled:text-muted-foreground/40 disabled:no-underline"
>
Save
</button>
{t("Save")}</button>
<button
type="button"
onClick={onCancel}
@@ -186,8 +189,7 @@ function KeyTableRow({
data-testid={`${testid}-cancel`}
className="shrink-0 text-xs text-muted-foreground/70 underline-offset-2 hover:text-foreground hover:underline"
>
Cancel
</button>
{t("Cancel")}</button>
{status.present && status.source === "file" && onDelete && (
<button
type="button"
@@ -196,13 +198,12 @@ function KeyTableRow({
data-testid={`${testid}-delete`}
className="shrink-0 text-xs text-muted-foreground/70 underline-offset-2 hover:text-red-500 hover:underline"
>
Delete
</button>
{t("Delete")}</button>
)}
</div>
{error && (
<p role="alert" data-testid={`${testid}-error`} className="px-1 text-xs text-red-500">
{error}
{t(error)}
</p>
)}
</div>
@@ -230,17 +231,16 @@ function KeyTableRow({
onClick={(e) => e.stopPropagation()}
className="text-[10px] text-muted-foreground/60 underline-offset-2 hover:text-foreground hover:underline"
>
key ↗
</a>
{t("key ↗")}</a>
)}
<span data-testid={`${testid}-status`} className="font-mono text-[11px] text-muted-foreground">
{keyStatusLine(status)}
{keyStatusLine(status, t)}
</span>
</span>
</button>
{error && (
<p role="alert" data-testid={`${testid}-error`} className="pl-6 text-xs text-red-500">
{error}
{t(error)}
</p>
)}
</div>
@@ -256,6 +256,7 @@ function AddProviderRow({
addable: ProviderKeyInfo[];
onPick: (p: ProviderKeyInfo) => void;
}) {
const { t } = useI18n();
const [open, setOpen] = useState(false);
const anchorRef = useRef<HTMLDivElement>(null);
@@ -290,7 +291,7 @@ function AddProviderRow({
onClick={() => setOpen((v) => !v)}
className="flex w-full items-center justify-between gap-3 rounded-sm px-1 py-1.5 text-left font-ui text-xs text-muted-foreground transition-colors hover:bg-accent/50 hover:text-foreground"
>
<span>+ Add provider key</span>
<span>{t("+ Add provider key")}</span>
{addable.length > 0 && <span className="font-mono text-[11px]">{addable.length}</span>}
</button>
{open && (
@@ -324,6 +325,7 @@ export function KeysSection({
}: {
initialRow?: string;
}) {
const { t } = useI18n();
const [statuses, setStatuses] = useState<AuthKeyStatus[] | null>(null);
const [providers, setProviders] = useState<ProviderKeyInfo[] | undefined>(undefined);
const [loading, setLoading] = useState(true);
@@ -397,7 +399,7 @@ export function KeysSection({
}
if (loading || statuses === null) {
return <p className="px-6 py-5 text-xs text-muted-foreground/70">Loading keys…</p>;
return <p className="px-6 py-5 text-xs text-muted-foreground/70">{t("Loading keys…")}</p>;
}
const { providerRows, toolRows, unknownRows, addable } = classifyKeys(statuses, providers);
@@ -413,7 +415,7 @@ export function KeysSection({
return (
<>
<SettingsGroup label="Model API keys" testid="group-provider-keys">
<SettingsGroup label={t("Model API keys")} testid="group-provider-keys">
{providerRows.map((status) => (
<KeyTableRow
key={status.id}
@@ -455,7 +457,7 @@ export function KeysSection({
</div>}
</SettingsGroup>
<SettingsGroup label="Subscriptions" testid="group-subscriptions">
<SettingsGroup label={t("Subscriptions")} testid="group-subscriptions">
<SubscriptionConnections />
</SettingsGroup>
@@ -497,6 +499,7 @@ function AddProviderEditor({
onCancel: () => void;
error: string | null;
}) {
const { t } = useI18n();
const [value, setValue] = useState("");
return (
<>
@@ -507,8 +510,8 @@ function AddProviderEditor({
onKeyDown={(e) => {
if (e.key === "Enter" && value.trim()) onSave(value);
}}
placeholder="Paste key…"
aria-label={`${provider.label} key`}
placeholder={t("Paste key…")}
aria-label={t("{v0} key", { v0: provider.label })}
data-testid={`key-${provider.id}-input`}
autoFocus
className="h-7 min-w-0 flex-1 rounded-sm border border-border bg-background px-1.5 font-mono text-xs text-foreground outline-none placeholder:text-muted-foreground/60 focus:border-muted-foreground/50"
@@ -520,19 +523,17 @@ function AddProviderEditor({
data-testid={`key-${provider.id}-save`}
className="shrink-0 text-xs text-foreground underline-offset-2 hover:underline disabled:text-muted-foreground/40 disabled:no-underline"
>
Save
</button>
{t("Save")}</button>
<button
type="button"
onClick={onCancel}
data-testid={`key-${provider.id}-cancel`}
className="shrink-0 text-xs text-muted-foreground/70 underline-offset-2 hover:text-foreground hover:underline"
>
Cancel
</button>
{t("Cancel")}</button>
{error && (
<p role="alert" className="w-full pl-0 text-xs text-red-500" data-testid={`key-${provider.id}-error`}>
{error}
{t(error)}
</p>
)}
</>