UI: согласованные тексты и терминология

- «Winrate» -> «Винрейт» в карточке статистики; в справке добавлена
  расшифровка «Винрейт (WR)» — WR остаётся в компактных таблицах;
- кнопки сохранения: «ОК» -> «Сохранить» (аккаунт, фракции, аккаунты
  в админке); отмена в модалках/формах — везде «Отмена», не «Назад»;
- пустые состояния приведены к шаблону «X пока нет.»;
- роли участников группы переведены: owner/member -> владелец/участник,
  бейдж admin -> «админ»; «Имя обновлено» -> «Ник обновлён»;
- «N игр.» в значении числа игроков -> «N игроков» со склонением
  (новый хелпер plural в domain/format.ts);
- «минимум 2 участника» -> «минимум 2 игрока» (в партии — игроки,
  в группе — участники).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-13 17:44:08 +03:00
co-authored by Claude Fable 5
parent 002c657855
commit e38555b74d
15 changed files with 30 additions and 19 deletions
@@ -28,7 +28,7 @@ export function AdminAccountsPage() {
if (editId == null || !name) return;
try {
await update.mutateAsync({ userId: editId, nickname: name });
toast.show("Имя обновлено");
toast.show("Ник обновлён");
setEditId(null);
} catch (e) {
toast.show(e instanceof ApiError ? e.message : "Не удалось переименовать");
@@ -63,7 +63,7 @@ export function AdminAccountsPage() {
disabled={update.isPending || !editName.trim() || editName.trim() === u.nickname}
onClick={saveName}
>
ОК
Сохранить
</button>
<button className="btn btn-ghost small" onClick={() => setEditId(null)}>
Отмена
@@ -72,7 +72,7 @@ export function AdminAccountsPage() {
) : (
<div className="row-between">
<div>
<b>{u.nickname}</b> {u.role === "admin" && <span className="badge">admin</span>}
<b>{u.nickname}</b> {u.role === "admin" && <span className="badge">админ</span>}
<div className="muted small">
#{u.id} · {u.auth_provider} · {u.is_active ? "активен" : "отключён"}
</div>
@@ -48,7 +48,7 @@ export function AdminFactionsPage() {
disabled={!names[f.id] || names[f.id] === f.name_ru}
onClick={() => save(f.id)}
>
ОК
Сохранить
</button>
</div>
</div>
+1 -1
View File
@@ -59,7 +59,7 @@ export function AdminLogsPage() {
)}
</div>
))}
{(data?.items ?? []).length === 0 && <div className="muted small">Событий нет.</div>}
{(data?.items ?? []).length === 0 && <div className="muted small">Событий пока нет.</div>}
</div>
)}
</div>
@@ -3,7 +3,7 @@ import { useMemo, useState } from "react";
import { ApiError } from "../../api/client";
import { ConfirmDialog } from "../../components/ConfirmDialog";
import { formatDate } from "../../domain/format";
import { formatDate, plural } from "../../domain/format";
import { Spinner } from "../../components/Spinner";
import { Switch } from "../../components/Switch";
import { useToast } from "../../context/ToastContext";
@@ -58,7 +58,8 @@ export function AdminRecordsPage() {
<b>#{m.id}</b> · {formatDate(m.played_at)}
{!grouped && <span className="muted small"> · {m.group_name}</span>}
<div className="muted small">
{m.player_count} игр. · автор {m.created_by}
{m.player_count} {plural(m.player_count, "игрок", "игрока", "игроков")} · автор{" "}
{m.created_by}
</div>
</div>
<div className="row" style={{ gap: 4 }}>
@@ -120,7 +121,7 @@ export function AdminRecordsPage() {
{isLoading ? (
<Spinner />
) : (matches ?? []).length === 0 ? (
<div className="muted small">Записей нет.</div>
<div className="muted small">Записей пока нет.</div>
) : grouped ? (
byGroup.map(([gid, { name, items }]) => (
<div key={gid} style={{ marginBottom: 16 }}>