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