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
+1 -1
View File
@@ -145,7 +145,7 @@ export function AccountPage() {
<div className="row">
<input value={nickname} onChange={(e) => setNickname(e.target.value)} placeholder={me.nickname} />
<button className="btn btn-primary" onClick={saveNick} disabled={updateNick.isPending}>
ОК
Сохранить
</button>
</div>
</div>
+1 -1
View File
@@ -134,7 +134,7 @@ export function CreateMatchPage() {
setError(null);
const filled = rows.filter((r) => r.user_id !== "" && r.faction_id !== "");
if (filled.length < 2) {
setError("Нужно минимум 2 участника с выбранным игроком и фракцией.");
setError("Нужно минимум 2 игрока с выбранной фракцией.");
return;
}
if (filled.length > MAX_MATCH_PLAYERS) {
+2 -1
View File
@@ -120,7 +120,8 @@ export function GroupSettingsPage() {
<div key={m.user_id} className="row-between">
{/* Кликабельна вся левая часть строки, не только текст ника. */}
<PlayerLink userId={m.user_id} style={{ flex: 1 }}>
{m.nickname} <span className="muted small">· {m.role}</span>
{m.nickname}{" "}
<span className="muted small">· {m.role === "owner" ? "владелец" : "участник"}</span>
</PlayerLink>
{isOwner && m.role !== "owner" && (
<button
+1 -1
View File
@@ -63,7 +63,7 @@ export function HelpPage() {
<div className="card">
<h3>Другие показатели</h3>
<ul className="small" style={{ margin: 0, paddingLeft: 18 }}>
<li><b>Винрейт</b> — доля партий, в которых игрок занял 1-е место.</li>
<li><b>Винрейт (WR)</b> — доля партий, в которых игрок занял 1-е место.</li>
<li><b>Среднее место</b> — среднее по завершённым партиям.</li>
<li>
При равенстве рейтинга топ сортируется по: винрейту, числу игр, среднему месту
+1 -1
View File
@@ -154,7 +154,7 @@ function TelegramNicknamePicker({
</div>
{error && <p className="error-text">{error}</p>}
<button className="btn btn-ghost small" onClick={onCancel}>
Назад
Отмена
</button>
</div>
);
-1
View File
@@ -293,7 +293,6 @@ export function MatchDetailPage() {
: "Сыгранная партия и её результаты будут удалены. Действие необратимо."
}
confirmLabel={inProgress ? "Отменить партию" : "Удалить"}
cancelLabel="Назад"
onCancel={() => setConfirmRemove(false)}
onConfirm={() => {
setConfirmRemove(false);
@@ -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 }}>