Files
wrapped/app/static/js/i18n.js
T
Sergey Antropoff 81afba2362
devops-tools/wrapped/wrapped-build/pipeline/head This commit looks good
devops-tools/wrapped/wrapped-deploy/pipeline/head This commit looks good
Убрана кнопка A2HS и ссылка «Как проверить» из футера; ссылка в модалке оформлена карточкой.
2026-07-30 00:25:46 +03:00

721 lines
43 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(() => {
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/<id>#<key>. 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 23 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 (110). 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/<id>#<key>. Часть после # не уходит на сервер в запросе страницы. Без фрагмента (или полного токена) 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 `<span class="expires-main">${main}</span><span class="expires-rel">${rel}</span>`;
}
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,
};
})();