2026-06-02 21:11:57 +08:00
"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" : "Используется для лент, у которых не задан собственный сервер." ,
2026-06-02 21:47:12 +08:00
"settings.ntfyAuth" : "Авторизация на сервере ntfy" ,
"settings.ntfyAuthHint" : "Нужно, если на сервере включён контроль доступа (ошибка 403 при отправке). Применяется к тесту, алертам и лентам без собственной авторизации." ,
2026-06-02 21:11:57 +08:00
"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" : "—" ,
2026-06-03 20:47:46 +08:00
"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" : "Все статьи отмечены прочитанными" ,
2026-06-02 21:11:57 +08:00
"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." ,
2026-06-02 21:47:12 +08:00
"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." ,
2026-06-02 21:11:57 +08:00
"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" : "—" ,
2026-06-03 20:47:46 +08:00
"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" ,
2026-06-02 21:11:57 +08:00
"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" ;
}