Русский и английский языки интерфейса
CI / test (push) Successful in 22s
Release / release (push) Successful in 29s

Язык выбирается в шапке панели, а до первого выбора берётся из настроек
браузера. Переведены интерфейс, подписи и описания всех параметров конфигов,
ошибки API и служебные сообщения менеджера в консоли.

Подписи параметров вынесены из app.js в hints-<язык>.json: их больше четырёхсот
на язык, и грузить чужой язык незачем. Сообщения менеджера переводятся на
выходе из API по словарю, где ключом служит русская фраза из кода, — вывод
самого игрового сервера через него проходит без изменений.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-05 21:14:21 +03:00
co-authored by Claude Opus 5
parent d5a308ac8d
commit 9a224bc070
19 changed files with 5417 additions and 857 deletions
+112 -94
View File
@@ -13,26 +13,34 @@
<div id="auth-screen" class="auth-screen hidden">
<form id="auth-form" class="auth-card">
<h1 class="auth-title">🧟 PZ Manager</h1>
<p id="auth-subtitle" class="auth-subtitle">Управление сервером Project Zomboid</p>
<p id="auth-subtitle" class="auth-subtitle" data-i18n="auth.subtitle"></p>
<label class="field" id="token-field" hidden>
<span>Код первичной настройки</span>
<span data-i18n="auth.token"></span>
<input type="text" name="token" autocomplete="off" spellcheck="false">
<small>Код напечатан в журнале менеджера при запуске: <code>journalctl -u pzmanager</code></small>
<small data-i18n-html="auth.tokenHint"></small>
</label>
<label class="field">
<span>Логин</span>
<span data-i18n="auth.login"></span>
<input type="text" name="login" autocomplete="username" required>
</label>
<label class="field">
<span>Пароль</span>
<span data-i18n="auth.password"></span>
<input type="password" name="password" autocomplete="current-password" required>
</label>
<p id="auth-error" class="error" hidden></p>
<button type="submit" class="btn btn-primary btn-block" id="auth-submit">Войти</button>
<button type="submit" class="btn btn-primary btn-block" id="auth-submit" data-i18n="auth.signIn"></button>
<label class="lang-picker lang-picker-auth">
<span data-i18n="lang.label"></span>
<select class="lang-select">
<option value="ru">Русский</option>
<option value="en">English</option>
</select>
</label>
</form>
</div>
@@ -47,36 +55,42 @@
</div>
<label class="profile-picker">
<span>Профиль</span>
<select id="profile-select" title="Профиль сервера: свой мир, конфиг и моды"></select>
<span data-i18n="profiles.picker"></span>
<select id="profile-select" data-i18n-title="profiles.pickerTitle"></select>
</label>
<div class="controls">
<button class="btn btn-start" id="btn-start">Запустить</button>
<button class="btn btn-stop" id="btn-stop">Остановить</button>
<button class="btn" id="btn-restart">Перезапустить</button>
<button class="btn btn-start" id="btn-start" data-i18n="btn.start"></button>
<button class="btn btn-stop" id="btn-stop" data-i18n="btn.stop"></button>
<button class="btn" id="btn-restart" data-i18n="btn.restart"></button>
</div>
<div class="spacer"></div>
<label class="lang-picker">
<select class="lang-select" data-i18n-title="lang.label">
<option value="ru">Русский</option>
<option value="en">English</option>
</select>
</label>
<span class="user" id="current-user"></span>
<button class="btn btn-ghost" id="btn-logout">Выйти</button>
<button class="btn btn-ghost" id="btn-logout" data-i18n="btn.logout"></button>
</header>
<div id="task-banner" class="task-banner" hidden>
<span class="spinner"></span>
<span id="task-name"></span>
<button class="btn btn-ghost btn-sm" id="btn-task-cancel">Отменить</button>
<button class="btn btn-ghost btn-sm" id="btn-task-cancel" data-i18n="btn.cancel"></button>
</div>
<nav class="tabs" id="tabs">
<button class="tab active" data-tab="overview">Обзор</button>
<button class="tab" data-tab="console">Консоль</button>
<button class="tab" data-tab="config">Конфиг сервера</button>
<button class="tab" data-tab="sandbox">Песочница</button>
<button class="tab" data-tab="mods">Моды</button>
<button class="tab" data-tab="backups">Бэкапы</button>
<button class="tab" data-tab="profiles">Профили</button>
<button class="tab" data-tab="settings">Настройки</button>
<button class="tab active" data-tab="overview" data-i18n="tab.overview"></button>
<button class="tab" data-tab="console" data-i18n="tab.console"></button>
<button class="tab" data-tab="config" data-i18n="tab.config"></button>
<button class="tab" data-tab="sandbox" data-i18n="tab.sandbox"></button>
<button class="tab" data-tab="mods" data-i18n="tab.mods"></button>
<button class="tab" data-tab="backups" data-i18n="tab.backups"></button>
<button class="tab" data-tab="profiles" data-i18n="tab.profiles"></button>
<button class="tab" data-tab="settings" data-i18n="tab.settings"></button>
</nav>
<main class="content">
@@ -85,7 +99,7 @@
<section class="panel active" data-panel="overview">
<div class="cards">
<div class="card">
<h3>Состояние</h3>
<h3 data-i18n="overview.stateCard"></h3>
<div class="stat" id="ov-state"></div>
<div class="muted" id="ov-profile"></div>
<div class="muted" id="ov-uptime"></div>
@@ -93,59 +107,59 @@
<div class="muted error" id="ov-lastexit"></div>
</div>
<div class="card">
<h3>Игроки онлайн</h3>
<h3 data-i18n="overview.playersCard"></h3>
<div class="stat" id="ov-players-count">0</div>
<ul class="player-list" id="ov-players"></ul>
</div>
<div class="card">
<h3>Процесс сервера</h3>
<div class="metric"><span>CPU</span><b id="ov-cpu"></b></div>
<div class="metric"><span>Память</span><b id="ov-rss"></b></div>
<div class="metric"><span>Потоки</span><b id="ov-threads"></b></div>
<h3 data-i18n="overview.processCard"></h3>
<div class="metric"><span data-i18n="overview.cpu"></span><b id="ov-cpu"></b></div>
<div class="metric"><span data-i18n="overview.memory"></span><b id="ov-rss"></b></div>
<div class="metric"><span data-i18n="overview.threads"></span><b id="ov-threads"></b></div>
</div>
<div class="card">
<h3>Хост</h3>
<div class="metric"><span>ОЗУ свободно</span><b id="ov-mem"></b></div>
<div class="metric"><span>Load average</span><b id="ov-load"></b></div>
<div class="metric"><span>Диск свободно</span><b id="ov-disk"></b></div>
<h3 data-i18n="overview.hostCard"></h3>
<div class="metric"><span data-i18n="overview.ramFree"></span><b id="ov-mem"></b></div>
<div class="metric"><span data-i18n="overview.load"></span><b id="ov-load"></b></div>
<div class="metric"><span data-i18n="overview.diskFree"></span><b id="ov-disk"></b></div>
</div>
</div>
<div class="card" id="install-card">
<h3>Серверные файлы</h3>
<h3 data-i18n="install.card"></h3>
<p id="install-state" class="muted"></p>
<button class="btn btn-primary" id="btn-install">Установить сервер через SteamCMD</button>
<p class="muted small" id="install-hint">Займёт несколько минут и около 3 ГБ на диске — прогресс виден во вкладке «Консоль».</p>
<button class="btn btn-primary" id="btn-install" data-i18n="install.install"></button>
<p class="muted small" id="install-hint" data-i18n="install.hintInstall"></p>
</div>
</section>
<!-- Консоль -->
<section class="panel" data-panel="console">
<div class="console-toolbar">
<label class="checkbox"><input type="checkbox" id="console-autoscroll" checked> Прокручивать за выводом</label>
<input type="search" id="console-filter" placeholder="Фильтр по строке…" class="input-sm">
<button class="btn btn-ghost btn-sm" id="btn-console-clear">Очистить экран</button>
<label class="checkbox"><input type="checkbox" id="console-autoscroll" checked>
<span data-i18n="console.autoscroll"></span></label>
<input type="search" id="console-filter" data-i18n-placeholder="console.filter" class="input-sm">
<button class="btn btn-ghost btn-sm" id="btn-console-clear" data-i18n="console.clear"></button>
</div>
<pre id="console-output" class="console"></pre>
<form class="console-input" id="console-form">
<input type="text" id="console-command" placeholder="Команда сервера, например: players, save, quit"
<input type="text" id="console-command" data-i18n-placeholder="console.placeholder"
autocomplete="off" spellcheck="false" disabled>
<button class="btn btn-primary" type="submit" disabled>Отправить</button>
<button class="btn btn-primary" type="submit" data-i18n="console.send" disabled></button>
</form>
<p class="muted small">Полезные команды: <code>players</code>, <code>save</code>, <code>servermsg "текст"</code>,
<code>kickuser "ник"</code>, <code>banuser "ник"</code>, <code>checkModsNeedUpdate</code>.</p>
<p class="muted small" data-i18n-html="console.help"></p>
</section>
<!-- Конфиг сервера -->
<section class="panel" data-panel="config">
<div class="panel-head">
<div>
<h2>Конфигурация сервера</h2>
<h2 data-i18n="config.title"></h2>
<p class="muted small" id="config-path"></p>
</div>
<div>
<input type="search" id="config-filter" placeholder="Поиск параметра…" class="input-sm">
<button class="btn btn-primary" id="btn-config-save" disabled>Сохранить</button>
<input type="search" id="config-filter" data-i18n-placeholder="config.search" class="input-sm">
<button class="btn btn-primary" id="btn-config-save" data-i18n="btn.save" disabled></button>
</div>
</div>
<p class="muted" id="config-empty" hidden></p>
@@ -156,12 +170,12 @@
<section class="panel" data-panel="sandbox">
<div class="panel-head">
<div>
<h2>Настройки песочницы</h2>
<h2 data-i18n="sandbox.title"></h2>
<p class="muted small" id="sandbox-path"></p>
</div>
<div>
<input type="search" id="sandbox-filter" placeholder="Поиск параметра…" class="input-sm">
<button class="btn btn-primary" id="btn-sandbox-save" disabled>Сохранить</button>
<input type="search" id="sandbox-filter" data-i18n-placeholder="config.search" class="input-sm">
<button class="btn btn-primary" id="btn-sandbox-save" data-i18n="btn.save" disabled></button>
</div>
</div>
<p class="muted" id="sandbox-empty" hidden></p>
@@ -172,38 +186,35 @@
<section class="panel" data-panel="mods">
<div class="panel-head">
<div>
<h2>Моды</h2>
<p class="muted small">Галочка включает мод, перетаскивание задаёт порядок загрузки.
Сервер сам скачивает моды из мастерской при запуске.</p>
<h2 data-i18n="mods.title"></h2>
<p class="muted small" data-i18n="mods.subtitle"></p>
</div>
<button class="btn btn-primary" id="btn-mods-save" disabled>Сохранить</button>
<button class="btn btn-primary" id="btn-mods-save" data-i18n="btn.save" disabled></button>
</div>
<p class="muted" id="mods-empty" hidden></p>
<div id="mods-body">
<form class="mod-add" id="mod-add-form">
<input type="text" id="mod-input" autocomplete="off"
placeholder="Ссылка на мод или коллекцию, либо просто ID">
<button class="btn" type="submit">Добавить мод</button>
<button class="btn" type="button" id="btn-add-collection">Добавить коллекцию</button>
<input type="text" id="mod-input" autocomplete="off" data-i18n-placeholder="mods.inputPlaceholder">
<button class="btn" type="submit" data-i18n="mods.add"></button>
<button class="btn" type="button" id="btn-add-collection" data-i18n="mods.addCollection"></button>
</form>
<p class="muted small">Коллекция разворачивается в список входящих в неё модов.
Сами файлы сервер скачивает сам при запуске — после этого моды можно включать галочками.</p>
<p class="muted small" data-i18n="mods.collectionHint"></p>
<div class="mod-toolbar">
<span class="muted small" id="mod-summary"></span>
<div class="spacer"></div>
<input type="search" id="mod-filter" class="input-sm" placeholder="Поиск по названию…">
<label class="checkbox"><input type="checkbox" id="mod-only-enabled"> Только включённые</label>
<input type="search" id="mod-filter" class="input-sm" data-i18n-placeholder="mods.search">
<label class="checkbox"><input type="checkbox" id="mod-only-enabled">
<span data-i18n="mods.onlyEnabled"></span></label>
</div>
<ul class="mod-list" id="mod-list"></ul>
<h3>Карты (Map)</h3>
<textarea id="map-list" rows="3" spellcheck="false"
placeholder="Например: Muldraugh, KY"></textarea>
<p class="muted small">Каждая карта с новой строки. Первой должна идти карта мода, последней — базовая.</p>
<h3 data-i18n="mods.mapsTitle"></h3>
<textarea id="map-list" rows="3" spellcheck="false" data-i18n-placeholder="mods.mapsPlaceholder"></textarea>
<p class="muted small" data-i18n="mods.mapsHint"></p>
</div>
</section>
@@ -211,17 +222,23 @@
<section class="panel" data-panel="backups">
<div class="panel-head">
<div>
<h2>Резервные копии</h2>
<h2 data-i18n="backups.title"></h2>
<p class="muted small" id="backup-dir"></p>
<p class="muted small">Бэкап снимается с активного профиля: его мир и конфиги.</p>
<p class="muted small" data-i18n="backups.note"></p>
</div>
<form id="backup-form">
<input type="text" id="backup-note" placeholder="Пометка (необязательно)" class="input-sm">
<button class="btn btn-primary" type="submit">Создать бэкап</button>
<input type="text" id="backup-note" data-i18n-placeholder="backups.notePlaceholder" class="input-sm">
<button class="btn btn-primary" type="submit" data-i18n="backups.create"></button>
</form>
</div>
<table class="table">
<thead><tr><th>Архив</th><th>Профиль</th><th>Размер</th><th>Создан</th><th></th></tr></thead>
<thead><tr>
<th data-i18n="backups.colArchive"></th>
<th data-i18n="backups.colProfile"></th>
<th data-i18n="backups.colSize"></th>
<th data-i18n="backups.colCreated"></th>
<th></th>
</tr></thead>
<tbody id="backup-rows"></tbody>
</table>
</section>
@@ -230,74 +247,74 @@
<section class="panel" data-panel="profiles">
<div class="panel-head">
<div>
<h2>Профили серверов</h2>
<p class="muted small">У каждого профиля свой мир, конфиг, песочница и набор модов.
Одновременно работает один — переключать можно на остановленном сервере.</p>
<h2 data-i18n="profiles.title"></h2>
<p class="muted small" data-i18n="profiles.subtitle"></p>
</div>
</div>
<div id="profile-cards" class="cards"></div>
<h3>Новый профиль</h3>
<h3 data-i18n="profiles.newTitle"></h3>
<form id="profile-add-form" class="fields">
<div class="cfg-field">
<label for="new-profile-id">Идентификатор</label>
<label for="new-profile-id" data-i18n="profiles.id"></label>
<input type="text" id="new-profile-id" placeholder="modded" autocomplete="off">
<div class="hint">Латиница, цифры, дефис. Станет именем конфига и папки мира.</div>
<div class="hint" data-i18n="profiles.idHint"></div>
</div>
<div class="cfg-field">
<label for="new-profile-title">Название</label>
<input type="text" id="new-profile-title" placeholder="Сборка с модами" autocomplete="off">
<div class="hint">Как профиль подписан в панели.</div>
<label for="new-profile-title" data-i18n="profiles.name"></label>
<input type="text" id="new-profile-title" data-i18n-placeholder="profiles.namePlaceholder" autocomplete="off">
<div class="hint" data-i18n="profiles.nameHint"></div>
</div>
<div class="cfg-field">
<label for="new-profile-memory">Память JVM</label>
<label for="new-profile-memory" data-i18n="profiles.memory"></label>
<input type="text" id="new-profile-memory" placeholder="8g" autocomplete="off">
<div class="hint">Применяется при запуске этого профиля.</div>
<div class="hint" data-i18n="profiles.memoryHint"></div>
</div>
<div class="cfg-field">
<label for="new-profile-copy">Скопировать настройки из</label>
<label for="new-profile-copy" data-i18n="profiles.copyFrom"></label>
<select id="new-profile-copy"></select>
<div class="hint">Копирует .ini и настройки песочницы. Мир останется новым.</div>
<div class="hint" data-i18n="profiles.copyHint"></div>
</div>
<div class="cfg-field">
<button class="btn btn-primary" type="submit">Создать профиль</button>
<button class="btn btn-primary" type="submit" data-i18n="profiles.create"></button>
</div>
</form>
</section>
<!-- Настройки -->
<section class="panel" data-panel="settings">
<h2>Версия панели</h2>
<h2 data-i18n="settings.versionTitle"></h2>
<div class="card update-card">
<div class="update-row">
<div>
<div class="stat" id="update-current"></div>
<p class="muted small" id="update-state">Проверяю обновления…</p>
<p class="muted small" id="update-state" data-i18n="update.checking"></p>
</div>
<div class="update-actions">
<button class="btn btn-sm" id="btn-update-check">Проверить</button>
<button class="btn btn-sm btn-primary" id="btn-update-apply" hidden>Обновить</button>
<button class="btn btn-sm" id="btn-manager-restart">Перезапустить панель</button>
<button class="btn btn-sm" id="btn-update-check" data-i18n="update.check"></button>
<button class="btn btn-sm btn-primary" id="btn-update-apply" data-i18n="update.apply" hidden></button>
<button class="btn btn-sm" id="btn-manager-restart" data-i18n="update.restart"></button>
</div>
</div>
<p class="muted small" id="update-manual" hidden></p>
<details id="update-notes" hidden>
<summary>Что нового</summary>
<summary data-i18n="update.whatsNew"></summary>
<div class="release-notes" id="update-notes-body"></div>
</details>
</div>
<h2>Настройки менеджера</h2>
<h2 data-i18n="settings.title"></h2>
<form id="settings-form" class="fields"></form>
<button class="btn btn-primary" id="btn-settings-save" disabled>Сохранить настройки</button>
<button class="btn btn-primary" id="btn-settings-save" data-i18n="settings.save" disabled></button>
<h2>Пользователи панели</h2>
<h2 data-i18n="settings.usersTitle"></h2>
<ul class="user-list" id="user-list"></ul>
<form id="user-add-form" class="mod-add">
<input type="text" id="new-user-login" placeholder="Логин" autocomplete="off">
<input type="password" id="new-user-password" placeholder="Пароль (минимум 8 символов)" autocomplete="new-password">
<button class="btn" type="submit">Добавить пользователя</button>
<input type="text" id="new-user-login" data-i18n-placeholder="users.loginPlaceholder" autocomplete="off">
<input type="password" id="new-user-password" data-i18n-placeholder="users.passwordPlaceholder"
autocomplete="new-password">
<button class="btn" type="submit" data-i18n="users.add"></button>
</form>
</section>
@@ -305,6 +322,7 @@
</div>
<div id="toasts" class="toasts"></div>
<script src="/i18n.js"></script>
<script src="/app.js"></script>
</body>
</html>