i18n(comic+guides+cards): translate 3 detail/progress views via $_()

- comic/views/DetailView: route through comic.detail.* + dynamic
  comic.styles.<id>; drop unused STYLE_LABELS import
- guides/views/DetailView: route through guides.detail.* + dynamic
  guides.categories.<id> / guides.difficulties.<id>; drop unused
  DIFFICULTY_LABELS + Section type
- cards/progress/+page: route through cards.progress.* (also fixes
  pre-existing typos "Fallig"/"Ubersicht"/"Lernsitzungen" via
  proper translations across all 5 locales)

Baseline 961 → 940 (-21).
This commit is contained in:
Till JS 2026-04-27 18:17:08 +02:00
parent e3c2b26510
commit 63b9ff4684
5 changed files with 99 additions and 68 deletions

View file

@ -15,12 +15,12 @@
import { comicStoriesTable } from '../collections';
import { comicStoriesStore } from '../stores/stories.svelte';
import { useStory } from '../queries';
import { STYLE_LABELS } from '../constants';
import PanelStrip from '../components/PanelStrip.svelte';
import PanelEditor from '../components/PanelEditor.svelte';
import BatchPanelEditor from '../components/BatchPanelEditor.svelte';
import StoryboardSuggester from '../components/StoryboardSuggester.svelte';
import { encryptRecord } from '$lib/data/crypto';
import { _ } from 'svelte-i18n';
import type { ComicPanelMeta, LocalComicStory } from '../types';
interface Props {
@ -48,7 +48,8 @@
async function handleDelete() {
if (!story) return;
if (!confirm(`Story "${story.title}" wirklich löschen?`)) return;
if (!confirm($_('comic.detail.confirm_delete_story', { values: { title: story.title } })))
return;
await comicStoriesStore.deleteStory(story.id);
await goto('/comic');
}
@ -65,12 +66,7 @@
*/
async function handleRemovePanel(panelId: string) {
if (!story) return;
if (
!confirm(
'Panel aus der Story entfernen? Das Bild bleibt in deiner Picture-Galerie und kann dort gelöscht werden.'
)
)
return;
if (!confirm($_('comic.detail.confirm_remove_panel'))) return;
const existing = await comicStoriesTable.get(story.id);
if (!existing) return;
@ -91,20 +87,20 @@
<a
href="/comic"
class="flex h-8 w-8 items-center justify-center rounded-lg text-muted-foreground hover:bg-muted"
aria-label="Zurück zu Comics"
aria-label={$_('comic.detail.back_aria')}
>
<ArrowLeft size={16} />
</a>
<span class="text-muted-foreground">Comics</span>
<span class="text-muted-foreground">{$_('comic.detail.breadcrumb')}</span>
</nav>
{#if !story}
{#if story$.loading}
<p class="text-sm text-muted-foreground">Lädt…</p>
<p class="text-sm text-muted-foreground">{$_('comic.detail.loading')}</p>
{:else}
<div class="rounded-2xl border border-dashed border-border bg-background/50 p-8 text-center">
<p class="text-sm font-medium text-foreground">Story nicht gefunden.</p>
<p class="mt-1 text-sm text-muted-foreground">Gelöscht oder in einem anderen Space.</p>
<p class="text-sm font-medium text-foreground">{$_('comic.detail.not_found')}</p>
<p class="mt-1 text-sm text-muted-foreground">{$_('comic.detail.not_found_hint')}</p>
</div>
{/if}
{:else}
@ -115,18 +111,23 @@
<h1 class="truncate text-lg font-semibold text-foreground">{story.title}</h1>
<div class="mt-1 flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
<span class="rounded-full bg-primary/10 px-2 py-0.5 font-medium text-primary">
{STYLE_LABELS[story.style].de}
{$_('comic.styles.' + story.style)}
</span>
<span>
{story.panelImageIds.length}
{story.panelImageIds.length === 1 ? 'Panel' : 'Panels'}
{story.panelImageIds.length === 1
? $_('comic.detail.panel_one', { values: { n: story.panelImageIds.length } })
: $_('comic.detail.panel_other', { values: { n: story.panelImageIds.length } })}
</span>
{#if story.characterMediaIds.length > 0}
<span class="text-border">·</span>
<span>
{story.characterMediaIds.length} Referenz{story.characterMediaIds.length === 1
? ''
: 'en'}
{story.characterMediaIds.length === 1
? $_('comic.detail.reference_one', {
values: { n: story.characterMediaIds.length },
})
: $_('comic.detail.reference_other', {
values: { n: story.characterMediaIds.length },
})}
</span>
{/if}
</div>
@ -140,8 +141,12 @@
<button
type="button"
onclick={handleToggleFavorite}
aria-label={story.isFavorite ? 'Favorit entfernen' : 'Als Favorit markieren'}
title={story.isFavorite ? 'Favorit entfernen' : 'Als Favorit markieren'}
aria-label={story.isFavorite
? $_('comic.detail.favorite_remove')
: $_('comic.detail.favorite_set')}
title={story.isFavorite
? $_('comic.detail.favorite_remove')
: $_('comic.detail.favorite_set')}
class="flex h-8 w-8 items-center justify-center rounded-md transition-colors {story.isFavorite
? 'text-rose-500 hover:bg-rose-500/10'
: 'text-muted-foreground hover:bg-muted hover:text-foreground'}"
@ -157,7 +162,7 @@
{#if story.storyContext}
<div class="rounded-md bg-muted/50 px-3 py-2 text-xs text-muted-foreground">
<strong class="text-foreground">Kontext:</strong>
<strong class="text-foreground">{$_('comic.detail.context_label')}</strong>
{story.storyContext}
</div>
{/if}
@ -166,7 +171,9 @@
<!-- Panels -->
<div class="space-y-3">
<div class="flex items-center justify-between">
<h2 class="text-sm font-semibold uppercase tracking-wider text-muted-foreground">Panels</h2>
<h2 class="text-sm font-semibold uppercase tracking-wider text-muted-foreground">
{$_('comic.detail.section_panels')}
</h2>
{#if editorMode === 'off' && !story.isArchived}
<div class="flex items-center gap-1">
<button
@ -175,25 +182,25 @@
class="inline-flex items-center gap-1.5 rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground transition-colors hover:bg-primary/90"
>
<Plus size={12} />
Panel
{$_('comic.detail.add_panel')}
</button>
<button
type="button"
onclick={() => (editorMode = 'batch')}
class="inline-flex items-center gap-1.5 rounded-md border border-border bg-background px-3 py-1.5 text-xs font-medium text-foreground transition-colors hover:bg-muted"
title="24 Panels in einem Rutsch generieren"
title={$_('comic.detail.add_batch_title')}
>
<Plus size={12} />
Batch
{$_('comic.detail.add_batch')}
</button>
<button
type="button"
onclick={() => (editorMode = 'ai')}
class="inline-flex items-center gap-1.5 rounded-md border border-primary/40 bg-primary/5 px-3 py-1.5 text-xs font-medium text-primary transition-colors hover:bg-primary/10"
title="KI schlägt Panels aus einem Tagebuch-Eintrag, Notiz oder Review vor"
title={$_('comic.detail.add_ai_title')}
>
<Sparkle size={12} weight="fill" />
Mit KI
{$_('comic.detail.add_ai')}
</button>
</div>
{/if}
@ -230,7 +237,7 @@
class="flex flex-1 items-center justify-center gap-2 rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground transition-colors hover:bg-muted"
>
<Archive size={14} />
{story.isArchived ? 'Wieder aktiv' : 'Archivieren'}
{story.isArchived ? $_('comic.detail.unarchive') : $_('comic.detail.archive')}
</button>
<button
type="button"
@ -238,7 +245,7 @@
class="flex flex-1 items-center justify-center gap-2 rounded-md border border-border bg-background px-3 py-2 text-sm text-error transition-colors hover:bg-error/10"
>
<Trash size={14} />
Löschen
{$_('comic.detail.delete')}
</button>
</div>
@ -246,8 +253,8 @@
<p
class="rounded-md border border-border bg-muted/30 px-3 py-2 text-xs text-muted-foreground"
>
<Sparkle size={12} class="inline" /> Archivierte Story — keine Panel-Generierung möglich, bis
wieder aktiviert.
<Sparkle size={12} class="inline" />
{$_('comic.detail.archived_hint')}
</p>
{/if}
{/if}

View file

@ -8,8 +8,9 @@
import type { ViewProps } from '$lib/app-registry';
import { useGuide, useSections, useSteps, useLatestRun, getStepProgress } from '../queries';
import { guidesStore } from '../stores/guides.svelte';
import { GUIDE_CATEGORIES, DIFFICULTY_LABELS } from '../types';
import type { Step, Section } from '../types';
import { GUIDE_CATEGORIES } from '../types';
import type { Step } from '../types';
import { _ } from 'svelte-i18n';
let { navigate, goBack, params }: ViewProps = $props();
let guideId = $derived((params.guideId as string) ?? '');
@ -84,7 +85,8 @@
async function handleDelete() {
if (!guide) return;
if (!confirm(`Guide "${guide.title}" wirklich löschen?`)) return;
if (!confirm($_('guides.detail.confirm_delete_guide', { values: { title: guide.title } })))
return;
await guidesStore.deleteGuide(guide.id);
goBack();
}
@ -115,26 +117,40 @@
</script>
{#if !guide}
<div class="loading">Lade Guide...</div>
<div class="loading">{$_('guides.detail.loading')}</div>
{:else}
<div class="detail">
<!-- Header -->
<header class="detail-header">
<div class="header-actions">
<button class="action-btn" onclick={startEdit}>Bearbeiten</button>
<button class="action-btn danger" onclick={handleDelete}>Löschen</button>
<button class="action-btn" onclick={startEdit}>{$_('guides.detail.action_edit')}</button>
<button class="action-btn danger" onclick={handleDelete}>
{$_('guides.detail.action_delete')}
</button>
</div>
</header>
<!-- Editing form -->
{#if editing}
<div class="edit-form">
<input class="title-input" bind:value={titleDraft} placeholder="Guide-Titel" />
<textarea class="desc-input" bind:value={descDraft} rows="3" placeholder="Beschreibung"
<input
class="title-input"
bind:value={titleDraft}
placeholder={$_('guides.detail.placeholder_title')}
/>
<textarea
class="desc-input"
bind:value={descDraft}
rows="3"
placeholder={$_('guides.detail.placeholder_description')}
></textarea>
<div class="form-actions">
<button class="action-btn" onclick={() => (editing = false)}>Abbrechen</button>
<button class="action-btn primary" onclick={saveEdit}>Speichern</button>
<button class="action-btn" onclick={() => (editing = false)}>
{$_('guides.detail.action_cancel')}
</button>
<button class="action-btn primary" onclick={saveEdit}>
{$_('guides.detail.action_save')}
</button>
</div>
</div>
{:else}
@ -142,9 +158,11 @@
{@const catInfo = GUIDE_CATEGORIES[guide.category]}
<div class="meta">
<div class="meta-badges">
<span class="badge {catInfo.color}">{catInfo.label}</span>
<span class="badge bg-muted/10">{DIFFICULTY_LABELS[guide.difficulty]}</span>
<span class="badge bg-muted/10">{guide.estimatedMinutes} min</span>
<span class="badge {catInfo.color}">{$_('guides.categories.' + guide.category)}</span>
<span class="badge bg-muted/10">{$_('guides.difficulties.' + guide.difficulty)}</span>
<span class="badge bg-muted/10">
{$_('guides.detail.minutes', { values: { n: guide.estimatedMinutes } })}
</span>
</div>
<h1 class="title">{guide.title}</h1>
<p class="description">{guide.description}</p>
@ -157,9 +175,11 @@
<div class="progress-header">
<span class="progress-label">
{#if isComplete}
Abgeschlossen
{$_('guides.detail.completed')}
{:else}
{run.completedStepIds.length} / {steps.length} Schritte
{$_('guides.detail.steps_progress', {
values: { done: run.completedStepIds.length, total: steps.length },
})}
{/if}
</span>
<span class="progress-pct">{progress}%</span>
@ -171,11 +191,15 @@
></div>
</div>
{#if isComplete}
<button class="action-btn small" onclick={resetProgress}>Fortschritt zurücksetzen</button>
<button class="action-btn small" onclick={resetProgress}>
{$_('guides.detail.reset_progress')}
</button>
{/if}
</div>
{:else if steps.length > 0}
<button class="action-btn primary" onclick={startGuide}>Guide starten</button>
<button class="action-btn primary" onclick={startGuide}>
{$_('guides.detail.start_guide')}
</button>
{/if}
<!-- Sections + Steps -->
@ -218,7 +242,7 @@
<input
type="text"
bind:value={newStepTitle}
placeholder="Neuer Schritt..."
placeholder={$_('guides.detail.placeholder_new_step')}
class="inline-input"
onkeydown={(e) => e.key === 'Enter' && addStep(section.id)}
/>
@ -234,7 +258,7 @@
newStepTitle = '';
}}
>
+ Schritt
{$_('guides.detail.add_step')}
</button>
{/if}
</div>
@ -273,7 +297,7 @@
<input
type="text"
bind:value={newSectionTitle}
placeholder="Neuer Abschnitt..."
placeholder={$_('guides.detail.placeholder_new_section')}
class="inline-input"
onkeydown={(e) => e.key === 'Enter' && addSection()}
/>
@ -289,7 +313,7 @@
newSectionTitle = '';
}}
>
+ Abschnitt
{$_('guides.detail.add_section')}
</button>
<button
class="add-link"
@ -298,7 +322,7 @@
newStepTitle = '';
}}
>
+ Schritt
{$_('guides.detail.add_step')}
</button>
</div>
{/if}
@ -308,7 +332,7 @@
<input
type="text"
bind:value={newStepTitle}
placeholder="Neuer Schritt..."
placeholder={$_('guides.detail.placeholder_new_step')}
class="inline-input"
onkeydown={(e) => e.key === 'Enter' && addStep(null)}
/>

View file

@ -4,6 +4,7 @@
import { ChartBar } from '@mana/shared-icons';
import type { Deck } from '$lib/modules/cards/types';
import { RoutePage } from '$lib/components/shell';
import { _ } from 'svelte-i18n';
// Get live query data from layout context
const allDecks: { readonly value: Deck[] } = getContext('cardDecks');
@ -13,29 +14,29 @@
</script>
<svelte:head>
<title>Fortschritt - Cards - Mana</title>
<title>{$_('cards.progress.page_title_html')}</title>
</svelte:head>
<RoutePage appId="cards" backHref="/cards">
<div class="mx-auto max-w-5xl space-y-6">
<div>
<h1 class="text-2xl font-bold text-foreground">Fortschritt</h1>
<p class="text-muted-foreground mt-1 text-sm">Verfolge deinen Lernfortschritt</p>
<h1 class="text-2xl font-bold text-foreground">{$_('cards.progress.heading')}</h1>
<p class="text-muted-foreground mt-1 text-sm">{$_('cards.progress.subtitle')}</p>
</div>
<!-- Stats Overview -->
<div class="grid grid-cols-1 gap-4 md:grid-cols-3">
<div class="rounded-xl border border-border bg-card p-4 text-center">
<div class="text-3xl font-bold text-foreground">{decks.length}</div>
<div class="text-sm text-muted-foreground">Decks</div>
<div class="text-sm text-muted-foreground">{$_('cards.progress.stat_decks')}</div>
</div>
<div class="rounded-xl border border-border bg-card p-4 text-center">
<div class="text-3xl font-bold text-foreground">{totalCards}</div>
<div class="text-sm text-muted-foreground">Karten gesamt</div>
<div class="text-sm text-muted-foreground">{$_('cards.progress.stat_total_cards')}</div>
</div>
<div class="rounded-xl border border-border bg-card p-4 text-center">
<div class="text-3xl font-bold text-orange-500">0</div>
<div class="text-sm text-muted-foreground">Fallig zur Wiederholung</div>
<div class="text-sm text-muted-foreground">{$_('cards.progress.stat_due')}</div>
</div>
</div>
@ -44,14 +45,14 @@
<h2 class="border-b border-border p-4 text-lg font-semibold text-foreground">
<span class="flex items-center gap-2">
<ChartBar size={20} />
Decks Ubersicht
{$_('cards.progress.section_overview')}
</span>
</h2>
{#if decks.length === 0}
<div class="py-12 text-center">
<div class="mb-4 text-4xl">🎯</div>
<p class="text-muted-foreground">Noch keine Lernsitzungen.</p>
<p class="mt-2 text-sm text-muted-foreground">Erstelle ein Deck und beginne zu lernen!</p>
<p class="text-muted-foreground">{$_('cards.progress.empty_title')}</p>
<p class="mt-2 text-sm text-muted-foreground">{$_('cards.progress.empty_hint')}</p>
</div>
{:else}
<div class="divide-y divide-border">
@ -62,7 +63,7 @@
<div>
<div class="font-medium text-foreground">{deck.title}</div>
<div class="text-sm text-muted-foreground">
{deck.cardCount || 0} Karten
{$_('cards.progress.deck_cards', { values: { n: deck.cardCount || 0 } })}
</div>
</div>
</div>