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 -22
View File
@@ -39,6 +39,7 @@ import {
type GraphSettings,
type UserSettings,
} from "@/src/core";
import { LanguageSelect, useI18n } from "@/components/i18n";
import { useAppearance } from "./appearance.tsx";
import { SettingsSection, SettingsRow, Toggle } from "./controls.tsx";
import { AgentSection } from "./agent-section.tsx";
@@ -144,6 +145,7 @@ export function SettingsDialogBody({
* retargets follow it exactly like initialSection below. */
initialRow?: string | null;
}) {
const { t, general, saveGeneral } = useI18n();
const { onAppearanceChange } = useAppearance();
const [record, setRecord] = useState<UserSettings | null | undefined>(
undefined,
@@ -232,6 +234,10 @@ export function SettingsDialogBody({
body: object,
) => {
setError(null);
if (section === "general") {
try { await saveGeneral(body); } catch (error) { setError(error instanceof Error ? error.message : "Could not save language settings"); }
return;
}
const res = await fetch(`/api/settings/${section}`, {
method: "PUT",
headers: { "content-type": "application/json" },
@@ -260,30 +266,30 @@ export function SettingsDialogBody({
if (section === "appearance")
onAppearanceChange(body as AppearanceSettings);
},
[onAppearanceChange],
[onAppearanceChange, saveGeneral, t],
);
return (
<div
role="dialog"
aria-modal="true"
aria-label="Settings"
aria-label={t("Settings")}
data-testid="settings-dialog"
className="settings-dialog-pop flex h-[620px] max-h-[85vh] w-[920px] max-w-[90vw] flex-col overflow-hidden rounded-md border border-border bg-card shadow-xl"
onClick={(e) => e.stopPropagation()}
>
{/* Header — the app's quiet CMU title language. */}
<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">Settings</h2>
<h2 className="font-title text-2xl tracking-tight">{t("Settings")}</h2>
<Hint
// Esc closes the dialog (its own listener) — surface it.
label="Close"
label={t("Close")}
binding="escape"
side="left"
>
<button
type="button"
aria-label="Close settings"
aria-label={t("Close settings")}
data-testid="settings-close"
className="ml-auto size-7 rounded-sm text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
onClick={onClose}
@@ -305,7 +311,7 @@ export function SettingsDialogBody({
{/* Left nav — text-only, the session-list mono language,
active = bg-accent. */}
<nav
aria-label="Settings sections"
aria-label={t("Settings sections")}
data-testid="settings-nav"
className="flex w-44 shrink-0 flex-col gap-1 border-r border-border p-2"
>
@@ -322,7 +328,7 @@ export function SettingsDialogBody({
}`}
onClick={() => setActive(s.id)}
>
{s.label}
{t(s.label)}
</button>
))}
</nav>
@@ -344,32 +350,32 @@ export function SettingsDialogBody({
className="m-4 rounded-sm border border-red-500/40 px-3 py-2 text-xs text-red-500"
data-testid="settings-error"
>
{error}
{t(error)}
</p>
)}
{record === undefined ? (
<p className="px-6 py-5 text-xs text-muted-foreground/70">
Loading settings…
</p>
{t("Loading settings…")}</p>
) : record === null ? (
<p className="px-6 py-5 text-xs text-muted-foreground/70">
Settings could not be loaded — refresh to retry.
</p>
{t("Settings could not be loaded — refresh to retry.")}</p>
) : (
<div>
{active === "general" && (
<SettingsSection
title="General"
title={t("General")}
testid="section-general"
>
<SettingsRow label={t("Interface language")} testid="row-language"><LanguageSelect field="language" /></SettingsRow>
<SettingsRow label={t("Speech recognition language")} testid="row-stt-language"><LanguageSelect field="sttLanguage" /></SettingsRow>
<SettingsRow
label="Auto-update"
label={t("Auto-update")}
hint={AUTO_UPDATE_HINT}
testid="row-auto-update"
>
<Toggle
checked={record.general?.autoUpdate ?? true}
label="auto update"
checked={general.autoUpdate ?? true}
label={t("auto update")}
onChange={(on) =>
void write("general", { autoUpdate: on })
}
@@ -378,13 +384,13 @@ export function SettingsDialogBody({
</SettingsSection>
)}
{active === "keys" && (
<SettingsSection title="Providers" testid="section-keys">
<SettingsSection title={t("Providers")} testid="section-keys">
<KeysSection initialRow={initialRow ?? undefined} />
</SettingsSection>
)}
{active === "agent" && (
<SettingsSection
title="Agent"
title={t("Agent")}
testid="section-agent"
>
<AgentSection
@@ -406,7 +412,7 @@ export function SettingsDialogBody({
)}
{active === "appearance" && (
<SettingsSection
title="Appearance"
title={t("Appearance")}
testid="section-appearance"
>
<AppearanceSection
@@ -419,7 +425,7 @@ export function SettingsDialogBody({
)}
{active === "graph" && (
<SettingsSection
title="Graph"
title={t("Graph")}
testid="section-graph"
>
<GraphSection
@@ -432,8 +438,8 @@ export function SettingsDialogBody({
)}
{active === "keyboard" && (
<SettingsSection
title="Keyboard"
hint="Stored on this device"
title={t("Keyboard")}
hint={t("Stored on this device")}
testid="section-keyboard"
>
<KeyboardSection />