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" : "—" ,
"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" : "—" ,
"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" ;
}