(() => { 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.copy.author": "Sergey Antropov", "about.open": "About Wrapped", "about.title": "About Wrapped", "about.p1": "Wrapped is a one-time secure drop for text, images and files.", "about.p2": "Send a note or code snippet, and attachments too: documents, archives, screenshots (drag-and-drop, file picker, or paste from the clipboard). Both text and files are encrypted in the browser before upload — only ciphertext reaches the server.", "about.p3": "The server never sees plaintext: encrypted data stays on the server until the recipient opens the link with the key and decrypts the package.", "about.p4": "After a successful unwrap the server copy is destroyed. The encryption key lives in the URL fragment (#…) and is not sent to the server with the page request. Optionally, a wrap can also be protected with a password.", "about.close": "Got it", "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.passwordProtected": "Password protected", "create.sharePassword": "Password", "create.passwordSeparateHint": "Do not send the password in the same chat as the link.", "create.qrHint": "Scan to open the share link", "create.share": "Share", "create.shareTitle": "Wrapped link", "create.shareText": "One-time secure link from Wrapped", "create.downloadQr": "Download QR", "create.checkLink": "Copy the share link", "create.checkPassword": "Send the password in a separate message", "create.checkExpires": "Note the expiry time", "create.trustKey": "The encryption key lives only in the link #fragment — the server never sees it.", "create.sizeMeter": "≈ {used} / {max}", "create.sizeNearLimit": "Approaching size limit", "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.evening": "Until evening (20:00)", "create.maxOpens": "Opens", "create.maxOpensOption": "{n}", "create.maxOpensInvalid": "Invalid number of opens.", "create.availableFrom": "Available from (optional)", "create.availableFromHint": "Empty = available immediately", "create.availableFromDate": "Pick date", "create.availableFromTime": "Pick time", "create.availableFromPast": "Available-from must not be in the past.", "create.availableFromAfterExpiry": "Available-from must be before expiry.", "create.opensBadge": "{n} opens", "create.availableFromBadge": "Available from {datetime}", "create.itemLabel": "Label (optional)", "create.itemLabelPlaceholder": "e.g. Instructions", "create.tpl.access.name": "Access / password", "create.tpl.access.body": "Access details\n\nURL:\nUsername:\nPassword:\n\nNotes:\n", "create.tpl.code.name": "Code + notes", "create.tpl.code.body": "One-time code / snippet\n\n```\n\n```\n\nNotes:\n", "create.tpl.fileNote.name": "File + note", "create.tpl.fileNote.body": "Attached file(s) — see below.\n\nWhat this is for:\nHow to use:\n", "unwrap.notYetTitle": "Not available yet", "unwrap.notYetHint": "This wrap opens at {datetime}.", "unwrap.notYetHintGeneric": "This wrap is not available yet.", "unwrap.stillOnServerTitle": "Still on the server", "unwrap.stillOnServerHint": "{n} open(s) remaining — ciphertext stays until the last unwrap.", "unwrap.opensRemaining": "{n} open(s) remaining after this session.", "verify.eyebrow": "Verify", "verify.title": "How to verify Wrapped", "verify.lede": "What leaves your browser, what stays on the server, and how the key in #fragment works.", "verify.s1.title": "Encryption in the browser", "verify.s1.body": "Text and files are packed and encrypted with Web Crypto (AES-GCM) before upload. The server receives only ciphertext plus metadata (TTL, MIME, size, optional password hash).", "verify.s2.title": "Key in the URL fragment", "verify.s2.body": "The share link looks like /w/#. The part after # never reaches the server in the page request. Without that fragment (or the full wrapped token), ciphertext cannot be decrypted.", "verify.s3.title": "Opens and destruction", "verify.s3.body": "By default a wrap can be opened once; then ciphertext is deleted. If the sender chose 2–3 opens, the server keeps ciphertext until the last successful unwrap. Expiry and password lockout still destroy the package.", "verify.s4.title": "Optional password", "verify.s4.body": "When a password is set, the server checks an Argon2 hash before releasing ciphertext. Wrong guesses are limited; empty password does not burn an attempt.", "verify.s5.title": "Available from", "verify.s5.body": "If \"available from\" is set, unwrap is rejected until that time. After that, normal open/expiry rules apply.", "verify.createCta": "Create a wrap", "about.verifyLink": "How to verify", "about.verifyHint": "What the server sees and how the #key works", "admin.limits.maxOpens": "Max opens per wrap", "admin.limits.maxOpensHint": "Ceiling for create UI (1–10). Default: 3.", "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.workingFetch": "Downloading…", "unwrap.workingDecrypt": "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.badToken": "Invalid token", "unwrap.badTokenHint": "Paste a full wrapped token or open a valid share link.", "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 after too many failed attempts.", "unwrap.unavailable": "Unavailable", "unwrap.unavailableHint": "Already used, expired, or invalid.", "unwrap.goneTitle": "This link is no longer available", "unwrap.goneHint": "Wrapped links are one-time and can expire. The ciphertext is gone from the server.", "unwrap.rateLimited": "Too many requests", "unwrap.rateLimitedHint": "Wait a minute and try again.", "unwrap.captchaFailed": "CAPTCHA failed", "unwrap.captchaFailedHint": "Complete the CAPTCHA and try again.", "unwrap.storageError": "Temporary storage error", "unwrap.storageErrorHint": "Try again in a moment.", "unwrap.decryptFailed": "Could not decrypt", "unwrap.decryptFailedHint": "The key in the link may be wrong. Ciphertext was already removed from the server.", "unwrap.createOwn": "Create your own wrap", "unwrap.tapPreview": "Tap image to enlarge", "unwrap.downloadAll": "Download all", "unwrap.trustKey": "The key was only in the link #fragment and was never sent to the server.", "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.nav.openMenu": "Menu", "admin.nav.closeMenu": "Close menu", "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": "Successful unwraps", "admin.stats.auditOk": "audit ok", "admin.stats.dbConsumed": "DB consumed", "admin.stats.last24h": "24h", "admin.stats.pendingTitle": "Not unwrapped", "admin.stats.items": "item(s)", "admin.stats.uploadedTitle": "Uploaded (all time)", "admin.stats.passwordBurnsTitle": "Burned by password", "admin.stats.passwordBurnsHint": "Destroyed after too many wrong passwords", "admin.stats.wrapsSection": "Wraps by status", "admin.stats.extraSection": "More", "admin.stats.failReasonsSection": "Unwrap fail reasons (all time)", "admin.stats.failReasons24hSection": "Unwrap fail reasons (24h)", "admin.stats.col.reason": "Reason", "admin.stats.noFailReasons": "No failed unwraps yet.", "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.copy.author": "Сергей Антропов", "about.open": "О проекте Wrapped", "about.title": "О проекте Wrapped", "about.p1": "Wrapped — сервис одноразовой безопасной передачи текста, изображений и файлов.", "about.p2": "Можно отправить заметку или код, а также вложения: документы, архивы, скриншоты (drag-and-drop, выбор с диска или вставка из буфера). И текст, и файлы шифруются в браузере до загрузки — на сервер уходит только ciphertext.", "about.p3": "Сервер никогда не видит plaintext: зашифрованные данные лежат на сервере до тех пор, пока получатель не откроет ссылку с ключом и не расшифрует пакет.", "about.p4": "После успешной расшифровки копия на сервере уничтожается. Ключ шифрования живёт во фрагменте URL (#…) и не уходит на сервер вместе с запросом страницы. При желании wrap можно дополнительно защитить паролем.", "about.close": "Понятно", "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.passwordProtected": "Защищено паролем", "create.sharePassword": "Пароль", "create.passwordSeparateHint": "Не отправляйте пароль в той же переписке, что и ссылку.", "create.qrHint": "Отсканируйте, чтобы открыть ссылку", "create.share": "Поделиться", "create.shareTitle": "Ссылка Wrapped", "create.shareText": "Одноразовая защищённая ссылка из Wrapped", "create.downloadQr": "Скачать QR", "create.checkLink": "Скопируйте ссылку", "create.checkPassword": "Отправьте пароль отдельным сообщением", "create.checkExpires": "Учтите срок действия", "create.trustKey": "Ключ шифрования только во фрагменте ссылки #… — сервер его не видит.", "create.sizeMeter": "≈ {used} / {max}", "create.sizeNearLimit": "Близко к лимиту размера", "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.evening": "До вечера (20:00)", "create.maxOpens": "Открытий", "create.maxOpensOption": "{n}", "create.maxOpensInvalid": "Недопустимое число открытий.", "create.availableFrom": "Доступно с (необязательно)", "create.availableFromHint": "Пусто = доступно сразу", "create.availableFromDate": "Выбрать дату", "create.availableFromTime": "Выбрать время", "create.availableFromPast": "«Доступно с» не может быть в прошлом.", "create.availableFromAfterExpiry": "«Доступно с» должно быть раньше срока истечения.", "create.opensBadge": "{n} открытий", "create.availableFromBadge": "Доступно с {datetime}", "create.itemLabel": "Подпись (необязательно)", "create.itemLabelPlaceholder": "напр. Инструкция", "create.tpl.access.name": "Доступ / пароль", "create.tpl.access.body": "Данные доступа\n\nURL:\nЛогин:\nПароль:\n\nЗаметки:\n", "create.tpl.code.name": "Код + заметка", "create.tpl.code.body": "Одноразовый код / фрагмент\n\n```\n\n```\n\nЗаметки:\n", "create.tpl.fileNote.name": "Файл + заметка", "create.tpl.fileNote.body": "Вложения — см. ниже.\n\nДля чего:\nКак пользоваться:\n", "unwrap.notYetTitle": "Ещё недоступно", "unwrap.notYetHint": "Этот wrap откроется {datetime}.", "unwrap.notYetHintGeneric": "Этот wrap ещё недоступен.", "unwrap.stillOnServerTitle": "Ещё на сервере", "unwrap.stillOnServerHint": "Осталось открытий: {n} — ciphertext хранится до последнего unwrap.", "unwrap.opensRemaining": "После этой сессии останется открытий: {n}.", "verify.eyebrow": "Проверка", "verify.title": "Как проверить Wrapped", "verify.lede": "Что уходит из браузера, что лежит на сервере и как работает ключ в #fragment.", "verify.s1.title": "Шифрование в браузере", "verify.s1.body": "Текст и файлы упаковываются и шифруются Web Crypto (AES-GCM) до загрузки. На сервер уходит только ciphertext и метаданные (TTL, MIME, размер, опциональный хеш пароля).", "verify.s2.title": "Ключ во фрагменте URL", "verify.s2.body": "Ссылка вида /w/#. Часть после # не уходит на сервер в запросе страницы. Без фрагмента (или полного токена) ciphertext не расшифровать.", "verify.s3.title": "Открытия и уничтожение", "verify.s3.body": "По умолчанию wrap открывается один раз — затем ciphertext удаляется. Если отправитель выбрал 2–3 открытия, сервер хранит ciphertext до последнего успешного unwrap. Срок и блокировка пароля по-прежнему уничтожают пакет.", "verify.s4.title": "Опциональный пароль", "verify.s4.body": "При пароле сервер проверяет Argon2-хеш до выдачи ciphertext. Число попыток ограничено; пустой пароль попытку не тратит.", "verify.s5.title": "Доступно с", "verify.s5.body": "Если задано «доступно с», unwrap отклоняется до этого момента. Дальше действуют обычные правила открытий и срока.", "verify.createCta": "Создать wrap", "about.verifyLink": "Как проверить", "about.verifyHint": "Что видит сервер и как работает ключ в #…", "admin.limits.maxOpens": "Макс. открытий на wrap", "admin.limits.maxOpensHint": "Потолок для UI создания (1–10). По умолчанию: 3.", "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.workingFetch": "Скачивание…", "unwrap.workingDecrypt": "Расшифровка…", "unwrap.destroyedTitle": "Копия на сервере уничтожена", "unwrap.destroyedHint": "Превью только в этой сессии браузера.", "unwrap.needKey": "Нет ключа шифрования. Открой полную ссылку (с #key) или вставь полный wrapped-токен.", "unwrap.badToken": "Неверный токен", "unwrap.badTokenHint": "Вставьте полный wrapped-токен или откройте корректную ссылку.", "unwrap.badPassword": "Неверный пароль", "unwrap.badPasswordHint": "Проверьте пароль и попробуйте снова.", "unwrap.passwordRequired": "Нужен пароль", "unwrap.passwordRequiredHint": "Этот wrap защищён паролем.", "unwrap.attemptsLeft": "Осталось попыток: {n} из {max}", "unwrap.passwordLocked": "Слишком много неверных паролей", "unwrap.passwordLockedHint": "Wrap уничтожен после исчерпания попыток.", "unwrap.unavailable": "Недоступно", "unwrap.unavailableHint": "Уже использовано, истекло или неверно.", "unwrap.goneTitle": "Ссылка больше недоступна", "unwrap.goneHint": "Wrapped-ссылки одноразовые и могут истекать. Ciphertext уже удалён с сервера.", "unwrap.rateLimited": "Слишком много запросов", "unwrap.rateLimitedHint": "Подождите минуту и попробуйте снова.", "unwrap.captchaFailed": "CAPTCHA не пройдена", "unwrap.captchaFailedHint": "Пройдите CAPTCHA и попробуйте снова.", "unwrap.storageError": "Временная ошибка хранилища", "unwrap.storageErrorHint": "Попробуйте чуть позже.", "unwrap.decryptFailed": "Не удалось расшифровать", "unwrap.decryptFailedHint": "Ключ в ссылке может быть неверным. Ciphertext уже удалён с сервера.", "unwrap.createOwn": "Создать свой wrap", "unwrap.tapPreview": "Нажмите на изображение, чтобы увеличить", "unwrap.downloadAll": "Скачать всё", "unwrap.trustKey": "Ключ был только во фрагменте ссылки #… и не уходил на сервер.", "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.nav.openMenu": "Меню", "admin.nav.closeMenu": "Закрыть меню", "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": "Успешные unwrap", "admin.stats.auditOk": "audit ok", "admin.stats.dbConsumed": "в БД consumed", "admin.stats.last24h": "за 24ч", "admin.stats.pendingTitle": "Не расшифровано", "admin.stats.items": "элемент(ов)", "admin.stats.uploadedTitle": "Загружено (за всё время)", "admin.stats.passwordBurnsTitle": "Сожжено паролем", "admin.stats.passwordBurnsHint": "Уничтожено после слишком многих неверных паролей", "admin.stats.wrapsSection": "Wraps по статусу", "admin.stats.extraSection": "Ещё", "admin.stats.failReasonsSection": "Причины ошибок unwrap (всё время)", "admin.stats.failReasons24hSection": "Причины ошибок unwrap (24ч)", "admin.stats.col.reason": "Причина", "admin.stats.noFailReasons": "Пока нет неудачных unwrap.", "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, }; })();