"use strict"; /* Lightweight i18n: dictionaries + t() + applyI18n(). Shared by login & app. */ const I18N = { ru: { "nav.feeds": "Ленты", "nav.history": "История", "nav.users": "Пользователи", "nav.settings": "Настройки", "topbar.logout": "Выйти", "theme.toggle": "Сменить тему", "stats.feeds": "лент", "stats.enabled": "активных", "stats.failing": "с ошибками", "stats.sent": "отправлено", "stats.failed": "сбоев", "chart.title": "Активность за 14 дней", "chart.sent": "Отправлено", "chart.failed": "Сбои", "chart.empty": "Нет данных за период", "feeds.heading": "RSS-ленты", "feeds.checkAll": "↻ Проверить все", "feeds.import": "⬆ Импорт OPML", "feeds.export": "⬇ Экспорт OPML", "feeds.add": "+ Добавить ленту", "feeds.empty": "Пока нет ни одной ленты. Добавьте первую, чтобы начать получать уведомления.", "feeds.never": "ещё не проверялась", "feeds.noTopic": "— тема не задана —", "history.heading": "История уведомлений", "history.refresh": "↻ Обновить", "history.clear": "Очистить", "history.search": "Поиск по заголовку или ленте…", "history.onlyErrors": "Только ошибки", "history.empty": "История пуста.", "users.heading": "Пользователи", "users.add": "+ Добавить пользователя", "users.admin": "👑 администратор", "users.viewer": "👁 наблюдатель", "settings.heading": "Настройки", "settings.ntfy": "ntfy", "settings.defaultServer": "Сервер ntfy по умолчанию", "settings.defaultServerHint": "Используется для лент, у которых не задан собственный сервер.", "settings.ntfyAuth": "Авторизация на сервере ntfy", "settings.ntfyAuthHint": "Нужно, если на сервере включён контроль доступа (ошибка 403 при отправке). Применяется к тесту, алертам и лентам без собственной авторизации.", "settings.testPh": "тема для теста, напр. my-news", "settings.testBtn": "Отправить тест", "settings.check": "Проверка", "settings.interval": "Интервал проверки по умолчанию (минуты)", "settings.intervalHint": "Можно переопределить для каждой ленты отдельно.", "settings.telegram": "Telegram", "settings.tgEnable": "Включить доставку в Telegram", "settings.tgToken": "Bot Token", "settings.tgChat": "Chat ID", "settings.tgHint": "Создайте бота через @BotFather, добавьте его в чат и укажите chat_id. Затем включите канал в нужных лентах.", "settings.webhook": "Webhook", "settings.whEnable": "Включить доставку через webhook", "settings.whUrl": "URL webhook", "settings.whHint": "POST с JSON: feed, feed_url, title, body, link, image.", "settings.alerts": "Оповещения администратора", "settings.alertEnable": "Уведомлять, если лента «упала»", "settings.alertTopic": "Тема ntfy для алертов", "settings.alertThreshold": "Порог (ошибок подряд)", "settings.auth": "Авторизация", "settings.authRequire": "Требовать вход в веб-панель", "settings.authHint": "Учётные записи управляются во вкладке «Пользователи».", "settings.save": "Сохранить настройки", "modal.addFeed": "Добавить ленту", "modal.editFeed": "Редактировать ленту", "modal.cancel": "Отмена", "modal.save": "Сохранить", "feed.url": "URL ленты *", "feed.title": "Название", "feed.titleOpt": "(необязательно, определится автоматически)", "feed.server": "Сервер ntfy", "feed.serverHint": "(пусто = по умолчанию)", "feed.topic": "Тема ntfy", "feed.priv": "Приватный ntfy-сервер (авторизация)", "feed.token": "Access token", "feed.tokenHint": "(tk_…, приоритетнее логина)", "feed.login": "Логин", "feed.password": "Пароль", "feed.priority": "Приоритет", "feed.p1": "1 — минимальный", "feed.p2": "2 — низкий", "feed.p3": "3 — обычный", "feed.p4": "4 — высокий", "feed.p5": "5 — максимальный", "feed.intervalMin": "Интервал, мин", "feed.intervalHint": "(0 = общий)", "feed.tags": "Теги / эмодзи", "feed.commaHint": "(через запятую)", "feed.filterInc": "Фильтр: только с этими словами", "feed.filterExc": "Фильтр: исключить слова", "feed.attach": "Прикреплять картинку", "feed.dupTg": "Дублировать в Telegram", "feed.toWebhook": "Отправлять в webhook", "feed.enabled": "Лента включена", "feed.preview": "👁 Предпросмотр", "feed.previewLoading": "Загрузка…", "feed.previewHint": "Введите URL и нажмите «Предпросмотр», чтобы увидеть последнюю запись.", "user.addTitle": "Добавить пользователя", "user.editTitle": "Редактировать пользователя", "user.login": "Логин *", "user.password": "Пароль", "user.pwReq": "*", "user.pwKeep": "(пусто = не менять)", "user.role": "Роль", "user.roleAdmin": "Администратор (полный доступ)", "user.roleViewer": "Наблюдатель (только просмотр)", "toast.feedDeleted": "Лента удалена", "toast.feedAdded": "Лента добавлена", "toast.feedUpdated": "Лента обновлена", "toast.saved": "Сохранено", "toast.deleted": "Удалён", "toast.checkDone": "Проверка завершена", "toast.historyCleared": "История очищена", "toast.settingsSaved": "Настройки сохранены", "toast.sentTo": "Отправлено в {dest}", "toast.imported": "Импортировано {added} из {total}", "toast.needTestTopic": "Укажите тему для теста", "toast.needUrl": "Сначала укажите URL ленты", "confirm.deleteFeed": "Удалить ленту «{name}»?", "confirm.deleteUser": "Удалить пользователя «{name}»?", "confirm.clearHistory": "Очистить всю историю?", "status.init": "Инициализировано ({n} записей)", "status.sent": "Отправлено {n} новых", "status.sentSkip": "Отправлено {n} новых, пропущено {s}", "status.filtered": "Без изменений (отфильтровано {s})", "status.nochange": "Без изменений", "status.parseError": "Ошибка: {msg}", "status.sendError": "Ошибка отправки: {msg}", "status.dash": "—", "nav.categories": "Категории", "categories.heading": "Категории", "categories.add": "+ Добавить категорию", "categories.empty": "Категорий пока нет.", "cat.addTitle": "Добавить категорию", "cat.editTitle": "Редактировать категорию", "cat.name": "Название *", "cat.sortOrder": "Порядок сортировки", "feed.category": "Категория", "feed.categoryNone": "— без категории —", "feed.fullContent": "Отправлять полный контент", "feed.fullContentHint": "Весь текст, все картинки и видео. Для ntfy — Markdown.", "settings.feedDefaults": "Значения по умолчанию для новых лент", "confirm.deleteCategory": "Удалить категорию «{name}»?", "confirm.deleteCategoryFeeds": "Удалить категорию «{name}»? {n} лент будут откреплены.", "toast.categoryAdded": "Категория добавлена", "toast.categoryUpdated": "Категория обновлена", "toast.categoryDeleted": "Категория удалена", "nav.reader": "Чтение", "reader.all": "Все", "reader.markAll": "Отметить все прочитанными", "reader.back": "← Назад", "reader.open": "Открыть оригинал →", "reader.empty": "Статей пока нет. Добавьте ленты, чтобы начать читать.", "feed.digest": "Дайджест", "feed.digestEnable": "Накапливать записи (дайджест)", "feed.digestPeriod": "Период дайджеста (часы)", "feed.fetchArticle": "Загружать полную статью (trafilatura)", "feed.fetchArticleHint": "Загружает страницу статьи и извлекает основной текст.", "settings.template": "Шаблон уведомлений", "settings.templateHint": "Переменные: {title}, {body}, {link}, {source}, {image_url}", "settings.proxyUrl": "URL прокси", "settings.proxyHint": "Например: http://proxy:8080 или socks5://proxy:1080", "feeds.backup": "💾 Бэкап", "feeds.restore": "📥 Восстановить", "confirm.restore": "Восстановление заменит всю текущую базу данных. Продолжить?", "toast.restored": "База восстановлена. Перезагрузка...", "toast.articlesMarked": "Все статьи отмечены прочитанными", "role.admin": "админ", "role.viewer": "наблюдатель", "login.subtitle": "Войдите, чтобы продолжить", "login.user": "Логин", "login.pass": "Пароль", "login.submit": "Войти", "login.error": "Неверный логин или пароль", }, en: { "nav.feeds": "Feeds", "nav.history": "History", "nav.users": "Users", "nav.settings": "Settings", "topbar.logout": "Log out", "theme.toggle": "Toggle theme", "stats.feeds": "feeds", "stats.enabled": "active", "stats.failing": "failing", "stats.sent": "sent", "stats.failed": "failed", "chart.title": "Activity (last 14 days)", "chart.sent": "Sent", "chart.failed": "Failed", "chart.empty": "No data for this period", "feeds.heading": "RSS feeds", "feeds.checkAll": "↻ Check all", "feeds.import": "⬆ Import OPML", "feeds.export": "⬇ Export OPML", "feeds.add": "+ Add feed", "feeds.empty": "No feeds yet. Add your first one to start receiving notifications.", "feeds.never": "not checked yet", "feeds.noTopic": "— no topic set —", "history.heading": "Notification history", "history.refresh": "↻ Refresh", "history.clear": "Clear", "history.search": "Search by title or feed…", "history.onlyErrors": "Errors only", "history.empty": "History is empty.", "users.heading": "Users", "users.add": "+ Add user", "users.admin": "👑 administrator", "users.viewer": "👁 viewer", "settings.heading": "Settings", "settings.ntfy": "ntfy", "settings.defaultServer": "Default ntfy server", "settings.defaultServerHint": "Used for feeds that don't define their own server.", "settings.ntfyAuth": "ntfy server authentication", "settings.ntfyAuthHint": "Needed if the server has access control enabled (403 on publish). Applies to the test, alerts and feeds without their own auth.", "settings.testPh": "topic to test, e.g. my-news", "settings.testBtn": "Send test", "settings.check": "Polling", "settings.interval": "Default poll interval (minutes)", "settings.intervalHint": "Can be overridden per feed.", "settings.telegram": "Telegram", "settings.tgEnable": "Enable Telegram delivery", "settings.tgToken": "Bot Token", "settings.tgChat": "Chat ID", "settings.tgHint": "Create a bot via @BotFather, add it to a chat and set the chat_id. Then enable the channel on the feeds you want.", "settings.webhook": "Webhook", "settings.whEnable": "Enable webhook delivery", "settings.whUrl": "Webhook URL", "settings.whHint": "POST with JSON: feed, feed_url, title, body, link, image.", "settings.alerts": "Admin alerts", "settings.alertEnable": "Notify when a feed keeps failing", "settings.alertTopic": "ntfy topic for alerts", "settings.alertThreshold": "Threshold (consecutive errors)", "settings.auth": "Authentication", "settings.authRequire": "Require login to the web panel", "settings.authHint": "Accounts are managed on the «Users» tab.", "settings.save": "Save settings", "modal.addFeed": "Add feed", "modal.editFeed": "Edit feed", "modal.cancel": "Cancel", "modal.save": "Save", "feed.url": "Feed URL *", "feed.title": "Title", "feed.titleOpt": "(optional, detected automatically)", "feed.server": "ntfy server", "feed.serverHint": "(empty = default)", "feed.topic": "ntfy topic", "feed.priv": "Private ntfy server (authentication)", "feed.token": "Access token", "feed.tokenHint": "(tk_…, takes precedence over login)", "feed.login": "Username", "feed.password": "Password", "feed.priority": "Priority", "feed.p1": "1 — min", "feed.p2": "2 — low", "feed.p3": "3 — default", "feed.p4": "4 — high", "feed.p5": "5 — max", "feed.intervalMin": "Interval, min", "feed.intervalHint": "(0 = global)", "feed.tags": "Tags / emojis", "feed.commaHint": "(comma separated)", "feed.filterInc": "Filter: only with these words", "feed.filterExc": "Filter: exclude words", "feed.attach": "Attach image", "feed.dupTg": "Mirror to Telegram", "feed.toWebhook": "Send to webhook", "feed.enabled": "Feed enabled", "feed.preview": "👁 Preview", "feed.previewLoading": "Loading…", "feed.previewHint": "Enter a URL and click «Preview» to see the latest entry.", "user.addTitle": "Add user", "user.editTitle": "Edit user", "user.login": "Username *", "user.password": "Password", "user.pwReq": "*", "user.pwKeep": "(empty = keep current)", "user.role": "Role", "user.roleAdmin": "Administrator (full access)", "user.roleViewer": "Viewer (read-only)", "toast.feedDeleted": "Feed deleted", "toast.feedAdded": "Feed added", "toast.feedUpdated": "Feed updated", "toast.saved": "Saved", "toast.deleted": "Deleted", "toast.checkDone": "Check complete", "toast.historyCleared": "History cleared", "toast.settingsSaved": "Settings saved", "toast.sentTo": "Sent to {dest}", "toast.imported": "Imported {added} of {total}", "toast.needTestTopic": "Enter a topic to test", "toast.needUrl": "Enter the feed URL first", "confirm.deleteFeed": "Delete feed «{name}»?", "confirm.deleteUser": "Delete user «{name}»?", "confirm.clearHistory": "Clear the entire history?", "status.init": "Initialized ({n} entries)", "status.sent": "Sent {n} new", "status.sentSkip": "Sent {n} new, skipped {s}", "status.filtered": "No changes (filtered out {s})", "status.nochange": "No changes", "status.parseError": "Error: {msg}", "status.sendError": "Send error: {msg}", "status.dash": "—", "nav.categories": "Categories", "categories.heading": "Categories", "categories.add": "+ Add category", "categories.empty": "No categories yet.", "cat.addTitle": "Add category", "cat.editTitle": "Edit category", "cat.name": "Name *", "cat.sortOrder": "Sort order", "feed.category": "Category", "feed.categoryNone": "— no category —", "feed.fullContent": "Send full content", "feed.fullContentHint": "Full text, all images and videos. For ntfy — Markdown.", "settings.feedDefaults": "Default values for new feeds", "confirm.deleteCategory": "Delete category «{name}»?", "confirm.deleteCategoryFeeds": "Delete category «{name}»? {n} feeds will be uncategorized.", "toast.categoryAdded": "Category added", "toast.categoryUpdated": "Category updated", "toast.categoryDeleted": "Category deleted", "nav.reader": "Reader", "reader.all": "All", "reader.markAll": "Mark all read", "reader.back": "← Back", "reader.open": "Open original →", "reader.empty": "No articles yet. Add feeds to start reading.", "feed.digest": "Digest", "feed.digestEnable": "Accumulate entries (digest)", "feed.digestPeriod": "Digest period (hours)", "feed.fetchArticle": "Fetch full article (trafilatura)", "feed.fetchArticleHint": "Fetches the article page and extracts main text.", "settings.template": "Notification template", "settings.templateHint": "Variables: {title}, {body}, {link}, {source}, {image_url}", "settings.proxyUrl": "Proxy URL", "settings.proxyHint": "Example: http://proxy:8080 or socks5://proxy:1080", "feeds.backup": "💾 Backup", "feeds.restore": "📥 Restore", "confirm.restore": "Restore will replace the entire database. Continue?", "toast.restored": "Database restored. Reloading...", "toast.articlesMarked": "All articles marked read", "role.admin": "admin", "role.viewer": "viewer", "login.subtitle": "Sign in to continue", "login.user": "Username", "login.pass": "Password", "login.submit": "Sign in", "login.error": "Wrong username or password", }, }; function getLang() { const l = localStorage.getItem("lang"); if (l === "ru" || l === "en") return l; return (navigator.language || "en").startsWith("ru") ? "ru" : "en"; } function setLang(lang) { localStorage.setItem("lang", lang); document.documentElement.lang = lang; } function t(key, params) { let s = (I18N[getLang()] || I18N.en)[key] ?? key; if (params) for (const k in params) s = s.replaceAll(`{${k}}`, params[k]); return s; } function applyI18n(root = document) { root.querySelectorAll("[data-i18n]").forEach(el => { el.textContent = t(el.getAttribute("data-i18n")); }); root.querySelectorAll("[data-i18n-ph]").forEach(el => { el.setAttribute("placeholder", t(el.getAttribute("data-i18n-ph"))); }); root.querySelectorAll("[data-i18n-title]").forEach(el => { el.setAttribute("title", t(el.getAttribute("data-i18n-title"))); }); } /* Theme + locale helpers shared across pages. */ function getTheme() { return localStorage.getItem("theme") === "light" ? "light" : "dark"; } function setTheme(theme) { localStorage.setItem("theme", theme); document.documentElement.setAttribute("data-theme", theme); } function localeTag() { return getLang() === "ru" ? "ru-RU" : "en-US"; }