Files
wrapped/app/static/js/i18n.js
T
inecs 806f2bdb1b
devops-tools/wrapped/wrapped-build/pipeline/head There was a failure building this commit
Выпущен 0.1.3: UX convenience pack, unwrap/admin polish и cache-bust статики.
Добавлены Share/QR PNG, zip all, size meter, PWA-manifest, системная тема и haptic на copy; улучшены success/unwrap и статистика админки (EN/RU).
2026-07-29 11:33:21 +03:00

633 lines
35 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.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.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,
};
})();