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
+19 -18
View File
@@ -1,4 +1,5 @@
<script lang="ts">
import { t } from 'svelte-i18n';
import { superForm, type SuperValidated } from 'sveltekit-superforms';
import type { z } from 'zod';
import type { createAppSchema } from '$lib/utils/validators.js';
@@ -24,7 +25,7 @@
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div>
<label for="name" class="mb-1 block text-sm font-medium text-card-foreground">
Name <span class="text-destructive">*</span>
{$t('app.name')} <span class="text-destructive">{$t('common.required')}</span>
</label>
<input
id="name"
@@ -32,7 +33,7 @@
type="text"
bind:value={$form.name}
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
placeholder="My Application"
placeholder={$t('app.name_placeholder')}
/>
{#if $errors.name}
<p class="mt-1 text-sm text-destructive">{$errors.name[0]}</p>
@@ -41,7 +42,7 @@
<div>
<label for="url" class="mb-1 block text-sm font-medium text-card-foreground">
URL <span class="text-destructive">*</span>
{$t('app.url')} <span class="text-destructive">{$t('common.required')}</span>
</label>
<input
id="url"
@@ -49,7 +50,7 @@
type="url"
bind:value={$form.url}
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
placeholder="https://my-app.local:8080"
placeholder={$t('app.url_placeholder')}
/>
{#if $errors.url}
<p class="mt-1 text-sm text-destructive">{$errors.url[0]}</p>
@@ -59,7 +60,7 @@
<div>
<label for="description" class="mb-1 block text-sm font-medium text-card-foreground">
Description
{$t('app.description')}
</label>
<input
id="description"
@@ -67,14 +68,14 @@
type="text"
bind:value={$form.description}
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
placeholder="Brief description of this app"
placeholder={$t('app.description_placeholder')}
/>
</div>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div>
<label for="category" class="mb-1 block text-sm font-medium text-card-foreground">
Category
{$t('app.category')}
</label>
<input
id="category"
@@ -82,13 +83,13 @@
type="text"
bind:value={$form.category}
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
placeholder="e.g. Media, Monitoring, Storage"
placeholder={$t('app.category_placeholder')}
/>
</div>
<div>
<label for="tags" class="mb-1 block text-sm font-medium text-card-foreground">
Tags
{$t('app.tags')}
</label>
<input
id="tags"
@@ -96,7 +97,7 @@
type="text"
bind:value={$form.tags}
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
placeholder="Comma-separated tags"
placeholder={$t('app.tags_placeholder')}
/>
</div>
</div>
@@ -117,7 +118,7 @@
onclick={() => (showAdvanced = !showAdvanced)}
class="text-sm text-muted-foreground hover:text-foreground"
>
{showAdvanced ? 'Hide' : 'Show'} Healthcheck Settings
{showAdvanced ? $t('app.healthcheck_hide') : $t('app.healthcheck_show')} {$t('app.healthcheck_toggle')}
</button>
{#if showAdvanced}
@@ -131,7 +132,7 @@
class="rounded border-input"
/>
<label for="healthcheckEnabled" class="text-sm text-card-foreground">
Enable Healthcheck
{$t('app.healthcheck_enabled')}
</label>
</div>
@@ -142,7 +143,7 @@
for="healthcheckMethod"
class="mb-1 block text-sm font-medium text-card-foreground"
>
Method
{$t('app.healthcheck_method')}
</label>
<select
id="healthcheckMethod"
@@ -160,7 +161,7 @@
for="healthcheckExpectedStatus"
class="mb-1 block text-sm font-medium text-card-foreground"
>
Expected Status
{$t('app.healthcheck_expected_status')}
</label>
<input
id="healthcheckExpectedStatus"
@@ -178,7 +179,7 @@
for="healthcheckTimeout"
class="mb-1 block text-sm font-medium text-card-foreground"
>
Timeout (ms)
{$t('app.healthcheck_timeout')}
</label>
<input
id="healthcheckTimeout"
@@ -198,7 +199,7 @@
for="healthcheckInterval"
class="mb-1 block text-sm font-medium text-card-foreground"
>
Interval (seconds)
{$t('app.healthcheck_interval')}
</label>
<input
id="healthcheckInterval"
@@ -221,9 +222,9 @@
class="rounded-md bg-primary px-6 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 focus:outline-none focus:ring-2 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
>
{#if $submitting}
Saving...
{$t('app.saving')}
{:else}
Save App
{$t('app.save')}
{/if}
</button>
</div>