806f2bdb1b
devops-tools/wrapped/wrapped-build/pipeline/head There was a failure building this commit
Добавлены Share/QR PNG, zip all, size meter, PWA-manifest, системная тема и haptic на copy; улучшены success/unwrap и статистика админки (EN/RU).
633 lines
35 KiB
JavaScript
633 lines
35 KiB
JavaScript
(() => {
|
||
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,
|
||
};
|
||
})();
|