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