feat: add Russian localization and independent STT languages
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
"use client";
|
||||
import { useI18n } from "@/components/i18n";
|
||||
|
||||
/**
|
||||
* The Agent section (settings-app wave): the THREE MODEL SLOTS, the tool
|
||||
* toggles + the search-engine preference, and the prompt override
|
||||
@@ -250,6 +252,7 @@ function MenuTrigger({
|
||||
/** The no-key search state grays the whole row; the trigger follows. */
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
@@ -263,7 +266,7 @@ function MenuTrigger({
|
||||
disabled ? "cursor-not-allowed" : "hover:border-muted-foreground/50"
|
||||
} ${widthClass}`}
|
||||
>
|
||||
<span className="min-w-0 flex-1 truncate">{value || emptyLabel}</span>
|
||||
<span className="min-w-0 flex-1 truncate">{t(value || emptyLabel)}</span>
|
||||
<span aria-hidden className="text-muted-foreground">
|
||||
▾
|
||||
</span>
|
||||
@@ -303,6 +306,7 @@ export function AgentSection({
|
||||
/** Writes the whole compaction section (replace-PUT discipline). */
|
||||
onCompactionChange: (next: CompactionSettings) => void;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
const [catalog, setCatalog] = useState<ModelCatalogEntry[] | null>(null);
|
||||
/** The three tiers' resolved pins — GET /api/models `slots`. The rows'
|
||||
* Default hints and thinking lists resolve from it. */
|
||||
@@ -492,7 +496,7 @@ export function AgentSection({
|
||||
>
|
||||
<span>{label}</span>
|
||||
<span className="text-muted-foreground">
|
||||
{live === null ? (active && <span aria-hidden>✓</span>) : live ? <span aria-hidden>✓ key</span> : <span aria-hidden>· no key</span>}
|
||||
{live === null ? (active && <span aria-hidden>✓</span>) : live ? <span aria-hidden>{t("✓ key")}</span> : <span aria-hidden>{t("· no key")}</span>}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
@@ -527,7 +531,7 @@ export function AgentSection({
|
||||
<Toggle
|
||||
checked={info.enabled}
|
||||
disabled={!fileExists}
|
||||
label={`prompt ${info.path}`}
|
||||
label={t("prompt {v0}", { v0: info.path })}
|
||||
onChange={(on) => onSectionChange(composePromptSwitch(agent, info.path, on))}
|
||||
/>
|
||||
</span>
|
||||
@@ -540,26 +544,26 @@ export function AgentSection({
|
||||
}}
|
||||
className="text-xs text-muted-foreground/70 underline-offset-2 hover:text-foreground hover:underline"
|
||||
>
|
||||
{open ? "Hide text" : "Show text"}
|
||||
{open ? t("Hide text") : t("Show text")}
|
||||
</button>
|
||||
{open && (
|
||||
<div className="mt-1 flex flex-col gap-2">
|
||||
<div>
|
||||
<p className="mb-1 text-xs uppercase tracking-widest text-muted-foreground/70">Source</p>
|
||||
<p className="mb-1 text-xs uppercase tracking-widest text-muted-foreground/70">{t("Source")}</p>
|
||||
<textarea
|
||||
readOnly
|
||||
value={info.sourceText}
|
||||
aria-label="The original instructions"
|
||||
aria-label={t("The original instructions")}
|
||||
className="h-24 w-full resize-y rounded-sm border border-border bg-muted p-2 font-ui text-xs text-muted-foreground"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<p className="mb-1 text-xs uppercase tracking-widest text-muted-foreground/70">Override</p>
|
||||
<p className="mb-1 text-xs uppercase tracking-widest text-muted-foreground/70">{t("Override")}</p>
|
||||
<textarea
|
||||
value={segmentDraft[info.path] ?? info.overrideText ?? ""}
|
||||
onChange={(e) => setSegmentDraft({ ...segmentDraft, [info.path]: e.target.value })}
|
||||
aria-label="Your version of the instructions"
|
||||
placeholder="Add your own instructions…"
|
||||
aria-label={t("Your version of the instructions")}
|
||||
placeholder={t("Add your own instructions…")}
|
||||
className="h-24 w-full resize-y rounded-sm border border-border bg-background p-2 font-ui text-xs text-foreground outline-none"
|
||||
/>
|
||||
</div>
|
||||
@@ -574,16 +578,14 @@ export function AgentSection({
|
||||
: "cursor-not-allowed text-xs text-muted-foreground/40 underline-offset-2"
|
||||
}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
{t("Save")}</button>
|
||||
{fileExists && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void deleteSegment(info.path)}
|
||||
className="text-xs text-muted-foreground/70 underline-offset-2 hover:text-red-500 hover:underline"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
{t("Delete")}</button>
|
||||
)}
|
||||
{/* The quiet inline confirmation — next to the
|
||||
action, not the old path-echoing line at the
|
||||
@@ -593,7 +595,7 @@ export function AgentSection({
|
||||
status reads unambiguously. */}
|
||||
{segmentStatus && (
|
||||
<p className="text-xs text-muted-foreground/70" data-testid="segment-status">
|
||||
{segmentStatus}
|
||||
{t(segmentStatus)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -614,7 +616,7 @@ export function AgentSection({
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsGroup label="Models" testid="group-models">
|
||||
<SettingsGroup label={t("Models")} testid="group-models">
|
||||
{MODEL_SLOTS.map((slot) => {
|
||||
const override = models?.[slot];
|
||||
const modelId = override?.model?.id ?? "";
|
||||
@@ -653,7 +655,7 @@ export function AgentSection({
|
||||
<MenuTrigger
|
||||
value={modelId ? prettyModel(modelId) : ""}
|
||||
emptyLabel="—"
|
||||
ariaLabel={`${SLOT_META[slot].label} model`}
|
||||
ariaLabel={t("{v0} model", { v0: t(SLOT_META[slot].label) })}
|
||||
testid={`slot-model-${slot}`}
|
||||
open={thisMenuOpen === "model"}
|
||||
onClick={() => setOpenMenu(thisMenuOpen === "model" ? null : { target: slot, kind: "model" })}
|
||||
@@ -665,7 +667,7 @@ export function AgentSection({
|
||||
// what the engine build runs). Display-only until a pick writes.
|
||||
value={thinking || (chosen ? defaultOrderLevel(levels) : "")}
|
||||
emptyLabel="—"
|
||||
ariaLabel={`${SLOT_META[slot].label} thinking`}
|
||||
ariaLabel={t("{v0} thinking", { v0: t(SLOT_META[slot].label) })}
|
||||
testid={`slot-thinking-${slot}`}
|
||||
open={thisMenuOpen === "thinking"}
|
||||
onClick={() => setOpenMenu(thisMenuOpen === "thinking" ? null : { target: slot, kind: "thinking" })}
|
||||
@@ -692,8 +694,8 @@ export function AgentSection({
|
||||
}}
|
||||
footer={
|
||||
catalog && catalog.length === 0
|
||||
? "No models — add a provider key in Keys"
|
||||
: "Runs every agent of this tier"
|
||||
? t("No models — add a provider key in Keys")
|
||||
: t("Runs every agent of this tier")
|
||||
}
|
||||
/>
|
||||
</MenuShell>
|
||||
@@ -723,7 +725,7 @@ export function AgentSection({
|
||||
})}
|
||||
</SettingsGroup>
|
||||
|
||||
<SettingsGroup label="Tools" testid="group-tools">
|
||||
<SettingsGroup label={t("Tools")} testid="group-tools">
|
||||
{/* The prober's graph tools (wave 7, owner ruling 2026-09-23):
|
||||
FIRST row of the group — its subject is the map, its
|
||||
mechanism is agent capability, so it lives in Tools but
|
||||
@@ -735,15 +737,15 @@ export function AgentSection({
|
||||
const graphOn = graph?.enabled ?? true;
|
||||
return (
|
||||
<SettingsRow
|
||||
label="Graph tools"
|
||||
hint={graphOn ? "Lets the tutor consult your knowledge map between sessions" : "Graph is disabled"}
|
||||
label={t("Graph tools")}
|
||||
hint={graphOn ? t("Lets the tutor consult your knowledge map between sessions") : t("Graph is disabled")}
|
||||
testid="graph-tools-row"
|
||||
dimmed={!graphOn}
|
||||
>
|
||||
<Toggle
|
||||
checked={agent?.graphTools?.enabled ?? true}
|
||||
disabled={!graphOn}
|
||||
label="graph tools"
|
||||
label={t("graph tools")}
|
||||
onChange={(on) => onSectionChange(composeGraphTools(agent, on))}
|
||||
/>
|
||||
</SettingsRow>
|
||||
@@ -757,13 +759,13 @@ export function AgentSection({
|
||||
const free = agent?.tools?.explain === true;
|
||||
return (
|
||||
<SettingsRow
|
||||
label="Quizzing"
|
||||
hint="How both tutors check your understanding"
|
||||
label={t("Quizzing")}
|
||||
hint={t("How both tutors check your understanding")}
|
||||
testid="quiz-mechanics-row"
|
||||
>
|
||||
<div
|
||||
role="radiogroup"
|
||||
aria-label="Quizzing mechanics"
|
||||
aria-label={t("Quizzing mechanics")}
|
||||
className="flex rounded-lg border border-border/60 bg-muted/30 p-0.5"
|
||||
>
|
||||
{(["choice", "free"] as const).map((m) => {
|
||||
@@ -781,7 +783,7 @@ export function AgentSection({
|
||||
active ? "bg-background text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
{m === "choice" ? "Multiple choice" : "Free recall"}
|
||||
{m === "choice" ? t("Multiple choice") : t("Free recall")}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
@@ -792,12 +794,12 @@ export function AgentSection({
|
||||
{TOGGLE_TOOLS.map((id) => (
|
||||
<SettingsRow
|
||||
key={id}
|
||||
label={`Tool · ${id}`}
|
||||
label={t("Tool · {v0}", { v0: id })}
|
||||
testid={`tool-row-${id}`}
|
||||
>
|
||||
<Toggle
|
||||
checked={agent?.tools?.[id] ?? true}
|
||||
label={`tool ${id}`}
|
||||
label={t("tool {v0}", { v0: id })}
|
||||
onChange={(on) => onSectionChange(composeTool(agent, id, on))}
|
||||
/>
|
||||
</SettingsRow>
|
||||
@@ -808,7 +810,7 @@ export function AgentSection({
|
||||
const open = openMenu?.target === "search";
|
||||
return (
|
||||
<SettingsRow
|
||||
label="Search engine"
|
||||
label={t("Search engine")}
|
||||
hint={search?.reason ?? undefined}
|
||||
testid="search-engine-row"
|
||||
dimmed={!anyKey}
|
||||
@@ -816,8 +818,8 @@ export function AgentSection({
|
||||
<span ref={open ? menuAnchorRef : undefined} className="relative flex items-center gap-2">
|
||||
<MenuTrigger
|
||||
value={engine === "tavily" ? "Tavily" : engine === "brave" ? "Brave" : ""}
|
||||
emptyLabel="Automatic"
|
||||
ariaLabel="Search engine"
|
||||
emptyLabel={t("Automatic")}
|
||||
ariaLabel={t("Search engine")}
|
||||
testid="search-engine-trigger"
|
||||
open={open}
|
||||
onClick={() => setOpenMenu(open ? null : { target: "search", kind: "engine" })}
|
||||
@@ -831,7 +833,7 @@ export function AgentSection({
|
||||
testid="search-engine-menu"
|
||||
menuClass="flex w-44 flex-col gap-0.5 rounded-xl border border-border bg-card p-2 text-xs shadow-xl"
|
||||
>
|
||||
{engineOption("auto", "Automatic", engine === null, null, () => {
|
||||
{engineOption("auto", t("Automatic"), engine === null, null, () => {
|
||||
onSectionChange(composeSearchEngine(agent, null));
|
||||
setOpenMenu(null);
|
||||
})}
|
||||
@@ -849,17 +851,17 @@ export function AgentSection({
|
||||
})()}
|
||||
</SettingsGroup>
|
||||
|
||||
<SettingsGroup label="Context" testid="group-context">
|
||||
<SettingsGroup label={t("Context")} testid="group-context">
|
||||
<SettingsRow
|
||||
label="Compaction threshold"
|
||||
hint={`${resolvedCompaction.thresholdTokens} tok`}
|
||||
label={t("Compaction threshold")}
|
||||
hint={t("{v0} tok", { v0: resolvedCompaction.thresholdTokens })}
|
||||
testid="compaction-row-threshold"
|
||||
>
|
||||
<CommitInput
|
||||
type="number"
|
||||
value={String(compaction?.thresholdTokens ?? "")}
|
||||
placeholder={String(resolvedCompaction.thresholdTokens)}
|
||||
ariaLabel="compaction threshold"
|
||||
ariaLabel={t("compaction threshold")}
|
||||
testid="input-compaction-threshold"
|
||||
onCommit={(v) => {
|
||||
// Empty = factory default (the budget row's discipline):
|
||||
@@ -882,9 +884,9 @@ export function AgentSection({
|
||||
</SettingsRow>
|
||||
</SettingsGroup>
|
||||
|
||||
<SettingsGroup label="System prompts" testid="group-prompts">
|
||||
<SettingsGroup label={t("System prompts")} testid="group-prompts">
|
||||
{prompts === null ? (
|
||||
<p className="text-xs text-muted-foreground/70">Loading prompt segments…</p>
|
||||
<p className="text-xs text-muted-foreground/70">{t("Loading prompt segments…")}</p>
|
||||
) : (
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* The owner's ruled order (2026-09-26): Teacher first,
|
||||
@@ -894,10 +896,10 @@ export function AgentSection({
|
||||
{sectionedSegments(prompts).map((section) => (
|
||||
<div key={section.id} data-testid={`prompt-section-${section.id}`}>
|
||||
<p className="mb-1 text-xs uppercase tracking-widest text-muted-foreground/70">
|
||||
{section.label}
|
||||
{t(section.label)}
|
||||
</p>
|
||||
{section.hint && (
|
||||
<p className="mb-1 text-xs text-muted-foreground/60">{section.hint}</p>
|
||||
<p className="mb-1 text-xs text-muted-foreground/60">{t(section.hint)}</p>
|
||||
)}
|
||||
<div className="flex flex-col gap-2">
|
||||
{section.segments.map((info) => segmentRow(info))}
|
||||
|
||||
Reference in New Issue
Block a user