diff --git a/frontend/src/app.css b/frontend/src/app.css index 5115edb..8dff938 100644 --- a/frontend/src/app.css +++ b/frontend/src/app.css @@ -13,12 +13,41 @@ --color-destructive: #ef4444; --color-card: #ffffff; --color-card-foreground: #18181b; + --color-success-bg: #f0fdf4; + --color-success-fg: #15803d; + --color-warning-bg: #fefce8; + --color-warning-fg: #a16207; + --color-error-bg: #fef2f2; + --color-error-fg: #dc2626; --font-sans: 'Inter', ui-sans-serif, system-ui, sans-serif; --radius: 0.5rem; } +/* Dark theme overrides */ +[data-theme="dark"] { + --color-background: #09090b; + --color-foreground: #fafafa; + --color-muted: #27272a; + --color-muted-foreground: #a1a1aa; + --color-border: #3f3f46; + --color-primary: #fafafa; + --color-primary-foreground: #18181b; + --color-accent: #27272a; + --color-accent-foreground: #fafafa; + --color-destructive: #f87171; + --color-card: #18181b; + --color-card-foreground: #fafafa; + --color-success-bg: #052e16; + --color-success-fg: #4ade80; + --color-warning-bg: #422006; + --color-warning-fg: #facc15; + --color-error-bg: #450a0a; + --color-error-fg: #f87171; +} + body { font-family: var(--font-sans); background-color: var(--color-background); color: var(--color-foreground); + transition: background-color 0.2s, color 0.2s; } diff --git a/frontend/src/lib/i18n/en.json b/frontend/src/lib/i18n/en.json new file mode 100644 index 0000000..eaa97c6 --- /dev/null +++ b/frontend/src/lib/i18n/en.json @@ -0,0 +1,176 @@ +{ + "app": { + "name": "Immich Watcher", + "tagline": "Album notifications" + }, + "nav": { + "dashboard": "Dashboard", + "servers": "Servers", + "trackers": "Trackers", + "templates": "Templates", + "targets": "Targets", + "users": "Users", + "logout": "Logout" + }, + "auth": { + "signIn": "Sign in", + "signInTitle": "Sign in to your account", + "signingIn": "Signing in...", + "username": "Username", + "password": "Password", + "confirmPassword": "Confirm password", + "setupTitle": "Welcome", + "setupDescription": "Create your admin account to get started", + "createAccount": "Create account", + "creatingAccount": "Creating account...", + "passwordMismatch": "Passwords do not match", + "passwordTooShort": "Password must be at least 6 characters", + "loginWithImmich": "Login with Immich", + "or": "or" + }, + "dashboard": { + "title": "Dashboard", + "description": "Overview of your Immich Watcher setup", + "servers": "Servers", + "activeTrackers": "Active Trackers", + "targets": "Targets", + "recentEvents": "Recent Events", + "noEvents": "No events yet. Create a tracker to start monitoring albums.", + "loading": "Loading..." + }, + "servers": { + "title": "Servers", + "description": "Manage Immich server connections", + "addServer": "Add Server", + "cancel": "Cancel", + "name": "Name", + "url": "Immich URL", + "urlPlaceholder": "http://immich:2283", + "apiKey": "API Key", + "connecting": "Connecting...", + "noServers": "No servers configured yet.", + "delete": "Delete", + "confirmDelete": "Delete this server?" + }, + "trackers": { + "title": "Trackers", + "description": "Monitor albums for changes", + "newTracker": "New Tracker", + "cancel": "Cancel", + "name": "Name", + "namePlaceholder": "Family photos tracker", + "server": "Server", + "selectServer": "Select server...", + "albums": "Albums", + "eventTypes": "Event Types", + "notificationTargets": "Notification Targets", + "scanInterval": "Scan Interval (seconds)", + "createTracker": "Create Tracker", + "noTrackers": "No trackers yet. Add a server first, then create a tracker.", + "active": "Active", + "paused": "Paused", + "pause": "Pause", + "resume": "Resume", + "delete": "Delete", + "confirmDelete": "Delete this tracker?", + "albums_count": "album(s)", + "every": "every", + "trackImages": "Track images", + "trackVideos": "Track videos", + "favoritesOnly": "Favorites only", + "includePeople": "Include people in notifications", + "includeAssetDetails": "Include asset details", + "maxAssetsToShow": "Max assets to show", + "sortBy": "Sort by", + "sortOrder": "Sort order", + "sortNone": "Original order", + "sortDate": "Date", + "sortRating": "Rating", + "sortName": "Name", + "sortRandom": "Random", + "ascending": "Ascending", + "descending": "Descending", + "quietHoursStart": "Quiet hours start", + "quietHoursEnd": "Quiet hours end" + }, + "templates": { + "title": "Templates", + "description": "Jinja2 message templates for notifications", + "newTemplate": "New Template", + "cancel": "Cancel", + "name": "Name", + "body": "Template Body (Jinja2)", + "variables": "Variables", + "preview": "Preview", + "edit": "Edit", + "delete": "Delete", + "confirmDelete": "Delete this template?", + "create": "Create Template", + "update": "Update Template", + "noTemplates": "No templates yet. A default template will be used if none is configured.", + "eventType": "Event type", + "allEvents": "All events", + "assetsAdded": "Assets added", + "assetsRemoved": "Assets removed", + "albumRenamed": "Album renamed", + "albumDeleted": "Album deleted" + }, + "targets": { + "title": "Targets", + "description": "Notification destinations (Telegram, webhooks)", + "addTarget": "Add Target", + "cancel": "Cancel", + "type": "Type", + "name": "Name", + "namePlaceholder": "My notifications", + "botToken": "Bot Token", + "chatId": "Chat ID", + "webhookUrl": "Webhook URL", + "create": "Add Target", + "test": "Test", + "delete": "Delete", + "confirmDelete": "Delete this target?", + "noTargets": "No notification targets configured yet.", + "testSent": "Test sent successfully!", + "aiCaptions": "Enable AI captions", + "telegramSettings": "Telegram Settings", + "maxMedia": "Max media to send", + "maxGroupSize": "Max group size", + "chunkDelay": "Delay between groups (ms)", + "maxAssetSize": "Max asset size (MB)", + "videoWarning": "Video size warning", + "disableUrlPreview": "Disable link previews", + "sendLargeAsDocuments": "Send large photos as documents" + }, + "users": { + "title": "Users", + "description": "Manage user accounts (admin only)", + "addUser": "Add User", + "cancel": "Cancel", + "username": "Username", + "password": "Password", + "role": "Role", + "roleUser": "User", + "roleAdmin": "Admin", + "create": "Create User", + "delete": "Delete", + "confirmDelete": "Delete this user?", + "joined": "joined" + }, + "common": { + "loading": "Loading...", + "save": "Save", + "cancel": "Cancel", + "delete": "Delete", + "edit": "Edit", + "close": "Close", + "confirm": "Confirm", + "error": "Error", + "success": "Success", + "language": "Language", + "theme": "Theme", + "light": "Light", + "dark": "Dark", + "system": "System" + } +} diff --git a/frontend/src/lib/i18n/index.ts b/frontend/src/lib/i18n/index.ts new file mode 100644 index 0000000..7d8c793 --- /dev/null +++ b/frontend/src/lib/i18n/index.ts @@ -0,0 +1,60 @@ +/** + * Simple i18n store using Svelte 5 runes. + * Supports nested keys like "nav.dashboard". + */ + +import en from './en.json'; +import ru from './ru.json'; + +export type Locale = 'en' | 'ru'; + +const translations: Record> = { en, ru }; + +let currentLocale = $state('en'); + +export function getLocale(): Locale { + return currentLocale; +} + +export function setLocale(locale: Locale) { + currentLocale = locale; + if (typeof localStorage !== 'undefined') { + localStorage.setItem('locale', locale); + } +} + +export function initLocale() { + if (typeof localStorage !== 'undefined') { + const saved = localStorage.getItem('locale') as Locale | null; + if (saved && saved in translations) { + currentLocale = saved; + return; + } + } + if (typeof navigator !== 'undefined') { + const lang = navigator.language.slice(0, 2); + if (lang in translations) { + currentLocale = lang as Locale; + } + } +} + +/** + * Get a translated string by dot-separated key. + * Falls back to English if key not found in current locale. + */ +export function t(key: string): string { + return resolve(translations[currentLocale], key) + ?? resolve(translations.en, key) + ?? key; +} + +function resolve(obj: any, path: string): string | undefined { + const parts = path.split('.'); + let current = obj; + for (const part of parts) { + if (current == null || typeof current !== 'object') return undefined; + current = current[part]; + } + return typeof current === 'string' ? current : undefined; +} diff --git a/frontend/src/lib/i18n/ru.json b/frontend/src/lib/i18n/ru.json new file mode 100644 index 0000000..8fee732 --- /dev/null +++ b/frontend/src/lib/i18n/ru.json @@ -0,0 +1,176 @@ +{ + "app": { + "name": "Immich Watcher", + "tagline": "Уведомления об альбомах" + }, + "nav": { + "dashboard": "Главная", + "servers": "Серверы", + "trackers": "Трекеры", + "templates": "Шаблоны", + "targets": "Получатели", + "users": "Пользователи", + "logout": "Выход" + }, + "auth": { + "signIn": "Войти", + "signInTitle": "Вход в аккаунт", + "signingIn": "Вход...", + "username": "Имя пользователя", + "password": "Пароль", + "confirmPassword": "Подтвердите пароль", + "setupTitle": "Добро пожаловать", + "setupDescription": "Создайте учётную запись администратора", + "createAccount": "Создать аккаунт", + "creatingAccount": "Создание...", + "passwordMismatch": "Пароли не совпадают", + "passwordTooShort": "Пароль должен быть не менее 6 символов", + "loginWithImmich": "Войти через Immich", + "or": "или" + }, + "dashboard": { + "title": "Главная", + "description": "Обзор настроек Immich Watcher", + "servers": "Серверы", + "activeTrackers": "Активные трекеры", + "targets": "Получатели", + "recentEvents": "Последние события", + "noEvents": "Событий пока нет. Создайте трекер для отслеживания альбомов.", + "loading": "Загрузка..." + }, + "servers": { + "title": "Серверы", + "description": "Управление подключениями к Immich", + "addServer": "Добавить сервер", + "cancel": "Отмена", + "name": "Название", + "url": "URL Immich", + "urlPlaceholder": "http://immich:2283", + "apiKey": "API ключ", + "connecting": "Подключение...", + "noServers": "Серверы не настроены.", + "delete": "Удалить", + "confirmDelete": "Удалить этот сервер?" + }, + "trackers": { + "title": "Трекеры", + "description": "Отслеживание изменений в альбомах", + "newTracker": "Новый трекер", + "cancel": "Отмена", + "name": "Название", + "namePlaceholder": "Трекер семейных фото", + "server": "Сервер", + "selectServer": "Выберите сервер...", + "albums": "Альбомы", + "eventTypes": "Типы событий", + "notificationTargets": "Получатели уведомлений", + "scanInterval": "Интервал проверки (секунды)", + "createTracker": "Создать трекер", + "noTrackers": "Трекеров пока нет. Сначала добавьте сервер, затем создайте трекер.", + "active": "Активен", + "paused": "Приостановлен", + "pause": "Пауза", + "resume": "Возобновить", + "delete": "Удалить", + "confirmDelete": "Удалить этот трекер?", + "albums_count": "альбом(ов)", + "every": "каждые", + "trackImages": "Отслеживать фото", + "trackVideos": "Отслеживать видео", + "favoritesOnly": "Только избранные", + "includePeople": "Включать людей в уведомления", + "includeAssetDetails": "Включать детали файлов", + "maxAssetsToShow": "Макс. файлов в уведомлении", + "sortBy": "Сортировка", + "sortOrder": "Порядок", + "sortNone": "Исходный порядок", + "sortDate": "Дата", + "sortRating": "Рейтинг", + "sortName": "Имя", + "sortRandom": "Случайный", + "ascending": "По возрастанию", + "descending": "По убыванию", + "quietHoursStart": "Тихие часы начало", + "quietHoursEnd": "Тихие часы конец" + }, + "templates": { + "title": "Шаблоны", + "description": "Шаблоны сообщений Jinja2 для уведомлений", + "newTemplate": "Новый шаблон", + "cancel": "Отмена", + "name": "Название", + "body": "Текст шаблона (Jinja2)", + "variables": "Переменные", + "preview": "Предпросмотр", + "edit": "Редактировать", + "delete": "Удалить", + "confirmDelete": "Удалить этот шаблон?", + "create": "Создать шаблон", + "update": "Обновить шаблон", + "noTemplates": "Шаблонов пока нет. Без шаблона будет использован шаблон по умолчанию.", + "eventType": "Тип события", + "allEvents": "Все события", + "assetsAdded": "Добавлены файлы", + "assetsRemoved": "Удалены файлы", + "albumRenamed": "Альбом переименован", + "albumDeleted": "Альбом удалён" + }, + "targets": { + "title": "Получатели", + "description": "Адреса уведомлений (Telegram, вебхуки)", + "addTarget": "Добавить получателя", + "cancel": "Отмена", + "type": "Тип", + "name": "Название", + "namePlaceholder": "Мои уведомления", + "botToken": "Токен бота", + "chatId": "ID чата", + "webhookUrl": "URL вебхука", + "create": "Добавить", + "test": "Тест", + "delete": "Удалить", + "confirmDelete": "Удалить этого получателя?", + "noTargets": "Получатели уведомлений не настроены.", + "testSent": "Тестовое уведомление отправлено!", + "aiCaptions": "Включить AI подписи", + "telegramSettings": "Настройки Telegram", + "maxMedia": "Макс. медиафайлов", + "maxGroupSize": "Макс. размер группы", + "chunkDelay": "Задержка между группами (мс)", + "maxAssetSize": "Макс. размер файла (МБ)", + "videoWarning": "Предупреждение о размере видео", + "disableUrlPreview": "Отключить превью ссылок", + "sendLargeAsDocuments": "Отправлять большие фото как документы" + }, + "users": { + "title": "Пользователи", + "description": "Управление аккаунтами (только админ)", + "addUser": "Добавить пользователя", + "cancel": "Отмена", + "username": "Имя пользователя", + "password": "Пароль", + "role": "Роль", + "roleUser": "Пользователь", + "roleAdmin": "Администратор", + "create": "Создать", + "delete": "Удалить", + "confirmDelete": "Удалить этого пользователя?", + "joined": "зарегистрирован" + }, + "common": { + "loading": "Загрузка...", + "save": "Сохранить", + "cancel": "Отмена", + "delete": "Удалить", + "edit": "Редактировать", + "close": "Закрыть", + "confirm": "Подтвердить", + "error": "Ошибка", + "success": "Успешно", + "language": "Язык", + "theme": "Тема", + "light": "Светлая", + "dark": "Тёмная", + "system": "Системная" + } +} diff --git a/frontend/src/lib/theme.svelte.ts b/frontend/src/lib/theme.svelte.ts new file mode 100644 index 0000000..b2fca62 --- /dev/null +++ b/frontend/src/lib/theme.svelte.ts @@ -0,0 +1,54 @@ +/** + * Theme management with Svelte 5 runes. + * Supports light, dark, and system preference. + */ + +export type Theme = 'light' | 'dark' | 'system'; + +let theme = $state('system'); +let resolved = $state<'light' | 'dark'>('light'); + +export function getTheme() { + return { + get current() { return theme; }, + get resolved() { return resolved; }, + get isDark() { return resolved === 'dark'; }, + }; +} + +export function setTheme(newTheme: Theme) { + theme = newTheme; + if (typeof localStorage !== 'undefined') { + localStorage.setItem('theme', newTheme); + } + applyTheme(); +} + +export function initTheme() { + if (typeof localStorage !== 'undefined') { + const saved = localStorage.getItem('theme') as Theme | null; + if (saved && ['light', 'dark', 'system'].includes(saved)) { + theme = saved; + } + } + applyTheme(); + + // Listen for system preference changes + if (typeof window !== 'undefined') { + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { + if (theme === 'system') applyTheme(); + }); + } +} + +function applyTheme() { + if (typeof document === 'undefined') return; + + if (theme === 'system') { + resolved = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; + } else { + resolved = theme; + } + + document.documentElement.setAttribute('data-theme', resolved); +} diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index 543cfd5..31b6285 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -4,16 +4,19 @@ import { goto } from '$app/navigation'; import { onMount } from 'svelte'; import { getAuth, loadUser, logout } from '$lib/auth.svelte'; + import { t, initLocale, getLocale, setLocale, type Locale } from '$lib/i18n'; + import { getTheme, initTheme, setTheme, type Theme } from '$lib/theme.svelte'; let { children } = $props(); const auth = getAuth(); + const theme = getTheme(); const navItems = [ - { href: '/', label: 'Dashboard', icon: '⊞' }, - { href: '/servers', label: 'Servers', icon: '⬡' }, - { href: '/trackers', label: 'Trackers', icon: '◎' }, - { href: '/templates', label: 'Templates', icon: '⎘' }, - { href: '/targets', label: 'Targets', icon: '◇' }, + { href: '/', key: 'nav.dashboard', icon: '⊞' }, + { href: '/servers', key: 'nav.servers', icon: '⬡' }, + { href: '/trackers', key: 'nav.trackers', icon: '◎' }, + { href: '/templates', key: 'nav.templates', icon: '⎘' }, + { href: '/targets', key: 'nav.targets', icon: '◇' }, ]; const isAuthPage = $derived( @@ -21,26 +24,52 @@ ); onMount(async () => { + initLocale(); + initTheme(); await loadUser(); if (!auth.user && !isAuthPage) { goto('/login'); } }); + + function cycleTheme() { + const order: Theme[] = ['light', 'dark', 'system']; + const idx = order.indexOf(theme.current); + setTheme(order[(idx + 1) % order.length]); + } + + function toggleLocale() { + setLocale(getLocale() === 'en' ? 'ru' : 'en'); + } {#if isAuthPage} {@render children()} {:else if auth.loading}
-

