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
+23 -25
View File
@@ -1,4 +1,6 @@
"use client";
import { useI18n } from "@/components/i18n";
import { createContext, Fragment, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
import { useChatSession } from "@/components/session";
@@ -225,6 +227,7 @@ export function Chat({
* only decides whether the pill rides the composer. */
initialConfigured?: boolean;
}) {
const { t } = useI18n();
// Reattach plumbing (background-turn runner wave): the hook holds the
// settled terminal's callback — a stable ref assigned below, once
// syncFromServer exists (refs, not closures: no stale capture).
@@ -1236,8 +1239,8 @@ export function Chat({
}}
onBlur={() => void commitRename(titleDraft)}
spellCheck={false}
aria-label="Session title"
title="Rename this session"
aria-label={t("Session title")}
title={t("Rename this session")}
className="lh-no-drag min-w-[8ch] max-w-[70%] shrink-0 bg-transparent font-title text-3xl tracking-tight caret-current outline-none [field-sizing:content]"
/>
{/* flex-1 spacer during rename too, so the chips stay at
@@ -1249,7 +1252,7 @@ export function Chat({
<>
<Hint
// F2 renames (registry) — surfaced on the hover.
label="Rename session"
label={t("Rename session")}
action="rename-session"
side="bottom"
>
@@ -1273,7 +1276,7 @@ export function Chat({
structure beside the phase chip — access to the full-DAG popup
at every width; the marginalia is the wide-screen glance view. */}
{planTree && (
<Hint label="Open the full plan" side="bottom">
<Hint label={t("Open the full plan")} side="bottom">
<button
type="button"
data-testid="plan-chip"
@@ -1284,8 +1287,7 @@ export function Chat({
className="lh-no-drag flex shrink-0 cursor-pointer items-center gap-1.5 rounded-full border border-border/80 px-2 py-0.5 text-xs font-medium uppercase tracking-wide text-muted-foreground transition-colors hover:border-border hover:text-foreground"
>
<WaypointsIcon className="size-3.5 text-primary" aria-hidden />
Plan
</button>
{t("Plan")}</button>
</Hint>
)}
{/* The extraction ring (owner-locked redesign, 2026-09-24): fills
@@ -1306,8 +1308,7 @@ export function Chat({
className="pointer-events-none flex shrink-0 select-none items-center gap-1 text-xs font-medium uppercase tracking-wide text-muted-foreground"
>
<DumbbellIcon className="size-3" />
Practice
</span>
{t("Practice")}</span>
) : (
<PhaseChip phase={phases.at(-1) ?? "probe"} />
)}
@@ -1321,8 +1322,8 @@ export function Chat({
<div data-session-find-root="" className={`${COLUMN} flex flex-1 flex-col gap-4 pt-4`}>
{messages.length === 0 && !busy && (
<ConversationEmptyState
title="New session"
description="The teacher will open by probing what you already know."
title={t("New session")}
description={t("The teacher will open by probing what you already know.")}
/>
)}
@@ -1373,8 +1374,7 @@ export function Chat({
className="my-4 flex items-center justify-center gap-1.5 font-ui text-xs font-medium uppercase tracking-wide text-muted-foreground"
>
<ArrowRightIcon className="size-3" />
Next problem
</div>
{t("Next problem")}</div>
);
default:
return null;
@@ -1432,7 +1432,7 @@ export function Chat({
{/* The rail GROUPS (owner round 4, 2026-10-05): the YOU heading
sits INSIDE the rail, same mono-xs register and same 1ch
rail as the thinking rows — one quote geometry. */}
<div className="font-medium uppercase tracking-wide text-muted-foreground">You</div>
<div className="font-medium uppercase tracking-wide text-muted-foreground">{t("You")}</div>
<div className="lh-prose whitespace-pre-wrap text-foreground">
{m.parts.filter((p) => p.type === "text").map((p) => p.text).join("\n")}
</div>
@@ -1465,8 +1465,7 @@ export function Chat({
) {
return (
<SystemLine key={`retry-${pi}`} testId="tool-retry">
Retrying…
</SystemLine>
{t("Retrying…")}</SystemLine>
);
}
switch (part.type) {
@@ -1562,21 +1561,20 @@ export function Chat({
BEFORE beginTurn; the settle kick runs after endTurn). */}
{compacting && (
<span className="flex items-center gap-2 font-ui text-xs font-medium uppercase tracking-wide text-muted-foreground">
<BrailleSpinner label="Compacting context" />
<Shimmer duration={1}>COMPACTING CONTEXT…</Shimmer>
<BrailleSpinner label={t("Compacting context")} />
<Shimmer duration={1}>{t("COMPACTING CONTEXT…")}</Shimmer>
</span>
)}
{(status === "submitted" || turnInFlight) && (
<span className="flex items-center gap-2 font-ui text-xs font-medium uppercase tracking-wide text-muted-foreground">
<BrailleSpinner label="Thinking" />
<BrailleSpinner label={t("Thinking")} />
<Shimmer duration={1}>
THINKING…
</Shimmer>
{t("THINKING…")}</Shimmer>
</span>
)}
{error && (
<div className="rounded-lg border border-destructive/50 bg-destructive/10 px-3 py-2 text-sm">
Something went wrong: {error.message}
{t("Something went wrong:")}{" "}{t(error.message)}
</div>
)}
</div>
@@ -1625,8 +1623,7 @@ export function Chat({
onClick={() => setSetupOpen(true)}
className="rounded-sm border border-border bg-card px-3 py-1.5 font-ui text-xs text-foreground transition-colors hover:border-muted-foreground/50 hover:bg-accent cursor-pointer"
>
Set up your models
</button>
{t("Set up your models")}</button>
</div>
)
)
@@ -1650,7 +1647,7 @@ export function Chat({
// applies.
nextProblem={nextReady}
onNext={sendNext}
placeholder={nextReady ? "Ask anything, or press Enter for the next one." : undefined}
placeholder={nextReady ? t("Ask anything, or press Enter for the next one.") : undefined}
/>
}
onSubmitAnswer={(output) => {
@@ -1889,6 +1886,7 @@ function DockOverlay({
* home and /chat/new carry, above the composer while slots are unset. */
setupCta?: React.ReactNode;
}) {
const { t } = useI18n();
const { isAtBottom, scrollToBottom, scrollRef } = useStickToBottomContext();
// Render-fresh bottom state (flicker round): the arrival block below reads
// it DURING RENDER — the same value the after-paint effect used to read a
@@ -2365,7 +2363,7 @@ function DockOverlay({
mid-question unmount), and widening past the floor fades it
back in. */}
{pending && plan && (
<Hint label="Open the full plan" side="bottom">
<Hint label={t("Open the full plan")} side="bottom">
<button
type="button"
data-testid="plan-marginalia"