feat: add Russian localization and independent STT languages
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
"use client";
|
||||
import { useI18n } from "@/components/i18n";
|
||||
|
||||
/**
|
||||
* The model menu (extracted 2026-09-20 from model-picker.tsx, owner ask:
|
||||
* the agent settings' role selects use the EXACT same dropdown as the
|
||||
@@ -53,6 +55,7 @@ export function ModelMenuList({
|
||||
/** Quiet footer note under the list (context-specific). */
|
||||
footer?: string;
|
||||
}) {
|
||||
const { t, language } = useI18n();
|
||||
const [query, setQuery] = useState("");
|
||||
const [menuIdx, setMenuIdx] = useState<number | null>(null);
|
||||
const searchRef = useRef<HTMLInputElement>(null);
|
||||
@@ -104,12 +107,12 @@ export function ModelMenuList({
|
||||
}
|
||||
}
|
||||
}}
|
||||
placeholder="Search models…"
|
||||
placeholder={t("Search models…")}
|
||||
className="lh-input mb-1.5 w-full rounded-lg border bg-background px-2 py-1.5 text-foreground outline-none focus:ring-1 focus:ring-ring"
|
||||
/>
|
||||
{!models && <div className="px-2 py-3 text-center text-muted-foreground">Loading models…</div>}
|
||||
{!models && <div className="px-2 py-3 text-center text-muted-foreground">{t("Loading models…")}</div>}
|
||||
{models && results.length === 0 && (
|
||||
<div className="px-2 py-3 text-center text-muted-foreground">No models match.</div>
|
||||
<div className="px-2 py-3 text-center text-muted-foreground">{t("No models match.")}</div>
|
||||
)}
|
||||
<ul ref={listRef} data-testid="model-list" className="flex max-h-72 flex-col overflow-y-auto px-1">
|
||||
{results.map((m, i) => (
|
||||
@@ -125,13 +128,13 @@ export function ModelMenuList({
|
||||
>
|
||||
<span className="flex items-center justify-between gap-2">
|
||||
<span className="truncate text-card-foreground">{m.name}</span>
|
||||
<span className="shrink-0 tabular-nums text-muted-foreground" title={subscriptionLabel(`${m.provider}:${m.id}`) ? "Subscription connection" : "USD per 1M tokens in/out"}>
|
||||
<span className="shrink-0 tabular-nums text-muted-foreground" title={subscriptionLabel(`${m.provider}:${m.id}`) ? t("Subscription connection") : t("USD per 1M tokens in/out")}>
|
||||
{subscriptionLabel(`${m.provider}:${m.id}`) ?? priceLabel(m.cost)}
|
||||
</span>
|
||||
</span>
|
||||
<span className="flex items-center justify-between gap-2 text-muted-foreground">
|
||||
<span className="truncate">{subscriptionLabel(`${m.provider}:${m.id}`) ? `${subscriptionLabel(`${m.provider}:${m.id}`)} (subscription)` : m.provider} · {m.id}</span>
|
||||
<span className="shrink-0 tabular-nums">{formatTokens(m.contextWindow)}</span>
|
||||
<span className="truncate">{subscriptionLabel(`${m.provider}:${m.id}`) ? t("{v0} (subscription)", { v0: subscriptionLabel(`${m.provider}:${m.id}`) }) : m.provider} · {m.id}</span>
|
||||
<span className="shrink-0 tabular-nums">{formatTokens(m.contextWindow, language)}</span>
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
@@ -160,22 +163,23 @@ export function ThinkingMenuList({
|
||||
empty?: string;
|
||||
footer?: string;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
return (
|
||||
<>
|
||||
{levels.length === 0 ? (
|
||||
<div className="px-2 py-3 text-center text-muted-foreground">{empty ?? "No thinking levels."}</div>
|
||||
<div className="px-2 py-3 text-center text-muted-foreground">{empty ?? t("No thinking levels.")}</div>
|
||||
) : (
|
||||
levels.map((level) => (
|
||||
<button
|
||||
key={level}
|
||||
key={t(level)}
|
||||
type="button"
|
||||
data-testid={`thinking-option-${level}`}
|
||||
data-testid={`thinking-option-${t(level)}`}
|
||||
onClick={() => onPick(level)}
|
||||
className={`flex items-center justify-between rounded-lg px-2 py-1.5 text-left transition-colors hover:bg-muted ${
|
||||
level === effectiveLevel ? "bg-muted/60 text-card-foreground" : "text-card-foreground"
|
||||
}`}
|
||||
>
|
||||
<span>{level}</span>
|
||||
<span>{t(level)}</span>
|
||||
{level === effectiveLevel && <span aria-hidden>✓</span>}
|
||||
</button>
|
||||
))
|
||||
@@ -199,6 +203,7 @@ export function ProviderMenuList({
|
||||
providers: { id: string; envVar: string; label: string }[];
|
||||
onPick: (provider: { id: string; envVar: string; label: string }) => void;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
const [query, setQuery] = useState("");
|
||||
const [menuIdx, setMenuIdx] = useState<number | null>(null);
|
||||
const searchRef = useRef<HTMLInputElement>(null);
|
||||
@@ -248,11 +253,11 @@ export function ProviderMenuList({
|
||||
}
|
||||
}
|
||||
}}
|
||||
placeholder="Search providers…"
|
||||
placeholder={t("Search providers…")}
|
||||
className="lh-input mb-1.5 w-full rounded-lg border bg-background px-2 py-1.5 text-foreground outline-none focus:ring-1 focus:ring-ring"
|
||||
/>
|
||||
{results.length === 0 && (
|
||||
<div className="px-2 py-3 text-center text-muted-foreground">No providers match.</div>
|
||||
<div className="px-2 py-3 text-center text-muted-foreground">{t("No providers match.")}</div>
|
||||
)}
|
||||
<ul ref={listRef} data-testid="provider-list" className="flex max-h-72 flex-col overflow-y-auto px-1">
|
||||
{results.map((p, i) => (
|
||||
@@ -283,7 +288,7 @@ export function ProviderMenuList({
|
||||
* with the default first and a searched list drops it honestly — Enter
|
||||
* can never pick a row that isn't on screen. Pure — exported for
|
||||
* tests. */
|
||||
export function fontMenuResults(query: string, families: string[]): Array<string | null> {
|
||||
export function fontMenuResults(query: string, families: string[], defaultFamily = "Source Sans 3"): Array<string | null> {
|
||||
const scored = families
|
||||
.map((f) => ({ f, s: fuzzyScore(query, f) }))
|
||||
.filter((r) => r.s !== null)
|
||||
@@ -291,7 +296,7 @@ export function fontMenuResults(query: string, families: string[]): Array<string
|
||||
.map((r) => r.f);
|
||||
// The default row's searchable identity: the factory family + the
|
||||
// word "default" (the suffix it renders with).
|
||||
const defaultScore = fuzzyScore(query, "Source Sans 3", "default");
|
||||
const defaultScore = fuzzyScore(query, defaultFamily, "default");
|
||||
if (query.trim() === "" || defaultScore !== null) return [null, ...scored];
|
||||
return scored;
|
||||
}
|
||||
@@ -300,10 +305,10 @@ export function fontMenuResults(query: string, families: string[]): Array<string
|
||||
* query that matches NO family becomes the "Use '<query>'" custom pick —
|
||||
* a matching family is the better pick, so the action row only exists
|
||||
* when there is nothing else to pick. Exported for tests. */
|
||||
export function customFamilyFor(query: string, families: string[]): string | null {
|
||||
export function customFamilyFor(query: string, families: string[], defaultFamily = "Source Sans 3"): string | null {
|
||||
const trimmed = query.trim();
|
||||
if (trimmed === "") return null;
|
||||
return fontMenuResults(query, families).length === 0 ? trimmed : null;
|
||||
return fontMenuResults(query, families, defaultFamily).length === 0 ? trimmed : null;
|
||||
}
|
||||
|
||||
/** The reading-font dropdown body — ONE search field (owner ask
|
||||
@@ -329,6 +334,7 @@ export function FontMenuList({
|
||||
/** Quiet footer note under the list. */
|
||||
footer?: string;
|
||||
}) {
|
||||
const { t, language } = useI18n();
|
||||
const [query, setQuery] = useState("");
|
||||
const [menuIdx, setMenuIdx] = useState<number | null>(null);
|
||||
const searchRef = useRef<HTMLInputElement>(null);
|
||||
@@ -343,10 +349,11 @@ export function FontMenuList({
|
||||
listRef.current?.querySelector(`[data-idx="${menuIdx}"]`)?.scrollIntoView({ block: "nearest" });
|
||||
}, [menuIdx]);
|
||||
|
||||
const results = fontMenuResults(query, families ?? []);
|
||||
const defaultFamily = language === "ru" ? "Noto Sans" : "Source Sans 3";
|
||||
const results = fontMenuResults(query, families ?? [], defaultFamily);
|
||||
// The custom-family action row: only when a typed query matches
|
||||
// nothing (a matching family is the better pick). Enter picks it.
|
||||
const customFamily = customFamilyFor(query, families ?? []);
|
||||
const customFamily = customFamilyFor(query, families ?? [], defaultFamily);
|
||||
const walkable: Array<string | null> = customFamily !== null ? ["__custom__"] : results;
|
||||
|
||||
return (
|
||||
@@ -378,10 +385,10 @@ export function FontMenuList({
|
||||
}
|
||||
}
|
||||
}}
|
||||
placeholder="Search fonts…"
|
||||
placeholder={t("Search fonts…")}
|
||||
className="lh-input mb-1.5 w-full rounded-lg border bg-background px-2 py-1.5 text-foreground outline-none focus:ring-1 focus:ring-ring"
|
||||
/>
|
||||
{!families && <div className="px-2 py-3 text-center text-muted-foreground">Loading fonts…</div>}
|
||||
{!families && <div className="px-2 py-3 text-center text-muted-foreground">{t("Loading fonts…")}</div>}
|
||||
<ul ref={listRef} data-testid="font-list" className="flex max-h-72 flex-col overflow-y-auto px-1">
|
||||
{families &&
|
||||
results.map((family, i) => {
|
||||
@@ -398,7 +405,7 @@ export function FontMenuList({
|
||||
i === menuIdx ? "bg-accent/40 ring-1 ring-inset ring-border" : ""
|
||||
}${effective && i !== menuIdx ? " bg-muted/60" : ""}`}
|
||||
>
|
||||
<span className="truncate text-card-foreground">{family ?? "Source Sans 3"}</span>
|
||||
<span className="truncate text-card-foreground">{family ?? (language === "ru" ? "Noto Sans" : "Source Sans 3")}</span>
|
||||
<span className="shrink-0 text-muted-foreground">{isDefault ? "default" : effective && <span aria-hidden>✓</span>}</span>
|
||||
</button>
|
||||
</li>
|
||||
@@ -415,8 +422,8 @@ export function FontMenuList({
|
||||
menuIdx === 0 ? "bg-accent/40 ring-1 ring-inset ring-border" : ""
|
||||
}`}
|
||||
>
|
||||
<span className="truncate text-card-foreground">Use “{customFamily}”</span>
|
||||
<span className="shrink-0 text-muted-foreground">custom</span>
|
||||
<span className="truncate text-card-foreground">{t("Use “")}{customFamily}”</span>
|
||||
<span className="shrink-0 text-muted-foreground">{t("custom")}</span>
|
||||
</button>
|
||||
</li>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user