Loading...

+

{t('common.loading')}

{:else if auth.user}
diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte index 689a651..d759495 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -1,39 +1,35 @@ - + {#if status}
-

Servers

+

{t('dashboard.servers')}

{status.servers}

-

Active Trackers

+

{t('dashboard.activeTrackers')}

{status.trackers.active} / {status.trackers.total}

-

Targets

+

{t('dashboard.targets')}

{status.targets}

-

Recent Events

+

{t('dashboard.recentEvents')}

{#if status.recent_events.length === 0} - -

No events yet. Create a tracker to start monitoring albums.

-
+

{t('dashboard.noEvents')}

{:else}
@@ -52,5 +48,5 @@ {/if} {:else} -

Loading...

+

{t('dashboard.loading')}

{/if} diff --git a/frontend/src/routes/login/+page.svelte b/frontend/src/routes/login/+page.svelte index 0549262..c2ff7d7 100644 --- a/frontend/src/routes/login/+page.svelte +++ b/frontend/src/routes/login/+page.svelte @@ -3,13 +3,18 @@ import { onMount } from 'svelte'; import { api } from '$lib/api'; import { login } from '$lib/auth.svelte'; + import { t, initLocale, getLocale, setLocale } from '$lib/i18n'; + import { initTheme, getTheme, setTheme, type Theme } from '$lib/theme.svelte'; + const theme = getTheme(); let username = $state(''); let password = $state(''); let error = $state(''); let submitting = $state(false); onMount(async () => { + initLocale(); + initTheme(); try { const res = await api<{ needs_setup: boolean }>('/auth/needs-setup'); if (res.needs_setup) goto('/setup'); @@ -33,40 +38,37 @@
-

Immich Watcher

-

Sign in to your account

+
+ + +
+

{t('app.name')}

+

{t('auth.signInTitle')}

{#if error} -
{error}
+
{error}
{/if}
- - + +
- - + +
-
diff --git a/frontend/src/routes/servers/+page.svelte b/frontend/src/routes/servers/+page.svelte index eeb9754..fc0cade 100644 --- a/frontend/src/routes/servers/+page.svelte +++ b/frontend/src/routes/servers/+page.svelte @@ -1,6 +1,7 @@ - + {#if showForm} - {#if error} -
{error}
- {/if} + {#if error}
{error}
{/if}
- - + +
- - + +
- - + +
{/if} {#if servers.length === 0 && !showForm} -

No servers configured yet.

+

{t('servers.noServers')}

{:else}
{#each servers as server} @@ -81,7 +70,7 @@

{server.name}

{server.url}

- +
{/each} diff --git a/frontend/src/routes/setup/+page.svelte b/frontend/src/routes/setup/+page.svelte index 6da8861..dfc9d89 100644 --- a/frontend/src/routes/setup/+page.svelte +++ b/frontend/src/routes/setup/+page.svelte @@ -1,6 +1,9 @@ @@ -33,50 +30,25 @@
-

Welcome

-

Create your admin account to get started

- - {#if error} -
{error}
- {/if} - +

{t('auth.setupTitle')}

+

{t('auth.setupDescription')}

+ {#if error}
{error}
{/if}
- - + +
- - + +
- - + +
-
diff --git a/frontend/src/routes/targets/+page.svelte b/frontend/src/routes/targets/+page.svelte index 3a615ee..860a376 100644 --- a/frontend/src/routes/targets/+page.svelte +++ b/frontend/src/routes/targets/+page.svelte @@ -1,100 +1,124 @@ - - {#if testResult} -
{testResult}
+
{testResult}
{/if} {#if showForm} - {#if error}
{error}
{/if} + {#if error}
{error}
{/if}
- + {t('targets.type')}
- - + +
{#if formType === 'telegram'}
- - + +
- - + +
+ + +
+ {t('targets.telegramSettings')} +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + +
+
{:else}
- - + +
{/if} - + + + +
{/if} {#if targets.length === 0 && !showForm} -

No notification targets configured yet.

+

{t('targets.noTargets')}

{:else}
{#each targets as target} @@ -110,8 +134,8 @@

- - + +
diff --git a/frontend/src/routes/templates/+page.svelte b/frontend/src/routes/templates/+page.svelte index 67383b9..d1dd5b2 100644 --- a/frontend/src/routes/templates/+page.svelte +++ b/frontend/src/routes/templates/+page.svelte @@ -1,113 +1,108 @@ - - {#if showForm} - {#if error}
{error}
{/if} + {#if error}
{error}
{/if}
-
- - +
+
+ + +
+
+ + +
- - + +

- Variables: {'{{ album_name }}'}, {'{{ added_count }}'}, {'{{ removed_count }}'}, {'{{ people }}'}, {'{{ change_type }}'}, {'{{ album_url }}'}, {'{{ added_assets }}'} + {t('templates.variables')}: {'{{ album_name }}'}, {'{{ added_count }}'}, {'{{ removed_count }}'}, {'{{ people }}'}, {'{{ change_type }}'}, {'{{ album_url }}'}, {'{{ added_assets }}'}, {'{{ old_name }}'}, {'{{ new_name }}'}

{/if} {#if templates.length === 0 && !showForm} -

No templates yet. A default template will be used if none is configured.

+

{t('templates.noTemplates')}

{:else}
- {#each templates as template} + {#each templates as tmpl}
-

{template.name}

-
{template.body.slice(0, 200)}{template.body.length > 200 ? '...' : ''}
- {#if preview && previewId === template.id && !showForm} -
+
+

{tmpl.name}

+ {#if tmpl.event_type} + {tmpl.event_type} + {/if} +
+
{tmpl.body.slice(0, 200)}{tmpl.body.length > 200 ? '...' : ''}
+ {#if preview && previewId === tmpl.id && !showForm} +
{preview}
{/if}
- - - + + +
diff --git a/frontend/src/routes/trackers/+page.svelte b/frontend/src/routes/trackers/+page.svelte index e1a15c1..5958d98 100644 --- a/frontend/src/routes/trackers/+page.svelte +++ b/frontend/src/routes/trackers/+page.svelte @@ -1,6 +1,7 @@ - - {#if showForm} - {#if error}
{error}
{/if} + {#if error}
{error}
{/if}
- - + +
- - + {#each servers as s}{/each}
{#if albums.length > 0}
- +
{#each albums as album}
{/if}
- +
{#each ['assets_added', 'assets_removed', 'album_renamed', 'album_sharing_changed', 'changed'] as evt}
+ + +
+ + + + + +
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ {#if targets.length > 0}
- +
- {#each targets as t} + {#each targets as tgt} {/each}
{/if} -
- - -
- + + {/if} {#if trackers.length === 0 && !showForm} -

No trackers yet. Add a server first, then create a tracker.

+

{t('trackers.noTrackers')}

{:else}
{#each trackers as tracker} @@ -149,17 +158,17 @@

{tracker.name}

- - {tracker.enabled ? 'Active' : 'Paused'} + + {tracker.enabled ? t('trackers.active') : t('trackers.paused')}
-

{tracker.album_ids.length} album(s) · every {tracker.scan_interval}s · {tracker.event_types.join(', ')}

+

{tracker.album_ids.length} {t('trackers.albums_count')} · {t('trackers.every')} {tracker.scan_interval}s · {tracker.event_types.join(', ')}

- +
diff --git a/frontend/src/routes/users/+page.svelte b/frontend/src/routes/users/+page.svelte index 8663180..9fac9b0 100644 --- a/frontend/src/routes/users/+page.svelte +++ b/frontend/src/routes/users/+page.svelte @@ -1,6 +1,7 @@ - + {#if showForm} - {#if error}
{error}
{/if} + {#if error}
{error}
{/if}
- - + +
- - + +
- - + +
- +
{/if} @@ -74,10 +63,10 @@

{user.username}

-

{user.role} · joined {new Date(user.created_at).toLocaleDateString()}

+

{user.role} · {t('users.joined')} {new Date(user.created_at).toLocaleDateString()}

{#if user.id !== auth.user?.id} - + {/if}