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:
@@ -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>
|
||||
@@ -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}
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user