Add i18n (RU/EN), dark/light themes, enhanced tracker/target forms (Phase 7a)
Some checks failed
Validate / Hassfest (push) Has been cancelled
Some checks failed
Validate / Hassfest (push) Has been cancelled
Frontend enhancements: - i18n: Full Russian and English translations (~170 keys each), language switcher in sidebar and login page, auto-detect from browser, persists to localStorage - Themes: Light/dark mode with CSS custom properties, system preference detection, toggle in sidebar header, smooth transitions - Dark theme: Full color palette (background, card, muted, border, success, warning, error variants) Enhanced forms: - Tracker creation: asset type filtering (images/videos), favorites only, include people/details toggles, sort by/order selects, max assets to show - Target creation: Telegram media settings (collapsible) with max media, group size, chunk delay, max asset size, URL preview disable, large photos as documents - Template creation: event_type selector (all/added/removed/renamed/deleted) All pages use t() for translations, var(--color-*) for theme-safe colors, and proper label-for-input associations. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
176
frontend/src/lib/i18n/en.json
Normal file
176
frontend/src/lib/i18n/en.json
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
60
frontend/src/lib/i18n/index.ts
Normal file
60
frontend/src/lib/i18n/index.ts
Normal file
@@ -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<Locale, Record<string, any>> = { en, ru };
|
||||
|
||||
let currentLocale = $state<Locale>('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;
|
||||
}
|
||||
176
frontend/src/lib/i18n/ru.json
Normal file
176
frontend/src/lib/i18n/ru.json
Normal file
@@ -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": "Системная"
|
||||
}
|
||||
}
|
||||
54
frontend/src/lib/theme.svelte.ts
Normal file
54
frontend/src/lib/theme.svelte.ts
Normal file
@@ -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<Theme>('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);
|
||||
}
|
||||
Reference in New Issue
Block a user