i18n(website): wire components + views to namespace — 68 strings cleared

Patches ListView, EditorView, SubmissionsView, BlockInspector,
ImageInspector, GalleryInspector, InsertPalette, PageList,
PublishBar, RollbackDialog, SiteSettingsDialog, DomainsSection,
TemplatePicker. Locale JSONs landed in 9e9f5ce64.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-27 01:14:16 +02:00
parent 9e9f5ce641
commit 6d1546975f
14 changed files with 223 additions and 164 deletions

View file

@ -1,4 +1,5 @@
<script lang="ts">
import { _ } from 'svelte-i18n';
import { formatDate } from '$lib/i18n/format';
import { useAllSites } from './queries';
@ -8,12 +9,12 @@
const now = Date.now();
const then = new Date(iso).getTime();
const diffMin = Math.floor((now - then) / 60_000);
if (diffMin < 1) return 'gerade eben';
if (diffMin < 60) return `vor ${diffMin} Min`;
if (diffMin < 1) return $_('website.list_view.relative_just_now');
if (diffMin < 60) return $_('website.list_view.relative_minutes', { values: { n: diffMin } });
const diffH = Math.floor(diffMin / 60);
if (diffH < 24) return `vor ${diffH} Std`;
if (diffH < 24) return $_('website.list_view.relative_hours', { values: { n: diffH } });
const diffD = Math.floor(diffH / 24);
if (diffD < 30) return `vor ${diffD} Tg`;
if (diffD < 30) return $_('website.list_view.relative_days', { values: { n: diffD } });
return formatDate(new Date(iso));
}
</script>
@ -21,18 +22,18 @@
<div class="wb-list">
<header class="wb-list__header">
<div>
<h2>Deine Websites</h2>
<h2>{$_('website.list_view.heading')}</h2>
<p class="wb-list__hint">
Block-Editor, veröffentlichen unter <code>mana.how</code>.
{$_('website.list_view.hint_prefix')} <code>mana.how</code>.
</p>
</div>
<a class="wb-list__new" href="/website/new">+ Neue Website</a>
<a class="wb-list__new" href="/website/new">{$_('website.list_view.action_new')}</a>
</header>
{#if sites.value.length === 0}
<div class="wb-list__empty">
<p>Noch keine Website. Wähl ein Template oder starte blank.</p>
<a class="wb-list__new" href="/website/new">+ Neue Website</a>
<p>{$_('website.list_view.empty_text')}</p>
<a class="wb-list__new" href="/website/new">{$_('website.list_view.action_new')}</a>
</div>
{:else}
<div class="wb-list__grid">
@ -48,7 +49,9 @@
class:wb-pill--green={site.publishedVersion}
class:wb-pill--amber={!site.publishedVersion}
>
{site.publishedVersion ? 'Veröffentlicht' : 'Entwurf'}
{site.publishedVersion
? $_('website.list_view.status_published')
: $_('website.list_view.status_draft')}
</span>
<span class="wb-card__time">{formatRelative(site.updatedAt)}</span>
</div>

View file

@ -1,5 +1,6 @@
<script lang="ts">
import type { Component } from 'svelte';
import { _ } from 'svelte-i18n';
import { getBlockSpec, type Block, type BlockInspectorProps } from '@mana/website-blocks';
import { blocksStore, InvalidBlockPropsError } from '../stores/blocks.svelte';
import { getEditorHistoryContext } from '../history.svelte';
@ -57,7 +58,7 @@
else await blocksStore.updateBlockProps(block.id, p);
} catch (err) {
if (err instanceof InvalidBlockPropsError) {
lastError = `Validation failed: ${err.message}`;
lastError = `${$_('website.block_inspector.err_validation_prefix')} ${err.message}`;
} else {
lastError = err instanceof Error ? err.message : String(err);
}
@ -65,7 +66,7 @@
}
async function onDelete() {
if (!confirm('Diesen Block löschen?')) return;
if (!confirm($_('website.block_inspector.confirm_delete'))) return;
if (history) await history.deleteBlock(block.id);
else await blocksStore.deleteBlock(block.id);
onDeleted?.();
@ -108,8 +109,8 @@
class="wb-inspector__action"
onclick={onMoveUp}
disabled={!canMoveUp}
title="Nach oben verschieben"
aria-label="Nach oben verschieben"
title={$_('website.block_inspector.action_move_up')}
aria-label={$_('website.block_inspector.action_move_up')}
>
</button>
@ -117,16 +118,16 @@
class="wb-inspector__action"
onclick={onMoveDown}
disabled={!canMoveDown}
title="Nach unten verschieben"
aria-label="Nach unten verschieben"
title={$_('website.block_inspector.action_move_down')}
aria-label={$_('website.block_inspector.action_move_down')}
>
</button>
<button
class="wb-inspector__action wb-inspector__action--delete"
onclick={onDelete}
title="Block löschen"
aria-label="Block löschen"
title={$_('website.block_inspector.action_delete')}
aria-label={$_('website.block_inspector.action_delete')}
>
×
</button>
@ -145,7 +146,9 @@
<p class="wb-inspector__error">{lastError}</p>
{/if}
{:else}
<p class="wb-inspector__empty">Unbekannter Block-Typ: {block.type}</p>
<p class="wb-inspector__empty">
{$_('website.block_inspector.unknown_type', { values: { type: block.type } })}
</p>
{/if}
</div>

View file

@ -1,4 +1,5 @@
<script lang="ts">
import { _ } from 'svelte-i18n';
import {
listDomains,
addDomain,
@ -42,7 +43,7 @@
async function onAdd() {
const host = newHost.trim().toLowerCase();
if (!host) {
addError = 'Hostname erforderlich';
addError = $_('website.domains.err_host_required');
return;
}
adding = true;
@ -70,7 +71,7 @@
}
async function onRemove(domainId: string, hostname: string) {
if (!confirm(`Domain "${hostname}" entfernen?`)) return;
if (!confirm($_('website.domains.confirm_remove', { values: { host: hostname } }))) return;
await removeDomain(siteId, domainId);
await load();
}
@ -82,10 +83,11 @@
<section class="wb-domains" aria-labelledby="wb-domains-title">
<header>
<h3 id="wb-domains-title">Eigene Domain</h3>
<h3 id="wb-domains-title">{$_('website.domains.heading')}</h3>
<p class="wb-domains__hint">
Verbinde einen eigenen Hostnamen (z.B. <code>meineseite.de</code>) mit dieser Website. Nur für
Founder-Tier.
{$_('website.domains.hint_prefix')} <code>meineseite.de</code>{$_(
'website.domains.hint_suffix'
)}
</p>
</header>
@ -102,12 +104,12 @@
>
<input
type="text"
placeholder="z.B. portfolio.deinedomain.de"
placeholder={$_('website.domains.placeholder_new')}
value={newHost}
oninput={(e) => (newHost = e.currentTarget.value)}
/>
<button class="wb-btn wb-btn--primary" disabled={adding || !newHost.trim()}>
{adding ? 'Füge hinzu…' : '+ Domain'}
{adding ? $_('website.domains.action_adding') : $_('website.domains.action_add')}
</button>
</form>
@ -116,9 +118,9 @@
{/if}
{#if domains === null}
<p class="wb-domains__empty">Lade…</p>
<p class="wb-domains__empty">{$_('website.domains.loading')}</p>
{:else if domains.length === 0}
<p class="wb-domains__empty">Noch keine eigenen Domains verbunden.</p>
<p class="wb-domains__empty">{$_('website.domains.empty')}</p>
{:else}
<ul class="wb-domains__list">
{#each domains as d (d.id)}
@ -135,13 +137,15 @@
onclick={() => onVerify(d.id)}
disabled={verifyingId === d.id}
>
{verifyingId === d.id ? 'Prüfe…' : 'Verify'}
{verifyingId === d.id
? $_('website.domains.action_verifying')
: $_('website.domains.action_verify')}
</button>
{/if}
<button
class="wb-btn wb-btn--icon wb-btn--danger"
onclick={() => onRemove(d.id, d.hostname)}
title="Entfernen">×</button
title={$_('website.domains.action_remove_title')}>×</button
>
</div>
</div>
@ -152,7 +156,7 @@
{#if d.status !== 'verified'}
<div class="wb-domain__dns">
<p class="wb-domain__dns-title">DNS konfigurieren:</p>
<p class="wb-domain__dns-title">{$_('website.domains.dns_section_title')}</p>
<div class="wb-dns-row">
<div>
<span class="wb-dns-type">CNAME</span>
@ -160,7 +164,7 @@
</div>
<button class="wb-dns-val" onclick={() => copyToClipboard(d.dnsTarget)}>
{d.dnsTarget}
<small>Klick zum Kopieren</small>
<small>{$_('website.domains.dns_copy_hint')}</small>
</button>
</div>
<div class="wb-dns-row">
@ -170,12 +174,11 @@
</div>
<button class="wb-dns-val" onclick={() => copyToClipboard(d.verificationToken)}>
{d.verificationToken}
<small>Klick zum Kopieren</small>
<small>{$_('website.domains.dns_copy_hint')}</small>
</button>
</div>
<p class="wb-domain__dns-note">
DNS-Änderungen brauchen meist 530 Minuten, bis sie weltweit propagiert sind. Danach
"Verify" klicken.
{$_('website.domains.dns_note')}
</p>
</div>
{/if}

View file

@ -3,6 +3,7 @@
* App-side gallery inspector with multi-image upload via mana-media.
* Overrides the URL-only fallback from @mana/website-blocks.
*/
import { _ } from 'svelte-i18n';
import type { BlockInspectorProps } from '@mana/website-blocks';
import type { GalleryProps, GalleryImage } from '@mana/website-blocks';
import { uploadImage, UploadError } from '../upload';
@ -61,12 +62,12 @@
<div class="wb-inspector">
<label class="wb-field">
<span>Überschrift</span>
<span>{$_('website.gallery_inspector.label_title')}</span>
<input
type="text"
value={block.props.title}
oninput={(e) => onChange({ title: e.currentTarget.value })}
placeholder="Optional"
placeholder={$_('website.gallery_inspector.placeholder_title')}
/>
</label>
@ -85,9 +86,9 @@
}}
>
{#if uploading}
<span>Lade hoch…</span>
<span>{$_('website.gallery_inspector.uploading')}</span>
{:else}
<span>+ Bilder hinzufügen — ziehen oder klicken (mehrere möglich)</span>
<span>{$_('website.gallery_inspector.drop_hint')}</span>
{/if}
</div>
<input
@ -105,17 +106,17 @@
<div class="wb-row">
<label class="wb-field">
<span>Layout</span>
<span>{$_('website.gallery_inspector.label_layout')}</span>
<select
value={block.props.layout}
onchange={(e) => onChange({ layout: e.currentTarget.value as GalleryProps['layout'] })}
>
<option value="grid">Grid</option>
<option value="masonry">Masonry</option>
<option value="grid">{$_('website.gallery_inspector.layout_grid')}</option>
<option value="masonry">{$_('website.gallery_inspector.layout_masonry')}</option>
</select>
</label>
<label class="wb-field">
<span>Spalten</span>
<span>{$_('website.gallery_inspector.label_columns')}</span>
<select
value={String(block.props.columns)}
onchange={(e) =>
@ -130,14 +131,14 @@
<div class="wb-row">
<label class="wb-field">
<span>Abstand</span>
<span>{$_('website.gallery_inspector.label_gap')}</span>
<select
value={block.props.gap}
onchange={(e) => onChange({ gap: e.currentTarget.value as GalleryProps['gap'] })}
>
<option value="sm">Klein</option>
<option value="md">Mittel</option>
<option value="lg">Groß</option>
<option value="sm">{$_('website.gallery_inspector.gap_sm')}</option>
<option value="md">{$_('website.gallery_inspector.gap_md')}</option>
<option value="lg">{$_('website.gallery_inspector.gap_lg')}</option>
</select>
</label>
<label class="wb-checkbox">
@ -146,12 +147,16 @@
checked={block.props.lightbox}
onchange={(e) => onChange({ lightbox: e.currentTarget.checked })}
/>
<span>Lightbox (Vollbild)</span>
<span>{$_('website.gallery_inspector.label_lightbox')}</span>
</label>
</div>
<div class="wb-images">
<div class="wb-images__head">Bilder ({block.props.images.length})</div>
<div class="wb-images__head">
{$_('website.gallery_inspector.images_count', {
values: { count: block.props.images.length },
})}
</div>
{#each block.props.images as img, i (i)}
<div class="wb-image-row">
<img src={img.url} alt={img.altText} class="wb-image-row__thumb" />
@ -160,13 +165,13 @@
type="text"
value={img.altText}
oninput={(e) => updateImage(i, { altText: e.currentTarget.value })}
placeholder="Alt-Text"
placeholder={$_('website.gallery_inspector.placeholder_alt')}
/>
<input
type="text"
value={img.caption}
oninput={(e) => updateImage(i, { caption: e.currentTarget.value })}
placeholder="Bildunterschrift"
placeholder={$_('website.gallery_inspector.placeholder_caption')}
/>
</div>
<div class="wb-image-row__actions">

View file

@ -4,6 +4,7 @@
* the URL-only fallback from @mana/website-blocks via the custom
* inspector registry (see inspector-overrides.ts).
*/
import { _ } from 'svelte-i18n';
import type { BlockInspectorProps } from '@mana/website-blocks';
import type { ImageProps } from '@mana/website-blocks';
import { uploadImage, UploadError } from '../upload';
@ -57,12 +58,12 @@
}}
>
{#if uploading}
<span class="wb-dropzone__hint">Lade hoch…</span>
<span class="wb-dropzone__hint">{$_('website.image_inspector.uploading')}</span>
{:else if block.props.url}
<img src={block.props.url} alt={block.props.altText} class="wb-dropzone__preview" />
<span class="wb-dropzone__hint">Klicken / ziehen, um zu ersetzen</span>
<span class="wb-dropzone__hint">{$_('website.image_inspector.replace_hint')}</span>
{:else}
<span class="wb-dropzone__hint">Bild hier hinziehen oder klicken</span>
<span class="wb-dropzone__hint">{$_('website.image_inspector.drop_hint')}</span>
{/if}
</div>
<input
@ -78,7 +79,7 @@
{/if}
<label class="wb-field">
<span>Oder URL einsetzen</span>
<span>{$_('website.image_inspector.label_url')}</span>
<input
type="url"
value={block.props.url}
@ -88,17 +89,17 @@
</label>
<label class="wb-field">
<span>Alt-Text *</span>
<span>{$_('website.image_inspector.label_alt')}</span>
<input
type="text"
value={block.props.altText}
oninput={(e) => onChange({ altText: e.currentTarget.value })}
placeholder="Beschreibung für Screenreader"
placeholder={$_('website.image_inspector.placeholder_alt')}
/>
</label>
<label class="wb-field">
<span>Bildunterschrift</span>
<span>{$_('website.image_inspector.label_caption')}</span>
<input
type="text"
value={block.props.caption}
@ -108,13 +109,13 @@
<div class="wb-row">
<label class="wb-field">
<span>Seitenverhältnis</span>
<span>{$_('website.image_inspector.label_aspect_ratio')}</span>
<select
value={block.props.aspectRatio}
onchange={(e) =>
onChange({ aspectRatio: e.currentTarget.value as ImageProps['aspectRatio'] })}
>
<option value="auto">Auto</option>
<option value="auto">{$_('website.image_inspector.aspect_auto')}</option>
<option value="21:9">21:9</option>
<option value="16:9">16:9</option>
<option value="4:3">4:3</option>
@ -123,26 +124,26 @@
</label>
<label class="wb-field">
<span>Breite</span>
<span>{$_('website.image_inspector.label_width')}</span>
<select
value={block.props.width}
onchange={(e) => onChange({ width: e.currentTarget.value as ImageProps['width'] })}
>
<option value="narrow">Schmal</option>
<option value="container">Container</option>
<option value="full">Vollbreit</option>
<option value="narrow">{$_('website.image_inspector.width_narrow')}</option>
<option value="container">{$_('website.image_inspector.width_container')}</option>
<option value="full">{$_('website.image_inspector.width_full')}</option>
</select>
</label>
</div>
<label class="wb-field">
<span>Füllung</span>
<span>{$_('website.image_inspector.label_fit')}</span>
<select
value={block.props.fit}
onchange={(e) => onChange({ fit: e.currentTarget.value as ImageProps['fit'] })}
>
<option value="cover">Zuschneiden</option>
<option value="contain">Einpassen</option>
<option value="cover">{$_('website.image_inspector.fit_cover')}</option>
<option value="contain">{$_('website.image_inspector.fit_contain')}</option>
</select>
</label>
</div>

View file

@ -1,4 +1,5 @@
<script lang="ts">
import { _ } from 'svelte-i18n';
import { getAllBlockSpecs } from '@mana/website-blocks';
interface Props {
@ -11,7 +12,7 @@
</script>
<div class="wb-palette">
<p class="wb-palette__label">Block einfügen</p>
<p class="wb-palette__label">{$_('website.insert_palette.label')}</p>
<div class="wb-palette__grid">
{#each specs as spec (spec.type)}
<button class="wb-palette__btn" onclick={() => onInsert(spec.type)} title={spec.label}>

View file

@ -1,5 +1,6 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { _ } from 'svelte-i18n';
import { pagesStore, InvalidPathError, DuplicatePathError } from '../stores/pages.svelte';
import type { WebsitePage } from '../types';
@ -29,7 +30,7 @@
const page = await pagesStore.createPage({
siteId,
path: draftPath,
title: draftTitle || 'Ohne Titel',
title: draftTitle || $_('website.page_list.default_title'),
});
showAdd = false;
await goto(`/website/${siteId}/edit/${page.id}`);
@ -44,10 +45,10 @@
ev.preventDefault();
ev.stopPropagation();
if (pages.length <= 1) {
alert('Mindestens eine Seite muss bestehen bleiben.');
alert($_('website.page_list.alert_min_one'));
return;
}
if (!confirm('Seite wirklich löschen?')) return;
if (!confirm($_('website.page_list.confirm_delete'))) return;
await pagesStore.deletePage(pageId);
// If the active page was deleted, navigate to another one.
if (pageId === activePageId) {
@ -59,8 +60,10 @@
<div class="wb-pages">
<div class="wb-pages__header">
<p class="wb-pages__label">Seiten</p>
<button class="wb-pages__add" onclick={startAdd} title="Neue Seite">+</button>
<p class="wb-pages__label">{$_('website.page_list.heading')}</p>
<button class="wb-pages__add" onclick={startAdd} title={$_('website.page_list.action_add')}
>+</button
>
</div>
<ul class="wb-pages__list">
@ -78,7 +81,7 @@
<button
class="wb-pages__delete"
onclick={(e) => deletePageById(p.id, e)}
title="Seite löschen">×</button
title={$_('website.page_list.action_delete')}>×</button
>
</a>
</li>
@ -88,31 +91,35 @@
{#if showAdd}
<div class="wb-pages__form">
<label class="wb-field">
<span>Titel</span>
<span>{$_('website.page_list.label_title')}</span>
<!-- svelte-ignore a11y_autofocus — inline add-page form; modal-style focus is expected -->
<input
type="text"
value={draftTitle}
oninput={(e) => (draftTitle = e.currentTarget.value)}
placeholder="Über uns"
placeholder={$_('website.page_list.placeholder_title')}
autofocus
/>
</label>
<label class="wb-field">
<span>Pfad</span>
<span>{$_('website.page_list.label_path')}</span>
<input
type="text"
value={draftPath}
oninput={(e) => (draftPath = e.currentTarget.value.toLowerCase())}
placeholder="/ueber-uns"
placeholder={$_('website.page_list.placeholder_path')}
/>
</label>
{#if addError}
<p class="wb-error">{addError}</p>
{/if}
<div class="wb-pages__form-actions">
<button class="wb-btn wb-btn--ghost" onclick={() => (showAdd = false)}>Abbrechen</button>
<button class="wb-btn wb-btn--primary" onclick={submitAdd}>Anlegen</button>
<button class="wb-btn wb-btn--ghost" onclick={() => (showAdd = false)}
>{$_('website.page_list.action_cancel')}</button
>
<button class="wb-btn wb-btn--primary" onclick={submitAdd}
>{$_('website.page_list.action_create')}</button
>
</div>
</div>
{/if}

View file

@ -1,4 +1,5 @@
<script lang="ts">
import { _ } from 'svelte-i18n';
import { sitesStore } from '../stores/sites.svelte';
import { PublishError } from '../publish';
import RollbackDialog from './RollbackDialog.svelte';
@ -46,7 +47,7 @@
}
async function onUnpublish() {
if (!confirm('Website offline nehmen? Besucher sehen dann 404.')) return;
if (!confirm($_('website.publish_bar.confirm_unpublish'))) return;
unpublishing = true;
lastError = null;
try {
@ -64,16 +65,16 @@
<div class="wb-publishbar">
<div class="wb-publishbar__status">
{#if site.publishedVersion}
<span class="wb-pill wb-pill--green">Live</span>
<span class="wb-pill wb-pill--green">{$_('website.publish_bar.badge_live')}</span>
<a class="wb-publishbar__link" href={publicUrl} target="_blank" rel="noopener">
{publicUrl}
</a>
{#if hasDraftAhead}
<span class="wb-pill wb-pill--amber">Unveröffentlichte Änderungen</span>
<span class="wb-pill wb-pill--amber">{$_('website.publish_bar.badge_dirty')}</span>
{/if}
{:else}
<span class="wb-pill wb-pill--gray">Entwurf</span>
<span class="wb-publishbar__hint">Noch nicht veröffentlicht</span>
<span class="wb-pill wb-pill--gray">{$_('website.publish_bar.badge_draft')}</span>
<span class="wb-publishbar__hint">{$_('website.publish_bar.hint_unpublished')}</span>
{/if}
</div>
@ -82,27 +83,33 @@
<button
class="wb-btn wb-btn--ghost"
onclick={() => (showHistory = true)}
title="Versionen einsehen / wiederherstellen"
title={$_('website.publish_bar.action_versions_title')}
>
Versionen
{$_('website.publish_bar.action_versions')}
</button>
<button
class="wb-btn wb-btn--ghost"
onclick={onUnpublish}
disabled={unpublishing || publishing}
>
{unpublishing ? 'Offline…' : 'Offline nehmen'}
{unpublishing
? $_('website.publish_bar.action_unpublishing')
: $_('website.publish_bar.action_unpublish')}
</button>
<button
class="wb-btn wb-btn--primary"
onclick={onPublish}
disabled={publishing || !hasDraftAhead}
>
{publishing ? 'Veröffentliche…' : 'Änderungen veröffentlichen'}
{publishing
? $_('website.publish_bar.action_publishing')
: $_('website.publish_bar.action_publish_changes')}
</button>
{:else}
<button class="wb-btn wb-btn--primary" onclick={onPublish} disabled={publishing}>
{publishing ? 'Veröffentliche…' : 'Veröffentlichen'}
{publishing
? $_('website.publish_bar.action_publishing')
: $_('website.publish_bar.action_publish')}
</button>
{/if}
</div>

View file

@ -1,4 +1,7 @@
<script lang="ts">
import { _ } from 'svelte-i18n';
import { locale } from 'svelte-i18n';
import { get } from 'svelte/store';
import { authStore } from '$lib/stores/auth.svelte';
import { sitesStore } from '../stores/sites.svelte';
import { fetchSnapshotHistory, PublishError, type SnapshotHistoryEntry } from '../publish';
@ -21,7 +24,7 @@
loadError = null;
try {
const token = await authStore.getValidToken();
if (!token) throw new Error('Nicht angemeldet');
if (!token) throw new Error($_('website.rollback_dialog.err_unauth'));
entries = await fetchSnapshotHistory(siteId, token);
} catch (err) {
loadError =
@ -42,7 +45,7 @@
});
async function onRollback(snapshotId: string) {
if (!confirm('Diese Version als aktuell veröffentlicht setzen?')) return;
if (!confirm($_('website.rollback_dialog.confirm_rollback'))) return;
rollingBackId = snapshotId;
actionError = null;
try {
@ -61,7 +64,7 @@
}
function formatDate(iso: string): string {
return new Date(iso).toLocaleString('de-DE');
return new Date(iso).toLocaleString(get(locale) ?? 'de');
}
</script>
@ -71,25 +74,29 @@
onkeydown={(e) => e.key === 'Escape' && onClose()}
role="button"
tabindex="-1"
aria-label="Schließen"
aria-label={$_('website.rollback_dialog.close_aria')}
></div>
<div class="wb-modal" role="dialog" aria-modal="true" aria-labelledby="wb-rollback-title">
<header class="wb-modal__head">
<div>
<h3 id="wb-rollback-title">Version-History</h3>
<p>Wähle eine ältere veröffentlichte Version, um sie wieder live zu stellen.</p>
<h3 id="wb-rollback-title">{$_('website.rollback_dialog.heading')}</h3>
<p>{$_('website.rollback_dialog.subtitle')}</p>
</div>
<button class="wb-modal__close" onclick={onClose} aria-label="Schließen">×</button>
<button
class="wb-modal__close"
onclick={onClose}
aria-label={$_('website.rollback_dialog.close_aria')}>×</button
>
</header>
<div class="wb-modal__body">
{#if loadError}
<p class="wb-error">{loadError}</p>
{:else if entries === null}
<p class="wb-empty">Lade…</p>
<p class="wb-empty">{$_('website.rollback_dialog.loading')}</p>
{:else if entries.length === 0}
<p class="wb-empty">Noch keine veröffentlichten Versionen.</p>
<p class="wb-empty">{$_('website.rollback_dialog.empty')}</p>
{:else}
{#if actionError}
<p class="wb-error">{actionError}</p>
@ -103,14 +110,18 @@
</div>
<div class="wb-row__actions">
{#if entry.isCurrent}
<span class="wb-pill wb-pill--current">Aktuell live</span>
<span class="wb-pill wb-pill--current"
>{$_('website.rollback_dialog.badge_current')}</span
>
{:else}
<button
class="wb-btn wb-btn--primary"
onclick={() => onRollback(entry.id)}
disabled={rollingBackId === entry.id || loading}
>
{rollingBackId === entry.id ? 'Stelle wieder her…' : 'Wiederherstellen'}
{rollingBackId === entry.id
? $_('website.rollback_dialog.action_restoring')
: $_('website.rollback_dialog.action_restore')}
</button>
{/if}
</div>

View file

@ -1,4 +1,5 @@
<script lang="ts">
import { _ } from 'svelte-i18n';
import { PRESET_LABELS, THEME_PRESETS, type ThemePreset } from '@mana/website-blocks/themes';
import { sitesStore } from '../stores/sites.svelte';
import DomainsSection from './DomainsSection.svelte';
@ -69,18 +70,22 @@
onkeydown={(e) => e.key === 'Escape' && onClose()}
role="button"
tabindex="-1"
aria-label="Schließen"
aria-label={$_('website.site_settings.close_aria')}
></div>
<div class="wb-modal" role="dialog" aria-modal="true" aria-labelledby="wb-settings-title">
<header class="wb-modal__head">
<h3 id="wb-settings-title">Website-Einstellungen</h3>
<button class="wb-modal__close" onclick={onClose} aria-label="Schließen">×</button>
<h3 id="wb-settings-title">{$_('website.site_settings.heading')}</h3>
<button
class="wb-modal__close"
onclick={onClose}
aria-label={$_('website.site_settings.close_aria')}>×</button
>
</header>
<div class="wb-modal__body">
<section class="wb-section">
<h4>Theme</h4>
<h4>{$_('website.site_settings.section_theme')}</h4>
<div class="wb-presets">
{#each presets as preset (preset)}
{@const tokens = THEME_PRESETS[preset]}
@ -104,14 +109,14 @@
<section class="wb-section">
<div class="wb-section__head">
<h4>Farben überschreiben</h4>
<h4>{$_('website.site_settings.section_overrides')}</h4>
<button class="wb-btn wb-btn--ghost wb-btn--sm" onclick={resetOverrides}>
Auf Preset zurücksetzen
{$_('website.site_settings.action_reset_overrides')}
</button>
</div>
<div class="wb-colors">
<label class="wb-color">
<span>Primär</span>
<span>{$_('website.site_settings.label_primary')}</span>
<input
type="color"
value={draftPrimary || previewTokens.primary}
@ -125,7 +130,7 @@
/>
</label>
<label class="wb-color">
<span>Hintergrund</span>
<span>{$_('website.site_settings.label_background')}</span>
<input
type="color"
value={draftBackground || previewTokens.background}
@ -139,7 +144,7 @@
/>
</label>
<label class="wb-color">
<span>Text</span>
<span>{$_('website.site_settings.label_foreground')}</span>
<input
type="color"
value={draftForeground || previewTokens.foreground}
@ -156,14 +161,14 @@
</section>
<section class="wb-section">
<h4>Footer</h4>
<h4>{$_('website.site_settings.section_footer')}</h4>
<label class="wb-field">
<span>Footer-Text</span>
<span>{$_('website.site_settings.label_footer_text')}</span>
<input
type="text"
value={draftFooterText}
oninput={(e) => (draftFooterText = e.currentTarget.value)}
placeholder="© 2026 — Meine Website"
placeholder={$_('website.site_settings.placeholder_footer_text')}
/>
</label>
</section>
@ -174,9 +179,11 @@
</div>
<footer class="wb-modal__foot">
<button class="wb-btn wb-btn--ghost" onclick={onClose} disabled={saving}>Abbrechen</button>
<button class="wb-btn wb-btn--ghost" onclick={onClose} disabled={saving}
>{$_('website.site_settings.action_cancel')}</button
>
<button class="wb-btn wb-btn--primary" onclick={save} disabled={saving}>
{saving ? 'Speichere…' : 'Speichern'}
{saving ? $_('website.site_settings.action_saving') : $_('website.site_settings.action_save')}
</button>
</footer>
</div>

View file

@ -1,5 +1,6 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { _ } from 'svelte-i18n';
import { SITE_TEMPLATES, type SiteTemplate } from '../templates';
import {
sitesStore,
@ -42,11 +43,11 @@
async function submit() {
error = null;
if (!selected) {
error = 'Bitte ein Template auswählen.';
error = $_('website.template_picker.err_select_template');
return;
}
if (!draftName.trim() || !isValidSlug(draftSlug)) {
error = 'Name und Slug sind erforderlich (240 Kleinbuchstaben/Zahlen/Bindestrich).';
error = $_('website.template_picker.err_invalid_form');
return;
}
creating = true;
@ -75,13 +76,10 @@
<div class="wb-templates">
<header class="wb-templates__head">
<div>
<h2>Neue Website</h2>
<p>
Such dir einen Startpunkt. Templates enthalten fertige Seiten und Blöcke — du kannst alles
später anpassen.
</p>
<h2>{$_('website.template_picker.heading')}</h2>
<p>{$_('website.template_picker.subtitle')}</p>
</div>
<a class="wb-templates__back" href="/website">← Zurück</a>
<a class="wb-templates__back" href="/website">{$_('website.template_picker.action_back')}</a>
</header>
<ul class="wb-templates__grid">
@ -98,7 +96,15 @@
<div class="wb-template__body">
<h3>{template.name}</h3>
<p>{template.description}</p>
<small>{template.pages.length} Seite{template.pages.length === 1 ? '' : 'n'}</small>
<small
>{template.pages.length === 1
? $_('website.template_picker.pages_count_singular', {
values: { count: template.pages.length },
})
: $_('website.template_picker.pages_count_plural', {
values: { count: template.pages.length },
})}</small
>
</div>
</button>
</li>
@ -107,19 +113,21 @@
{#if selected}
<section class="wb-templates__config" aria-labelledby="wb-config-title">
<h3 id="wb-config-title">Mit "{selected.name}" starten</h3>
<h3 id="wb-config-title">
{$_('website.template_picker.config_title', { values: { name: selected.name } })}
</h3>
<div class="wb-row">
<label class="wb-field">
<span>Name</span>
<span>{$_('website.template_picker.label_name')}</span>
<input
type="text"
value={draftName}
oninput={(e) => onNameInput(e.currentTarget.value)}
placeholder="Meine Website"
placeholder={$_('website.template_picker.placeholder_name')}
/>
</label>
<label class="wb-field">
<span>Slug (URL)</span>
<span>{$_('website.template_picker.label_slug')}</span>
<div class="wb-slug">
<span class="wb-slug__prefix">/s/</span>
<input
@ -137,10 +145,12 @@
<div class="wb-actions">
<button class="wb-btn wb-btn--ghost" onclick={() => (selected = null)} disabled={creating}>
Abbrechen
{$_('website.template_picker.action_cancel')}
</button>
<button class="wb-btn wb-btn--primary" onclick={submit} disabled={creating}>
{creating ? 'Erstelle…' : `Mit "${selected.name}" starten`}
{creating
? $_('website.template_picker.action_creating')
: $_('website.template_picker.action_create', { values: { name: selected.name } })}
</button>
</div>
</section>

View file

@ -1,5 +1,6 @@
<script lang="ts">
import { untrack } from 'svelte';
import { _ } from 'svelte-i18n';
import {
useAllSites,
useAllPages,
@ -115,8 +116,10 @@
class="wb-toolbar__btn"
onclick={() => history.undo()}
disabled={!history.canUndo}
title={history.undoLabel ? `Rückgängig: ${history.undoLabel}` : 'Rückgängig (⌘Z)'}
aria-label="Rückgängig"
title={history.undoLabel
? $_('website.editor.undo_with_label', { values: { label: history.undoLabel } })
: $_('website.editor.undo_default')}
aria-label={$_('website.editor.undo_aria')}
>
</button>
@ -124,8 +127,10 @@
class="wb-toolbar__btn"
onclick={() => history.redo()}
disabled={!history.canRedo}
title={history.redoLabel ? `Wiederholen: ${history.redoLabel}` : 'Wiederholen (⌘⇧Z)'}
aria-label="Wiederholen"
title={history.redoLabel
? $_('website.editor.redo_with_label', { values: { label: history.redoLabel } })
: $_('website.editor.redo_default')}
aria-label={$_('website.editor.redo_aria')}
>
</button>
@ -135,8 +140,12 @@
<main class="wb-editor__center">
{#if pageBlocks.length === 0}
<div class="wb-editor__empty">
<h3>Leere Seite</h3>
<p>Öffne den Tab <strong>Einfügen</strong> rechts, um den ersten Block zu setzen.</p>
<h3>{$_('website.editor.empty_title')}</h3>
<p>
{$_('website.editor.empty_hint_prefix')}
<strong>{$_('website.editor.empty_hint_strong')}</strong>
{$_('website.editor.empty_hint_suffix')}
</p>
</div>
{:else}
<div class="wb-editor__preview">
@ -151,7 +160,7 @@
</main>
<aside class="wb-editor__sidebar">
<div class="wb-tabs" role="tablist" aria-label="Editor-Panels">
<div class="wb-tabs" role="tablist" aria-label={$_('website.editor.panels_aria')}>
<button
class="wb-tab"
class:wb-tab--active={activeTab === 'pages'}
@ -159,7 +168,7 @@
aria-selected={activeTab === 'pages'}
onclick={() => (activeTab = 'pages')}
>
Seiten
{$_('website.editor.tab_pages')}
</button>
<button
class="wb-tab"
@ -168,7 +177,7 @@
aria-selected={activeTab === 'insert'}
onclick={() => (activeTab = 'insert')}
>
Einfügen
{$_('website.editor.tab_insert')}
</button>
<button
class="wb-tab"
@ -177,7 +186,7 @@
aria-selected={activeTab === 'block'}
onclick={() => (activeTab = 'block')}
>
Block
{$_('website.editor.tab_block')}
</button>
</div>
@ -194,7 +203,7 @@
<button
class="wb-editor__settings-btn"
onclick={() => (showSettings = true)}
title="Website-Einstellungen"
title={$_('website.editor.settings_btn_title')}
>
</button>
@ -217,7 +226,7 @@
/>
{:else}
<p class="wb-editor__inspector-empty">
Wähle einen Block in der Vorschau, um ihn zu bearbeiten.
{$_('website.editor.inspector_empty')}
</p>
{/if}
</div>

View file

@ -1,4 +1,7 @@
<script lang="ts">
import { _ } from 'svelte-i18n';
import { locale } from 'svelte-i18n';
import { get } from 'svelte/store';
import { authStore } from '$lib/stores/auth.svelte';
import { fetchSubmissions, deleteSubmission, type SubmissionEntry } from '../publish';
@ -17,7 +20,7 @@
loadError = null;
try {
const token = await authStore.getValidToken();
if (!token) throw new Error('Nicht angemeldet');
if (!token) throw new Error($_('website.submissions.err_unauth'));
entries = await fetchSubmissions(siteId, token);
} catch (err) {
loadError = err instanceof Error ? err.message : String(err);
@ -33,7 +36,7 @@
});
async function remove(submissionId: string) {
if (!confirm('Submission wirklich löschen?')) return;
if (!confirm($_('website.submissions.confirm_delete'))) return;
const token = await authStore.getValidToken();
if (!token) return;
await deleteSubmission(siteId, submissionId, token);
@ -41,29 +44,31 @@
}
function formatDate(iso: string): string {
return new Date(iso).toLocaleString('de-DE');
return new Date(iso).toLocaleString(get(locale) ?? 'de');
}
</script>
<div class="wb-submissions">
<header class="wb-submissions__head">
<div>
<h2>Eingegangen</h2>
<p>Formular-Einsendungen von deiner Website.</p>
<h2>{$_('website.submissions.heading')}</h2>
<p>{$_('website.submissions.subtitle')}</p>
</div>
<button class="wb-btn wb-btn--ghost" onclick={load} disabled={loading}>
{loading ? 'Lade…' : 'Aktualisieren'}
{loading
? $_('website.submissions.action_loading')
: $_('website.submissions.action_refresh')}
</button>
</header>
{#if loadError}
<p class="wb-error">{loadError}</p>
{:else if entries === null}
<div class="wb-submissions__empty">Lade…</div>
<div class="wb-submissions__empty">{$_('website.submissions.loading')}</div>
{:else if entries.length === 0}
<div class="wb-submissions__empty">
<p>Noch keine Einsendungen.</p>
<small>Sobald jemand ein Formular auf deiner Website ausfüllt, landet es hier.</small>
<p>{$_('website.submissions.empty_title')}</p>
<small>{$_('website.submissions.empty_hint')}</small>
</div>
{:else}
<ul class="wb-submissions__list">

View file

@ -213,19 +213,6 @@
"apps/mana/apps/web/src/lib/modules/todo/components/SyncIndicator.svelte": 4,
"apps/mana/apps/web/src/lib/modules/todo/ListView.svelte": 1,
"apps/mana/apps/web/src/lib/modules/todo/views/DetailView.svelte": 8,
"apps/mana/apps/web/src/lib/modules/website/components/BlockInspector.svelte": 3,
"apps/mana/apps/web/src/lib/modules/website/components/DomainsSection.svelte": 4,
"apps/mana/apps/web/src/lib/modules/website/components/GalleryInspector.svelte": 9,
"apps/mana/apps/web/src/lib/modules/website/components/ImageInspector.svelte": 13,
"apps/mana/apps/web/src/lib/modules/website/components/InsertPalette.svelte": 1,
"apps/mana/apps/web/src/lib/modules/website/components/PageList.svelte": 7,
"apps/mana/apps/web/src/lib/modules/website/components/PublishBar.svelte": 4,
"apps/mana/apps/web/src/lib/modules/website/components/RollbackDialog.svelte": 6,
"apps/mana/apps/web/src/lib/modules/website/components/SiteSettingsDialog.svelte": 11,
"apps/mana/apps/web/src/lib/modules/website/components/TemplatePicker.svelte": 2,
"apps/mana/apps/web/src/lib/modules/website/ListView.svelte": 1,
"apps/mana/apps/web/src/lib/modules/website/views/EditorView.svelte": 5,
"apps/mana/apps/web/src/lib/modules/website/views/SubmissionsView.svelte": 2,
"apps/mana/apps/web/src/lib/modules/wetter/components/CurrentConditions.svelte": 2,
"apps/mana/apps/web/src/lib/modules/wetter/components/HourlyForecast.svelte": 1,
"apps/mana/apps/web/src/lib/modules/wetter/components/LocationPicker.svelte": 5,