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
+17 -5
View File
@@ -1,4 +1,5 @@
<script lang="ts">
import { t } from 'svelte-i18n';
import Section from '$lib/components/section/Section.svelte';
interface SectionData {
@@ -25,21 +26,32 @@
}>;
}
interface Props {
sections: SectionData[];
interface AppData {
id: string;
name: string;
url: string;
icon: string | null;
iconType: string;
description: string | null;
statuses: Array<{ status: string; responseTime: number | null }>;
}
let { sections }: Props = $props();
interface Props {
sections: SectionData[];
allApps?: AppData[];
}
let { sections, allApps = [] }: Props = $props();
</script>
<div class="space-y-6">
{#if sections.length === 0}
<div class="rounded-xl border border-border bg-card/50 p-12 text-center">
<p class="text-muted-foreground">This board has no sections yet.</p>
<p class="text-muted-foreground">{$t('board.no_sections')}</p>
</div>
{:else}
{#each sections as section (section.id)}
<Section {section} />
<Section {section} {allApps} />
{/each}
{/if}
</div>
+4 -3
View File
@@ -1,4 +1,5 @@
<script lang="ts">
import { t } from 'svelte-i18n';
import DynamicIcon from '$lib/components/ui/DynamicIcon.svelte';
interface BoardSummary {
@@ -39,12 +40,12 @@
</h3>
{#if board.isDefault}
<span class="shrink-0 rounded bg-primary/15 px-1.5 py-0.5 text-xs text-primary">
Default
{$t('board.default')}
</span>
{/if}
{#if board.isGuestAccessible}
<span class="shrink-0 rounded bg-accent px-1.5 py-0.5 text-xs text-accent-foreground">
Guest
{$t('board.guest')}
</span>
{/if}
</div>
@@ -52,7 +53,7 @@
<p class="mt-1 line-clamp-2 text-sm text-muted-foreground">{board.description}</p>
{/if}
<p class="mt-2 text-xs text-muted-foreground/70">
{sectionCount} section{sectionCount === 1 ? '' : 's'}
{$t('board.sections_count', { values: { count: sectionCount } })}
</p>
</div>
</div>
+3 -2
View File
@@ -1,4 +1,5 @@
<script lang="ts">
import { t } from 'svelte-i18n';
import DynamicIcon from '$lib/components/ui/DynamicIcon.svelte';
interface Props {
@@ -30,14 +31,14 @@
href="/boards"
class="rounded-lg border border-border px-3 py-2 text-sm text-foreground transition-colors hover:bg-accent"
>
All Boards
{$t('board.all_boards')}
</a>
{#if canEdit}
<a
href="/boards/{boardId}/edit"
class="rounded-lg bg-primary px-3 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
>
Edit
{$t('board.edit')}
</a>
{/if}
</div>
@@ -1,4 +1,5 @@
<script lang="ts">
import { t } from 'svelte-i18n';
import { dndzone } from 'svelte-dnd-action';
import DraggableSection from '$lib/components/section/DraggableSection.svelte';
@@ -36,7 +37,7 @@
addWidgetSectionId: string | null;
onToggleAddWidget: (sectionId: string) => void;
onDeleteSection: (sectionId: string) => void;
onAddWidget: (sectionId: string, appId: string) => void;
onAddWidget: (sectionId: string, widgetData: string) => void;
onDeleteWidget: (widgetId: string) => void;
}
@@ -99,7 +100,7 @@
{#if sections.length === 0}
<div class="rounded-xl border border-border bg-card/50 p-8 text-center">
<p class="text-muted-foreground">No sections yet. Add one to get started.</p>
<p class="text-muted-foreground">{$t('board.no_sections')}</p>
</div>
{:else}
<div