diff --git a/frontend/src/lib/components/LocaleSelector.svelte b/frontend/src/lib/components/LocaleSelector.svelte new file mode 100644 index 0000000..ed39bbd --- /dev/null +++ b/frontend/src/lib/components/LocaleSelector.svelte @@ -0,0 +1,764 @@ + + +
+ {#if codes.length === 0} +
+ +

{t('locales.empty')}

+
+ {:else} + + {/if} + + +
+ {#if !addOpen} + + {:else} +
+
+ + setTimeout(() => { if (addOpen && !addQuery) closeAdd(); }, 150)} + placeholder={t('locales.searchPlaceholder')} + class="ls-add-input" + autocomplete="off" + spellcheck="false" + type="text" + /> + +
+ +
+ {#each suggestions as s, i (s.code)} + + {/each} + + {#if canAddCustom} + + {/if} + + {#if suggestions.length === 0 && !canAddCustom} +
{t('locales.noSuggestions')}
+ {/if} +
+
+ {/if} +
+ +

+ + {t('locales.orderHint')} +

+
+ + diff --git a/frontend/src/lib/components/TimezoneSelector.svelte b/frontend/src/lib/components/TimezoneSelector.svelte new file mode 100644 index 0000000..94865d7 --- /dev/null +++ b/frontend/src/lib/components/TimezoneSelector.svelte @@ -0,0 +1,585 @@ + + +
+ + + + {#if open} +
+ +
+ + + ESC +
+ + + {#if !query} +
+ + +
+ {/if} + + +
+ {#if filtered.length === 0} +
{t('timezone.noMatches')}
+ {:else} + {#each groups as g (g.region)} +
+
+ {g.region} + {g.items.length} +
+ {#each g.items as tz (tz)} + {@const parts = splitTz(tz)} + {@const idx = flat.indexOf(tz)} + {@const hl = idx === highlightIdx} + {@const sel = tz === value} + + {/each} +
+ {/each} + {/if} +
+
+ {/if} +
+ + diff --git a/frontend/src/lib/i18n/en.json b/frontend/src/lib/i18n/en.json index 2dce5a9..245b032 100644 --- a/frontend/src/lib/i18n/en.json +++ b/frontend/src/lib/i18n/en.json @@ -671,13 +671,29 @@ "telegram": "Telegram", "webhookSecret": "Webhook Secret", "webhookSecretHint": "Secret token to verify webhook requests from Telegram", - "cacheTtl": "Media Cache TTL (hours)", - "cacheTtlHint": "How long to cache uploaded Telegram file_ids before re-uploading", + "cacheTtl": "URL Cache TTL (hours)", + "cacheTtlHint": "How long to keep URL-keyed Telegram file_ids (e.g. shared links). Set 0 to disable TTL. The asset cache uses content hashing (thumbhash) and ignores this.", + "cacheMaxEntries": "Cache Max Entries", + "cacheMaxEntriesHint": "Upper bound per cache (URL and asset). Oldest entries are evicted first (LRU). Default 5000.", + "cacheStats": "Cache contents", + "cacheStatsHint": "Size shown is the total bytes of media originally uploaded to Telegram for cached entries — i.e. approximate re-upload bandwidth the cache is saving. The cache file itself is only a few KB; the media lives on Telegram's servers.", + "cacheStatsUrl": "URL cache", + "cacheStatsAsset": "Asset cache", + "cacheStatsEntries": "entries", + "cacheStatsEmpty": "empty", + "cacheStatsOldest": "oldest", + "cacheStatsNewest": "newest", + "clearCache": "Clear Media Cache", + "clearCacheHint": "Delete cached Telegram file_ids. Next send will re-upload media.", + "clearCacheConfirmTitle": "Clear Telegram cache?", + "clearCacheConfirm": "This removes all cached Telegram file_ids. Subsequent notifications will re-upload media, which may take longer and use more bandwidth.", + "clearCacheConfirmBtn": "Clear cache", + "clearCacheDone": "Telegram cache cleared", "timezone": "Timezone", "timezoneHint": "IANA timezone (e.g. UTC, Europe/Warsaw, America/New_York). Used to interpret HH:MM fields like quiet hours.", "locales": "Template Languages", "supportedLocales": "Supported Locales", - "supportedLocalesHint": "Comma-separated locale codes for template editing (e.g. en,ru,de,fr)", + "supportedLocalesHint": "Languages available when authoring notification and command templates. Built-in defaults ship for English and Russian; other languages start empty.", "saved": "Settings saved" }, "hints": { @@ -813,6 +829,29 @@ "showDetails": "Show details", "hideDetails": "Hide details" }, + "timezone": { + "searchPlaceholder": "Search cities or IANA codes…", + "detect": "Detect", + "utc": "UTC", + "noMatches": "No timezones match" + }, + "locales": { + "empty": "No languages selected. Add one below to start authoring templates.", + "add": "Add language", + "searchPlaceholder": "Search or type a code (e.g. de-CH)…", + "addCustom": "Add custom code", + "noSuggestions": "No matches. Type a valid locale code (2–3 letters).", + "primary": "Primary", + "shipped": "Built-in", + "shippedHint": "Default notification & command templates ship for this language.", + "makePrimary": "Make primary", + "moveUp": "Move up", + "moveDown": "Move down", + "remove": "Remove", + "removeLast": "At least one language is required", + "reorder": "Drag to reorder", + "orderHint": "First language is the primary fallback when a translation is missing. Drag to reorder." + }, "snack": { "eventsCleared": "{count} event(s) cleared", "providerSaved": "Provider saved", diff --git a/frontend/src/lib/i18n/ru.json b/frontend/src/lib/i18n/ru.json index 71c5986..ce6ef6a 100644 --- a/frontend/src/lib/i18n/ru.json +++ b/frontend/src/lib/i18n/ru.json @@ -671,13 +671,29 @@ "telegram": "Telegram", "webhookSecret": "Секрет вебхука", "webhookSecretHint": "Секретный токен для проверки запросов вебхука от Telegram", - "cacheTtl": "TTL кэша медиа (часы)", - "cacheTtlHint": "Сколько хранить кэш Telegram file_id перед повторной загрузкой", + "cacheTtl": "TTL URL-кэша (часы)", + "cacheTtlHint": "Сколько хранить Telegram file_id, привязанные к URL (напр. публичные ссылки). 0 — отключить TTL. Кэш ассетов использует хэширование содержимого (thumbhash) и не зависит от этой настройки.", + "cacheMaxEntries": "Макс. записей в кэше", + "cacheMaxEntriesHint": "Верхний предел записей в каждом кэше (URL и ассеты). При превышении удаляются самые старые (LRU). По умолчанию 5000.", + "cacheStats": "Содержимое кэша", + "cacheStatsHint": "Показываемый размер — это суммарный объём медиа, который был изначально загружен в Telegram для закэшированных записей, т.е. приблизительный объём повторных загрузок, который экономит кэш. Сам файл кэша занимает лишь несколько КБ; медиа хранится на серверах Telegram.", + "cacheStatsUrl": "Кэш URL", + "cacheStatsAsset": "Кэш ассетов", + "cacheStatsEntries": "записей", + "cacheStatsEmpty": "пусто", + "cacheStatsOldest": "самая старая", + "cacheStatsNewest": "самая свежая", + "clearCache": "Очистить кэш медиа", + "clearCacheHint": "Удалить кэшированные Telegram file_id. При следующей отправке медиа будут загружены заново.", + "clearCacheConfirmTitle": "Очистить кэш Telegram?", + "clearCacheConfirm": "Это удалит все кэшированные Telegram file_id. Следующие уведомления будут повторно загружать медиа, что может занять больше времени и трафика.", + "clearCacheConfirmBtn": "Очистить кэш", + "clearCacheDone": "Кэш Telegram очищен", "timezone": "Часовой пояс", "timezoneHint": "Часовой пояс IANA (например UTC, Europe/Warsaw, America/New_York). Используется для интерпретации полей HH:MM, таких как тихие часы.", "locales": "Языки шаблонов", "supportedLocales": "Поддерживаемые локали", - "supportedLocalesHint": "Коды локалей через запятую для редактирования шаблонов (например en,ru,de,fr)", + "supportedLocalesHint": "Языки, доступные для редактирования шаблонов уведомлений и команд. Встроенные шаблоны поставляются для английского и русского; другие языки начинают с пустых.", "saved": "Настройки сохранены" }, "hints": { @@ -813,6 +829,29 @@ "showDetails": "Показать детали", "hideDetails": "Скрыть детали" }, + "timezone": { + "searchPlaceholder": "Поиск по городам или IANA-кодам…", + "detect": "Определить", + "utc": "UTC", + "noMatches": "Нет совпадений" + }, + "locales": { + "empty": "Языки не выбраны. Добавьте язык ниже, чтобы начать редактирование шаблонов.", + "add": "Добавить язык", + "searchPlaceholder": "Найти или ввести код (например de-CH)…", + "addCustom": "Добавить свой код", + "noSuggestions": "Ничего не найдено. Введите код локали (2–3 буквы).", + "primary": "Основной", + "shipped": "Встроенный", + "shippedHint": "Для этого языка есть встроенные шаблоны уведомлений и команд.", + "makePrimary": "Сделать основным", + "moveUp": "Выше", + "moveDown": "Ниже", + "remove": "Удалить", + "removeLast": "Должен быть хотя бы один язык", + "reorder": "Перетащите для изменения порядка", + "orderHint": "Первый язык используется как основной при отсутствии перевода. Перетаскивайте, чтобы изменить порядок." + }, "snack": { "eventsCleared": "Очищено событий: {count}", "providerSaved": "Провайдер сохранён", diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index d8c4eb3..2025a1a 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -226,24 +226,15 @@ { href: '/targets', key: 'nav.targets', icon: 'mdiTarget' }, ]); - // "More" panel items — everything not in the bottom bar - const mobileMoreItems = $derived([ - { href: '/providers', key: 'nav.providers', icon: 'mdiServer' }, - { href: '/bots?tab=telegram', key: 'nav.bots', icon: 'mdiRobot' }, - { href: '/actions', key: 'nav.actions', icon: 'mdiPlayCircleOutline' }, - { href: '/tracking-configs', key: 'nav.configs', icon: 'mdiCog' }, - { href: '/template-configs', key: 'nav.templates', icon: 'mdiFileDocumentEdit' }, - { href: '/command-configs', key: 'nav.configs', icon: 'mdiConsoleLine' }, - { href: '/command-template-configs', key: 'nav.templates', icon: 'mdiCodeBracesBox' }, - ...(auth.isAdmin ? [ - { href: '/settings', key: 'nav.settings', icon: 'mdiCogOutline' }, - { href: '/settings/backup', key: 'nav.backup', icon: 'mdiBackupRestore' }, - { href: '/users', key: 'nav.users', icon: 'mdiAccountGroup' }, - ] : []), - ]); - + // "More" panel mirrors the full desktop sidebar tree so every subnode is + // reachable on mobile (previously it was a flat hand-picked list that + // hid all target types, bot channels, and several nested pages). let mobileMoreOpen = $state(false); + function closeMobileMore() { + mobileMoreOpen = false; + } + const isAuthPage = $derived( page.url.pathname === '/login' || page.url.pathname === '/setup' ); @@ -538,7 +529,7 @@ -