(() => { const KEY = "wrapped.lang"; const dict = { en: { "footer.tagline": "Zero-knowledge · one-time · encrypted", "footer.lede": "Wrap text, images or files into a one-time token. Encryption happens in your browser — the server never sees plaintext.", "footer.cap.text": "Text", "footer.cap.text.hint": "Secrets, configs, notes", "footer.cap.media": "Images & files", "footer.cap.media.hint": "Screenshots, archives, docs", "footer.cap.browser": "Encrypted in-browser", "footer.cap.browser.hint": "Only ciphertext reaches the server", "footer.pillar.zk": "Zero-knowledge", "footer.pillar.once": "One-time", "footer.pillar.encrypted": "Encrypted", "footer.pillar.zk.hint": "Server never sees content", "footer.pillar.once.hint": "Gone after unwrap", "footer.pillar.encrypted.hint": "Key lives only in your link", "footer.copy.author": "Sergey Antropov", "brand.tagline": "Wrap. Send. Vanish.", "create.eyebrow": "Secure drop", "create.title": "Wrap. Send. Vanish.", "create.lede": "Wrap text, images or files into a one-time token. Encryption happens in your browser — the server never sees plaintext.", "create.language": "Highlight language", "create.languagePickTitle": "Text type", "create.languagePickHint": "Choose how the text should be highlighted.", "create.text": "Text", "create.textPlaceholder": "Paste secrets, configs, notes…", "create.drop": "Drop files here, click to browse, or paste a screenshot (⌘V / Ctrl+V)", "create.ttl": "Time to live", "create.password": "Password (optional)", "create.passwordPlaceholder": "Extra unlock secret", "create.passwordGenerate": "Generate password", "create.passwordGenerateTip": "Generate a strong password", "create.submit": "Create wrapped token", "create.working": "Encrypting and uploading…", "create.openToken": "Open a token", "create.successTitle": "Token issued", "create.successWarnTitle": "One-time unwrap", "create.successWarnHint": "After opening, ciphertext is deleted on the server.", "create.shareLink": "Share link", "create.token": "Wrapped token", "create.another": "Create another", "create.needPayload": "Add text or at least one file.", "create.tooLarge": "Package exceeds max size.", "create.mimeDenied": "One or more MIME types are not allowed.", "create.expires": "Expires {datetime}", "create.expiresRelative": "{relative}", "create.ttl.1h": "1 hour", "create.ttl.6h": "6 hours", "create.ttl.24h": "24 hours", "create.ttl.3d": "3 days", "create.ttl.7d": "7 days", "create.ttl.default": "Default ({seconds} s)", "lang.plaintext": "Plain text", "lang.markdown": "Markdown", "lang.json": "JSON", "lang.yaml": "YAML", "lang.python": "Python", "lang.javascript": "JavaScript", "lang.typescript": "TypeScript", "lang.go": "Go", "lang.rust": "Rust", "lang.sql": "SQL", "lang.bash": "Bash", "lang.html": "HTML", "lang.css": "CSS", "relative.inMinutes": "in {n} min", "relative.inHours": "in {n} h", "relative.inDays": "in {n} d", "relative.soon": "soon", "unwrap.eyebrow": "Unwrap", "unwrap.title": "Reveal once", "unwrap.lede": "Paste a wrapped token or open a share link. Ciphertext is fetched once and deleted from the server.", "unwrap.token": "Wrapped token or ID", "unwrap.tokenPlaceholder": "wrapped_v1.… or wrap id", "unwrap.password": "Password (if set)", "unwrap.submit": "Unwrap", "unwrap.working": "Decrypting…", "unwrap.destroyedTitle": "Server copy destroyed", "unwrap.destroyedHint": "Preview lives only in this browser session.", "unwrap.needKey": "Missing encryption key. Open the full share link (with #key) or paste the full wrapped token.", "unwrap.badPassword": "Wrong password", "unwrap.badPasswordHint": "Check the password and try again.", "unwrap.passwordRequired": "Password required", "unwrap.passwordRequiredHint": "This wrap is password-protected.", "unwrap.attemptsLeft": "{n} of {max} attempts left", "unwrap.passwordLocked": "Too many wrong passwords", "unwrap.passwordLockedHint": "This wrap has been destroyed.", "unwrap.unavailable": "Unavailable (already used, expired, or invalid).", "common.copy": "Copy", "common.copied": "Copied", "common.download": "Download", "common.error": "Something went wrong.", "common.working": "Working…", "admin.nav.stats": "Stats", "admin.nav.settings": "Settings", "admin.nav.audit": "Audit", "admin.nav.danger": "Danger", "admin.nav.site": "Site", "admin.nav.logout": "Logout", "admin.brand.tagline": "Admin console", "admin.settings.title": "Settings", "admin.stats.title": "Statistics", "admin.stats.updated": "Updated", "admin.stats.minioTitle": "MinIO (pending ciphertext)", "admin.stats.objects": "object(s)", "admin.stats.dbPending": "DB pending", "admin.stats.unwrappedTitle": "Unwrapped (all time)", "admin.stats.auditOk": "audit ok", "admin.stats.last24h": "24h", "admin.stats.pendingTitle": "Not unwrapped", "admin.stats.items": "item(s)", "admin.stats.uploadedTitle": "Uploaded (all time)", "admin.stats.wrapsSection": "Wraps by status", "admin.stats.extraSection": "More", "admin.stats.col.status": "Status", "admin.stats.col.count": "Count", "admin.stats.col.size": "Size", "admin.stats.col.items": "Items", "admin.stats.status.pending": "pending", "admin.stats.status.consumed": "consumed", "admin.stats.status.expired": "expired", "admin.stats.total": "Total", "admin.stats.withPassword": "With password", "admin.stats.created24h": "Created (24h)", "admin.stats.created7d": "Created (7d)", "admin.stats.createsOk": "Creates ok (audit)", "admin.stats.createsFail": "Creates fail (audit)", "admin.stats.unwrapsFail": "Unwrap fails (audit)", "admin.audit.title": "Audit", "admin.tab.limits": "Limits", "admin.tab.rate": "Rate limits", "admin.tab.mime": "MIME", "admin.tab.password": "Password", "admin.tab.captcha": "CAPTCHA", "admin.login.sub": "Admin", "admin.login.username": "Username", "admin.login.password": "Password", "admin.login.submit": "Sign in", "admin.login.error.rate": "Too many attempts. Try later.", "admin.login.error.bad": "Invalid credentials", "admin.flash.saved": "Settings saved.", "admin.flash.purgedPrefix": "Purged", "admin.flash.purgedWraps": "wrap(s) and", "admin.flash.purgedObjects": "object(s).", "admin.flash.purgeConfirm": "Type PURGE to confirm forced cleanup.", "admin.flash.purgeError": "Purge failed. Check logs / MinIO connectivity.", "admin.limits.title": "Limits", "admin.limits.maxUpload": "Max upload (MB)", "admin.limits.defaultTtl": "Default TTL (hours)", "admin.limits.maxTtl": "Max TTL (hours)", "admin.limits.auditRetention": "Audit retention (days)", "admin.rate.title": "Rate limits (per IP / minute)", "admin.rate.create": "Create", "admin.rate.unwrap": "Unwrap", "admin.rate.adminLogin": "Admin login", "admin.mime.title": "MIME allowlist", "admin.mime.hint": "One pattern per line. Supports exact types and wildcards like image/*.", "admin.mime.examples": "Examples / suggested defaults", "admin.passwordMode.title": "Password mode", "admin.passwordMode.client_only": "Password also wraps ciphertext in the browser. Server stores an Argon2id hash and rejects wrong passwords before the one-time unwrap (mistypes do not burn the package).", "admin.passwordMode.server_gate": "Server stores an Argon2id hash and releases ciphertext only after a correct password. Ciphertext is still encrypted client-side with the same password.", "admin.password.maxAttempts": "Wrong password attempts (unwrap)", "admin.password.maxAttemptsHint": "After this many wrong passwords the wrap is destroyed. Default: 3.", "admin.captcha.title": "CAPTCHA", "admin.captcha.provider": "Provider", "admin.captcha.turnstileSite": "Turnstile site key", "admin.captcha.hcaptchaSite": "hCaptcha site key", "admin.captcha.secrets": "Secrets:", "admin.captcha.set": "set", "admin.captcha.missing": "missing", "admin.captcha.viaEnv": "(via env TURNSTILE_SECRET_KEY / HCAPTCHA_SECRET_KEY)", "admin.captcha.helperTitle": "How to get CAPTCHA tokens", "admin.captcha.turnstile.1": "Open Cloudflare Turnstile dashboard", "admin.captcha.turnstile.2": "Create a site widget for your domain (use localhost for dev)", "admin.captcha.turnstile.3": "Copy Site Key into the field above", "admin.captcha.turnstile.4": "Copy Secret Key into env TURNSTILE_SECRET_KEY and restart", "admin.captcha.turnstile.5": "Set provider to turnstile", "admin.captcha.hcaptcha.1": "Open hCaptcha dashboard", "admin.captcha.hcaptcha.2": "New site → copy Sitekey here", "admin.captcha.hcaptcha.3": "Secret into env HCAPTCHA_SECRET_KEY, restart", "admin.captcha.hcaptcha.4": "Set provider to hcaptcha", "admin.settings.save": "Save settings", "admin.danger.title": "Danger zone", "admin.danger.hint": "Force-delete all wraps from the database and all ciphertext objects in MinIO (wraps/). Audit log is kept. This cannot be undone.", "admin.danger.confirmLabel": "Type PURGE to confirm", "admin.danger.submit": "Clear all wraps & files", "admin.danger.confirmDialog": "Delete ALL wraps and files? This cannot be undone.", "admin.danger.confirmTitle": "Confirm purge", "admin.danger.confirmOk": "Delete everything", "modal.confirm.title": "Confirm", "modal.confirm.ok": "Confirm", "modal.confirm.cancel": "Cancel", "admin.audit.eventType": "Event type", "admin.audit.wrapId": "Wrap ID", "admin.audit.filter": "Filter", "admin.audit.allEvents": "All events", "admin.audit.perPage": "Per page", "admin.audit.clear": "Clear", "admin.audit.clearConfirmTitle": "Clear audit", "admin.audit.clearConfirm": "Delete all audit events? This cannot be undone.", "admin.audit.clearConfirmOk": "Clear all", "admin.audit.clearedPrefix": "Cleared", "admin.audit.clearedSuffix": "audit event(s).", "admin.audit.shown": "events shown", "admin.audit.of": "of", "admin.audit.events": "events", "admin.audit.page": "Page", "admin.audit.prev": "Prev", "admin.audit.next": "Next", "admin.audit.col.time": "Time", "admin.audit.col.event": "Event", "admin.audit.col.ok": "OK", "admin.audit.col.wrap": "Wrap", "admin.audit.col.details": "Details", "admin.audit.yes": "yes", "admin.audit.no": "no", "admin.audit.empty": "No events", }, ru: { "footer.tagline": "Zero-knowledge · одноразово · зашифровано", "footer.lede": "Упакуй текст, картинки или файлы в одноразовый токен. Шифрование в браузере — сервер не видит plaintext.", "footer.cap.text": "Текст", "footer.cap.text.hint": "Секреты, конфиги, заметки", "footer.cap.media": "Картинки и файлы", "footer.cap.media.hint": "Скриншоты, архивы, документы", "footer.cap.browser": "Шифрование в браузере", "footer.cap.browser.hint": "На сервер уходит только ciphertext", "footer.pillar.zk": "Zero-knowledge", "footer.pillar.once": "Одноразово", "footer.pillar.encrypted": "Зашифровано", "footer.pillar.zk.hint": "Сервер не видит содержимое", "footer.pillar.once.hint": "После открытия — удаление", "footer.pillar.encrypted.hint": "Ключ только в вашей ссылке", "footer.copy.author": "Сергей Антропов", "brand.tagline": "Упакуй. Отправь. Исчезни.", "create.eyebrow": "Безопасная передача", "create.title": "Упакуй. Отправь. Исчезни.", "create.lede": "Упакуй текст, картинки или файлы в одноразовый токен. Шифрование в браузере — сервер не видит plaintext.", "create.language": "Язык подсветки", "create.languagePickTitle": "Тип текста", "create.languagePickHint": "Выберите, как подсвечивать текст.", "create.text": "Текст", "create.textPlaceholder": "Вставь секреты, конфиги, заметки…", "create.drop": "Перетащи файлы, выбери или вставь скриншот из буфера (⌘V / Ctrl+V)", "create.ttl": "Время жизни", "create.password": "Пароль (необязательно)", "create.passwordPlaceholder": "Дополнительный секрет", "create.passwordGenerate": "Сгенерировать пароль", "create.passwordGenerateTip": "Сгенерировать надёжный пароль", "create.submit": "Создать wrapped-токен", "create.working": "Шифрование и загрузка…", "create.openToken": "Открыть токен", "create.successTitle": "Токен выдан", "create.successWarnTitle": "Расшифруй один раз", "create.successWarnHint": "После открытия ciphertext удаляется на сервере.", "create.shareLink": "Ссылка", "create.token": "Wrapped-токен", "create.another": "Создать ещё", "create.needPayload": "Добавь текст или хотя бы один файл.", "create.tooLarge": "Пакет превышает лимит размера.", "create.mimeDenied": "Один или несколько MIME-типов запрещены.", "create.expires": "Истекает {datetime}", "create.expiresRelative": "{relative}", "create.ttl.1h": "1 час", "create.ttl.6h": "6 часов", "create.ttl.24h": "24 часа", "create.ttl.3d": "3 дня", "create.ttl.7d": "7 дней", "create.ttl.default": "По умолчанию ({seconds} с)", "lang.plaintext": "Обычный текст", "lang.markdown": "Markdown", "lang.json": "JSON", "lang.yaml": "YAML", "lang.python": "Python", "lang.javascript": "JavaScript", "lang.typescript": "TypeScript", "lang.go": "Go", "lang.rust": "Rust", "lang.sql": "SQL", "lang.bash": "Bash", "lang.html": "HTML", "lang.css": "CSS", "relative.inMinutes": "через {n} мин", "relative.inHours": "через {n} ч", "relative.inDays": "через {n} дн", "relative.soon": "скоро", "unwrap.eyebrow": "Расшифруй", "unwrap.title": "Открыть один раз", "unwrap.lede": "Вставь wrapped-токен или открой ссылку. Ciphertext скачивается один раз и удаляется на сервере.", "unwrap.token": "Wrapped-токен или ID", "unwrap.tokenPlaceholder": "wrapped_v1.… или ID", "unwrap.password": "Пароль (если задан)", "unwrap.submit": "Расшифровать", "unwrap.working": "Расшифровка…", "unwrap.destroyedTitle": "Копия на сервере уничтожена", "unwrap.destroyedHint": "Превью только в этой сессии браузера.", "unwrap.needKey": "Нет ключа шифрования. Открой полную ссылку (с #key) или вставь полный wrapped-токен.", "unwrap.badPassword": "Неверный пароль", "unwrap.badPasswordHint": "Проверьте пароль и попробуйте снова.", "unwrap.passwordRequired": "Нужен пароль", "unwrap.passwordRequiredHint": "Этот wrap защищён паролем.", "unwrap.attemptsLeft": "Осталось попыток: {n} из {max}", "unwrap.passwordLocked": "Слишком много неверных паролей", "unwrap.passwordLockedHint": "Этот wrap уничтожен.", "unwrap.unavailable": "Недоступно (уже использовано, истекло или неверно).", "common.copy": "Копировать", "common.copied": "Скопировано", "common.download": "Скачать", "common.error": "Что-то пошло не так.", "common.working": "Подождите…", "admin.nav.stats": "Статистика", "admin.nav.settings": "Настройки", "admin.nav.audit": "Аудит", "admin.nav.danger": "Опасная зона", "admin.nav.site": "Сайт", "admin.nav.logout": "Выйти", "admin.brand.tagline": "Консоль администратора", "admin.settings.title": "Настройки", "admin.stats.title": "Статистика", "admin.stats.updated": "Обновлено", "admin.stats.minioTitle": "MinIO (нерасшифрованный ciphertext)", "admin.stats.objects": "объект(ов)", "admin.stats.dbPending": "в БД pending", "admin.stats.unwrappedTitle": "Расшифровано (за всё время)", "admin.stats.auditOk": "audit ok", "admin.stats.last24h": "за 24ч", "admin.stats.pendingTitle": "Не расшифровано", "admin.stats.items": "элемент(ов)", "admin.stats.uploadedTitle": "Загружено (за всё время)", "admin.stats.wrapsSection": "Wraps по статусу", "admin.stats.extraSection": "Ещё", "admin.stats.col.status": "Статус", "admin.stats.col.count": "Кол-во", "admin.stats.col.size": "Размер", "admin.stats.col.items": "Элементы", "admin.stats.status.pending": "pending", "admin.stats.status.consumed": "consumed", "admin.stats.status.expired": "expired", "admin.stats.total": "Всего", "admin.stats.withPassword": "С паролем", "admin.stats.created24h": "Создано (24ч)", "admin.stats.created7d": "Создано (7д)", "admin.stats.createsOk": "Создания ok (audit)", "admin.stats.createsFail": "Создания fail (audit)", "admin.stats.unwrapsFail": "Ошибки расшифровки (audit)", "admin.audit.title": "Аудит", "admin.tab.limits": "Лимиты", "admin.tab.rate": "Rate limits", "admin.tab.mime": "MIME", "admin.tab.password": "Пароль", "admin.tab.captcha": "CAPTCHA", "admin.login.sub": "Админ", "admin.login.username": "Логин", "admin.login.password": "Пароль", "admin.login.submit": "Войти", "admin.login.error.rate": "Слишком много попыток. Попробуйте позже.", "admin.login.error.bad": "Неверный логин или пароль", "admin.flash.saved": "Настройки сохранены.", "admin.flash.purgedPrefix": "Удалено", "admin.flash.purgedWraps": "wrap(ов) и", "admin.flash.purgedObjects": "объект(ов).", "admin.flash.purgeConfirm": "Введите PURGE для подтверждения очистки.", "admin.flash.purgeError": "Очистка не удалась. Проверьте логи / MinIO.", "admin.limits.title": "Лимиты", "admin.limits.maxUpload": "Макс. размер (МБ)", "admin.limits.defaultTtl": "TTL по умолчанию (часы)", "admin.limits.maxTtl": "Макс. TTL (часы)", "admin.limits.auditRetention": "Хранение аудита (дни)", "admin.rate.title": "Rate limits (на IP / минуту)", "admin.rate.create": "Создание", "admin.rate.unwrap": "Расшифровка", "admin.rate.adminLogin": "Вход в админку", "admin.mime.title": "MIME allowlist", "admin.mime.hint": "Один шаблон на строку. Точные типы и wildcards вроде image/*.", "admin.mime.examples": "Примеры / рекомендуемые значения", "admin.passwordMode.title": "Режим пароля", "admin.passwordMode.client_only": "Пароль также оборачивает ciphertext в браузере. Сервер хранит Argon2id-хеш и отклоняет неверный пароль до одноразовой расшифровки (опечатка не сжигает пакет).", "admin.passwordMode.server_gate": "Сервер хранит Argon2id-хеш и отдаёт ciphertext только после верного пароля. Ciphertext по-прежнему шифруется на клиенте тем же паролем.", "admin.password.maxAttempts": "Неверных вводов пароля (расшифровка)", "admin.password.maxAttemptsHint": "После стольких неверных паролей wrap уничтожается. По умолчанию: 3.", "admin.captcha.title": "CAPTCHA", "admin.captcha.provider": "Провайдер", "admin.captcha.turnstileSite": "Turnstile site key", "admin.captcha.hcaptchaSite": "hCaptcha site key", "admin.captcha.secrets": "Секреты:", "admin.captcha.set": "задан", "admin.captcha.missing": "нет", "admin.captcha.viaEnv": "(через env TURNSTILE_SECRET_KEY / HCAPTCHA_SECRET_KEY)", "admin.captcha.helperTitle": "Как получить токены CAPTCHA", "admin.captcha.turnstile.1": "Откройте Cloudflare Turnstile dashboard", "admin.captcha.turnstile.2": "Создайте виджет для домена (для dev — localhost)", "admin.captcha.turnstile.3": "Скопируйте Site Key в поле выше", "admin.captcha.turnstile.4": "Secret Key в env TURNSTILE_SECRET_KEY и перезапустите", "admin.captcha.turnstile.5": "Выберите provider = turnstile", "admin.captcha.hcaptcha.1": "Откройте hCaptcha dashboard", "admin.captcha.hcaptcha.2": "Новый сайт → Sitekey сюда", "admin.captcha.hcaptcha.3": "Secret в env HCAPTCHA_SECRET_KEY, перезапуск", "admin.captcha.hcaptcha.4": "Выберите provider = hcaptcha", "admin.settings.save": "Сохранить", "admin.danger.title": "Опасная зона", "admin.danger.hint": "Принудительно удаляет все wraps из БД и все ciphertext-объекты в MinIO (wraps/). Аудит сохраняется. Отменить нельзя.", "admin.danger.confirmLabel": "Введите PURGE для подтверждения", "admin.danger.submit": "Очистить все wraps и файлы", "admin.danger.confirmDialog": "Удалить ВСЕ wraps и файлы? Это нельзя отменить.", "admin.danger.confirmTitle": "Подтверждение очистки", "admin.danger.confirmOk": "Удалить всё", "modal.confirm.title": "Подтверждение", "modal.confirm.ok": "Подтвердить", "modal.confirm.cancel": "Отмена", "admin.audit.eventType": "Тип события", "admin.audit.wrapId": "Wrap ID", "admin.audit.filter": "Фильтр", "admin.audit.allEvents": "Все события", "admin.audit.perPage": "На странице", "admin.audit.clear": "Очистить", "admin.audit.clearConfirmTitle": "Очистить аудит", "admin.audit.clearConfirm": "Удалить все события аудита? Это нельзя отменить.", "admin.audit.clearConfirmOk": "Очистить всё", "admin.audit.clearedPrefix": "Удалено", "admin.audit.clearedSuffix": "событий аудита.", "admin.audit.shown": "событий показано", "admin.audit.of": "из", "admin.audit.events": "событий", "admin.audit.page": "Страница", "admin.audit.prev": "Назад", "admin.audit.next": "Далее", "admin.audit.col.time": "Время", "admin.audit.col.event": "Событие", "admin.audit.col.ok": "OK", "admin.audit.col.wrap": "Wrap", "admin.audit.col.details": "Детали", "admin.audit.yes": "да", "admin.audit.no": "нет", "admin.audit.empty": "Нет событий", }, }; const listeners = []; function current() { const stored = localStorage.getItem(KEY); if (stored === "en" || stored === "ru") return stored; return "ru"; } function t(key, vars) { const lang = current(); let text = (dict[lang] && dict[lang][key]) || dict.en[key] || key; if (vars) { Object.entries(vars).forEach(([k, v]) => { text = text.replaceAll(`{${k}}`, String(v)); }); } return text; } function locale() { return current() === "ru" ? "ru-RU" : "en-GB"; } function expiresParts(iso) { const d = new Date(iso); if (Number.isNaN(d.getTime())) { return { datetime: String(iso), relative: "" }; } const datetime = new Intl.DateTimeFormat(locale(), { day: "numeric", month: "long", year: "numeric", hour: "2-digit", minute: "2-digit", }).format(d); const diffMs = d.getTime() - Date.now(); let relative = t("relative.soon"); if (diffMs > 0) { const mins = Math.round(diffMs / 60000); if (mins < 60) relative = t("relative.inMinutes", { n: Math.max(1, mins) }); else if (mins < 60 * 48) relative = t("relative.inHours", { n: Math.round(mins / 60) }); else relative = t("relative.inDays", { n: Math.round(mins / (60 * 24)) }); } return { datetime, relative }; } function formatExpires(iso) { const { datetime, relative } = expiresParts(iso); return `${t("create.expires", { datetime })} · ${t("create.expiresRelative", { relative })}`; } function formatExpiresHtml(iso) { const { datetime, relative } = expiresParts(iso); const main = t("create.expires", { datetime }); const rel = t("create.expiresRelative", { relative }); return `${main}${rel}`; } function apply() { const lang = current(); document.documentElement.lang = lang; const label = document.getElementById("lang-label"); if (label) label.textContent = lang.toUpperCase(); document.querySelectorAll("[data-i18n]").forEach((el) => { el.textContent = t(el.getAttribute("data-i18n")); }); document.querySelectorAll("[data-i18n-placeholder]").forEach((el) => { el.setAttribute("placeholder", t(el.getAttribute("data-i18n-placeholder"))); }); document.querySelectorAll("[data-i18n-tooltip]").forEach((el) => { el.textContent = t(el.getAttribute("data-i18n-tooltip")); }); listeners.forEach((fn) => { try { fn(lang); } catch { /* ignore */ } }); } function onChange(fn) { listeners.push(fn); } function toggle() { const next = current() === "en" ? "ru" : "en"; localStorage.setItem(KEY, next); apply(); } document.addEventListener("DOMContentLoaded", () => { apply(); const btn = document.getElementById("lang-toggle"); if (btn) btn.addEventListener("click", toggle); }); window.WrappedI18n = { t, apply, current, locale, formatExpires, formatExpiresHtml, onChange, }; })();