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
+22 -23
View File
@@ -1,4 +1,6 @@
"use client";
import { useI18n } from "@/components/i18n";
import { useCallback, useEffect, useRef, useState } from "react";
import { useActivity } from "@/components/activity-dock";
@@ -183,6 +185,7 @@ export function CoverageMenuBody({
extracting: boolean;
onExtract: () => void;
}) {
const { t, language } = useI18n();
const busy = coverage.running || extracting;
const complete = coverage.uncoveredTokens === 0 && coverage.uncoveredCount === 0;
return (
@@ -190,17 +193,15 @@ export function CoverageMenuBody({
{busy ? (
<span className="flex items-center gap-1.5 text-muted-foreground" data-testid="ctx-coverage-running">
<span className="size-1.5 animate-pulse rounded-full bg-foreground/70" aria-hidden />
extracting…
</span>
{t("extracting…")}</span>
) : complete ? (
<span className="text-muted-foreground">fully extracted</span>
<span className="text-muted-foreground">{t("fully extracted")}</span>
) : (
<span className="flex flex-col">
<span className="tabular-nums text-foreground">
{formatTokens(coverage.uncoveredTokens)} tok uncovered
</span>
{formatTokens(coverage.uncoveredTokens, language)}{" "}{t("tok uncovered")}</span>
<span className="tabular-nums text-muted-foreground">
{coverage.uncoveredCount} message{coverage.uncoveredCount === 1 ? "" : "s"} unextracted
{t("{count} messages unextracted", { count: coverage.uncoveredCount })}
</span>
</span>
)}
@@ -211,7 +212,7 @@ export function CoverageMenuBody({
disabled={busy || complete}
className="flex items-center justify-center rounded-lg border px-2 py-1 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground disabled:opacity-40 disabled:hover:bg-transparent"
>
{busy ? "Running" : "Extract"}
{busy ? t("Running") : t("Extract")}
</button>
</div>
);
@@ -241,6 +242,7 @@ export function ringFill(uncoveredTokens: number, chunkTokens: number): number {
* threshold/kill-switch re-read live).
*/
export function CoverageRing({ chatId, refreshKey = 0 }: { chatId: string; refreshKey?: number }) {
const { t } = useI18n();
const [graph, setGraph] = useState<GraphRead | null>(null);
const [coverage, setCoverage] = useState<SessionCoverage | null>(null);
const [open, setOpen] = useState(false);
@@ -301,7 +303,7 @@ export function CoverageRing({ chatId, refreshKey = 0 }: { chatId: string; refre
<div ref={rootRef} className="lh-no-drag relative shrink-0">
<Hint
// Trimmed (owner, subtlety round 2026-09-25).
label="How much of this session isn't mapped yet"
label={t("How much of this session isn't mapped yet")}
side="bottom"
>
<button
@@ -359,46 +361,43 @@ export function CoverageRingGlyph({ fill, running }: { fill: number; running: bo
* covered/uncovered tokens, message coverage, the threshold, the last
* fold's age. */
export function CoverageRingDetail({ coverage, chunkTokens }: { coverage: SessionCoverage; chunkTokens: number }) {
const { t, language } = useI18n();
return (
<>
<div className="mb-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">
Extraction coverage
</div>
{t("Extraction coverage")}</div>
<div className="flex flex-col gap-1 text-muted-foreground">
<div className="flex justify-between gap-2">
<span>Uncovered</span>
<span>{t("Uncovered")}</span>
<span className="tabular-nums text-card-foreground">
{formatTokens(coverage.uncoveredTokens)} tokens
</span>
{formatTokens(coverage.uncoveredTokens, language)}{" "}{t("tokens")}</span>
</div>
<div className="flex justify-between gap-2">
<span>Covered</span>
<span>{t("Covered")}</span>
<span className="tabular-nums text-card-foreground">
{formatTokens(coverage.coveredTokens)} tokens
</span>
{formatTokens(coverage.coveredTokens, language)}{" "}{t("tokens")}</span>
</div>
<div className="flex justify-between gap-2">
<span>Messages</span>
<span>{t("Messages")}</span>
<span className="tabular-nums text-card-foreground">
{coverage.coveredCount} / {coverage.totalMessages}
</span>
</div>
<div className="flex justify-between gap-2">
<span>Chunk threshold</span>
<span>{t("Chunk threshold")}</span>
<span className="tabular-nums text-card-foreground">
{formatTokens(chunkTokens)} tokens
</span>
{formatTokens(chunkTokens, language)}{" "}{t("tokens")}</span>
</div>
<div className="flex justify-between gap-2">
<span>Last extraction</span>
<span>{t("Last extraction")}</span>
<span className="tabular-nums text-card-foreground">
{coverage.lastFoldedAt ? timeAgo(coverage.lastFoldedAt) : "—"}
</span>
</div>
{coverage.running && (
<div className="flex justify-between gap-2" data-testid="coverage-ring-running">
<span>Status</span>
<span className="text-card-foreground">extracting…</span>
<span>{t("Status")}</span>
<span className="text-card-foreground">{t("extracting…")}</span>
</div>
)}
</div>