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
+30 -28
View File
@@ -1,4 +1,6 @@
"use client";
import { useI18n } from "@/components/i18n";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { ArrowDownIcon, ChevronRightIcon, PlayIcon } from "lucide-react";
@@ -105,6 +107,7 @@ export function QuestionDock({
dictationClaim = true,
compactionRunning = false,
}: QuestionDockProps) {
const { t } = useI18n();
// The probe-phase names (owner, 2026-10-09) canonicalize onto their
// teach twins for every dock concern — parse dispatch, labels, kinds:
// the probe quiz ("probe") is quiz-shaped, the probe free-recall
@@ -114,7 +117,7 @@ export function QuestionDock({
// The chip's display name carries the tool's OWN name (owner
// 2026-10-09): probe-phase questions label Probe/Recall in the
// composer; teach tools keep the standing labels.
const toolLabel = toolName === "probe" ? "Probe" : toolName === "recall" ? "Recall" : undefined;
const toolLabel = toolName === "probe" ? t("Probe") : toolName === "recall" ? t("Recall") : undefined;
// Parse, don't cast (src/core/tool-parts.ts): the model authored this
// input, and only the schema the tool declared vouches for its shape.
// The dock mounts on input-available parts (already validated at the
@@ -126,8 +129,7 @@ export function QuestionDock({
if (!parsed) {
return (
<div data-testid="malformed-question" className="rounded-xl border border-dashed bg-card/60 p-3 text-sm text-muted-foreground">
This question arrived malformed — the transcript keeps the retry.
</div>
{t("This question arrived malformed — the transcript keeps the retry.")}</div>
);
}
return (
@@ -142,7 +144,7 @@ export function QuestionDock({
label={
(canonical === "quiz" || canonical === "explain") &&
normalizeQuizType((parsed as { type?: unknown }).type) === "predict"
? "Prediction"
? t("Prediction")
: undefined
}
/>
@@ -201,6 +203,7 @@ export function QuestionDockBar({
* label Probe/Recall — the phase boundary visible in the composer. */
toolLabel?: string;
}) {
const { t } = useI18n();
if (variant === "reveal") {
return (
<button
@@ -213,11 +216,11 @@ export function QuestionDockBar({
<PlayIcon className="size-3.5" />
</span>
<span className="min-w-0 flex-1">
<span className="block text-sm font-medium">Check your understanding</span>
<span className="block text-sm font-medium">{t("Check your understanding")}</span>
<span className="block truncate text-xs text-muted-foreground">{teaserText(question)}</span>
</span>
<span className="flex shrink-0 items-center gap-1 text-xs font-medium text-primary">
Open <ChevronRightIcon className="size-3.5" />
{t("Open")}{" "}<ChevronRightIcon className="size-3.5" />
</span>
</button>
);
@@ -232,11 +235,11 @@ export function QuestionDockBar({
<span className="shrink-0 text-xs font-medium uppercase tracking-wide text-muted-foreground">
{label ??
toolLabel ??
(toolName === "quiz" ? "Check" : toolName === "explain" ? "Explain" : "Question")}
(toolName === "quiz" ? t("Check") : toolName === "explain" ? t("Explain") : t("Question"))}
</span>
<span className="min-w-0 flex-1 truncate text-muted-foreground">{teaserText(question)}</span>
<span className="flex shrink-0 items-center gap-1 text-xs font-medium">
Answer <ArrowDownIcon className="size-3.5" />
{t("Answer")}{" "}<ArrowDownIcon className="size-3.5" />
</span>
</button>
);
@@ -254,6 +257,7 @@ function QuestionDockForm({
dictationClaim = true,
compactionRunning = false,
}: Omit<QuestionDockProps, "expanded" | "onExpand" | "reveal">) {
const { t } = useI18n();
const kind = toolQuestionKind(toolName);
// Canonical QuestionData, built once per question. Quiz options are
@@ -547,17 +551,17 @@ function QuestionDockForm({
<div data-session-find-root="" className="max-h-[var(--dock-max-h,55dvh)] overflow-y-auto px-4 pt-3">
<div className="mb-1 text-xs font-medium uppercase tracking-wide text-muted-foreground">
{toolName === "probe"
? "Probe"
? t("Probe")
: toolName === "recall"
? "Recall"
? t("Recall")
: kind === "quiz"
? qd.questionType === "predict"
? "Prediction"
: "Check"
? t("Prediction")
: t("Check")
: kind === "explain"
? "Explain"
: "Question"}
{qd.mode === "multi-select" && " · select all that apply"}
? t("Explain")
: t("Question")}
{qd.mode === "multi-select" && t(" · select all that apply")}
</div>
<div className="mb-3 font-medium">
<MessageResponse>{qd.question}</MessageResponse>
@@ -644,7 +648,7 @@ function QuestionDockForm({
>
{sel.dontKnow && <span className="size-1.5 rounded-full bg-current" />}
</span>
<span>I don't know</span>
<span>{t("I don't know")}</span>
</button>
)}
@@ -710,7 +714,7 @@ function QuestionDockForm({
submit();
}
}}
placeholder="Type your own answer…"
placeholder={t("Type your own answer…")}
className="lh-input w-full min-w-0 flex-1 bg-transparent outline-none placeholder:text-muted-foreground"
onClick={(e) => e.stopPropagation()}
/>
@@ -742,8 +746,7 @@ function QuestionDockForm({
sel.dontKnow ? "text-muted-foreground/50" : "text-muted-foreground",
)}
>
Confidence
</span>
{t("Confidence")}</span>
<div
className={cn("relative h-4 min-w-0 flex-1", sel.dontKnow && "opacity-40")}
data-testid="confidence-track"
@@ -771,9 +774,9 @@ function QuestionDockForm({
step={1}
value={CONFIDENCE_STOPS[confIdx]}
disabled={sel.dontKnow}
aria-label="Confidence — the probability your selected answer is correct"
aria-valuetext={`${CONFIDENCE_STOPS[confIdx]} percent`}
title="The probability your selected answer is correct — 50% is a coin flip. Adjust with ←/→."
aria-label={t("Confidence — the probability your selected answer is correct")}
aria-valuetext={t("{v0} percent", { v0: CONFIDENCE_STOPS[confIdx] })}
title={t("The probability your selected answer is correct — 50% is a coin flip. Adjust with ←/→.")}
data-testid="confidence-slider"
onChange={(e) => {
// Snap to the nearest stop; the rendered VALUE is the stop's own
@@ -828,7 +831,7 @@ function QuestionDockForm({
className="rounded-[10px] border border-border/70 bg-background/60 transition-colors focus-within:border-primary/50"
>
<div className="px-2.5 pt-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">
{qd.mode === "text" ? "Your answer" : "YOUR THINKING"}
{qd.mode === "text" ? t("Your answer") : t("YOUR THINKING")}
</div>
{qd.mode === "text" ? (
<textarea
@@ -854,7 +857,7 @@ function QuestionDockForm({
submit();
}
}}
placeholder="Type your answer…"
placeholder={t("Type your answer…")}
rows={1}
className="field-sizing-content lh-input max-h-48 min-h-10 w-full resize-none bg-transparent px-2.5 py-2 outline-none placeholder:text-muted-foreground"
/>
@@ -884,7 +887,7 @@ function QuestionDockForm({
submit();
}
}}
placeholder="Why this answer? Whatever comes to mind."
placeholder={t("Why this answer? Whatever comes to mind.")}
rows={1}
className={cn(
"field-sizing-content lh-input max-h-48 w-full resize-none bg-transparent px-2.5 py-2 outline-none placeholder:text-muted-foreground/70",
@@ -896,15 +899,14 @@ function QuestionDockForm({
)}
<div className="flex items-center justify-between gap-2 p-1">
<span aria-hidden className="pl-1.5 text-xs text-muted-foreground">
{compactionRunning ? "compacting session…" : "↵ submits · ⇧↵ newline"}
{compactionRunning ? t("compacting session…") : t("↵ submits · ⇧↵ newline")}
</span>
<Button
type="submit"
disabled={!canSubmit || compactionRunning}
className="ml-auto h-9 rounded-[6px] px-3.5 text-sm font-medium"
>
Submit answer
</Button>
{t("Submit answer")}</Button>
</div>
</div>
</form>