feat: locale-aware notification templates + UX improvements

- Add locale support to notification templates (matching command template
  pattern): TemplateSlot now has locale field with (config_id, slot_name,
  locale) uniqueness, nested API format {slot: {locale: template}}
- Migration merges separate EN/RU system configs into unified per-provider
  configs; seeds create one config per provider with multi-locale slots
- Locale-aware dispatch with EN fallback in NotificationDispatcher
- Frontend locale tabs (EN/RU) on template config editor
- Fix tracking config cards not showing default provider icons
- Global provider filter, search palette, and various UX polish
This commit is contained in:
2026-03-23 19:08:48 +03:00
parent 6a559bfcd2
commit 37388c430c
30 changed files with 628 additions and 318 deletions
@@ -14,17 +14,19 @@
import Hint from '$lib/components/Hint.svelte';
import IconButton from '$lib/components/IconButton.svelte';
import IconGridSelect from '$lib/components/IconGridSelect.svelte';
import { providerTypeItems, providerTypeFilterItems, sortByItems, sortOrderItems, albumModeItems, assetTypeItems, memorySourceItems } from '$lib/grid-items';
import { providerTypeItems, providerTypeFilterItems, sortByItems, sortOrderItems, albumModeItems, assetTypeItems, memorySourceItems, providerDefaultIcon } from '$lib/grid-items';
import { snackSuccess, snackError } from '$lib/stores/snackbar.svelte';
import { highlightFromUrl } from '$lib/highlight';
import { globalProviderFilter } from '$lib/stores/provider-filter.svelte';
import type { TrackingConfig } from '$lib/types';
let allConfigs = $derived(trackingConfigsCache.items);
let filterText = $state('');
let filterType = $state('');
let effectiveType = $derived(globalProviderFilter.providerType || filterType);
let configs = $derived(allConfigs.filter(c =>
(!filterText || c.name.toLowerCase().includes(filterText.toLowerCase())) &&
(!filterType || c.provider_type === filterType)
(!effectiveType || c.provider_type === effectiveType)
));
let loaded = $state(false);
let showForm = $state(false);
@@ -262,9 +264,11 @@
<div class="flex items-center gap-2 mb-3">
<input type="text" bind:value={filterText} placeholder={t('common.filterByName')}
class="flex-1 px-3 py-1.5 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]" />
{#if !globalProviderFilter.id}
<div class="w-48">
<IconGridSelect items={providerTypeFilterItems()} bind:value={filterType} columns={2} compact />
</div>
{/if}
</div>
{/if}
@@ -283,7 +287,7 @@
<div class="flex items-center justify-between">
<div>
<div class="flex items-center gap-2">
{#if config.icon}<MdiIcon name={config.icon} />{/if}
<span style="color: var(--color-primary);"><MdiIcon name={providerDefaultIcon({ icon: config.icon, type: config.provider_type })} size={20} /></span>
<p class="font-medium">{config.name}</p>
<span class="text-xs px-1.5 py-0.5 rounded bg-[var(--color-muted)] text-[var(--color-muted-foreground)] font-mono">{config.provider_type}</span>
</div>