i18n(wetter+profile+contacts): translate 3 detail/freeform/comparison views via $_()

- wetter/components/SourceComparison: route through wetter.comparison.*
  (also fixes pre-existing typos verfuegbar/Gefuehlt → verfügbar/gefühlt
  via proper translations across all 5 locales). Renamed unused #each
  param `_` → `_ignored` to avoid shadowing svelte-i18n's $_.
- profile/ContextFreeform: route through profile.freeform.*; injected
  markdown source label uses i18n key too
- contacts/[id]/+page: route through contacts.detail.*; replaces typoed
  "endgueltig loeschen"/"geloescht"/"Loeschen"/"Zurueck"/"E-Mail-Mobil"
  fallbacks with proper umlauted translations. Drop unused Observable
  import.

Baseline 940 → 920 (-20).
This commit is contained in:
Till JS 2026-04-27 18:23:29 +02:00
parent c2660dd6b2
commit 3abcbd4f4d
4 changed files with 104 additions and 69 deletions

View file

@ -9,8 +9,8 @@
import { PencilSimple, Eye, LinkSimple, X } from '@mana/shared-icons';
import { crawlUrlViaApi, type CrawlMode } from '$lib/modules/kontext/api';
import { requireAuth } from '$lib/auth/require-auth.svelte';
import { _ } from 'svelte-i18n';
const PLACEHOLDER = 'Was soll Mana sonst noch über dich wissen?';
const SAVE_DEBOUNCE_MS = 500;
let urlPanelOpen = $state(false);
@ -95,7 +95,7 @@
if (!trimmed) return;
const ok = await requireAuth({
feature: 'context-url-import',
reason: 'Das Crawlen einer Web-Seite läuft serverseitig und erfordert ein Mana-Konto.',
reason: $_('profile.freeform.auth_reason_crawl'),
});
if (!ok) return;
importing = true;
@ -123,12 +123,13 @@
});
if (phaseTimer) clearTimeout(phaseTimer);
importPhase = 'appending';
const header = `## ${result.title}\n\n_Quelle: ${result.sourceUrl}_\n\n`;
const sourceLabel = $_('profile.freeform.crawl_source_label');
const header = `## ${result.title}\n\n_${sourceLabel}: ${result.sourceUrl}_\n\n`;
await userContextStore.appendFreeform(header + result.content);
if (mode === 'edit' && ctx) draft = ctx.freeform;
closeUrlPanel();
} catch (err) {
importError = err instanceof Error ? err.message : 'Import fehlgeschlagen';
importError = err instanceof Error ? err.message : $_('profile.freeform.error_import_failed');
} finally {
if (phaseTimer) clearTimeout(phaseTimer);
clearInterval(tick);
@ -152,21 +153,29 @@
<div class="freeform">
<header class="bar">
<div class="status">
{#if saveState === 'pending'}<span class="status-text">Speichert…</span>
{:else if saveState === 'saved'}<span class="status-text saved">Gespeichert</span>{/if}
{#if saveState === 'pending'}<span class="status-text">{$_('profile.freeform.saving')}</span>
{:else if saveState === 'saved'}<span class="status-text saved"
>{$_('profile.freeform.saved')}</span
>{/if}
</div>
<div class="actions">
<button
class="mode-btn"
class:active={urlPanelOpen}
onclick={() => (urlPanelOpen ? closeUrlPanel() : (urlPanelOpen = true))}
title="Web-Seite crawlen und anhängen"
title={$_('profile.freeform.toggle_url_title')}
>
<LinkSimple size={14} /><span>Aus URL</span>
<LinkSimple size={14} /><span>{$_('profile.freeform.action_from_url')}</span>
</button>
<button class="mode-btn" onclick={toggleMode} title="Cmd/Ctrl + E">
{#if mode === 'view'}<PencilSimple size={14} /><span>Bearbeiten</span>
{:else}<Eye size={14} /><span>Ansicht</span>{/if}
<button
class="mode-btn"
onclick={toggleMode}
title={$_('profile.freeform.toggle_mode_title')}
>
{#if mode === 'view'}<PencilSimple size={14} /><span
>{$_('profile.freeform.action_edit')}</span
>
{:else}<Eye size={14} /><span>{$_('profile.freeform.action_view')}</span>{/if}
</button>
</div>
</header>
@ -178,37 +187,40 @@
type="url"
bind:value={importUrl}
required
placeholder="https://example.com/article"
placeholder={$_('profile.freeform.url_placeholder')}
disabled={importing}
class="url-input"
/>
<button type="submit" disabled={importing || !importUrl.trim()} class="url-submit">
{#if importing}{importPhase === 'crawling'
? 'Crawle…'
? $_('profile.freeform.phase_crawling')
: importPhase === 'summarizing'
? 'Fasse zusammen…'
: 'Speichere…'}{:else}Einfügen{/if}
? $_('profile.freeform.phase_summarizing')
: $_('profile.freeform.phase_appending')}{:else}{$_(
'profile.freeform.action_insert'
)}{/if}
</button>
<button
type="button"
onclick={closeUrlPanel}
disabled={importing}
class="url-close"
title="Schließen"><X size={14} /></button
title={$_('profile.freeform.action_close_title')}><X size={14} /></button
>
</div>
<div class="url-opts">
<label class:disabled={importing}
><input type="radio" bind:group={importMode} value="single" disabled={importing} /> Nur diese
Seite</label
><input type="radio" bind:group={importMode} value="single" disabled={importing} />
{$_('profile.freeform.option_single')}</label
>
<label class:disabled={importing}
><input type="radio" bind:group={importMode} value="deep" disabled={importing} /> Ganze Website
(max. 20)</label
><input type="radio" bind:group={importMode} value="deep" disabled={importing} />
{$_('profile.freeform.option_deep')}</label
>
<span class="url-sep">·</span>
<label class:disabled={importing}
><input type="checkbox" bind:checked={importSummarize} disabled={importing} /> Mit KI zusammenfassen</label
><input type="checkbox" bind:checked={importSummarize} disabled={importing} />
{$_('profile.freeform.option_summarize')}</label
>
</div>
{#if importError}<p class="url-error">{importError}</p>{/if}
@ -221,14 +233,16 @@
bind:value={draft}
oninput={scheduleSave}
onblur={flush}
placeholder={PLACEHOLDER}
placeholder={$_('profile.freeform.placeholder')}
></textarea>
{:else if renderedHtml}
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
<article class="prose">{@html renderedHtml}</article>
{:else}
<button class="empty" onclick={() => (mode = 'edit')}
><span>{PLACEHOLDER}</span><span class="hint">Klicken zum Bearbeiten</span></button
><span>{$_('profile.freeform.placeholder')}</span><span class="hint"
>{$_('profile.freeform.empty_hint')}</span
></button
>
{/if}
</div>

View file

@ -5,8 +5,9 @@
-->
<script lang="ts">
import { formatDate, formatTime } from '$lib/i18n/format';
import { getComparison, type CompareResponse, type ModelComparison } from '../api';
import { getComparison, type CompareResponse } from '../api';
import { getWeatherIcon, getWeatherLabel } from '../weather-icons';
import { _ } from 'svelte-i18n';
interface Props {
lat: number;
@ -32,7 +33,7 @@
try {
data = await getComparison(lt, ln);
} catch (e) {
error = e instanceof Error ? e.message : 'Vergleichsdaten nicht verfuegbar';
error = e instanceof Error ? e.message : $_('wetter.comparison.error_unavailable');
} finally {
loading = false;
}
@ -56,18 +57,18 @@
<div class="comparison-section">
<div class="section-header">
<span class="section-label">Modell-Vergleich</span>
<span class="section-label">{$_('wetter.comparison.section_label')}</span>
<span class="section-sub">{locationName}</span>
</div>
{#if loading}
<div class="loading">Modelle werden verglichen...</div>
<div class="loading">{$_('wetter.comparison.loading')}</div>
{:else if error}
<div class="error">{error}</div>
{:else if data}
<!-- Current conditions comparison -->
<div class="compare-block">
<span class="block-label">Aktuell</span>
<span class="block-label">{$_('wetter.comparison.block_current')}</span>
<div class="model-cards">
{#each data.models as model (model.id)}
{@const c = model.current}
@ -80,7 +81,7 @@
</div>
<div class="model-desc">{model.description}</div>
{#if model.error || !c}
<div class="model-error">Nicht verfuegbar</div>
<div class="model-error">{$_('wetter.comparison.model_unavailable')}</div>
{:else}
<div class="model-current">
<span class="mc-icon"
@ -91,19 +92,19 @@
</div>
<div class="model-details">
<div class="md-row">
<span class="md-label">Gefuehlt</span>
<span class="md-label">{$_('wetter.comparison.label_apparent')}</span>
<span class="md-val">{Math.round(c.apparent_temperature ?? 0)}°</span>
</div>
<div class="md-row">
<span class="md-label">Wind</span>
<span class="md-label">{$_('wetter.comparison.label_wind')}</span>
<span class="md-val">{Math.round(c.wind_speed_10m ?? 0)} km/h</span>
</div>
<div class="md-row">
<span class="md-label">Niederschlag</span>
<span class="md-label">{$_('wetter.comparison.label_precip')}</span>
<span class="md-val">{(c.precipitation ?? 0).toFixed(1)} mm</span>
</div>
<div class="md-row">
<span class="md-label">Feuchtigkeit</span>
<span class="md-label">{$_('wetter.comparison.label_humidity')}</span>
<span class="md-val">{c.relative_humidity_2m ?? 0}%</span>
</div>
</div>
@ -115,14 +116,14 @@
<!-- Daily forecast comparison -->
<div class="compare-block">
<span class="block-label">7-Tage-Vergleich</span>
{#each Array.from( { length: Math.min(7, data.models[0]?.daily?.time?.length ?? 0) } ) as _, dayIdx}
<span class="block-label">{$_('wetter.comparison.block_seven_day')}</span>
{#each Array.from( { length: Math.min(7, data.models[0]?.daily?.time?.length ?? 0) } ) as _ignored, dayIdx}
{@const dateStr = data.models[0]?.daily?.time?.[dayIdx] ?? ''}
{@const dayLabel =
dayIdx === 0
? 'Heute'
? $_('wetter.comparison.day_today')
: dayIdx === 1
? 'Morgen'
? $_('wetter.comparison.day_tomorrow')
: formatDate(new Date(dateStr), { weekday: 'short', day: 'numeric' })}
<div class="day-compare">
<div class="day-compare-header">{dayLabel}</div>
@ -161,7 +162,7 @@
<!-- DWD Alerts -->
{#if data.alerts.length > 0}
<div class="compare-block">
<span class="block-label">DWD Wetterwarnungen</span>
<span class="block-label">{$_('wetter.comparison.block_alerts')}</span>
{#each data.alerts.slice(0, 5) as alert}
<div class="alert-row">
<span
@ -177,9 +178,10 @@
{/if}
<div class="fetched-at">
Abgerufen: {formatTime(new Date(data.fetchedAt))}
{$_('wetter.comparison.fetched_at')}
{formatTime(new Date(data.fetchedAt))}
<button class="refresh-btn" onclick={() => loadComparison(lat, lon)} disabled={loading}>
Aktualisieren
{$_('wetter.comparison.action_refresh')}
</button>
</div>
{/if}

View file

@ -4,7 +4,6 @@
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { getContext } from 'svelte';
import type { Observable } from 'dexie';
import { type Contact, contactsStore, getDisplayName, getInitials } from '$lib/modules/contacts';
import {
CaretLeft,
@ -94,7 +93,10 @@
async function handleDelete() {
if (!contact) return;
if (!confirm(`"${getDisplayName(contact)}" endgueltig loeschen?`)) return;
if (
!confirm($_('contacts.detail.confirm_delete', { values: { name: getDisplayName(contact) } }))
)
return;
await contactsStore.deleteContact(contact.id);
goto('/contacts');
}
@ -113,10 +115,16 @@
</script>
<svelte:head>
<title>{contact ? getDisplayName(contact) : 'Kontakt'} - Mana</title>
<title
>{$_('contacts.detail.page_title_html', {
values: {
name: contact ? getDisplayName(contact) : $_('contacts.detail.page_title_fallback'),
},
})}</title
>
</svelte:head>
<RoutePage appId="contacts" backHref="/contacts" title="Kontakt">
<RoutePage appId="contacts" backHref="/contacts" title={$_('contacts.detail.page_title_fallback')}>
<div class="mx-auto max-w-2xl">
<!-- Back Link -->
<a
@ -124,20 +132,22 @@
class="mb-4 inline-flex items-center gap-1.5 text-sm text-muted-foreground transition-colors hover:text-foreground"
>
<CaretLeft size={16} />
Kontakte
{$_('contacts.detail.breadcrumb')}
</a>
{#if !contact}
<div class="flex flex-col items-center py-16 text-center">
<h2 class="text-lg font-semibold text-foreground">Kontakt nicht gefunden</h2>
<h2 class="text-lg font-semibold text-foreground">
{$_('contacts.detail.empty_title')}
</h2>
<p class="mt-1 text-sm text-muted-foreground">
Dieser Kontakt existiert nicht oder wurde geloescht.
{$_('contacts.detail.empty_hint')}
</p>
<a
href="/contacts"
class="mt-4 rounded-lg bg-primary px-4 py-2 text-sm font-medium text-primary-foreground"
>
Zurueck zu Kontakten
{$_('contacts.detail.empty_back')}
</a>
</div>
{:else}
@ -179,7 +189,7 @@
<button
onclick={() => (showShare = true)}
class="rounded-lg p-2 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
title="Kurzlink teilen"
title={$_('contacts.detail.action_share_title')}
>
<ShareNetwork size={18} />
</button>
@ -193,7 +203,9 @@
<button
onclick={handleToggleFavorite}
class="rounded-lg p-2 text-muted-foreground transition-colors hover:bg-muted hover:text-amber-500"
title={contact.isFavorite ? 'Favorit entfernen' : 'Zu Favoriten'}
title={contact.isFavorite
? $_('contacts.detail.action_unfavorite_title')
: $_('contacts.detail.action_favorite_title')}
>
{#if contact.isFavorite}
<Star weight="fill" size={18} class="text-amber-500" />
@ -204,14 +216,14 @@
<button
onclick={handleArchive}
class="rounded-lg p-2 text-muted-foreground transition-colors hover:bg-muted"
title="Archivieren"
title={$_('contacts.detail.action_archive_title')}
>
<Archive size={18} />
</button>
<button
onclick={handleDelete}
class="rounded-lg p-2 text-muted-foreground transition-colors hover:bg-muted hover:text-red-500"
title="Loeschen"
title={$_('contacts.detail.action_delete_title')}
>
<Trash size={18} />
</button>
@ -448,7 +460,8 @@
href="tel:{contact.phone}"
class="flex items-center gap-1.5 rounded-lg border border-border px-3 py-2 text-sm font-medium text-foreground transition-colors hover:bg-muted"
>
<Phone size={16} /> Anrufen
<Phone size={16} />
{$_('contacts.detail.quick_call')}
</a>
{/if}
{#if contact.email}
@ -456,7 +469,8 @@
href="mailto:{contact.email}"
class="flex items-center gap-1.5 rounded-lg border border-border px-3 py-2 text-sm font-medium text-foreground transition-colors hover:bg-muted"
>
<Envelope size={16} /> E-Mail
<Envelope size={16} />
{$_('contacts.detail.quick_email')}
</a>
{/if}
{#if contact.mobile}
@ -464,7 +478,8 @@
href="sms:{contact.mobile}"
class="flex items-center gap-1.5 rounded-lg border border-border px-3 py-2 text-sm font-medium text-foreground transition-colors hover:bg-muted"
>
<DeviceMobile size={16} /> SMS
<DeviceMobile size={16} />
{$_('contacts.detail.quick_sms')}
</a>
{/if}
</div>
@ -475,7 +490,7 @@
<!-- Contact Info -->
<div class="rounded-xl border border-border bg-card p-5">
<h2 class="mb-3 text-sm font-semibold uppercase tracking-wider text-muted-foreground">
Kontakt
{$_('contacts.detail.section_contact')}
</h2>
<div class="space-y-3">
{#if contact.email}
@ -492,7 +507,9 @@
<a href="tel:{contact.mobile}" class="text-sm text-primary hover:underline"
>{contact.mobile}</a
>
<span class="text-xs text-muted-foreground">Mobil</span>
<span class="text-xs text-muted-foreground"
>{$_('contacts.detail.label_mobile')}</span
>
</div>
{/if}
{#if contact.phone}
@ -501,12 +518,16 @@
<a href="tel:{contact.phone}" class="text-sm text-primary hover:underline"
>{contact.phone}</a
>
<span class="text-xs text-muted-foreground">Telefon</span>
<span class="text-xs text-muted-foreground"
>{$_('contacts.detail.label_phone')}</span
>
</div>
{/if}
</div>
{#if !contact.email && !contact.phone && !contact.mobile}
<p class="text-sm text-muted-foreground">Keine Kontaktdaten hinterlegt.</p>
<p class="text-sm text-muted-foreground">
{$_('contacts.detail.empty_contact_info')}
</p>
{/if}
</div>
@ -514,7 +535,7 @@
{#if contact.company || contact.jobTitle || contact.website}
<div class="rounded-xl border border-border bg-card p-5">
<h2 class="mb-3 text-sm font-semibold uppercase tracking-wider text-muted-foreground">
Arbeit
{$_('contacts.detail.section_work')}
</h2>
<div class="space-y-3">
{#if contact.company}
@ -550,7 +571,7 @@
{#if contact.street || contact.city || contact.postalCode || contact.country}
<div class="rounded-xl border border-border bg-card p-5">
<h2 class="mb-3 text-sm font-semibold uppercase tracking-wider text-muted-foreground">
Adresse
{$_('contacts.detail.section_address')}
</h2>
<div class="flex items-start gap-3">
<MapPin size={16} class="mt-0.5 flex-shrink-0 text-muted-foreground" />
@ -585,7 +606,7 @@
{#if contact.notes}
<div class="rounded-xl border border-border bg-card p-5">
<h2 class="mb-3 text-sm font-semibold uppercase tracking-wider text-muted-foreground">
Notizen
{$_('contacts.detail.section_notes')}
</h2>
<p class="whitespace-pre-wrap text-sm text-foreground">{contact.notes}</p>
</div>
@ -595,7 +616,7 @@
{#if contact.linkedin || contact.twitter || contact.instagram || contact.github}
<div class="rounded-xl border border-border bg-card p-5">
<h2 class="mb-3 text-sm font-semibold uppercase tracking-wider text-muted-foreground">
Social Media
{$_('contacts.detail.section_social')}
</h2>
<div class="space-y-3">
{#if contact.linkedin}
@ -658,7 +679,7 @@
{#if contact.tags.length > 0}
<div class="rounded-xl border border-border bg-card p-5">
<h2 class="mb-3 text-sm font-semibold uppercase tracking-wider text-muted-foreground">
Tags
{$_('contacts.detail.section_tags')}
</h2>
<div class="flex flex-wrap gap-2">
{#each contact.tags as tag (tag.id)}
@ -674,7 +695,7 @@
<!-- Metadata -->
<div class="rounded-xl border border-border bg-card p-5">
<div class="grid grid-cols-2 gap-y-2 text-xs text-muted-foreground">
<span>Erstellt</span>
<span>{$_('contacts.detail.meta_created')}</span>
<span
>{formatDate(new Date(contact.createdAt), {
day: 'numeric',
@ -682,7 +703,7 @@
year: 'numeric',
})}</span
>
<span>Aktualisiert</span>
<span>{$_('contacts.detail.meta_updated')}</span>
<span
>{formatDate(new Date(contact.updatedAt), {
day: 'numeric',

View file

@ -149,7 +149,6 @@
"apps/mana/apps/web/src/lib/modules/presi/views/DetailView.svelte": 4,
"apps/mana/apps/web/src/lib/modules/profile/components/MeImageSlotCard.svelte": 3,
"apps/mana/apps/web/src/lib/modules/profile/components/MeImageTile.svelte": 3,
"apps/mana/apps/web/src/lib/modules/profile/ContextFreeform.svelte": 7,
"apps/mana/apps/web/src/lib/modules/profile/ContextInterview.svelte": 6,
"apps/mana/apps/web/src/lib/modules/profile/ListView.svelte": 6,
"apps/mana/apps/web/src/lib/modules/profile/MeImagesView.svelte": 2,
@ -189,7 +188,6 @@
"apps/mana/apps/web/src/lib/modules/wetter/components/HourlyForecast.svelte": 1,
"apps/mana/apps/web/src/lib/modules/wetter/components/LocationPicker.svelte": 5,
"apps/mana/apps/web/src/lib/modules/wetter/components/NowcastBar.svelte": 1,
"apps/mana/apps/web/src/lib/modules/wetter/components/SourceComparison.svelte": 7,
"apps/mana/apps/web/src/lib/modules/wetter/components/WeatherAlerts.svelte": 1,
"apps/mana/apps/web/src/lib/modules/wetter/ListView.svelte": 2,
"apps/mana/apps/web/src/lib/modules/who/ListView.svelte": 5,
@ -215,7 +213,7 @@
"apps/mana/apps/web/src/routes/(app)/citycorners/cities/[slug]/+layout.svelte": 1,
"apps/mana/apps/web/src/routes/(app)/comic/new/+page.svelte": 1,
"apps/mana/apps/web/src/routes/(app)/companion/+page.svelte": 2,
"apps/mana/apps/web/src/routes/(app)/contacts/[id]/+page.svelte": 7,
"apps/mana/apps/web/src/routes/(app)/contacts/[id]/+page.svelte": 1,
"apps/mana/apps/web/src/routes/(app)/context/documents/[id]/+page.svelte": 3,
"apps/mana/apps/web/src/routes/(app)/context/documents/+page.svelte": 5,
"apps/mana/apps/web/src/routes/(app)/context/spaces/[id]/+page.svelte": 3,