feat: add Russian localization and independent STT languages
This commit is contained in:
+26
-26
@@ -1,4 +1,7 @@
|
||||
"use client";
|
||||
import { countLabel } from "@/src/core";
|
||||
import { useI18n } from "@/components/i18n";
|
||||
|
||||
|
||||
/**
|
||||
* THE CHAIN TABLE (chains overlay; owner refinements round 2, 2026-09-29):
|
||||
@@ -143,6 +146,7 @@ export function ChainTableBody({
|
||||
selectMode?: boolean;
|
||||
onSelect?: () => void;
|
||||
}) {
|
||||
const { t, language } = useI18n();
|
||||
const byId = useMemo(() => new Map(nodes.map((n) => [n.id, n])), [nodes]);
|
||||
const chains = useMemo(() => chainsForSelection(nodes, storedChains), [nodes, storedChains]);
|
||||
const searching = query.trim() !== "";
|
||||
@@ -246,7 +250,7 @@ export function ChainTableBody({
|
||||
law: later in the DOM). */}
|
||||
<TopBar>
|
||||
<span className="min-w-0 flex-1 truncate text-xs text-muted-foreground">
|
||||
{chains.length} {chains.length === 1 ? "chain" : "chains"} · {nodes.length} {nodes.length === 1 ? "node" : "nodes"}
|
||||
{countLabel(language, chains.length, "chain", "chains")} · {countLabel(language, nodes.length, "node", "nodes")}
|
||||
</span>
|
||||
<div className="relative shrink-0">
|
||||
<SearchIcon className="absolute left-2.5 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground" />
|
||||
@@ -254,8 +258,8 @@ export function ChainTableBody({
|
||||
value={query}
|
||||
data-testid="chain-table-search"
|
||||
onChange={(e) => onQuery(e.target.value)}
|
||||
placeholder="Search nodes…"
|
||||
aria-label="Search nodes"
|
||||
placeholder={t("Search nodes…")}
|
||||
aria-label={t("Search nodes")}
|
||||
spellCheck={false}
|
||||
className="lh-no-drag h-8 w-56 rounded-lg border bg-background pl-8 pr-3 text-xs outline-none placeholder:text-muted-foreground focus:border-ring"
|
||||
/>
|
||||
@@ -264,7 +268,7 @@ export function ChainTableBody({
|
||||
type="button"
|
||||
data-testid="chain-table-close"
|
||||
onClick={onClose}
|
||||
aria-label="Close chains"
|
||||
aria-label={t("Close chains")}
|
||||
className="lh-no-drag flex size-8 shrink-0 items-center justify-center rounded-lg border border-border/70 bg-card/80 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
|
||||
>
|
||||
<XIcon className="size-4" />
|
||||
@@ -288,7 +292,7 @@ export function ChainTableBody({
|
||||
aria-sort={sort.col === "name" ? (sort.dir === 1 ? "ascending" : "descending") : "none"}
|
||||
className="flex flex-1 items-center gap-1 text-left transition-colors hover:text-foreground"
|
||||
>
|
||||
Name {dirIcon("name")}
|
||||
{t("Name")}{" "}{dirIcon("name")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -297,7 +301,7 @@ export function ChainTableBody({
|
||||
aria-sort={sort.col === "props" ? (sort.dir === 1 ? "ascending" : "descending") : "none"}
|
||||
className="flex w-14 shrink-0 items-center justify-end gap-1 text-right transition-colors hover:text-foreground"
|
||||
>
|
||||
Props {dirIcon("props")}
|
||||
{t("Props")}{" "}{dirIcon("props")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -306,14 +310,13 @@ export function ChainTableBody({
|
||||
aria-sort={sort.col === "created" ? (sort.dir === 1 ? "ascending" : "descending") : "none"}
|
||||
className="flex w-20 shrink-0 items-center justify-end gap-1 text-right transition-colors hover:text-foreground"
|
||||
>
|
||||
Created {dirIcon("created")}
|
||||
{t("Created")}{" "}{dirIcon("created")}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{groups.length === 0 && (
|
||||
<div data-testid="chain-table-nomatch" className="px-4 pt-3 text-xs text-muted-foreground">
|
||||
No matching chains.
|
||||
</div>
|
||||
{t("No matching chains.")}</div>
|
||||
)}
|
||||
|
||||
{groups.map((group) => {
|
||||
@@ -348,15 +351,15 @@ export function ChainTableBody({
|
||||
indeterminate={selectedCount > 0 && selectedCount < group.nodeIds.length}
|
||||
onToggle={(next) => onToggleNodes(group.nodeIds, next)}
|
||||
testid={`cluster-checkbox-${group.key}`}
|
||||
label={`Select everything born in ${group.label ?? "unlabeled"}`}
|
||||
label={t("Select everything born in {v0}", { v0: group.label ?? "unlabeled" })}
|
||||
/>
|
||||
<Expander open={open} onClick={() => onToggleCluster(group.key)} testid={`cluster-toggle-${group.key}`} label={`Expand cluster ${group.label ?? "unlabeled"}`} />
|
||||
<Expander open={open} onClick={() => onToggleCluster(group.key)} testid={`cluster-toggle-${group.key}`} label={t("Expand cluster {v0}", { v0: group.label ?? "unlabeled" })} />
|
||||
<span className="flex shrink-0 items-center justify-center text-muted-foreground">
|
||||
{open ? <FolderOpenIcon className="size-3.5" /> : <FolderIcon className="size-3.5" />}
|
||||
</span>
|
||||
<span className="min-w-0 flex-1 truncate text-sm font-medium">
|
||||
{group.label ?? <span className="text-muted-foreground/60">unlabeled</span>}
|
||||
{group.kind === "notes" && <span className="ml-2 text-[11px] uppercase tracking-widest text-muted-foreground/60">notes</span>}
|
||||
{group.label ?? <span className="text-muted-foreground/60">{t("unlabeled")}</span>}
|
||||
{group.kind === "notes" && <span className="ml-2 text-[11px] uppercase tracking-widest text-muted-foreground/60">{t("notes")}</span>}
|
||||
</span>
|
||||
<span className="w-14 shrink-0 text-right text-xs tabular-nums text-muted-foreground">
|
||||
{group.nodeIds.length}
|
||||
@@ -397,9 +400,9 @@ export function ChainTableBody({
|
||||
indeterminate={selectedInChain > 0 && selectedInChain < memberIds.length}
|
||||
onToggle={(next) => onToggleNodes(memberIds, next)}
|
||||
testid={`chain-checkbox-${chain.id}`}
|
||||
label={`Select chain ${nameOf(chain.conclusion)}`}
|
||||
label={t("Select chain {v0}", { v0: nameOf(chain.conclusion) })}
|
||||
/>
|
||||
<Expander open={chainOpen} onClick={() => onToggleChain(chain.id)} testid={`chain-toggle-${chain.id}`} label={`Expand chain ${nameOf(chain.conclusion)}`} />
|
||||
<Expander open={chainOpen} onClick={() => onToggleChain(chain.id)} testid={`chain-toggle-${chain.id}`} label={t("Expand chain {v0}", { v0: nameOf(chain.conclusion) })} />
|
||||
{/* The chain line: proposition names as faint-outlined
|
||||
chips. THE LOGIC IS THE PAYLOAD (owner rulings,
|
||||
2026-09-30): premises join with the LaTeX LAND (∧),
|
||||
@@ -465,7 +468,7 @@ export function ChainTableBody({
|
||||
checked={selection.has(id)}
|
||||
onToggle={(next) => onToggleNodes([id], next)}
|
||||
testid={`prop-checkbox-${id}`}
|
||||
label={`Select node ${node.name}`}
|
||||
label={t("Select node {v0}", { v0: node.name })}
|
||||
/>
|
||||
<span className="min-w-0 flex-1 text-xs leading-relaxed text-muted-foreground">
|
||||
{node.statement}
|
||||
@@ -493,8 +496,8 @@ export function ChainTableBody({
|
||||
className="pointer-events-auto flex items-center gap-3 rounded-full border bg-card px-4 py-2 shadow-lg"
|
||||
>
|
||||
<span data-testid="chain-selected-summary" className="text-xs font-medium">
|
||||
{selection.size} {selection.size === 1 ? "node" : "nodes"} →{" "}
|
||||
{derived.length} {derived.length === 1 ? "chain" : "chains"}
|
||||
{countLabel(language, selection.size, "node", "nodes")} →{" "}
|
||||
{countLabel(language, derived.length, "chain", "chains")}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
@@ -502,19 +505,17 @@ export function ChainTableBody({
|
||||
onClick={onClear}
|
||||
className="rounded-full px-2.5 py-1 text-xs text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
{t("Clear")}</button>
|
||||
{selectMode ? (
|
||||
<button
|
||||
type="button"
|
||||
data-testid="chain-practice-done"
|
||||
onClick={onSelect}
|
||||
aria-label={`Done picking — ${selection.size} ${selection.size === 1 ? "proposition" : "propositions"} selected`}
|
||||
aria-label={t("Done picking — {v0} {v1} selected", { v0: selection.size, v1: selection.size === 1 ? "proposition" : "propositions" })}
|
||||
className="flex items-center gap-1.5 rounded-full bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground transition-colors hover:bg-primary/80"
|
||||
>
|
||||
<DumbbellIcon aria-hidden="true" className="size-3.5" />
|
||||
Done
|
||||
</button>
|
||||
{t("Done")}</button>
|
||||
) : (
|
||||
<>
|
||||
{/* The danger fence (owner ruling): a divider quarantines
|
||||
@@ -526,12 +527,11 @@ export function ChainTableBody({
|
||||
type="button"
|
||||
data-testid="chain-delete"
|
||||
onClick={onDelete}
|
||||
aria-label={`Delete ${selection.size} selected ${selection.size === 1 ? "node" : "nodes"}`}
|
||||
aria-label={t("Delete {v0} selected {v1}", { v0: selection.size, v1: selection.size === 1 ? "node" : "nodes" })}
|
||||
className="flex items-center gap-1.5 rounded-full border border-destructive/50 bg-destructive/10 px-3 py-1.5 text-xs font-medium text-destructive transition-colors hover:bg-destructive/20"
|
||||
>
|
||||
<Trash2Icon aria-hidden="true" className="size-3.5" />
|
||||
Delete
|
||||
</button>
|
||||
{t("Delete")}</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user