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
+262 -27
View File
@@ -3,6 +3,220 @@
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) {
if (!bytes) return '—';
const units = ['B', 'KB', 'MB', 'GB'];
@@ -67,7 +281,6 @@
const statusEl = document.getElementById('srv-status');
const playersEl = document.getElementById('srv-players');
const versionEl = document.getElementById('srv-version');
const dot = document.querySelector('#srv-status .status-dot');
if (!statusEl) return;
try {
@@ -75,37 +288,53 @@
const data = await res.json();
if (data.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">Online</span>';
statusEl.dataset.state = 'online';
if (playersEl) playersEl.textContent = `${data.players_online} / ${data.players_max}`;
if (versionEl && data.version) versionEl.textContent = data.version;
} else {
if (dot) dot.className = 'status-dot status-dot--offline';
statusEl.innerHTML = '<span class="status-dot status-dot--offline"></span><span class="status-text--offline">Offline</span>';
statusEl.dataset.state = 'offline';
if (playersEl) playersEl.textContent = '— / —';
}
} 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';
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 = [
{ file: '1.jpg', type: 'image', caption: 'Zern' },
{ file: '2.mp4', type: 'video', caption: null },
{ file: '3.jpg', type: 'image', caption: 'Это честно не я' },
{ file: '4.mp4', type: 'video', caption: null },
{ file: '5.jpg', type: 'image', caption: 'Спавн ТЦ (старое)' },
{ file: '6.jpg', type: 'image', caption: 'Спавн склад (старое)' },
{ file: '7.jpg', type: 'image', caption: null },
{ file: '8.jpg', type: 'image', caption: null },
{ file: '9.jpg', type: 'image', caption: null },
{ file: '10.jpg', type: 'image', caption: null },
{ file: '11.jpg', type: 'image', caption: null },
{ file: '12.jpg', type: 'image', caption: null },
{ file: '13.jpg', type: 'image', caption: null },
{ file: '14.jpg', type: 'image', caption: null },
{ file: '15.jpg', type: 'image', caption: null },
{ file: '1.jpg', type: 'image', captionKey: 'carousel.cap1' },
{ file: '2.mp4', type: 'video', captionKey: null },
{ file: '3.jpg', type: 'image', captionKey: 'carousel.cap3' },
{ file: '4.mp4', type: 'video', captionKey: null },
{ file: '5.jpg', type: 'image', captionKey: 'carousel.cap5' },
{ file: '6.jpg', type: 'image', captionKey: 'carousel.cap6' },
{ file: '7.jpg', type: 'image', captionKey: null },
{ file: '8.jpg', type: 'image', captionKey: null },
{ file: '9.jpg', type: 'image', captionKey: null },
{ file: '10.jpg', type: 'image', captionKey: null },
{ file: '11.jpg', type: 'image', captionKey: null },
{ file: '12.jpg', type: 'image', captionKey: null },
{ file: '13.jpg', type: 'image', captionKey: null },
{ file: '14.jpg', type: 'image', captionKey: null },
{ file: '15.jpg', type: 'image', captionKey: null },
];
function initCarousel() {
@@ -131,15 +360,16 @@
} else {
const img = document.createElement('img');
img.src = '/highlights/' + h.file;
img.alt = h.caption || 'Zern highlight';
img.alt = h.captionKey ? tr(h.captionKey) : tr('carousel.alt');
img.loading = 'lazy';
slide.appendChild(img);
}
if (h.caption) {
if (h.captionKey) {
const cap = document.createElement('span');
cap.className = 'carousel-caption';
cap.textContent = h.caption;
cap.dataset.captionKey = h.captionKey;
cap.textContent = tr(h.captionKey);
slide.appendChild(cap);
}
@@ -202,7 +432,7 @@
if (!res.ok) throw new Error('news fetch failed');
const data = await res.json();
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 = '';
news.forEach((n) => {
@@ -225,7 +455,7 @@
}
const title = document.createElement('h3');
title.textContent = n.title || 'Update';
title.textContent = n.title || tr('news.update');
const body = document.createElement('p');
const text = stripMarkup(n.body);
@@ -238,7 +468,7 @@
});
} catch (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', () => {
applyI18n();
const toggle = document.getElementById('langToggle');
if (toggle) toggle.addEventListener('click', toggleLang);
loadLauncherInfo();
loadMirrors();
loadNews();