site: add RU/EN language toggle with full i18n on landing

This commit is contained in:
SashegDev
2026-08-20 09:31:16 +00:00
parent aaecf7e395
commit 3755411954
3 changed files with 359 additions and 93 deletions
+25
View File
@@ -104,6 +104,30 @@ ul { list-style: none; }
.nav-cta:hover { background: var(--accent-soft); transform: translateY(-1px); } .nav-cta:hover { background: var(--accent-soft); transform: translateY(-1px); }
.nav-right {
display: flex;
align-items: center;
gap: 16px;
}
.lang-toggle {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 8px;
background: transparent;
color: var(--text-secondary);
font-size: 13px;
font-weight: 600;
letter-spacing: 0.4px;
cursor: pointer;
transition: color .2s ease, border-color .2s ease, background .2s ease;
}
.lang-toggle:hover { color: var(--text); border-color: rgba(255, 255, 255, 0.3); background: rgba(255, 255, 255, 0.04); }
/* ---------- Hero ---------- */ /* ---------- Hero ---------- */
.hero { .hero {
@@ -804,6 +828,7 @@ ul { list-style: none; }
@media (max-width: 720px) { @media (max-width: 720px) {
.nav-links { display: none; } .nav-links { display: none; }
.nav-cta { display: none; }
.section { padding: 90px 20px; } .section { padding: 90px 20px; }
.features-grid, .news-grid, .theme-grid { grid-template-columns: 1fr; } .features-grid, .news-grid, .theme-grid { grid-template-columns: 1fr; }
.server-stats { grid-template-columns: 1fr; } .server-stats { grid-template-columns: 1fr; }
+72 -66
View File
@@ -1,5 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="ru">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
@@ -20,14 +20,20 @@
<span>Zern</span> <span>Zern</span>
</a> </a>
<ul class="nav-links"> <ul class="nav-links">
<li><a href="#server">Server</a></li> <li><a href="#server" data-i18n="nav.server">Сервер</a></li>
<li><a href="#highlights">Highlights</a></li> <li><a href="#highlights" data-i18n="nav.highlights">Хайлайты</a></li>
<li><a href="#theme">Season</a></li> <li><a href="#theme" data-i18n="nav.season">Сезон</a></li>
<li><a href="#features">Features</a></li> <li><a href="#features" data-i18n="nav.features">Возможности</a></li>
<li><a href="#download">Download</a></li> <li><a href="#download" data-i18n="nav.download">Скачать</a></li>
<li><a href="#news">News</a></li> <li><a href="#news" data-i18n="nav.news">Новости</a></li>
</ul> </ul>
<a class="nav-cta" href="#download">Play now</a> <div class="nav-right">
<button class="lang-toggle" id="langToggle" type="button" aria-label="Switch language">
<span class="lang-toggle-label" id="langToggleLabel">EN</span>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><path d="M2 12h20"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
</button>
<a class="nav-cta" href="#download" data-i18n="nav.play">Играть сейчас</a>
</div>
</div> </div>
</nav> </nav>
@@ -40,21 +46,21 @@
<div class="hero-diamond hero-diamond--3" aria-hidden="true"></div> <div class="hero-diamond hero-diamond--3" aria-hidden="true"></div>
<div class="hero-inner" data-parallax="0.15"> <div class="hero-inner" data-parallax="0.15">
<p class="hero-kicker">CUSTOM MINECRAFT LAUNCHER</p> <p class="hero-kicker" data-i18n="hero.kicker">КАСТОМНЫЙ MINECRAFT ЛАУНЧЕР</p>
<h1 class="hero-title">Awaken from <br><span class="hero-title-accent">the cold.</span></h1> <h1 class="hero-title" data-i18n-html="hero.title">Очнись от<br><span class="hero-title-accent">холода.</span></h1>
<p class="hero-sub">Zern brings your favourite modpacks to life. One launcher, every loader, no hassle — built for the ZernMC server and beyond.</p> <p class="hero-sub" data-i18n="hero.sub">Zern оживляет твои любимые модпаки. Один лаунчер, любой загрузчик, без лишних хлопот — создан для сервера ZernMC и не только.</p>
<div class="hero-actions"> <div class="hero-actions">
<a class="btn btn-primary" href="#download"> <a class="btn btn-primary" href="#download">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3v12"/><path d="m7 10 5 5 5-5"/><path d="M4 21h16"/></svg> <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3v12"/><path d="m7 10 5 5 5-5"/><path d="M4 21h16"/></svg>
Download launcher <span data-i18n="hero.download">Скачать лаунчер</span>
</a> </a>
<a class="btn btn-ghost" href="#features">Explore</a> <a class="btn btn-ghost" href="#features" data-i18n="hero.explore">Обзор</a>
</div> </div>
<p class="hero-meta">v<span id="hero-version"></span> · <span id="hero-size"></span> · Windows</p> <p class="hero-meta">v<span id="hero-version"></span> · <span id="hero-size"></span> · Windows</p>
</div> </div>
<div class="scroll-hint" data-parallax="0.3"> <div class="scroll-hint" data-parallax="0.3">
<span>Scroll to explore</span> <span data-i18n="hero.scroll">Листай вниз</span>
<div class="scroll-hint-line"><i></i></div> <div class="scroll-hint-line"><i></i></div>
</div> </div>
</header> </header>
@@ -64,24 +70,24 @@
<section class="section server" id="server"> <section class="section server" id="server">
<div class="section-head"> <div class="section-head">
<span class="section-index">01 / 06</span> <span class="section-index">01 / 06</span>
<h2 class="section-title">The server is <span class="accent">alive.</span></h2> <h2 class="section-title" data-i18n-html="server.title">Сервер <span class="accent">жив.</span></h2>
<p class="section-sub">ZernMC is a custom Minecraft server with its own lore, hand-written plugins and a living community. This is what's happening right now.</p> <p class="section-sub" data-i18n="server.sub">ZernMC — это кастомный Minecraft-сервер со своим лором, самописными плагинами и живым комьюнити. Вот что происходит прямо сейчас.</p>
</div> </div>
<div class="server-stats"> <div class="server-stats">
<div class="server-card"> <div class="server-card">
<div class="server-card-label">Status</div> <div class="server-card-label" data-i18n="server.status">Статус</div>
<div class="server-card-value" id="srv-status"><span class="status-dot status-dot--loading"></span> Checking…</div> <div class="server-card-value" id="srv-status"><span class="status-dot status-dot--loading"></span> <span data-i18n="server.checking">Проверяем…</span></div>
<div class="server-card-sub" id="srv-version">1.20.1 / OBТ</div> <div class="server-card-sub" id="srv-version">1.20.1 / ОБТ</div>
</div> </div>
<div class="server-card"> <div class="server-card">
<div class="server-card-label">Players online</div> <div class="server-card-label" data-i18n="server.players">Игроков онлайн</div>
<div class="server-card-value" id="srv-players">— / —</div> <div class="server-card-value" id="srv-players">— / —</div>
<div class="server-card-sub">mc.zernmc.ru</div> <div class="server-card-sub">mc.zernmc.ru</div>
</div> </div>
<div class="server-card"> <div class="server-card">
<div class="server-card-label">Mode</div> <div class="server-card-label" data-i18n="server.mode">Режим</div>
<div class="server-card-value">Survival <span class="server-season">· Season Prologue</span></div> <div class="server-card-value">Survival <span class="server-season">· <span data-i18n="server.season">Сезон Prologue</span></span></div>
<div class="server-card-sub">1.21.1 NeoForge · Create: Aeronautics</div> <div class="server-card-sub">1.21.1 NeoForge · Create: Aeronautics</div>
</div> </div>
</div> </div>
@@ -90,14 +96,14 @@
<section class="section highlights" id="highlights"> <section class="section highlights" id="highlights">
<div class="section-head"> <div class="section-head">
<span class="section-index">02 / 06</span> <span class="section-index">02 / 06</span>
<h2 class="section-title">Server <span class="accent">highlights.</span></h2> <h2 class="section-title" data-i18n-html="highlights.title">Хайлайты <span class="accent">сервера.</span></h2>
<p class="section-sub">Moments captured on the ZernMC server — wars, builds and chaos.</p> <p class="section-sub" data-i18n="highlights.sub">Моменты, запечатлённые на сервере ZernMC — войны, постройки и хаос.</p>
</div> </div>
<div class="carousel"> <div class="carousel">
<div class="carousel-track" id="carousel"></div> <div class="carousel-track" id="carousel"></div>
<button class="carousel-btn carousel-btn--prev" id="carouselPrev" aria-label="Previous"></button> <button class="carousel-btn carousel-btn--prev" id="carouselPrev" data-i18n-aria="carousel.prev"></button>
<button class="carousel-btn carousel-btn--next" id="carouselNext" aria-label="Next"></button> <button class="carousel-btn carousel-btn--next" id="carouselNext" data-i18n-aria="carousel.next"></button>
<div class="carousel-dots" id="carouselDots"></div> <div class="carousel-dots" id="carouselDots"></div>
</div> </div>
</section> </section>
@@ -105,8 +111,8 @@
<section class="section features" id="features"> <section class="section features" id="features">
<div class="section-head"> <div class="section-head">
<span class="section-index">03 / 06</span> <span class="section-index">03 / 06</span>
<h2 class="section-title">Everything <span class="accent">you need.</span></h2> <h2 class="section-title" data-i18n-html="features.title">Всё, что <span class="accent">тебе нужно.</span></h2>
<p class="section-sub">A launcher that does the heavy lifting, so you can just play.</p> <p class="section-sub" data-i18n="features.sub">Лаунчер, который делает всю тяжёлую работу, чтобы ты просто играл.</p>
</div> </div>
<div class="features-grid"> <div class="features-grid">
@@ -115,8 +121,8 @@
<div class="feature-icon" aria-hidden="true"> <div class="feature-icon" aria-hidden="true">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M13 2 3 14h9l-1 8 10-12h-9l1-8z"/></svg> <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M13 2 3 14h9l-1 8 10-12h-9l1-8z"/></svg>
</div> </div>
<h3>Forge, NeoForge, Fabric</h3> <h3 data-i18n="features.forge.title">Forge, NeoForge, Fabric</h3>
<p>Every major mod loader handled automatically. Libraries are prefetched and verified, so installs just work — even on slow networks.</p> <p data-i18n="features.forge.sub">Все основные модлоадеры подхватываются автоматически. Библиотеки предзагружаются и проверяются, так что установка работает — даже на медленном интернете.</p>
</article> </article>
<article class="feature-card"> <article class="feature-card">
@@ -124,8 +130,8 @@
<div class="feature-icon" aria-hidden="true"> <div class="feature-icon" aria-hidden="true">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8"/><path d="M12 17v4"/></svg> <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8"/><path d="M12 17v4"/></svg>
</div> </div>
<h3>Dark, fast, native UI</h3> <h3 data-i18n="features.ui.title">Тёмный, быстрый нативный UI</h3>
<p>A lightweight JavaFX interface with a clean dark theme. Starts quick, responds instantly, and stays out of your way.</p> <p data-i18n="features.ui.sub">Лёгкий интерфейс на JavaFX с чистым тёмным оформлением. Быстро запускается, мгновенно реагирует и не мешает.</p>
</article> </article>
<article class="feature-card"> <article class="feature-card">
@@ -133,8 +139,8 @@
<div class="feature-icon" aria-hidden="true"> <div class="feature-icon" aria-hidden="true">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M5 18H3a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2h-2"/><path d="M9 22v-4"/><path d="M15 22v-4"/></svg> <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M5 18H3a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2h-2"/><path d="M9 22v-4"/><path d="M15 22v-4"/></svg>
</div> </div>
<h3>Smart mirrors & proxy</h3> <h3 data-i18n="features.proxy.title">Умные зеркала и прокси</h3>
<p>Downloads route through fast mirrors with automatic failover. Blocked or throttled? The proxy takes over so you never stall at 80%.</p> <p data-i18n="features.proxy.sub">Загрузки идут через быстрые зеркала с автоматическим фолбэком. Заблокировали или зарезали скорость? Прокси подхватит — ты больше не зависнешь на 80%.</p>
</article> </article>
<article class="feature-card"> <article class="feature-card">
@@ -142,8 +148,8 @@
<div class="feature-icon" aria-hidden="true"> <div class="feature-icon" aria-hidden="true">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2a10 10 0 0 0-9 14l2 5 5-2A10 10 0 1 0 12 2z"/><path d="M8.5 12h.01"/><path d="M12 12h.01"/><path d="M15.5 12h.01"/></svg> <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2a10 10 0 0 0-9 14l2 5 5-2A10 10 0 1 0 12 2z"/><path d="M8.5 12h.01"/><path d="M12 12h.01"/><path d="M15.5 12h.01"/></svg>
</div> </div>
<h3>News & server status</h3> <h3 data-i18n="features.news.title">Новости и статус сервера</h3>
<p>Changelogs, events and updates stream straight into the launcher. You always know what's new before you hit play.</p> <p data-i18n="features.news.sub">Чейнджлоги, ивенты и обновления приходят прямо в лаунчер. Ты всегда знаешь, что нового, ещё до нажатия «Играть».</p>
</article> </article>
<article class="feature-card"> <article class="feature-card">
@@ -151,8 +157,8 @@
<div class="feature-icon" aria-hidden="true"> <div class="feature-icon" aria-hidden="true">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg> <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
</div> </div>
<h3>One-click pack install</h3> <h3 data-i18n="features.pack.title">Установка сборки в один клик</h3>
<p>Pick a pack, hit install. Version manifests, vanilla assets and libraries are pulled and verified automatically. Simple.</p> <p data-i18n="features.pack.sub">Выбери сборку и жми «Установить». Манифесты версий, ванильные ассеты и библиотеки подтягиваются и проверяются автоматически. Просто.</p>
</article> </article>
</div> </div>
</section> </section>
@@ -160,40 +166,40 @@
<section class="section theme" id="theme"> <section class="section theme" id="theme">
<div class="section-head section-head--center"> <div class="section-head section-head--center">
<span class="section-index">04 / 06</span> <span class="section-index">04 / 06</span>
<h2 class="section-title">What's inside <span class="accent">the season.</span></h2> <h2 class="section-title" data-i18n-html="theme.title">Что внутри <span class="accent">сезона.</span></h2>
<p class="section-sub">Prologue — a modded season built around Create: Aeronautics, war and survival. OBT is the testing ground for the whole idea.</p> <p class="section-sub" data-i18n="theme.sub">Prologue — модовый сезон на базе Create: Aeronautics, война и выживание. ОБТ — полигон, где всё это обкатывается.</p>
</div> </div>
<div class="theme-grid"> <div class="theme-grid">
<article class="theme-card"> <article class="theme-card">
<span class="theme-tag">OBT · 1.20.1 Forge</span> <span class="theme-tag" data-i18n="theme.zarya.tag">ОБТ · 1.20.1 Forge</span>
<h3>War in Zarya</h3> <h3 data-i18n="theme.zarya.title">Война в Заре</h3>
<p>Two-day event set in the city of Zarya: the Fenix group attacks, Lastochka defends, and civilians pick a side — or rise up. Military rules apply.</p> <p data-i18n="theme.zarya.sub">Двухдневный ивент в городе Заря: группировка «Феникс» атакует, «Ласточка» обороняется, а жители выбирают сторону — или поднимают бунт. Военные правила.</p>
</article> </article>
<article class="theme-card"> <article class="theme-card">
<span class="theme-tag">OBT · test stage</span> <span class="theme-tag" data-i18n="theme.obt.tag">ОБТ · тестовая стадия</span>
<h3>Open beta server</h3> <h3 data-i18n="theme.obt.title">Открытый бета-сервер</h3>
<p>The beta is an event server: city battles, an atomic-reactor city-life scenario and rule sets that will shape the real season.</p> <p data-i18n="theme.obt.sub">Бета — это ивентовый сервер: городские бои, сценарий атомного реактора и правила, которые лягут в основу настоящего сезона.</p>
</article> </article>
<article class="theme-card"> <article class="theme-card">
<span class="theme-tag">Season · 1.21.1 NeoForge</span> <span class="theme-tag" data-i18n="theme.air.tag">Сезон · 1.21.1 NeoForge</span>
<h3>Ships in the sky</h3> <h3 data-i18n="theme.air.title">Корабли в небе</h3>
<p>Create: Aeronautics gives you airships, submarines and flying contraptions. Trains, diesel engines, big cannons and a nuclear reactor to master.</p> <p data-i18n="theme.air.sub">Create: Aeronautics даёт дирижабли, субмарины и летающие механизмы. Поезда, дизели, большие пушки и атомный реактор — осваивай.</p>
</article> </article>
<article class="theme-card"> <article class="theme-card">
<span class="theme-tag">Season · Prologue</span> <span class="theme-tag" data-i18n="theme.year.tag">Сезон · Prologue</span>
<h3>A year-long run</h3> <h3 data-i18n="theme.year.title">Марафон на год</h3>
<p>Each season is a marathon, not a sprint. Start this chapter from day one and shape it with the community.</p> <p data-i18n="theme.year.sub">Сезон — это марафон, а не спринт. Начни эту главу с первого дня и строй её вместе с комьюнити.</p>
</article> </article>
<article class="theme-card"> <article class="theme-card">
<span class="theme-tag">Server</span> <span class="theme-tag" data-i18n="theme.lore.tag">Сервер</span>
<h3>Own lore & plugins</h3> <h3 data-i18n="theme.lore.title">Свой лор и плагины</h3>
<p>Hand-written plugins, events written for the server story, and systems the launcher integrates with out of the box.</p> <p data-i18n="theme.lore.sub">Самописные плагины, ивенты под историю сервера и системы, которые из коробки работают с лаунчером.</p>
</article> </article>
<article class="theme-card"> <article class="theme-card">
<span class="theme-tag">Community</span> <span class="theme-tag" data-i18n="theme.community.tag">Комьюнити</span>
<h3>Voice & friends</h3> <h3 data-i18n="theme.community.title">Голос и друзья</h3>
<p>Simple Voice Chat in-game, a friends system in the launcher and a Telegram community that runs the show — decisions are voted on by players.</p> <p data-i18n="theme.community.sub">Simple Voice Chat в игре, система друзей в лаунчере и Telegram-комьюнити, которое реально рулит — решения принимаются голосованием игроков.</p>
</article> </article>
</div> </div>
</section> </section>
@@ -202,8 +208,8 @@
<div class="download-inner"> <div class="download-inner">
<div class="section-head section-head--center"> <div class="section-head section-head--center">
<span class="section-index">05 / 06</span> <span class="section-index">05 / 06</span>
<h2 class="section-title">Get <span class="accent">Zern.</span></h2> <h2 class="section-title" data-i18n-html="download.title">Скачать <span class="accent">Zern.</span></h2>
<p class="section-sub">Download the launcher for Windows and join the ZernMC server.</p> <p class="section-sub" data-i18n="download.sub">Скачай лаунчер для Windows и заходи на сервер ZernMC.</p>
</div> </div>
<div class="download-card"> <div class="download-card">
@@ -216,22 +222,22 @@
</div> </div>
<a class="btn btn-primary btn-block" id="download-btn" href="/launcher/download/latest"> <a class="btn btn-primary btn-block" id="download-btn" href="/launcher/download/latest">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3v12"/><path d="m7 10 5 5 5-5"/><path d="M4 21h16"/></svg> <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3v12"/><path d="m7 10 5 5 5-5"/><path d="M4 21h16"/></svg>
Download latest <span data-i18n="download.latest">Скачать актуальную версию</span>
</a> </a>
<div class="download-mirrors" id="mirrors"> <div class="download-mirrors" id="mirrors">
<span class="mirrors-label">Mirrors</span> <span class="mirrors-label" data-i18n="download.mirrors">Зеркала</span>
</div> </div>
</div> </div>
<p class="download-note">No paywalls, no ads. Just a launcher. System requirements: Windows 10+, 4 GB RAM.</p> <p class="download-note" data-i18n="download.note">Без платных подписок и рекламы. Просто лаунчер. Системные требования: Windows 10+, 4 ГБ ОЗУ.</p>
</div> </div>
</section> </section>
<section class="section news" id="news"> <section class="section news" id="news">
<div class="section-head"> <div class="section-head">
<span class="section-index">06 / 06</span> <span class="section-index">06 / 06</span>
<h2 class="section-title">What's <span class="accent">new.</span></h2> <h2 class="section-title" data-i18n-html="news.title">Что <span class="accent">нового.</span></h2>
<p class="section-sub">Latest changelogs and updates from the Zern team.</p> <p class="section-sub" data-i18n="news.sub">Последние чейнджлоги и обновления команды Zern.</p>
</div> </div>
<div class="news-grid" id="news-grid"> <div class="news-grid" id="news-grid">
@@ -249,7 +255,7 @@
<img src="/img/zernmc.svg" alt="Zern" width="28" height="28"> <img src="/img/zernmc.svg" alt="Zern" width="28" height="28">
<span>ZernMC</span> <span>ZernMC</span>
</div> </div>
<p class="footer-note">© <span id="year">2026</span> ZernMC. Not affiliated with Mojang or Microsoft. Minecraft is a trademark of Mojang AB.</p> <p class="footer-note">© <span id="year">2026</span> ZernMC. <span data-i18n="footer.note">Не аффилирован с Mojang или Microsoft. Minecraft — торговая марка Mojang AB.</span></p>
</div> </div>
</footer> </footer>
+262 -27
View File
@@ -3,6 +3,220 @@
const API = ''; // same origin (root -> launcher server) const API = ''; // same origin (root -> launcher server)
const I18N = {
ru: {
'nav.server': 'Сервер',
'nav.highlights': 'Хайлайты',
'nav.season': 'Сезон',
'nav.features': 'Возможности',
'nav.download': 'Скачать',
'nav.news': 'Новости',
'nav.play': 'Играть сейчас',
'hero.kicker': 'КАСТОМНЫЙ MINECRAFT ЛАУНЧЕР',
'hero.title': 'Очнись от<br><span class="hero-title-accent">холода.</span>',
'hero.sub': 'Zern оживляет твои любимые модпаки. Один лаунчер, любой загрузчик, без лишних хлопот — создан для сервера ZernMC и не только.',
'hero.download': 'Скачать лаунчер',
'hero.explore': 'Обзор',
'hero.scroll': 'Листай вниз',
'server.title': 'Сервер <span class="accent">жив.</span>',
'server.sub': 'ZernMC — это кастомный Minecraft-сервер со своим лором, самописными плагинами и живым комьюнити. Вот что происходит прямо сейчас.',
'server.status': 'Статус',
'server.checking': 'Проверяем…',
'server.players': 'Игроков онлайн',
'server.mode': 'Режим',
'server.season': 'Сезон Prologue',
'server.online': 'Онлайн',
'server.offline': 'Оффлайн',
'server.unavailable': 'Недоступно',
'highlights.title': 'Хайлайты <span class="accent">сервера.</span>',
'highlights.sub': 'Моменты, запечатлённые на сервере ZernMC — войны, постройки и хаос.',
'carousel.prev': 'Назад',
'carousel.next': 'Вперёд',
'carousel.cap1': 'Zern',
'carousel.cap3': 'Это честно не я',
'carousel.cap5': 'Спавн ТЦ (старое)',
'carousel.cap6': 'Спавн склад (старое)',
'carousel.alt': 'Хайлайт Zern',
'features.title': 'Всё, что <span class="accent">тебе нужно.</span>',
'features.sub': 'Лаунчер, который делает всю тяжёлую работу, чтобы ты просто играл.',
'features.forge.title': 'Forge, NeoForge, Fabric',
'features.forge.sub': 'Все основные модлоадеры подхватываются автоматически. Библиотеки предзагружаются и проверяются, так что установка работает — даже на медленном интернете.',
'features.ui.title': 'Тёмный, быстрый нативный UI',
'features.ui.sub': 'Лёгкий интерфейс на JavaFX с чистым тёмным оформлением. Быстро запускается, мгновенно реагирует и не мешает.',
'features.proxy.title': 'Умные зеркала и прокси',
'features.proxy.sub': 'Загрузки идут через быстрые зеркала с автоматическим фолбэком. Заблокировали или зарезали скорость? Прокси подхватит — ты больше не зависнешь на 80%.',
'features.news.title': 'Новости и статус сервера',
'features.news.sub': 'Чейнджлоги, ивенты и обновления приходят прямо в лаунчер. Ты всегда знаешь, что нового, ещё до нажатия «Играть».',
'features.pack.title': 'Установка сборки в один клик',
'features.pack.sub': 'Выбери сборку и жми «Установить». Манифесты версий, ванильные ассеты и библиотеки подтягиваются и проверяются автоматически. Просто.',
'theme.title': 'Что внутри <span class="accent">сезона.</span>',
'theme.sub': 'Prologue — модовый сезон на базе Create: Aeronautics, война и выживание. ОБТ — полигон, где всё это обкатывается.',
'theme.zarya.tag': 'ОБТ · 1.20.1 Forge',
'theme.zarya.title': 'Война в Заре',
'theme.zarya.sub': 'Двухдневный ивент в городе Заря: группировка «Феникс» атакует, «Ласточка» обороняется, а жители выбирают сторону — или поднимают бунт. Военные правила.',
'theme.obt.tag': 'ОБТ · тестовая стадия',
'theme.obt.title': 'Открытый бета-сервер',
'theme.obt.sub': 'Бета — это ивентовый сервер: городские бои, сценарий атомного реактора и правила, которые лягут в основу настоящего сезона.',
'theme.air.tag': 'Сезон · 1.21.1 NeoForge',
'theme.air.title': 'Корабли в небе',
'theme.air.sub': 'Create: Aeronautics даёт дирижабли, субмарины и летающие механизмы. Поезда, дизели, большие пушки и атомный реактор — осваивай.',
'theme.year.tag': 'Сезон · Prologue',
'theme.year.title': 'Марафон на год',
'theme.year.sub': 'Сезон — это марафон, а не спринт. Начни эту главу с первого дня и строй её вместе с комьюнити.',
'theme.lore.tag': 'Сервер',
'theme.lore.title': 'Свой лор и плагины',
'theme.lore.sub': 'Самописные плагины, ивенты под историю сервера и системы, которые из коробки работают с лаунчером.',
'theme.community.tag': 'Комьюнити',
'theme.community.title': 'Голос и друзья',
'theme.community.sub': 'Simple Voice Chat в игре, система друзей в лаунчере и Telegram-комьюнити, которое реально рулит — решения принимаются голосованием игроков.',
'download.title': 'Скачать <span class="accent">Zern.</span>',
'download.sub': 'Скачай лаунчер для Windows и заходи на сервер ZernMC.',
'download.latest': 'Скачать актуальную версию',
'download.mirrors': 'Зеркала',
'download.note': 'Без платных подписок и рекламы. Просто лаунчер. Системные требования: Windows 10+, 4 ГБ ОЗУ.',
'news.title': 'Что <span class="accent">нового.</span>',
'news.sub': 'Последние чейнджлоги и обновления команды Zern.',
'news.empty': 'Новостей пока нет.',
'news.error': 'Новости временно недоступны.',
'news.update': 'Обновление',
'footer.note': 'Не аффилирован с Mojang или Microsoft. Minecraft — торговая марка Mojang AB.'
},
en: {
'nav.server': 'Server',
'nav.highlights': 'Highlights',
'nav.season': 'Season',
'nav.features': 'Features',
'nav.download': 'Download',
'nav.news': 'News',
'nav.play': 'Play now',
'hero.kicker': 'CUSTOM MINECRAFT LAUNCHER',
'hero.title': 'Awaken from<br><span class="hero-title-accent">the cold.</span>',
'hero.sub': 'Zern brings your favourite modpacks to life. One launcher, every loader, no hassle — built for the ZernMC server and beyond.',
'hero.download': 'Download launcher',
'hero.explore': 'Explore',
'hero.scroll': 'Scroll to explore',
'server.title': 'The server is <span class="accent">alive.</span>',
'server.sub': 'ZernMC is a custom Minecraft server with its own lore, hand-written plugins and a living community. This is what\'s happening right now.',
'server.status': 'Status',
'server.checking': 'Checking…',
'server.players': 'Players online',
'server.mode': 'Mode',
'server.season': 'Season Prologue',
'server.online': 'Online',
'server.offline': 'Offline',
'server.unavailable': 'Unavailable',
'highlights.title': 'Server <span class="accent">highlights.</span>',
'highlights.sub': 'Moments captured on the ZernMC server — wars, builds and chaos.',
'carousel.prev': 'Previous',
'carousel.next': 'Next',
'carousel.cap1': 'Zern',
'carousel.cap3': 'This is honestly not me',
'carousel.cap5': 'Spawn mall (old)',
'carousel.cap6': 'Spawn storage (old)',
'carousel.alt': 'Zern highlight',
'features.title': 'Everything <span class="accent">you need.</span>',
'features.sub': 'A launcher that does the heavy lifting, so you can just play.',
'features.forge.title': 'Forge, NeoForge, Fabric',
'features.forge.sub': 'Every major mod loader handled automatically. Libraries are prefetched and verified, so installs just work — even on slow networks.',
'features.ui.title': 'Dark, fast, native UI',
'features.ui.sub': 'A lightweight JavaFX interface with a clean dark theme. Starts quick, responds instantly, and stays out of your way.',
'features.proxy.title': 'Smart mirrors & proxy',
'features.proxy.sub': 'Downloads route through fast mirrors with automatic failover. Blocked or throttled? The proxy takes over so you never stall at 80%.',
'features.news.title': 'News & server status',
'features.news.sub': 'Changelogs, events and updates stream straight into the launcher. You always know what\'s new before you hit play.',
'features.pack.title': 'One-click pack install',
'features.pack.sub': 'Pick a pack, hit install. Version manifests, vanilla assets and libraries are pulled and verified automatically. Simple.',
'theme.title': 'What\'s inside <span class="accent">the season.</span>',
'theme.sub': 'Prologue — a modded season built around Create: Aeronautics, war and survival. OBT is the testing ground for the whole idea.',
'theme.zarya.tag': 'OBT · 1.20.1 Forge',
'theme.zarya.title': 'War in Zarya',
'theme.zarya.sub': 'Two-day event set in the city of Zarya: the Fenix group attacks, Lastochka defends, and civilians pick a side — or rise up. Military rules apply.',
'theme.obt.tag': 'OBT · test stage',
'theme.obt.title': 'Open beta server',
'theme.obt.sub': 'The beta is an event server: city battles, an atomic-reactor city-life scenario and rule sets that will shape the real season.',
'theme.air.tag': 'Season · 1.21.1 NeoForge',
'theme.air.title': 'Ships in the sky',
'theme.air.sub': 'Create: Aeronautics gives you airships, submarines and flying contraptions. Trains, diesel engines, big cannons and a nuclear reactor to master.',
'theme.year.tag': 'Season · Prologue',
'theme.year.title': 'A year-long run',
'theme.year.sub': 'Each season is a marathon, not a sprint. Start this chapter from day one and shape it with the community.',
'theme.lore.tag': 'Server',
'theme.lore.title': 'Own lore & plugins',
'theme.lore.sub': 'Hand-written plugins, events written for the server story, and systems the launcher integrates with out of the box.',
'theme.community.tag': 'Community',
'theme.community.title': 'Voice & friends',
'theme.community.sub': 'Simple Voice Chat in-game, a friends system in the launcher and a Telegram community that runs the show — decisions are voted on by players.',
'download.title': 'Get <span class="accent">Zern.</span>',
'download.sub': 'Download the launcher for Windows and join the ZernMC server.',
'download.latest': 'Download latest',
'download.mirrors': 'Mirrors',
'download.note': 'No paywalls, no ads. Just a launcher. System requirements: Windows 10+, 4 GB RAM.',
'news.title': 'What\'s <span class="accent">new.</span>',
'news.sub': 'Latest changelogs and updates from the Zern team.',
'news.empty': 'No news yet.',
'news.error': 'News is temporarily unavailable.',
'news.update': 'Update',
'footer.note': 'Not affiliated with Mojang or Microsoft. Minecraft is a trademark of Mojang AB.'
}
};
const STORAGE_KEY = 'zern_lang';
function getLang() {
try {
const saved = localStorage.getItem(STORAGE_KEY);
if (saved === 'ru' || saved === 'en') return saved;
} catch (e) { /* ignore */ }
return 'ru'; // default: Russian
}
let currentLang = getLang();
function tr(key) {
const dict = I18N[currentLang] || I18N.ru;
return dict[key] != null ? dict[key] : key;
}
function applyI18n() {
document.documentElement.lang = currentLang;
const toggle = document.getElementById('langToggleLabel');
if (toggle) toggle.textContent = currentLang === 'ru' ? 'EN' : 'RU';
document.querySelectorAll('[data-i18n]').forEach((el) => {
el.textContent = tr(el.dataset.i18n);
});
document.querySelectorAll('[data-i18n-html]').forEach((el) => {
el.innerHTML = tr(el.dataset.i18nHtml);
});
document.querySelectorAll('[data-i18n-aria]').forEach((el) => {
el.setAttribute('aria-label', tr(el.dataset.i18nAria));
});
// Carousel captions & alts are created dynamically with tr() at init
document.querySelectorAll('.carousel-caption[data-caption-key]').forEach((el) => {
el.textContent = tr(el.dataset.captionKey);
});
document.querySelectorAll('.carousel-slide img').forEach((img) => {
const slide = img.closest('.carousel-slide');
const cap = slide && slide.querySelector('.carousel-caption[data-caption-key]');
if (cap) img.alt = tr(cap.dataset.captionKey);
else img.alt = tr('carousel.alt');
});
// Live-update dynamic strings
const statusValue = document.getElementById('srv-status');
if (statusValue && statusValue.dataset.state) {
updateStatusUI(statusValue.dataset.state);
}
}
function toggleLang() {
currentLang = currentLang === 'ru' ? 'en' : 'ru';
try { localStorage.setItem(STORAGE_KEY, currentLang); } catch (e) { /* ignore */ }
applyI18n();
}
function fmtSize(bytes) { function fmtSize(bytes) {
if (!bytes) return '—'; if (!bytes) return '—';
const units = ['B', 'KB', 'MB', 'GB']; const units = ['B', 'KB', 'MB', 'GB'];
@@ -67,7 +281,6 @@
const statusEl = document.getElementById('srv-status'); const statusEl = document.getElementById('srv-status');
const playersEl = document.getElementById('srv-players'); const playersEl = document.getElementById('srv-players');
const versionEl = document.getElementById('srv-version'); const versionEl = document.getElementById('srv-version');
const dot = document.querySelector('#srv-status .status-dot');
if (!statusEl) return; if (!statusEl) return;
try { try {
@@ -75,37 +288,53 @@
const data = await res.json(); const data = await res.json();
if (data.online) { if (data.online) {
if (dot) dot.className = 'status-dot status-dot--online'; statusEl.dataset.state = 'online';
statusEl.innerHTML = '<span class="status-dot status-dot--online"></span><span class="status-text--online">Online</span>';
if (playersEl) playersEl.textContent = `${data.players_online} / ${data.players_max}`; if (playersEl) playersEl.textContent = `${data.players_online} / ${data.players_max}`;
if (versionEl && data.version) versionEl.textContent = data.version; if (versionEl && data.version) versionEl.textContent = data.version;
} else { } else {
if (dot) dot.className = 'status-dot status-dot--offline'; statusEl.dataset.state = 'offline';
statusEl.innerHTML = '<span class="status-dot status-dot--offline"></span><span class="status-text--offline">Offline</span>';
if (playersEl) playersEl.textContent = '— / —'; if (playersEl) playersEl.textContent = '— / —';
} }
} catch (e) { } catch (e) {
statusEl.dataset.state = 'unavailable';
if (playersEl) playersEl.textContent = '— / —';
}
updateStatusUI(statusEl.dataset.state);
}
function updateStatusUI(state) {
const statusEl = document.getElementById('srv-status');
const dot = document.querySelector('#srv-status .status-dot');
if (!statusEl) return;
if (state === 'online') {
if (dot) dot.className = 'status-dot status-dot--online';
statusEl.innerHTML = '<span class="status-dot status-dot--online"></span><span class="status-text--online">' + tr('server.online') + '</span>';
} else if (state === 'offline') {
if (dot) dot.className = 'status-dot status-dot--offline'; if (dot) dot.className = 'status-dot status-dot--offline';
statusEl.innerHTML = '<span class="status-dot status-dot--offline"></span><span>Unavailable</span>'; statusEl.innerHTML = '<span class="status-dot status-dot--offline"></span><span class="status-text--offline">' + tr('server.offline') + '</span>';
} else {
if (dot) dot.className = 'status-dot status-dot--offline';
statusEl.innerHTML = '<span class="status-dot status-dot--offline"></span><span>' + tr('server.unavailable') + '</span>';
} }
} }
const HIGHLIGHTS = [ const HIGHLIGHTS = [
{ file: '1.jpg', type: 'image', caption: 'Zern' }, { file: '1.jpg', type: 'image', captionKey: 'carousel.cap1' },
{ file: '2.mp4', type: 'video', caption: null }, { file: '2.mp4', type: 'video', captionKey: null },
{ file: '3.jpg', type: 'image', caption: 'Это честно не я' }, { file: '3.jpg', type: 'image', captionKey: 'carousel.cap3' },
{ file: '4.mp4', type: 'video', caption: null }, { file: '4.mp4', type: 'video', captionKey: null },
{ file: '5.jpg', type: 'image', caption: 'Спавн ТЦ (старое)' }, { file: '5.jpg', type: 'image', captionKey: 'carousel.cap5' },
{ file: '6.jpg', type: 'image', caption: 'Спавн склад (старое)' }, { file: '6.jpg', type: 'image', captionKey: 'carousel.cap6' },
{ file: '7.jpg', type: 'image', caption: null }, { file: '7.jpg', type: 'image', captionKey: null },
{ file: '8.jpg', type: 'image', caption: null }, { file: '8.jpg', type: 'image', captionKey: null },
{ file: '9.jpg', type: 'image', caption: null }, { file: '9.jpg', type: 'image', captionKey: null },
{ file: '10.jpg', type: 'image', caption: null }, { file: '10.jpg', type: 'image', captionKey: null },
{ file: '11.jpg', type: 'image', caption: null }, { file: '11.jpg', type: 'image', captionKey: null },
{ file: '12.jpg', type: 'image', caption: null }, { file: '12.jpg', type: 'image', captionKey: null },
{ file: '13.jpg', type: 'image', caption: null }, { file: '13.jpg', type: 'image', captionKey: null },
{ file: '14.jpg', type: 'image', caption: null }, { file: '14.jpg', type: 'image', captionKey: null },
{ file: '15.jpg', type: 'image', caption: null }, { file: '15.jpg', type: 'image', captionKey: null },
]; ];
function initCarousel() { function initCarousel() {
@@ -131,15 +360,16 @@
} else { } else {
const img = document.createElement('img'); const img = document.createElement('img');
img.src = '/highlights/' + h.file; img.src = '/highlights/' + h.file;
img.alt = h.caption || 'Zern highlight'; img.alt = h.captionKey ? tr(h.captionKey) : tr('carousel.alt');
img.loading = 'lazy'; img.loading = 'lazy';
slide.appendChild(img); slide.appendChild(img);
} }
if (h.caption) { if (h.captionKey) {
const cap = document.createElement('span'); const cap = document.createElement('span');
cap.className = 'carousel-caption'; cap.className = 'carousel-caption';
cap.textContent = h.caption; cap.dataset.captionKey = h.captionKey;
cap.textContent = tr(h.captionKey);
slide.appendChild(cap); slide.appendChild(cap);
} }
@@ -202,7 +432,7 @@
if (!res.ok) throw new Error('news fetch failed'); if (!res.ok) throw new Error('news fetch failed');
const data = await res.json(); const data = await res.json();
const news = (data.news || []).slice(0, 6); const news = (data.news || []).slice(0, 6);
if (!news.length) { grid.innerHTML = '<p class="section-sub">No news yet.</p>'; return; } if (!news.length) { grid.innerHTML = '<p class="section-sub">' + tr('news.empty') + '</p>'; return; }
grid.innerHTML = ''; grid.innerHTML = '';
news.forEach((n) => { news.forEach((n) => {
@@ -225,7 +455,7 @@
} }
const title = document.createElement('h3'); const title = document.createElement('h3');
title.textContent = n.title || 'Update'; title.textContent = n.title || tr('news.update');
const body = document.createElement('p'); const body = document.createElement('p');
const text = stripMarkup(n.body); const text = stripMarkup(n.body);
@@ -238,7 +468,7 @@
}); });
} catch (e) { } catch (e) {
console.warn('[site] failed to load news', e); console.warn('[site] failed to load news', e);
grid.innerHTML = '<p class="section-sub">News is temporarily unavailable.</p>'; grid.innerHTML = '<p class="section-sub">' + tr('news.error') + '</p>';
} }
} }
@@ -290,6 +520,11 @@
} }
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
applyI18n();
const toggle = document.getElementById('langToggle');
if (toggle) toggle.addEventListener('click', toggleLang);
loadLauncherInfo(); loadLauncherInfo();
loadMirrors(); loadMirrors();
loadNews(); loadNews();