feat(phase2): localization EN/RU + additional widget types

- Add svelte-i18n with 224 translation keys (English + Russian)
- Language switcher in header (EN/RU toggle, persists to localStorage)
- Extract all hardcoded strings from 37 component/page files
- Add 4 new widget types: Bookmark, Note (markdown), Embed (iframe), Status
- WidgetRenderer dispatches by type, WidgetGrid supports full-width widgets
- Type-specific config forms in board editor
- Install marked for markdown rendering
This commit is contained in:
2026-03-24 23:18:05 +03:00
parent bf4e5089ee
commit 477c0e4d52
52 changed files with 1776 additions and 395 deletions
+16 -11
View File
@@ -1,5 +1,7 @@
<script lang="ts">
import { t } from 'svelte-i18n';
import ThemeToggle from './ThemeToggle.svelte';
import LanguageSwitcher from './LanguageSwitcher.svelte';
import SearchTrigger from '$lib/components/search/SearchTrigger.svelte';
import { ui } from '$lib/stores/ui.svelte.js';
import { theme, type BackgroundType } from '$lib/stores/theme.svelte.js';
@@ -13,11 +15,11 @@
let showUserMenu = $state(false);
let showBgMenu = $state(false);
const bgOptions: { value: BackgroundType; label: string }[] = [
{ value: 'mesh', label: 'Mesh Gradient' },
{ value: 'particles', label: 'Particles' },
{ value: 'aurora', label: 'Aurora' },
{ value: 'none', label: 'None' }
const bgOptions: { value: BackgroundType; labelKey: string }[] = [
{ value: 'mesh', labelKey: 'bg.mesh' },
{ value: 'particles', labelKey: 'bg.particles' },
{ value: 'aurora', labelKey: 'bg.aurora' },
{ value: 'none', labelKey: 'bg.none' }
];
function handleClickOutside(e: MouseEvent) {
@@ -42,7 +44,7 @@
type="button"
onclick={() => ui.toggleSidebar()}
class="inline-flex items-center justify-center rounded-md p-2 text-muted-foreground hover:bg-accent hover:text-foreground"
aria-label="Toggle sidebar"
aria-label={$t('sidebar.toggle')}
>
<svg
class="h-5 w-5"
@@ -72,8 +74,8 @@
type="button"
onclick={() => (showBgMenu = !showBgMenu)}
class="inline-flex items-center justify-center rounded-md p-2 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
title="Background effect"
aria-label="Change background effect"
title={$t('bg.title')}
aria-label={$t('bg.aria_label')}
>
<svg
class="h-5 w-5"
@@ -119,7 +121,7 @@
{:else}
<span class="h-3 w-3"></span>
{/if}
{opt.label}
{$t(opt.labelKey)}
</button>
{/each}
</div>
@@ -129,6 +131,9 @@
<!-- Theme toggle -->
<ThemeToggle />
<!-- Language switcher -->
<LanguageSwitcher />
<!-- User menu -->
{#if user}
<div class="user-menu-container relative">
@@ -175,7 +180,7 @@
<polyline points="16 17 21 12 16 7" />
<line x1="21" y1="12" x2="9" y2="12" />
</svg>
Sign Out
{$t('auth.logout')}
</button>
</form>
</div>
@@ -186,7 +191,7 @@
href="/login"
class="rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:bg-primary/90"
>
Sign In
{$t('auth.login')}
</a>
{/if}
</header>
@@ -0,0 +1,19 @@
<script lang="ts">
import { locale } from 'svelte-i18n';
import { storeLocale } from '$lib/i18n/index.js';
function toggleLocale() {
const next = $locale === 'ru' ? 'en' : 'ru';
locale.set(next);
storeLocale(next);
}
</script>
<button
type="button"
onclick={toggleLocale}
class="inline-flex items-center justify-center rounded-md px-2 py-1.5 text-xs font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-foreground focus:outline-none focus:ring-2 focus:ring-ring"
title={$locale === 'ru' ? 'Switch to English' : 'Переключить на русский'}
>
{$locale === 'ru' ? 'RU' : 'EN'}
</button>
+2 -1
View File
@@ -1,4 +1,5 @@
<script lang="ts">
import { t } from 'svelte-i18n';
import type { Snippet } from 'svelte';
import Sidebar from './Sidebar.svelte';
import Header from './Header.svelte';
@@ -40,7 +41,7 @@
type="button"
class="fixed inset-0 z-30 bg-black/50"
onclick={() => ui.closeMobileSidebar()}
aria-label="Close sidebar"
aria-label={$t('sidebar.close')}
></button>
{/if}
+13 -12
View File
@@ -1,4 +1,5 @@
<script lang="ts">
import { t } from 'svelte-i18n';
import { ui } from '$lib/stores/ui.svelte.js';
import { page } from '$app/stores';
import DynamicIcon from '$lib/components/ui/DynamicIcon.svelte';
@@ -46,10 +47,10 @@
<rect x="14" y="14" width="7" height="7" />
<rect x="3" y="14" width="7" height="7" />
</svg>
<span class="text-sm font-semibold">App Launcher</span>
<span class="text-sm font-semibold">{$t('app_name')}</span>
</a>
{:else}
<a href="/" class="mx-auto text-sidebar-primary" title="App Launcher">
<a href="/" class="mx-auto text-sidebar-primary" title={$t('app_name')}>
<svg
class="h-6 w-6"
xmlns="http://www.w3.org/2000/svg"
@@ -75,7 +76,7 @@
<div class="mb-3">
{#if !collapsed}
<p class="mb-1 px-2 text-xs font-medium uppercase tracking-wider text-sidebar-foreground/50">
Navigation
{$t('nav.navigation')}
</p>
{/if}
@@ -84,7 +85,7 @@
class="flex items-center gap-2 rounded-md px-2 py-2 text-sm transition-colors {isActive('/boards')
? 'bg-sidebar-accent text-sidebar-accent-foreground'
: 'text-sidebar-foreground hover:bg-sidebar-accent/50'}"
title={collapsed ? 'Boards' : undefined}
title={collapsed ? $t('nav.boards') : undefined}
>
<svg
class="h-4 w-4 shrink-0"
@@ -100,7 +101,7 @@
<line x1="3" y1="9" x2="21" y2="9" />
<line x1="9" y1="21" x2="9" y2="9" />
</svg>
{#if !collapsed}<span>Boards</span>{/if}
{#if !collapsed}<span>{$t('nav.boards')}</span>{/if}
</a>
<a
@@ -108,7 +109,7 @@
class="flex items-center gap-2 rounded-md px-2 py-2 text-sm transition-colors {isActive('/apps')
? 'bg-sidebar-accent text-sidebar-accent-foreground'
: 'text-sidebar-foreground hover:bg-sidebar-accent/50'}"
title={collapsed ? 'Apps' : undefined}
title={collapsed ? $t('nav.apps') : undefined}
>
<svg
class="h-4 w-4 shrink-0"
@@ -126,7 +127,7 @@
d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"
/>
</svg>
{#if !collapsed}<span>Apps</span>{/if}
{#if !collapsed}<span>{$t('nav.apps')}</span>{/if}
</a>
</div>
@@ -137,7 +138,7 @@
<p
class="mb-1 px-2 text-xs font-medium uppercase tracking-wider text-sidebar-foreground/50"
>
Boards
{$t('nav.boards')}
</p>
{/if}
@@ -174,7 +175,7 @@
<p
class="mb-1 px-2 text-xs font-medium uppercase tracking-wider text-sidebar-foreground/50"
>
Admin
{$t('nav.admin')}
</p>
{/if}
@@ -183,7 +184,7 @@
class="flex items-center gap-2 rounded-md px-2 py-2 text-sm transition-colors {isActive('/admin')
? 'bg-sidebar-accent text-sidebar-accent-foreground'
: 'text-sidebar-foreground hover:bg-sidebar-accent/50'}"
title={collapsed ? 'Admin Panel' : undefined}
title={collapsed ? $t('nav.admin_panel') : undefined}
>
<svg
class="h-4 w-4 shrink-0"
@@ -200,7 +201,7 @@
/>
<circle cx="12" cy="12" r="3" />
</svg>
{#if !collapsed}<span>Admin Panel</span>{/if}
{#if !collapsed}<span>{$t('nav.admin_panel')}</span>{/if}
</a>
</div>
{/if}
@@ -213,7 +214,7 @@
type="button"
onclick={() => ui.toggleSidebar()}
class="flex w-full items-center justify-center rounded-md p-2 text-sidebar-foreground transition-colors hover:bg-sidebar-accent"
title={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
title={collapsed ? $t('sidebar.expand') : $t('sidebar.collapse')}
>
<svg
class="h-4 w-4 transition-transform duration-200"
+7 -6
View File
@@ -1,18 +1,19 @@
<script lang="ts">
import { t } from 'svelte-i18n';
import { theme } from '$lib/stores/theme.svelte.js';
const modeIcons: Record<string, { path: string; label: string }> = {
const modeIcons: Record<string, { path: string; labelKey: string }> = {
light: {
path: 'M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z',
label: 'Light'
labelKey: 'theme.light'
},
dark: {
path: 'M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z',
label: 'Dark'
labelKey: 'theme.dark'
},
system: {
path: 'M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z',
label: 'System'
labelKey: 'theme.system'
}
};
@@ -23,8 +24,8 @@
type="button"
onclick={() => theme.cycleMode()}
class="inline-flex items-center justify-center rounded-md p-2 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground focus:outline-none focus:ring-2 focus:ring-ring"
title="Theme: {currentIcon.label}"
aria-label="Toggle theme (current: {currentIcon.label})"
title={$t('theme.title', { values: { mode: $t(currentIcon.labelKey) } })}
aria-label={$t('theme.toggle', { values: { mode: $t(currentIcon.labelKey) } })}
>
<svg
class="h-5 w-5"