Features: feed CRUD, per-feed ntfy target (incl. private servers), Telegram/webhook channels, keyword filters, image attachments, per-feed intervals, OPML import/export, notification history & stats, users with roles, admin alerts, RU/EN i18n, light/dark theme, notification preview, history search, activity chart. Dockerized. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,336 @@
|
||||
"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.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": "—",
|
||||
|
||||
"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.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": "—",
|
||||
|
||||
"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";
|
||||
}
|
||||
Reference in New Issue
Block a user