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
+17 -16
View File
@@ -1,13 +1,15 @@
"use client";
import { useI18n } from "@/components/i18n";
import { useEffect, useMemo, useRef, useState, type ReactNode } from "react";
import { subscriptionLabels, subscriptionLabel, type SessionPricing, type TurnUsage } from "@/src/core";
import { subscriptionLabels, subscriptionLabel, type Language, type SessionPricing, type TurnUsage } from "@/src/core";
import { formatTokens, formatUsd, planCostLabel, prettyModel } from "@/components/usage-format";
import { ModelControl, catalogLimits, useModelCatalog } from "@/components/model-picker";
export function formatContextMeasurement(tokens: number | null, limit: number | null): string {
const numerator = tokens === null ? "?" : formatTokens(tokens);
return limit === null ? numerator : `${numerator} / ${formatTokens(limit)}`;
export function formatContextMeasurement(tokens: number | null, limit: number | null, language: Language = "en"): string {
const numerator = tokens === null ? "?" : formatTokens(tokens, language);
return limit === null ? numerator : `${numerator} / ${formatTokens(limit, language)}`;
}
/**
@@ -53,6 +55,7 @@ export function StatsBar({
* data (which renders nothing pre-chat on that surface). */
leadingSlot?: ReactNode;
}) {
const { t, language } = useI18n();
const [open, setOpen] = useState(false);
const [selectedModel, setSelectedModel] = useState<string | undefined>();
// Same dropdown discipline as the model control (owner, df4433f treatment,
@@ -84,13 +87,13 @@ export function StatsBar({
const { catalog } = useModelCatalog();
const limits = useMemo(() => (catalog ? catalogLimits(catalog) : undefined), [catalog]);
const limit = selectedModel ? limits?.get(selectedModel) ?? null : null;
const contextLabel = formatContextMeasurement(contextTokens, limit);
const contextLabel = formatContextMeasurement(contextTokens, limit, language);
const { cost = 0, unpriced = turns.length, subscription = 0 } = pricing ?? {};
// Honest cost: show dollars only when at least one turn priced;
// otherwise "—" (never a fake $0.0000).
const pricedCount = turns.length - unpriced - subscription;
const providers = pricing?.subscriptionProviders ?? subscriptionLabels(turns.map((turn) => turn.model));
const costLabel = planCostLabel(pricedCount > 0 ? cost : null, providers);
const costLabel = planCostLabel(pricedCount > 0 ? cost : null, providers, (value) => formatUsd(value, language));
return (
<div
@@ -135,23 +138,22 @@ export function StatsBar({
className="absolute bottom-full left-1/2 z-40 mb-1.5 w-72 -translate-x-1/2 rounded-xl border bg-card p-3 text-xs shadow-xl"
>
<div className="mb-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">
Session stats
</div>
{t("Session stats")}</div>
<div className="flex flex-col gap-1 text-muted-foreground">
<div className="flex justify-between gap-2">
<span>Context (last call)</span>
<span>{t("Context (last call)")}</span>
<span className="tabular-nums text-card-foreground">
{contextLabel}
</span>
</div>
<div className="flex justify-between gap-2">
<span>{subscription ? "Usage" : "Total cost"}</span>
<span>{subscription ? t("Usage") : t("Total cost")}</span>
<span className="tabular-nums text-card-foreground">{costLabel}</span>
</div>
{providers.includes("ChatGPT") && <a href="https://chatgpt.com/settings/usage" target="_blank" rel="noreferrer" className="underline underline-offset-2">Manage ChatGPT usage</a>}
{providers.includes("ChatGPT") && <a href="https://chatgpt.com/settings/usage" target="_blank" rel="noreferrer" className="underline underline-offset-2">{t("Manage ChatGPT usage")}</a>}
{unpriced > 0 && (
<div className="flex justify-between gap-2">
<span>Unpriced turns</span>
<span>{t("Unpriced turns")}</span>
<span className="tabular-nums text-card-foreground">{unpriced}</span>
</div>
)}
@@ -159,8 +161,7 @@ export function StatsBar({
{turns.length > 0 && (
<>
<div className="mb-1 mt-2.5 border-t pt-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">
Turns
</div>
{t("Turns")}</div>
<ul className="flex max-h-56 flex-col gap-1 overflow-y-auto">
{turns.map((turn, i) => {
const c = pricing?.turnCosts[turn.id] ?? null;
@@ -170,8 +171,8 @@ export function StatsBar({
#{i + 1} · {prettyModel(turn.model, true)}
</span>
<span className="flex shrink-0 items-center gap-2 tabular-nums text-card-foreground">
<span className="text-muted-foreground">{turn.steps.length} steps</span>
<span>{subscriptionLabel(turn.model) ?? (c === null ? "—" : formatUsd(c))}</span>
<span className="text-muted-foreground">{turn.steps.length}{" "}{t("steps")}</span>
<span>{subscriptionLabel(turn.model) ?? (c === null ? "—" : formatUsd(c, language))}</span>
</span>
</li>
);