Русский и английский языки интерфейса
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
+245 -681
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+672
View File
@@ -0,0 +1,672 @@
"use strict";
// ---------------------------------------------------------------------------
// Локализация интерфейса
// ---------------------------------------------------------------------------
// Язык выбирается человеком и хранится в localStorage; пока выбора не было,
// панель ориентируется на язык браузера. Строки лежат плоским словарём:
// ключ -> текст, подстановки записываются как {name}.
const LANG_KEY = "pzm-lang";
const LANGS = ["ru", "en"];
const FALLBACK_LANG = "en";
function detectLang() {
let saved = null;
try {
saved = localStorage.getItem(LANG_KEY);
} catch (error) {
// Приватный режим может запрещать хранилище — тогда просто определяем язык.
}
if (LANGS.includes(saved)) return saved;
const preferred = navigator.languages && navigator.languages.length
? navigator.languages
: [navigator.language || ""];
for (const tag of preferred) {
const code = String(tag).toLowerCase().split("-")[0];
if (LANGS.includes(code)) return code;
}
return FALLBACK_LANG;
}
const LANG = detectLang();
// Сервер отдаёт свои сообщения об ошибках на языке панели, и узнаёт его из
// куки: заголовок Accept-Language говорит о браузере, а не о выборе человека.
document.cookie = `pzm_lang=${LANG}; path=/; max-age=31536000; samesite=lax`;
document.documentElement.lang = LANG;
function setLang(lang) {
if (!LANGS.includes(lang) || lang === LANG) return;
try {
localStorage.setItem(LANG_KEY, lang);
} catch (error) {
// Без хранилища выбор не переживёт перезагрузку, но применится сейчас.
}
document.cookie = `pzm_lang=${lang}; path=/; max-age=31536000; samesite=lax`;
// Перерисовать всё на месте было бы можно, но перезагрузка честнее: она
// заново читает и подписи параметров, и данные с сервера.
location.reload();
}
// t возвращает строку по ключу. Неизвестный ключ — это ошибка перевода, а не
// повод показать пустое место: отдаём сам ключ, его видно при проверке.
function t(key, vars) {
const table = STRINGS[LANG] || {};
let text = table[key];
if (text === undefined) text = (STRINGS[FALLBACK_LANG] || {})[key];
if (text === undefined) return key;
if (!vars) return text;
return text.replace(/\{(\w+)\}/g, (match, name) =>
(vars[name] === undefined ? match : String(vars[name])));
}
// Разметка помечена атрибутами: data-i18n ставит текст, а варианты с суффиксом
// — соответствующий атрибут. Так HTML остаётся читаемым и не обрастает кодом.
function applyStaticTranslations(root = document) {
root.querySelectorAll("[data-i18n]").forEach((el) => {
el.textContent = t(el.dataset.i18n);
});
root.querySelectorAll("[data-i18n-html]").forEach((el) => {
el.innerHTML = t(el.dataset.i18nHtml);
});
root.querySelectorAll("[data-i18n-placeholder]").forEach((el) => {
el.placeholder = t(el.dataset.i18nPlaceholder);
});
root.querySelectorAll("[data-i18n-title]").forEach((el) => {
el.title = t(el.dataset.i18nTitle);
});
}
const STRINGS = {
ru: {
"lang.label": "Язык",
"auth.subtitle": "Управление сервером Project Zomboid",
"auth.subtitleSetup": "Создайте учётную запись администратора",
"auth.token": "Код первичной настройки",
"auth.tokenHint": "Код напечатан в журнале менеджера при запуске: <code>journalctl -u pzmanager</code>",
"auth.login": "Логин",
"auth.password": "Пароль",
"auth.signIn": "Войти",
"auth.createAdmin": "Создать администратора",
"auth.expired": "Сессия истекла, войдите заново",
"error.http": "Ошибка {status}",
"error.offline": "Нет связи с менеджером",
"btn.start": "Запустить",
"btn.stop": "Остановить",
"btn.restart": "Перезапустить",
"btn.cancelRestart": "Отменить перезапуск",
"btn.logout": "Выйти",
"btn.cancel": "Отменить",
"btn.save": "Сохранить",
"btn.saveCount": "Сохранить ({count})",
"btn.delete": "Удалить",
"state.stopped": "Остановлен",
"state.restarting": "Ждёт перезапуска",
"state.starting": "Запускается",
"state.running": "Работает",
"state.stopping": "Останавливается",
"state.restartIn": "Перезапуск через {sec} с",
"tab.overview": "Обзор",
"tab.console": "Консоль",
"tab.config": "Конфиг сервера",
"tab.sandbox": "Песочница",
"tab.mods": "Моды",
"tab.backups": "Бэкапы",
"tab.profiles": "Профили",
"tab.settings": "Настройки",
"overview.stateCard": "Состояние",
"overview.playersCard": "Игроки онлайн",
"overview.processCard": "Процесс сервера",
"overview.hostCard": "Хост",
"overview.cpu": "CPU",
"overview.memory": "Память",
"overview.threads": "Потоки",
"overview.ramFree": "ОЗУ свободно",
"overview.load": "Load average",
"overview.diskFree": "Диск свободно",
"overview.uptime": "Работает: {value}",
"overview.profile": "Профиль: {title}",
"overview.lastExit": "Последнее завершение: {reason}",
"overview.memoryOf": "{free} из {total}",
"overview.loadCores": "{load} на {count} ядер",
"install.card": "Серверные файлы",
"install.installed": "Серверные файлы установлены.",
"install.missing": "Серверные файлы не найдены — сервер ещё не установлен.",
"install.install": "Установить сервер через SteamCMD",
"install.update": "Обновить сервер через SteamCMD",
"install.hintInstall": "Займёт несколько минут и около 3 ГБ на диске — прогресс виден во вкладке «Консоль».",
"install.hintUpdate": "Сервер должен быть остановлен. Обновление занимает несколько минут — прогресс виден во вкладке «Консоль».",
"install.confirmInstall": "Установить сервер через SteamCMD?",
"install.confirmUpdate": "Обновить серверные файлы через SteamCMD?",
"install.started": "Задача запущена — следите за вкладкой «Консоль»",
"confirm.stop": "Остановить игровой сервер? Игроки будут отключены.",
"confirm.restart": "Перезапустить сервер? Игроки будут отключены.",
"task.done": "{name} — готово",
"console.autoscroll": "Прокручивать за выводом",
"console.filter": "Фильтр по строке…",
"console.clear": "Очистить экран",
"console.placeholder": "Команда сервера, например: players, save, quit",
"console.placeholderOffline": "Сервер остановлен — консоль принимает команды только на работающем",
"console.send": "Отправить",
"console.help": "Полезные команды: <code>players</code>, <code>save</code>, <code>servermsg \"текст\"</code>, <code>kickuser \"ник\"</code>, <code>banuser \"ник\"</code>, <code>checkModsNeedUpdate</code>.",
"config.title": "Конфигурация сервера",
"config.search": "Поиск параметра…",
"config.saved": "Конфиг сохранён. Изменения применятся после перезапуска сервера.",
"config.noChanges": "Нет изменений",
"config.changedCount": "Изменено параметров: {count}",
"config.noChangesTitle": "Изменений нет",
"config.yes": "да",
"config.no": "нет",
"config.anticheatLabel": "Античит: проверка {number}",
"config.anticheatHint": "Одна из встроенных проверок. Отключайте только при ложных срабатываниях.",
"sandbox.title": "Настройки песочницы",
"sandbox.saved": "Песочница сохранена. Часть параметров действует только на новый мир.",
"group.other": "Прочее",
"group.config.general": "Основное",
"group.config.access": "Доступ и учётные записи",
"group.config.network": "Сеть и порты",
"group.config.pvp": "PVP и урон",
"group.config.world": "Мир и правила",
"group.config.backups": "Бэкапы самой игры",
"group.config.mods": "Моды",
"group.config.chat": "Чат и голос",
"group.config.badwords": "Фильтр брани",
"group.config.visibility": "Видимость игроков",
"group.config.safehouse": "Убежища",
"group.config.factions": "Фракции и войны",
"group.config.radio": "Рации",
"group.config.discord": "Discord",
"group.config.anticheat": "Античит и журналы",
"group.config.vehicles": "Транспорт",
"group.sandbox.zombies": "Зомби",
"group.sandbox.world": "Мир и время",
"group.sandbox.weather": "Погода",
"group.sandbox.loot": "Лут и предметы",
"group.sandbox.character": "Персонаж и навыки",
"group.sandbox.food": "Еда, вода и хозяйство",
"group.sandbox.animals": "Животные",
"group.sandbox.vehicles": "Транспорт",
"group.sandbox.firearms": "Огнестрельное оружие",
"group.sandbox.power": "Электричество и огонь",
"group.sandbox.service": "Служебное",
"group.sandbox.ZombieLore": "Зомби: повадки и характеристики",
"group.sandbox.ZombieConfig": "Зомби: популяция и респавн",
"group.sandbox.MultiplierConfig": "Множители опыта навыков",
"group.sandbox.Map": "Карта",
"group.sandbox.Basement": "Подвалы",
"mods.title": "Моды",
"mods.subtitle": "Галочка включает мод, перетаскивание задаёт порядок загрузки. Сервер сам скачивает моды из мастерской при запуске.",
"mods.inputPlaceholder": "Ссылка на мод или коллекцию, либо просто ID",
"mods.add": "Добавить мод",
"mods.addCollection": "Добавить коллекцию",
"mods.collectionHint": "Коллекция разворачивается в список входящих в неё модов. Сами файлы сервер скачивает сам при запуске — после этого моды можно включать галочками.",
"mods.search": "Поиск по названию…",
"mods.onlyEnabled": "Только включённые",
"mods.mapsTitle": "Карты (Map)",
"mods.mapsPlaceholder": "Например: Muldraugh, KY",
"mods.mapsHint": "Каждая карта с новой строки. Первой должна идти карта мода, последней — базовая.",
"mods.summary": "Модов: {total}, включено: {enabled}, пакетов мастерской: {workshop}",
"mods.local": "локальный мод",
"mods.notFound": "файлы не найдены — сервер скачает их при запуске",
"mods.notDownloaded": "ещё не скачан — включить можно после запуска сервера",
"mods.unnamed": "Мод {id}",
"mods.unknownID": "Mod ID неизвестен",
"mods.dragTitle": "Перетащите, чтобы изменить порядок",
"mods.dragDisabled": "Порядок задаётся у включённых модов",
"mods.toggleTitle": "Включить мод на сервере",
"mods.up": "Выше",
"mods.down": "Ниже",
"mods.remove": "Убрать",
"mods.empty": "Ничего не найдено",
"mods.unsaved": "Есть несохранённые изменения",
"mods.badInput": "Не похоже на ссылку или ID мода",
"mods.duplicate": "Этот мод уже в списке",
"mods.needCollection": "Вставьте ссылку на коллекцию или её ID",
"mods.collectionAdded": "Из коллекции добавлено модов: {added} (всего в ней {total})",
"mods.collectionKnown": "Все {total} модов коллекции уже были в списке",
"mods.saved": "Список модов сохранён. Перезапустите сервер, чтобы он их подтянул.",
"backups.title": "Резервные копии",
"backups.note": "Бэкап снимается с активного профиля: его мир и конфиги.",
"backups.notePlaceholder": "Пометка (необязательно)",
"backups.create": "Создать бэкап",
"backups.colArchive": "Архив",
"backups.colProfile": "Профиль",
"backups.colSize": "Размер",
"backups.colCreated": "Создан",
"backups.download": "Скачать",
"backups.restore": "Восстановить",
"backups.empty": "Пока ни одного архива",
"backups.started": "Бэкап запущен — прогресс во вкладке «Консоль»",
"backups.confirmRestore": "Восстановить мир из {name}? Текущий мир будет перезаписан.\nПеред восстановлением менеджер сам сделает страховочный бэкап.",
"backups.restoreStarted": "Восстановление запущено",
"backups.confirmDelete": "Удалить архив {name}? Это действие необратимо.",
"backups.deleted": "Архив удалён",
"profiles.title": "Профили серверов",
"profiles.subtitle": "У каждого профиля свой мир, конфиг, песочница и набор модов. Одновременно работает один — переключать можно на остановленном сервере.",
"profiles.picker": "Профиль",
"profiles.pickerTitle": "Профиль сервера: свой мир, конфиг и моды",
"profiles.pickerLocked": "Остановите сервер, чтобы переключить профиль",
"profiles.active": "активный",
"profiles.worldReady": "мир создан",
"profiles.configured": "настроен, мир ещё не создан",
"profiles.never": "ни разу не запускался",
"profiles.name": "Название",
"profiles.memoryMax": "Память JVM, максимум",
"profiles.memoryMin": "Память JVM, начальная",
"profiles.memoryMinPlaceholder": "не задана",
"profiles.memoryMinHint": "Пусто — JVM берёт память по мере надобности. Если задать, она займёт этот объём сразу при старте и не сможет подняться, когда его нет.",
"profiles.adminPassword": "Пароль админа игры",
"profiles.adminPasswordSet": "пароль задан",
"profiles.adminPasswordUnset": "не задан",
"profiles.activate": "Сделать активным",
"profiles.memoryExample": "Например 4g",
"profiles.memoryHost": "На машине {total} ОЗУ, свободно {free}. Оставьте запас системе.",
"profiles.switched": "Активный профиль переключён",
"profiles.saved": "Профиль сохранён",
"profiles.confirmDelete": "Убрать профиль {id} из панели?\n\nМир и конфиги останутся на диске — профиль можно вернуть, создав его с тем же идентификатором.",
"profiles.deleted": "Профиль удалён",
"profiles.created": "Профиль создан",
"profiles.newTitle": "Новый профиль",
"profiles.id": "Идентификатор",
"profiles.idHint": "Латиница, цифры, дефис. Станет именем конфига и папки мира.",
"profiles.nameHint": "Как профиль подписан в панели.",
"profiles.namePlaceholder": "Сборка с модами",
"profiles.memory": "Память JVM",
"profiles.memoryHint": "Применяется при запуске этого профиля.",
"profiles.copyFrom": "Скопировать настройки из",
"profiles.copyHint": "Копирует .ini и настройки песочницы. Мир останется новым.",
"profiles.copyNone": "не копировать",
"profiles.create": "Создать профиль",
"settings.versionTitle": "Версия панели",
"settings.title": "Настройки менеджера",
"settings.save": "Сохранить настройки",
"settings.saveCount": "Сохранить настройки ({count})",
"settings.changedCount": "Изменено полей: {count}",
"settings.saved": "Настройки сохранены",
"settings.configPath": "Файл конфигурации: {path}",
"settings.usersTitle": "Пользователи панели",
"settings.listen": "Адрес веб-панели",
"settings.listenHint": "Например 127.0.0.1:8080. Применится после перезапуска pzmanager",
"settings.serverDir": "Директория сервера",
"settings.serverDirHint": "Куда SteamCMD ставит файлы Project Zomboid",
"settings.zomboidDir": "Директория данных (~/Zomboid)",
"settings.zomboidDirHint": "Здесь лежат миры, конфиги и логи игры",
"settings.steamcmdPath": "Путь к steamcmd.sh",
"settings.steamcmdPathHint": "Пусто — найти в PATH или скачать автоматически",
"settings.autostart": "Запускать сервер вместе с менеджером",
"settings.autorestart": "Поднимать сервер после падения",
"settings.stopTimeout": "Таймаут остановки, сек",
"settings.stopTimeoutHint": "Сколько ждать после команды quit до SIGTERM",
"settings.logBuffer": "Строк консоли в памяти",
"settings.backupDir": "Директория бэкапов",
"settings.backupSchedule": "Автобэкап, минут",
"settings.backupScheduleHint": "0 — отключить автоматические бэкапы",
"settings.backupKeep": "Хранить архивов",
"settings.backupKeepHint": "0 — не удалять старые",
"settings.backupStopServer": "Останавливать сервер на время бэкапа",
"settings.backupStopServerHint": "Даёт согласованный снимок, но выкидывает игроков",
"users.loginPlaceholder": "Логин",
"users.passwordPlaceholder": "Пароль (минимум 8 символов)",
"users.add": "Добавить пользователя",
"users.changePassword": "Сменить пароль",
"users.newPassword": "Новый пароль для {login} (минимум 8 символов):",
"users.passwordChanged": "Пароль изменён",
"users.confirmDelete": "Удалить пользователя {login}?",
"users.deleted": "Пользователь удалён",
"users.added": "Пользователь добавлен",
"update.checking": "Проверяю обновления…",
"update.check": "Проверить",
"update.apply": "Обновить",
"update.applyTo": "Обновить до {version}",
"update.restart": "Перезапустить панель",
"update.restartTitle": "Перезапустить службу панели",
"update.restartUnavailable": "Недоступно: панель не может управлять своей службой",
"update.failed": "Не удалось проверить обновления: {error}",
"update.available": "Доступна версия {version}",
"update.manual": "Обновить можно командой: {command}",
"update.current": "Установлена последняя версия ({version})",
"update.unknown": "Сведений о релизах нет",
"update.whatsNew": "Что нового",
"update.whatsNewCount": "Что нового (версий: {count})",
"update.noNotes": "Описание не заполнено",
"update.fullNotes": "Полное описание релизов",
"update.hasNew": "Есть новая версия: {version}",
"update.upToDate": "Обновлений нет",
"update.confirm": "Обновить панель до {version}?\n\nИгровой сервер будет остановлен, панель перезапустится.",
"update.started": "Обновление запущено",
"update.follow": "Ход обновления: journalctl -u pzmanager-update -f",
"update.confirmRestart": "Перезапустить панель?\n\nИгровой сервер будет остановлен.",
"update.restarting": "Панель перезапускается",
"update.waiting": "{message} — жду, пока панель поднимется",
"update.waitingState": "{message}…",
"update.timeout": "Панель не поднялась за пять минут — проверьте journalctl -u pzmanager",
"unit.b": "Б",
"unit.kb": "КБ",
"unit.mb": "МБ",
"unit.gb": "ГБ",
"unit.tb": "ТБ",
"unit.days": "{value} д",
"unit.hours": "{value} ч",
"unit.minutes": "{value} мин",
"locale.tag": "ru-RU",
},
en: {
"lang.label": "Language",
"auth.subtitle": "Project Zomboid server management",
"auth.subtitleSetup": "Create an administrator account",
"auth.token": "Setup code",
"auth.tokenHint": "The code is printed to the manager log at startup: <code>journalctl -u pzmanager</code>",
"auth.login": "Login",
"auth.password": "Password",
"auth.signIn": "Sign in",
"auth.createAdmin": "Create administrator",
"auth.expired": "Session expired, please sign in again",
"error.http": "Error {status}",
"error.offline": "No connection to the manager",
"btn.start": "Start",
"btn.stop": "Stop",
"btn.restart": "Restart",
"btn.cancelRestart": "Cancel restart",
"btn.logout": "Sign out",
"btn.cancel": "Cancel",
"btn.save": "Save",
"btn.saveCount": "Save ({count})",
"btn.delete": "Delete",
"state.stopped": "Stopped",
"state.restarting": "Waiting to restart",
"state.starting": "Starting",
"state.running": "Running",
"state.stopping": "Stopping",
"state.restartIn": "Restarting in {sec} s",
"tab.overview": "Overview",
"tab.console": "Console",
"tab.config": "Server config",
"tab.sandbox": "Sandbox",
"tab.mods": "Mods",
"tab.backups": "Backups",
"tab.profiles": "Profiles",
"tab.settings": "Settings",
"overview.stateCard": "State",
"overview.playersCard": "Players online",
"overview.processCard": "Server process",
"overview.hostCard": "Host",
"overview.cpu": "CPU",
"overview.memory": "Memory",
"overview.threads": "Threads",
"overview.ramFree": "RAM free",
"overview.load": "Load average",
"overview.diskFree": "Disk free",
"overview.uptime": "Uptime: {value}",
"overview.profile": "Profile: {title}",
"overview.lastExit": "Last exit: {reason}",
"overview.memoryOf": "{free} of {total}",
"overview.loadCores": "{load} on {count} cores",
"install.card": "Server files",
"install.installed": "Server files are installed.",
"install.missing": "Server files not found — the server is not installed yet.",
"install.install": "Install server via SteamCMD",
"install.update": "Update server via SteamCMD",
"install.hintInstall": "Takes a few minutes and about 3 GB of disk space — watch the progress on the Console tab.",
"install.hintUpdate": "The server must be stopped. The update takes a few minutes — watch the progress on the Console tab.",
"install.confirmInstall": "Install the server via SteamCMD?",
"install.confirmUpdate": "Update the server files via SteamCMD?",
"install.started": "Task started — watch the Console tab",
"confirm.stop": "Stop the game server? Players will be disconnected.",
"confirm.restart": "Restart the server? Players will be disconnected.",
"task.done": "{name} — done",
"console.autoscroll": "Scroll with output",
"console.filter": "Filter by text…",
"console.clear": "Clear screen",
"console.placeholder": "Server command, for example: players, save, quit",
"console.placeholderOffline": "Server stopped — the console only accepts commands while it runs",
"console.send": "Send",
"console.help": "Handy commands: <code>players</code>, <code>save</code>, <code>servermsg \"text\"</code>, <code>kickuser \"name\"</code>, <code>banuser \"name\"</code>, <code>checkModsNeedUpdate</code>.",
"config.title": "Server configuration",
"config.search": "Find a setting…",
"config.saved": "Config saved. Changes apply after a server restart.",
"config.noChanges": "Nothing changed",
"config.changedCount": "Settings changed: {count}",
"config.noChangesTitle": "No changes",
"config.yes": "yes",
"config.no": "no",
"config.anticheatLabel": "Anti-cheat: check {number}",
"config.anticheatHint": "One of the built-in checks. Disable it only if it fires on legitimate players.",
"sandbox.title": "Sandbox settings",
"sandbox.saved": "Sandbox saved. Some settings only affect a freshly created world.",
"group.other": "Other",
"group.config.general": "General",
"group.config.access": "Access and accounts",
"group.config.network": "Network and ports",
"group.config.pvp": "PVP and damage",
"group.config.world": "World and rules",
"group.config.backups": "In-game backups",
"group.config.mods": "Mods",
"group.config.chat": "Chat and voice",
"group.config.badwords": "Profanity filter",
"group.config.visibility": "Player visibility",
"group.config.safehouse": "Safehouses",
"group.config.factions": "Factions and wars",
"group.config.radio": "Radio",
"group.config.discord": "Discord",
"group.config.anticheat": "Anti-cheat and logs",
"group.config.vehicles": "Vehicles",
"group.sandbox.zombies": "Zombies",
"group.sandbox.world": "World and time",
"group.sandbox.weather": "Weather",
"group.sandbox.loot": "Loot and items",
"group.sandbox.character": "Character and skills",
"group.sandbox.food": "Food, water and farming",
"group.sandbox.animals": "Animals",
"group.sandbox.vehicles": "Vehicles",
"group.sandbox.firearms": "Firearms",
"group.sandbox.power": "Power and fire",
"group.sandbox.service": "Service",
"group.sandbox.ZombieLore": "Zombies: behaviour and traits",
"group.sandbox.ZombieConfig": "Zombies: population and respawn",
"group.sandbox.MultiplierConfig": "Skill XP multipliers",
"group.sandbox.Map": "Map",
"group.sandbox.Basement": "Basements",
"mods.title": "Mods",
"mods.subtitle": "The checkbox enables a mod, dragging sets the load order. The server downloads mods from the Workshop on startup.",
"mods.inputPlaceholder": "Link to a mod or collection, or just an ID",
"mods.add": "Add mod",
"mods.addCollection": "Add collection",
"mods.collectionHint": "A collection expands into the list of mods it contains. The server downloads the files itself on startup — after that the mods can be enabled with checkboxes.",
"mods.search": "Search by name…",
"mods.onlyEnabled": "Enabled only",
"mods.mapsTitle": "Maps (Map)",
"mods.mapsPlaceholder": "For example: Muldraugh, KY",
"mods.mapsHint": "One map per line. The mod map goes first, the base map last.",
"mods.summary": "Mods: {total}, enabled: {enabled}, Workshop items: {workshop}",
"mods.local": "local mod",
"mods.notFound": "files not found — the server will download them on startup",
"mods.notDownloaded": "not downloaded yet — can be enabled after a server start",
"mods.unnamed": "Mod {id}",
"mods.unknownID": "Mod ID unknown",
"mods.dragTitle": "Drag to change the order",
"mods.dragDisabled": "Order applies to enabled mods only",
"mods.toggleTitle": "Enable the mod on the server",
"mods.up": "Up",
"mods.down": "Down",
"mods.remove": "Remove",
"mods.empty": "Nothing found",
"mods.unsaved": "There are unsaved changes",
"mods.badInput": "That does not look like a mod link or ID",
"mods.duplicate": "This mod is already on the list",
"mods.needCollection": "Paste a collection link or its ID",
"mods.collectionAdded": "Mods added from the collection: {added} (of {total} in it)",
"mods.collectionKnown": "All {total} mods of the collection were already on the list",
"mods.saved": "Mod list saved. Restart the server so it picks them up.",
"backups.title": "Backups",
"backups.note": "The backup is taken from the active profile: its world and configs.",
"backups.notePlaceholder": "Note (optional)",
"backups.create": "Create backup",
"backups.colArchive": "Archive",
"backups.colProfile": "Profile",
"backups.colSize": "Size",
"backups.colCreated": "Created",
"backups.download": "Download",
"backups.restore": "Restore",
"backups.empty": "No archives yet",
"backups.started": "Backup started — progress on the Console tab",
"backups.confirmRestore": "Restore the world from {name}? The current world will be overwritten.\nThe manager takes a safety backup first.",
"backups.restoreStarted": "Restore started",
"backups.confirmDelete": "Delete the archive {name}? This cannot be undone.",
"backups.deleted": "Archive deleted",
"profiles.title": "Server profiles",
"profiles.subtitle": "Every profile has its own world, config, sandbox and mod set. Only one runs at a time — switch it while the server is stopped.",
"profiles.picker": "Profile",
"profiles.pickerTitle": "Server profile: its own world, config and mods",
"profiles.pickerLocked": "Stop the server to switch the profile",
"profiles.active": "active",
"profiles.worldReady": "world created",
"profiles.configured": "configured, world not created yet",
"profiles.never": "never started",
"profiles.name": "Name",
"profiles.memoryMax": "JVM memory, maximum",
"profiles.memoryMin": "JVM memory, initial",
"profiles.memoryMinPlaceholder": "not set",
"profiles.memoryMinHint": "Empty — the JVM takes memory as it needs it. If set, it grabs that much at startup and fails to start when the machine has less.",
"profiles.adminPassword": "Game admin password",
"profiles.adminPasswordSet": "password set",
"profiles.adminPasswordUnset": "not set",
"profiles.activate": "Make active",
"profiles.memoryExample": "For example 4g",
"profiles.memoryHost": "The machine has {total} of RAM, {free} free. Leave some for the system.",
"profiles.switched": "Active profile switched",
"profiles.saved": "Profile saved",
"profiles.confirmDelete": "Remove the profile {id} from the panel?\n\nThe world and configs stay on disk — the profile can be brought back by creating it with the same identifier.",
"profiles.deleted": "Profile deleted",
"profiles.created": "Profile created",
"profiles.newTitle": "New profile",
"profiles.id": "Identifier",
"profiles.idHint": "Latin letters, digits, hyphen. Becomes the config name and the world folder.",
"profiles.nameHint": "How the profile is labelled in the panel.",
"profiles.namePlaceholder": "Modded run",
"profiles.memory": "JVM memory",
"profiles.memoryHint": "Applied when this profile starts.",
"profiles.copyFrom": "Copy settings from",
"profiles.copyHint": "Copies the .ini and the sandbox settings. The world stays new.",
"profiles.copyNone": "do not copy",
"profiles.create": "Create profile",
"settings.versionTitle": "Panel version",
"settings.title": "Manager settings",
"settings.save": "Save settings",
"settings.saveCount": "Save settings ({count})",
"settings.changedCount": "Fields changed: {count}",
"settings.saved": "Settings saved",
"settings.configPath": "Configuration file: {path}",
"settings.usersTitle": "Panel users",
"settings.listen": "Web panel address",
"settings.listenHint": "For example 127.0.0.1:8080. Applies after a pzmanager restart",
"settings.serverDir": "Server directory",
"settings.serverDirHint": "Where SteamCMD installs the Project Zomboid files",
"settings.zomboidDir": "Data directory (~/Zomboid)",
"settings.zomboidDirHint": "Worlds, configs and game logs live here",
"settings.steamcmdPath": "Path to steamcmd.sh",
"settings.steamcmdPathHint": "Empty — look it up in PATH or download automatically",
"settings.autostart": "Start the server together with the manager",
"settings.autorestart": "Bring the server back up after a crash",
"settings.stopTimeout": "Stop timeout, sec",
"settings.stopTimeoutHint": "How long to wait after the quit command before SIGTERM",
"settings.logBuffer": "Console lines kept in memory",
"settings.backupDir": "Backup directory",
"settings.backupSchedule": "Auto backup, minutes",
"settings.backupScheduleHint": "0 — turn automatic backups off",
"settings.backupKeep": "Archives to keep",
"settings.backupKeepHint": "0 — never delete old ones",
"settings.backupStopServer": "Stop the server while backing up",
"settings.backupStopServerHint": "Gives a consistent snapshot, but kicks the players",
"users.loginPlaceholder": "Login",
"users.passwordPlaceholder": "Password (at least 8 characters)",
"users.add": "Add user",
"users.changePassword": "Change password",
"users.newPassword": "New password for {login} (at least 8 characters):",
"users.passwordChanged": "Password changed",
"users.confirmDelete": "Delete the user {login}?",
"users.deleted": "User deleted",
"users.added": "User added",
"update.checking": "Checking for updates…",
"update.check": "Check",
"update.apply": "Update",
"update.applyTo": "Update to {version}",
"update.restart": "Restart panel",
"update.restartTitle": "Restart the panel service",
"update.restartUnavailable": "Unavailable: the panel cannot manage its own service",
"update.failed": "Could not check for updates: {error}",
"update.available": "Version {version} is available",
"update.manual": "Update with this command: {command}",
"update.current": "The latest version is installed ({version})",
"update.unknown": "No release information",
"update.whatsNew": "What's new",
"update.whatsNewCount": "What's new ({count} versions)",
"update.noNotes": "No description",
"update.fullNotes": "Full release notes",
"update.hasNew": "A new version is available: {version}",
"update.upToDate": "No updates",
"update.confirm": "Update the panel to {version}?\n\nThe game server will be stopped and the panel will restart.",
"update.started": "Update started",
"update.follow": "Update progress: journalctl -u pzmanager-update -f",
"update.confirmRestart": "Restart the panel?\n\nThe game server will be stopped.",
"update.restarting": "The panel is restarting",
"update.waiting": "{message} — waiting for the panel to come back",
"update.waitingState": "{message}…",
"update.timeout": "The panel did not come back within five minutes — check journalctl -u pzmanager",
"unit.b": "B",
"unit.kb": "KB",
"unit.mb": "MB",
"unit.gb": "GB",
"unit.tb": "TB",
"unit.days": "{value} d",
"unit.hours": "{value} h",
"unit.minutes": "{value} min",
"locale.tag": "en-US",
},
};
+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>
+19
View File
@@ -547,3 +547,22 @@ input.input-sm, .input-sm { width: auto; padding: 6px 10px; font-size: 13px; }
#update-manual { font-family: var(--mono); word-break: break-all; }
.update-available { color: var(--accent); }
/* Переключатель языка: в шапке — только список, на экране входа — с подписью. */
.lang-picker {
display: inline-flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: var(--muted);
}
.lang-picker select {
width: auto;
padding: 5px 10px;
font-size: 13px;
}
.lang-picker-auth {
justify-content: center;
margin-top: 18px;
width: 100%;
}