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
@@ -1,4 +1,5 @@
<script lang="ts">
import { t } from 'svelte-i18n';
import { search } from '$lib/stores/search.svelte.js';
import SearchResult from './SearchResult.svelte';
@@ -33,7 +34,7 @@
<div
class="w-full max-w-lg rounded-lg border border-border bg-popover shadow-2xl"
role="dialog"
aria-label="Search"
aria-label={$t('search.placeholder')}
>
<!-- Input -->
<div class="flex items-center gap-2 border-b border-border px-4 py-3">
@@ -54,7 +55,7 @@
bind:this={inputEl}
bind:value={search.query}
type="text"
placeholder="Search apps and boards..."
placeholder={$t('search.placeholder')}
class="flex-1 bg-transparent text-sm text-foreground placeholder:text-muted-foreground focus:outline-none"
/>
<kbd
@@ -76,17 +77,17 @@
<p class="py-6 text-center text-sm text-destructive">{search.error}</p>
{:else if search.query.length < 2}
<p class="py-6 text-center text-sm text-muted-foreground">
Type at least 2 characters to search
{$t('search.min_chars')}
</p>
{:else if search.results.length === 0}
<p class="py-6 text-center text-sm text-muted-foreground">
No results for "{search.query}"
{$t('search.no_results', { values: { query: search.query } })}
</p>
{:else}
{#if appResults.length > 0}
<div class="mb-2">
<p class="mb-1 px-3 text-xs font-medium uppercase tracking-wider text-muted-foreground">
Apps
{$t('search.apps')}
</p>
{#each appResults as result (result.id)}
<SearchResult {result} onselect={() => search.close()} />
@@ -97,7 +98,7 @@
{#if boardResults.length > 0}
<div>
<p class="mb-1 px-3 text-xs font-medium uppercase tracking-wider text-muted-foreground">
Boards
{$t('search.boards')}
</p>
{#each boardResults as result (result.id)}
<SearchResult {result} onselect={() => search.close()} />