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
+25 -22
View File
@@ -1,4 +1,6 @@
"use client";
import { useI18n } from "@/components/i18n";
import { MessageResponse } from "@/components/ai-elements/message";
import { cn } from "@/lib/utils";
@@ -56,6 +58,7 @@ export interface PracticeThinkingData {
}
export function ProblemCard({ part }: ArtifactPartProps) {
const { t } = useI18n();
// Always open, no chevron (owner round, 2026-10-05): the record IS the
// statement — same container + eyebrow discipline as the answered card
// and the dock (which mirrors this in Slice D). The commitment never
@@ -64,7 +67,7 @@ export function ProblemCard({ part }: ArtifactPartProps) {
const statement = typeof input.statement === "string" ? input.statement : "";
return (
<div data-testid="problem-card" className="lh-card-enter my-1 rounded-xl border bg-card p-4">
<div className="mb-1 text-xs font-medium uppercase tracking-wide text-muted-foreground">Problem</div>
<div className="mb-1 text-xs font-medium uppercase tracking-wide text-muted-foreground">{t("Problem")}</div>
<div className="lh-prose text-[15px]">
<MessageResponse>{repairMathForDisplay(statement)}</MessageResponse>
</div>
@@ -94,6 +97,7 @@ const OUTCOME_CHIP: Record<string, string> = {
};
export function ConcludeCard({ part }: ArtifactPartProps) {
const { t } = useI18n();
// The conclusion redesigned (owner round, 2026-10-05): the SOLUTION is
// the hero — the modelSolution committed at pose time (part.reveal, the
// view-seam join), shown verbatim, never re-authored. The verdict sits
@@ -111,8 +115,7 @@ export function ConcludeCard({ part }: ArtifactPartProps) {
if (outcome === "dismissed") {
return (
<div data-testid="conclude-card" className="my-1 px-1 text-sm text-muted-foreground/70">
Problem dismissed.
</div>
{t("Problem dismissed.")}</div>
);
}
@@ -122,10 +125,9 @@ export function ConcludeCard({ part }: ArtifactPartProps) {
eyebrow + the outcome in the quiz cards' EXACT chip register —
no more two same-weight headings stacked. */}
<div className="mb-1 flex items-center gap-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">
Concluded
{(OUTCOME_LABEL[outcome] ?? "") !== "" && outcome !== "dismissed" && (
{t("Concluded")}{(OUTCOME_LABEL[outcome] ?? "") !== "" && outcome !== "dismissed" && (
<span className={cn("rounded px-1.5 py-0.5", OUTCOME_CHIP[outcome] ?? OUTCOME_CHIP.movedOn)}>
{OUTCOME_LABEL[outcome]}
{t(OUTCOME_LABEL[outcome] ?? outcome)}
</span>
)}
</div>
@@ -142,7 +144,7 @@ export function ConcludeCard({ part }: ArtifactPartProps) {
<>
<div className="my-3 border-t" />
<div>
<div className="mb-0.5 text-xs font-medium uppercase tracking-wide text-muted-foreground">Model solution</div>
<div className="mb-0.5 text-xs font-medium uppercase tracking-wide text-muted-foreground">{t("Model solution")}</div>
<div className="lh-prose text-[15px]">
<MessageResponse>{repairMathForDisplay(reveal)}</MessageResponse>
</div>
@@ -177,6 +179,7 @@ function MetadataPopover({
concludeInput: { outcome?: string; rung?: unknown; missClass?: unknown; retest?: unknown };
problemInput: { mode?: unknown; target?: unknown };
}) {
const { t } = useI18n();
const [open, setOpen] = useState(false);
const triggerRef = useRef<HTMLButtonElement | null>(null);
const rootRef = useRef<HTMLDivElement | null>(null);
@@ -239,8 +242,7 @@ function MetadataPopover({
aria-expanded={open}
className="flex items-center gap-1 text-xs font-medium uppercase tracking-wide text-muted-foreground transition-colors hover:text-foreground"
>
Metadata
</button>
{t("Metadata")}</button>
{open &&
anchor &&
createPortal(
@@ -256,12 +258,12 @@ function MetadataPopover({
className="z-50 w-80 max-w-[calc(100vw-1rem)] rounded-xl border bg-card p-4 text-left shadow-lg"
>
<dl className="grid grid-cols-[auto_1fr] gap-x-4 gap-y-1 text-xs">
<dt className="text-muted-foreground">Mode</dt>
<dd className="font-mono">{mode || "—"}</dd>
<dt className="text-muted-foreground">Targets</dt>
<dt className="text-muted-foreground">{t("Mode")}</dt>
<dd className="font-mono">{t(mode || "—")}</dd>
<dt className="text-muted-foreground">{t("Targets")}</dt>
<dd className="font-mono">
{targets.length === 0 ? (
"untagged"
t("untagged")
) : (
<span>
{targets.map((t, i) => (
@@ -273,14 +275,14 @@ function MetadataPopover({
</span>
)}
</dd>
<dt className="text-muted-foreground">Outcome</dt>
<dd className="font-mono">{outcome || "—"}</dd>
<dt className="text-muted-foreground">Hint rung</dt>
<dd className="font-mono">{rung || "none"}</dd>
<dt className="text-muted-foreground">Miss class</dt>
<dd className="font-mono">{miss || "none"}</dd>
<dt className="text-muted-foreground">Retest</dt>
<dd className="font-mono">{retest === undefined ? "—" : retest ? "yes" : "no"}</dd>
<dt className="text-muted-foreground">{t("Outcome")}</dt>
<dd className="font-mono">{t(outcome || "—")}</dd>
<dt className="text-muted-foreground">{t("Hint rung")}</dt>
<dd className="font-mono">{t(rung || "none")}</dd>
<dt className="text-muted-foreground">{t("Miss class")}</dt>
<dd className="font-mono">{t(miss || "none")}</dd>
<dt className="text-muted-foreground">{t("Retest")}</dt>
<dd className="font-mono">{retest === undefined ? "—" : retest ? t("yes") : t("no")}</dd>
</dl>
</div>,
document.body,
@@ -290,9 +292,10 @@ function MetadataPopover({
}
export function ThinkingCard({ data }: { data: PracticeThinkingData }) {
const { t } = useI18n();
// The unified row (owner rounds, 2026-10-05): one style, one label —
// 50%-italic quote block on expand. Legacy records' commit flag is
// deliberately unread (one lane now). A TRANSCRIPT row (owner round,
// 2026-10-07): full-width band + rail, arriving expanded.
return <ThinkingBlock testId="thinking-card" label="YOUR THINKING" text={data.text} defaultOpen />;
return <ThinkingBlock testId="thinking-card" label={t("YOUR THINKING")} text={data.text} defaultOpen />;
}