i18n(drink+habits+picture): translate 3 list views via $_()

- drink/ListView: route through drink.list_view.* (today/log/empty
  + create form + ctx-menu); drops unused PencilSimple equivalent
- habits/ListView: route through habits.list_view.* (voice
  capture + tally grid + create form + ctx-menu); drops unused
  PencilSimple icon import
- picture/ListView: route through picture.list_view.* (drop overlay,
  action strip, view-mode titles, search placeholder, empty states,
  lightbox actions)

Baseline 833 → 818 (-15).
This commit is contained in:
Till JS 2026-04-27 22:36:57 +02:00
parent 0986d07a7d
commit 4237d84c18
4 changed files with 67 additions and 46 deletions

View file

@ -25,6 +25,7 @@
import { DynamicIcon } from '@mana/shared-ui/atoms'; import { DynamicIcon } from '@mana/shared-ui/atoms';
import { IconPicker } from '@mana/shared-ui/molecules'; import { IconPicker } from '@mana/shared-ui/molecules';
import { Trash, Pause, Play } from '@mana/shared-icons'; import { Trash, Pause, Play } from '@mana/shared-icons';
import { _ } from 'svelte-i18n';
let entries$ = useAllDrinkEntries(); let entries$ = useAllDrinkEntries();
let presets$ = useAllDrinkPresets(); let presets$ = useAllDrinkPresets();
@ -84,7 +85,9 @@
? [ ? [
{ {
id: 'archive', id: 'archive',
label: ctxMenuPreset.state.target.isArchived ? 'Aktivieren' : 'Archivieren', label: ctxMenuPreset.state.target.isArchived
? $_('drink.list_view.ctx_activate')
: $_('drink.list_view.ctx_archive'),
icon: ctxMenuPreset.state.target.isArchived ? Play : Pause, icon: ctxMenuPreset.state.target.isArchived ? Play : Pause,
action: () => { action: () => {
const target = ctxMenuPreset.state.target; const target = ctxMenuPreset.state.target;
@ -94,7 +97,7 @@
{ id: 'div', label: '', type: 'divider' as const }, { id: 'div', label: '', type: 'divider' as const },
{ {
id: 'delete', id: 'delete',
label: 'Löschen', label: $_('drink.list_view.ctx_delete'),
icon: Trash, icon: Trash,
variant: 'danger' as const, variant: 'danger' as const,
action: () => { action: () => {
@ -112,7 +115,7 @@
? [ ? [
{ {
id: 'delete', id: 'delete',
label: 'Löschen', label: $_('drink.list_view.ctx_delete'),
icon: Trash, icon: Trash,
variant: 'danger' as const, variant: 'danger' as const,
action: () => { action: () => {
@ -171,7 +174,7 @@
<!-- Daily Progress --> <!-- Daily Progress -->
<div class="progress-section"> <div class="progress-section">
<div class="progress-header"> <div class="progress-header">
<span class="progress-label">Heute</span> <span class="progress-label">{$_('drink.list_view.section_today')}</span>
<span class="progress-value" class:goal-reached={goalReached}> <span class="progress-value" class:goal-reached={goalReached}>
{formatMl(todayTotalMl)} {formatMl(todayTotalMl)}
<span class="progress-goal">/ {formatMl(DEFAULT_DAILY_GOAL_ML)}</span> <span class="progress-goal">/ {formatMl(DEFAULT_DAILY_GOAL_ML)}</span>
@ -206,7 +209,7 @@
{#if !showCreate} {#if !showCreate}
<button class="preset-item add-btn" onclick={() => (showCreate = true)}> <button class="preset-item add-btn" onclick={() => (showCreate = true)}>
<span class="add-icon">+</span> <span class="add-icon">+</span>
<span class="preset-name">Neu</span> <span class="preset-name">{$_('drink.list_view.action_new')}</span>
</button> </button>
{/if} {/if}
</div> </div>
@ -228,7 +231,7 @@
<input <input
class="create-input" class="create-input"
type="text" type="text"
placeholder="Name (z.B. Espresso)..." placeholder={$_('drink.list_view.placeholder_name')}
bind:value={newName} bind:value={newName}
autofocus autofocus
/> />
@ -271,9 +274,11 @@
onclick={() => { onclick={() => {
showCreate = false; showCreate = false;
showIconPicker = false; showIconPicker = false;
}}>Abbrechen</button }}>{$_('drink.list_view.action_cancel')}</button
>
<button type="submit" class="btn-create" disabled={!newName.trim()}
>{$_('drink.list_view.action_create')}</button
> >
<button type="submit" class="btn-create" disabled={!newName.trim()}>Erstellen</button>
</div> </div>
</form> </form>
{/if} {/if}
@ -281,7 +286,7 @@
<!-- Today's Log --> <!-- Today's Log -->
{#if todayEntries.length > 0} {#if todayEntries.length > 0}
<div class="today-log"> <div class="today-log">
<div class="log-label">Verlauf</div> <div class="log-label">{$_('drink.list_view.section_log')}</div>
{#each todayEntries as entry (entry.id)} {#each todayEntries as entry (entry.id)}
{#if editingId === entry.id} {#if editingId === entry.id}
<!-- svelte-ignore a11y_no_static_element_interactions --> <!-- svelte-ignore a11y_no_static_element_interactions -->
@ -328,9 +333,9 @@
{#if activePresets.length === 0 && !showCreate} {#if activePresets.length === 0 && !showCreate}
<div class="empty"> <div class="empty">
<p>Noch keine Getränke-Presets.</p> <p>{$_('drink.list_view.empty_title')}</p>
<button class="empty-add-btn" onclick={() => (showCreate = true)} <button class="empty-add-btn" onclick={() => (showCreate = true)}
>Erstes Getränk anlegen</button >{$_('drink.list_view.empty_action')}</button
> >
</div> </div>
{/if} {/if}

View file

@ -19,8 +19,9 @@
import { toastStore } from '@mana/shared-ui/toast'; import { toastStore } from '@mana/shared-ui/toast';
import { DynamicIcon } from '@mana/shared-ui/atoms'; import { DynamicIcon } from '@mana/shared-ui/atoms';
import { IconPicker } from '@mana/shared-ui/molecules'; import { IconPicker } from '@mana/shared-ui/molecules';
import { PencilSimple, Trash, Pause, Play } from '@mana/shared-icons'; import { Trash, Pause, Play } from '@mana/shared-icons';
import VoiceCaptureBar from '$lib/components/voice/VoiceCaptureBar.svelte'; import VoiceCaptureBar from '$lib/components/voice/VoiceCaptureBar.svelte';
import { _ } from 'svelte-i18n';
let { navigate, goBack, params }: ViewProps = $props(); let { navigate, goBack, params }: ViewProps = $props();
@ -70,10 +71,12 @@
async function handleVoiceComplete(blob: Blob, durationMs: number) { async function handleVoiceComplete(blob: Blob, durationMs: number) {
const result = await habitsStore.logFromVoice(blob, durationMs, 'de'); const result = await habitsStore.logFromVoice(blob, durationMs, 'de');
if (!result) { if (!result) {
toastStore.error('Routine nicht erkannt. Versuche den Namen direkt zu sagen, z.B. "Kaffee".'); toastStore.error($_('habits.list_view.voice_error_unrecognized'));
return; return;
} }
toastStore.success(`${result.habitTitle} geloggt`); toastStore.success(
$_('habits.list_view.voice_logged', { values: { title: result.habitTitle } })
);
// Reuse the existing pulse animation by finding the matching habit id // Reuse the existing pulse animation by finding the matching habit id
const matched = habits.find((h) => h.title === result.habitTitle); const matched = habits.find((h) => h.title === result.habitTitle);
if (matched) { if (matched) {
@ -103,7 +106,7 @@
? [ ? [
{ {
id: 'log', id: 'log',
label: 'Loggen', label: $_('habits.list_view.ctx_log'),
icon: Play, icon: Play,
action: () => { action: () => {
const target = ctxMenu.state.target; const target = ctxMenu.state.target;
@ -112,7 +115,9 @@
}, },
{ {
id: 'archive', id: 'archive',
label: ctxMenu.state.target.isArchived ? 'Aktivieren' : 'Archivieren', label: ctxMenu.state.target.isArchived
? $_('habits.list_view.ctx_activate')
: $_('habits.list_view.ctx_archive'),
icon: ctxMenu.state.target.isArchived ? Play : Pause, icon: ctxMenu.state.target.isArchived ? Play : Pause,
action: () => { action: () => {
const target = ctxMenu.state.target; const target = ctxMenu.state.target;
@ -125,7 +130,7 @@
{ id: 'div', label: '', type: 'divider' as const }, { id: 'div', label: '', type: 'divider' as const },
{ {
id: 'delete', id: 'delete',
label: 'Löschen', label: $_('habits.list_view.ctx_delete'),
icon: Trash, icon: Trash,
variant: 'danger' as const, variant: 'danger' as const,
action: () => { action: () => {
@ -152,9 +157,9 @@
<div class="habits-list-view"> <div class="habits-list-view">
<!-- Voice quick-log --> <!-- Voice quick-log -->
<VoiceCaptureBar <VoiceCaptureBar
idleLabel="Routine sprechen" idleLabel={$_('habits.list_view.voice_idle_label')}
feature="habits-voice-log" feature="habits-voice-log"
reason="Routinen-Logs werden in deinem persönlichen Kalender gespeichert. Dafür brauchst du ein Mana-Konto." reason={$_('habits.list_view.voice_reason')}
onComplete={handleVoiceComplete} onComplete={handleVoiceComplete}
/> />
@ -185,7 +190,7 @@
{#if !showCreate} {#if !showCreate}
<button class="tally-item add-btn" onclick={() => (showCreate = true)}> <button class="tally-item add-btn" onclick={() => (showCreate = true)}>
<span class="add-icon">+</span> <span class="add-icon">+</span>
<span class="tally-name">Neu</span> <span class="tally-name">{$_('habits.list_view.action_new')}</span>
</button> </button>
{/if} {/if}
</div> </div>
@ -207,7 +212,7 @@
<input <input
class="create-input" class="create-input"
type="text" type="text"
placeholder="Routinen-Name..." placeholder={$_('habits.list_view.placeholder_name')}
bind:value={newTitle} bind:value={newTitle}
autofocus autofocus
/> />
@ -243,9 +248,11 @@
onclick={() => { onclick={() => {
showCreate = false; showCreate = false;
showIconPicker = false; showIconPicker = false;
}}>Abbrechen</button }}>{$_('habits.list_view.action_cancel')}</button
>
<button type="submit" class="btn-create" disabled={!newTitle.trim()}
>{$_('habits.list_view.action_create')}</button
> >
<button type="submit" class="btn-create" disabled={!newTitle.trim()}>Erstellen</button>
</div> </div>
</form> </form>
{/if} {/if}
@ -253,7 +260,7 @@
<!-- Recent Logs --> <!-- Recent Logs -->
{#if todayLogs.length > 0} {#if todayLogs.length > 0}
<div class="recent-logs"> <div class="recent-logs">
<div class="recent-label">Heute</div> <div class="recent-label">{$_('habits.list_view.section_today')}</div>
{#each todayLogs as log (log.id)} {#each todayLogs as log (log.id)}
{@const habit = habitMap.get(log.habitId)} {@const habit = habitMap.get(log.habitId)}
{#if habit} {#if habit}
@ -279,9 +286,9 @@
{#if activeHabits.length === 0 && !showCreate} {#if activeHabits.length === 0 && !showCreate}
<div class="empty"> <div class="empty">
<p>Noch keine Routinen angelegt.</p> <p>{$_('habits.list_view.empty_title')}</p>
<button class="empty-add-btn" onclick={() => (showCreate = true)} <button class="empty-add-btn" onclick={() => (showCreate = true)}
>Erste Routine erstellen</button >{$_('habits.list_view.empty_action')}</button
> >
</div> </div>
{/if} {/if}

View file

@ -34,6 +34,7 @@
getImagesByTags, getImagesByTags,
} from './queries'; } from './queries';
import type { Image, LocalImage } from './types'; import type { Image, LocalImage } from './types';
import { _ } from 'svelte-i18n';
const MEDIA_URL = import.meta.env.PUBLIC_MANA_MEDIA_URL || 'http://localhost:3015'; const MEDIA_URL = import.meta.env.PUBLIC_MANA_MEDIA_URL || 'http://localhost:3015';
@ -240,7 +241,7 @@
{#if dragActive} {#if dragActive}
<div class="drop-overlay"> <div class="drop-overlay">
<UploadSimple size={40} weight="bold" /> <UploadSimple size={40} weight="bold" />
<span>Bilder ablegen</span> <span>{$_('picture.list_view.drop_overlay')}</span>
</div> </div>
{/if} {/if}
@ -250,15 +251,19 @@
type="button" type="button"
class="action-btn action-btn-upload" class="action-btn action-btn-upload"
onclick={() => fileInput?.click()} onclick={() => fileInput?.click()}
title="Bilder hochladen" title={$_('picture.list_view.action_upload_title')}
> >
<UploadSimple size={14} /> <UploadSimple size={14} />
<span class="action-label">Upload</span> <span class="action-label">{$_('picture.list_view.action_upload')}</span>
</button> </button>
<a href="/picture/generate" class="action-btn action-btn-primary" title="Neues Bild generieren"> <a
href="/picture/generate"
class="action-btn action-btn-primary"
title={$_('picture.list_view.action_generate_title')}
>
<Plus size={14} /> <Plus size={14} />
<span class="action-label">Generieren</span> <span class="action-label">{$_('picture.list_view.action_generate')}</span>
</a> </a>
<button <button
@ -266,10 +271,10 @@
onclick={() => imagesStore.toggleFavoritesFilter()} onclick={() => imagesStore.toggleFavoritesFilter()}
class="action-btn" class="action-btn"
class:action-btn-active={imagesStore.showFavoritesOnly} class:action-btn-active={imagesStore.showFavoritesOnly}
title="Nur Favoriten anzeigen" title={$_('picture.list_view.action_favorites_title')}
> >
<Heart size={12} weight={imagesStore.showFavoritesOnly ? 'fill' : 'regular'} /> <Heart size={12} weight={imagesStore.showFavoritesOnly ? 'fill' : 'regular'} />
<span class="action-label">Favoriten</span> <span class="action-label">{$_('picture.list_view.action_favorites')}</span>
{#if favoriteCount > 0}<span class="action-count">{favoriteCount}</span>{/if} {#if favoriteCount > 0}<span class="action-count">{favoriteCount}</span>{/if}
</button> </button>
@ -281,7 +286,7 @@
onclick={() => pictureViewStore.setViewMode('single')} onclick={() => pictureViewStore.setViewMode('single')}
class="view-btn" class="view-btn"
class:active={pictureViewStore.viewMode === 'single'} class:active={pictureViewStore.viewMode === 'single'}
title="Liste" title={$_('picture.list_view.view_list_title')}
> >
<Rows size={12} /> <Rows size={12} />
</button> </button>
@ -289,7 +294,7 @@
onclick={() => pictureViewStore.setViewMode('grid3')} onclick={() => pictureViewStore.setViewMode('grid3')}
class="view-btn" class="view-btn"
class:active={pictureViewStore.viewMode === 'grid3'} class:active={pictureViewStore.viewMode === 'grid3'}
title="Mittel" title={$_('picture.list_view.view_medium_title')}
> >
<GridFour size={12} /> <GridFour size={12} />
</button> </button>
@ -297,7 +302,7 @@
onclick={() => pictureViewStore.setViewMode('grid5')} onclick={() => pictureViewStore.setViewMode('grid5')}
class="view-btn" class="view-btn"
class:active={pictureViewStore.viewMode === 'grid5'} class:active={pictureViewStore.viewMode === 'grid5'}
title="Klein" title={$_('picture.list_view.view_small_title')}
> >
<SquaresFour size={12} /> <SquaresFour size={12} />
</button> </button>
@ -314,7 +319,7 @@
<input <input
type="text" type="text"
bind:value={searchQuery} bind:value={searchQuery}
placeholder="Prompts durchsuchen…" placeholder={$_('picture.list_view.placeholder_search')}
class="w-full rounded-md border border-border bg-background py-1 pl-8 pr-2.5 text-xs text-foreground placeholder:text-muted-foreground focus:border-primary focus:ring-1 focus:ring-primary" class="w-full rounded-md border border-border bg-background py-1 pl-8 pr-2.5 text-xs text-foreground placeholder:text-muted-foreground focus:border-primary focus:ring-1 focus:ring-primary"
/> />
</div> </div>
@ -359,14 +364,19 @@
{#if filteredImages.length === 0} {#if filteredImages.length === 0}
<div class="empty-state"> <div class="empty-state">
<SquaresFour size={48} weight="thin" class="text-muted-foreground/30" /> <SquaresFour size={48} weight="thin" class="text-muted-foreground/30" />
<h3>{allImages.length === 0 ? 'Noch keine Bilder' : 'Keine Ergebnisse'}</h3> <h3>
{allImages.length === 0
? $_('picture.list_view.empty_no_images')
: $_('picture.list_view.empty_no_results')}
</h3>
<p> <p>
{allImages.length === 0 {allImages.length === 0
? 'Generiere dein erstes Bild mit KI oder lade welche hoch' ? $_('picture.list_view.empty_hint_no_images')
: 'Passe deine Filter an'} : $_('picture.list_view.empty_hint_no_results')}
</p> </p>
{#if allImages.length === 0} {#if allImages.length === 0}
<a href="/picture/generate" class="empty-cta">Erstes Bild generieren</a> <a href="/picture/generate" class="empty-cta">{$_('picture.list_view.empty_cta_first')}</a
>
{/if} {/if}
</div> </div>
{:else} {:else}
@ -409,7 +419,9 @@
weight={selectedImage.isFavorite ? 'fill' : 'regular'} weight={selectedImage.isFavorite ? 'fill' : 'regular'}
class={selectedImage.isFavorite ? 'text-red-500' : 'text-muted-foreground'} class={selectedImage.isFavorite ? 'text-red-500' : 'text-muted-foreground'}
/> />
{selectedImage.isFavorite ? 'Entfernen' : 'Favorit'} {selectedImage.isFavorite
? $_('picture.list_view.action_unfavorite')
: $_('picture.list_view.action_favorite')}
</button> </button>
<button <button
type="button" type="button"
@ -417,7 +429,7 @@
class="flex items-center gap-1.5 rounded-md border border-border px-3 py-1.5 text-sm font-medium transition-colors hover:bg-muted" class="flex items-center gap-1.5 rounded-md border border-border px-3 py-1.5 text-sm font-medium transition-colors hover:bg-muted"
> >
<Archive size={14} class="text-muted-foreground" /> <Archive size={14} class="text-muted-foreground" />
Archivieren {$_('picture.list_view.action_archive')}
</button> </button>
{/if} {/if}
{/snippet} {/snippet}

View file

@ -106,12 +106,10 @@
"apps/mana/apps/web/src/lib/modules/core/widgets/RecentContactsWidget.svelte": 2, "apps/mana/apps/web/src/lib/modules/core/widgets/RecentContactsWidget.svelte": 2,
"apps/mana/apps/web/src/lib/modules/core/widgets/TasksTodayWidget.svelte": 1, "apps/mana/apps/web/src/lib/modules/core/widgets/TasksTodayWidget.svelte": 1,
"apps/mana/apps/web/src/lib/modules/core/widgets/UpcomingEventsWidget.svelte": 1, "apps/mana/apps/web/src/lib/modules/core/widgets/UpcomingEventsWidget.svelte": 1,
"apps/mana/apps/web/src/lib/modules/drink/ListView.svelte": 5,
"apps/mana/apps/web/src/lib/modules/goals/ListView.svelte": 1, "apps/mana/apps/web/src/lib/modules/goals/ListView.svelte": 1,
"apps/mana/apps/web/src/lib/modules/guides/ListView.svelte": 1, "apps/mana/apps/web/src/lib/modules/guides/ListView.svelte": 1,
"apps/mana/apps/web/src/lib/modules/habits/components/HabitDetail.svelte": 5, "apps/mana/apps/web/src/lib/modules/habits/components/HabitDetail.svelte": 5,
"apps/mana/apps/web/src/lib/modules/habits/components/HabitForm.svelte": 3, "apps/mana/apps/web/src/lib/modules/habits/components/HabitForm.svelte": 3,
"apps/mana/apps/web/src/lib/modules/habits/ListView.svelte": 5,
"apps/mana/apps/web/src/lib/modules/inventory/ListView.svelte": 1, "apps/mana/apps/web/src/lib/modules/inventory/ListView.svelte": 1,
"apps/mana/apps/web/src/lib/modules/inventory/views/DetailView.svelte": 5, "apps/mana/apps/web/src/lib/modules/inventory/views/DetailView.svelte": 5,
"apps/mana/apps/web/src/lib/modules/journal/ListView.svelte": 6, "apps/mana/apps/web/src/lib/modules/journal/ListView.svelte": 6,
@ -133,7 +131,6 @@
"apps/mana/apps/web/src/lib/modules/photos/ListView.svelte": 4, "apps/mana/apps/web/src/lib/modules/photos/ListView.svelte": 4,
"apps/mana/apps/web/src/lib/modules/picture/components/ImageLightbox.svelte": 3, "apps/mana/apps/web/src/lib/modules/picture/components/ImageLightbox.svelte": 3,
"apps/mana/apps/web/src/lib/modules/picture/components/ReferenceImagePicker.svelte": 1, "apps/mana/apps/web/src/lib/modules/picture/components/ReferenceImagePicker.svelte": 1,
"apps/mana/apps/web/src/lib/modules/picture/ListView.svelte": 5,
"apps/mana/apps/web/src/lib/modules/places/ListView.svelte": 2, "apps/mana/apps/web/src/lib/modules/places/ListView.svelte": 2,
"apps/mana/apps/web/src/lib/modules/plants/views/DetailView.svelte": 1, "apps/mana/apps/web/src/lib/modules/plants/views/DetailView.svelte": 1,
"apps/mana/apps/web/src/lib/modules/playground/ListView.svelte": 2, "apps/mana/apps/web/src/lib/modules/playground/ListView.svelte": 2,