i18n(firsts): wire ListView to namespace — 15 strings cleared

Patches tabs, quick-add, category filter, search, stats, dream/lived
edit forms, repeat picker, expectation-vs-reality labels, context menu,
all empty states, people-view "Alleine" fallback. CATEGORY_LABELS +
PRIORITY_LABELS routed through firsts.categories.* / firsts.priorities.*
keys; constants kept in milestones/categories.ts for non-Svelte
callers. Locale-aware Date via get(locale).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-27 01:59:09 +02:00
parent d5d2b6fcf8
commit fa5dbb2cfc
3 changed files with 87 additions and 61 deletions

View file

@ -3,12 +3,12 @@
Track first-time experiences: dreams (bucket list) and lived moments. Track first-time experiences: dreams (bucket list) and lived moments.
--> -->
<script lang="ts"> <script lang="ts">
import { _, locale } from 'svelte-i18n';
import { get } from 'svelte/store';
import { useAllFirsts, useDreams, useLivedFirsts, searchFirsts, groupByPerson } from './queries'; import { useAllFirsts, useDreams, useLivedFirsts, searchFirsts, groupByPerson } from './queries';
import { firstsStore } from './stores/firsts.svelte'; import { firstsStore } from './stores/firsts.svelte';
import { import {
CATEGORY_LABELS,
CATEGORY_COLORS, CATEGORY_COLORS,
PRIORITY_LABELS,
type First, type First,
type FirstCategory, type FirstCategory,
type FirstPriority, type FirstPriority,
@ -182,7 +182,9 @@
? [ ? [
{ {
id: 'pin', id: 'pin',
label: ctxMenu.state.target.isPinned ? 'Lösen' : 'Pinnen', label: ctxMenu.state.target.isPinned
? $_('firsts.list_view.ctx_unpin')
: $_('firsts.list_view.ctx_pin'),
icon: PushPin, icon: PushPin,
action: () => { action: () => {
const target = ctxMenu.state.target; const target = ctxMenu.state.target;
@ -191,7 +193,7 @@
}, },
{ {
id: 'archive', id: 'archive',
label: 'Archivieren', label: $_('firsts.list_view.ctx_archive'),
icon: Archive, icon: Archive,
action: () => { action: () => {
const target = ctxMenu.state.target; const target = ctxMenu.state.target;
@ -201,7 +203,7 @@
{ id: 'div', label: '', type: 'divider' as const }, { id: 'div', label: '', type: 'divider' as const },
{ {
id: 'delete', id: 'delete',
label: 'Löschen', label: $_('firsts.list_view.ctx_delete'),
icon: Trash, icon: Trash,
variant: 'danger' as const, variant: 'danger' as const,
action: () => { action: () => {
@ -214,7 +216,7 @@
); );
function formatDate(iso: string): string { function formatDate(iso: string): string {
return new Date(iso).toLocaleDateString('de-DE', { return new Date(iso).toLocaleDateString(get(locale) ?? 'de', {
day: 'numeric', day: 'numeric',
month: 'short', month: 'short',
year: 'numeric', year: 'numeric',
@ -232,14 +234,14 @@
class:active={activeTab === 'timeline'} class:active={activeTab === 'timeline'}
onclick={() => (activeTab = 'timeline')} onclick={() => (activeTab = 'timeline')}
> >
Timeline {$_('firsts.list_view.tab_timeline')}
</button> </button>
<button <button
class="tab" class="tab"
class:active={activeTab === 'dreams'} class:active={activeTab === 'dreams'}
onclick={() => (activeTab = 'dreams')} onclick={() => (activeTab = 'dreams')}
> >
Dreams {$_('firsts.list_view.tab_dreams')}
{#if dreams.length > 0} {#if dreams.length > 0}
<span class="tab-count">{dreams.length}</span> <span class="tab-count">{dreams.length}</span>
{/if} {/if}
@ -249,7 +251,7 @@
class:active={activeTab === 'people'} class:active={activeTab === 'people'}
onclick={() => (activeTab = 'people')} onclick={() => (activeTab = 'people')}
> >
People {$_('firsts.list_view.tab_people')}
</button> </button>
</div> </div>
@ -258,25 +260,25 @@
<div class="quick-top"> <div class="quick-top">
<select class="cat-select" bind:value={newCategory}> <select class="cat-select" bind:value={newCategory}>
{#each CATEGORIES as cat} {#each CATEGORIES as cat}
<option value={cat}>{CATEGORY_LABELS[cat].de}</option> <option value={cat}>{$_('firsts.categories.' + cat)}</option>
{/each} {/each}
</select> </select>
<input <input
class="add-input" class="add-input"
type="text" type="text"
placeholder={newAsDream placeholder={newAsDream
? 'Neues erstes Mal erträumen... (Enter)' ? $_('firsts.list_view.placeholder_dream')
: 'Neues erstes Mal eintragen... (Enter)'} : $_('firsts.list_view.placeholder_lived')}
bind:value={newTitle} bind:value={newTitle}
onkeydown={handleQuickCreate} onkeydown={handleQuickCreate}
/> />
</div> </div>
<div class="quick-toggle"> <div class="quick-toggle">
<button class="toggle-btn" class:active={newAsDream} onclick={() => (newAsDream = true)}> <button class="toggle-btn" class:active={newAsDream} onclick={() => (newAsDream = true)}>
Dream {$_('firsts.list_view.toggle_dream')}
</button> </button>
<button class="toggle-btn" class:active={!newAsDream} onclick={() => (newAsDream = false)}> <button class="toggle-btn" class:active={!newAsDream} onclick={() => (newAsDream = false)}>
Erlebt {$_('firsts.list_view.toggle_lived')}
</button> </button>
</div> </div>
</form> </form>
@ -289,7 +291,7 @@
class:active={categoryFilter === null} class:active={categoryFilter === null}
onclick={() => (categoryFilter = null)} onclick={() => (categoryFilter = null)}
> >
Alle {$_('firsts.list_view.filter_all')}
</button> </button>
{#each CATEGORIES as cat} {#each CATEGORIES as cat}
<button <button
@ -298,7 +300,7 @@
style="--cat-color: {CATEGORY_COLORS[cat]}" style="--cat-color: {CATEGORY_COLORS[cat]}"
onclick={() => (categoryFilter = categoryFilter === cat ? null : cat)} onclick={() => (categoryFilter = categoryFilter === cat ? null : cat)}
> >
{CATEGORY_LABELS[cat].de} {$_('firsts.categories.' + cat)}
</button> </button>
{/each} {/each}
</div> </div>
@ -309,7 +311,7 @@
<input <input
class="search-input" class="search-input"
type="text" type="text"
placeholder="Erste Male durchsuchen..." placeholder={$_('firsts.list_view.placeholder_search')}
bind:value={searchQuery} bind:value={searchQuery}
/> />
{/if} {/if}
@ -317,8 +319,12 @@
<!-- Stats ribbon --> <!-- Stats ribbon -->
{#if allFirsts.length > 0} {#if allFirsts.length > 0}
<div class="insights"> <div class="insights">
<span class="ins-stat">{lived.length} erlebt</span> <span class="ins-stat"
<span class="ins-stat">{dreams.length} Dreams</span> >{$_('firsts.list_view.stat_lived', { values: { count: lived.length } })}</span
>
<span class="ins-stat"
>{$_('firsts.list_view.stat_dreams', { values: { count: dreams.length } })}</span
>
</div> </div>
{/if} {/if}
@ -342,47 +348,47 @@
</div> </div>
<label class="ed-field"> <label class="ed-field">
<span class="ed-label">Wann?</span> <span class="ed-label">{$_('firsts.list_view.convert_when')}</span>
<input type="date" bind:value={convertDate} class="ed-input-sm" /> <input type="date" bind:value={convertDate} class="ed-input-sm" />
</label> </label>
<label class="ed-field"> <label class="ed-field">
<span class="ed-label">Ich dachte vorher...</span> <span class="ed-label">{$_('firsts.list_view.convert_expected_label')}</span>
<textarea <textarea
class="ed-textarea" class="ed-textarea"
bind:value={convertExpectation} bind:value={convertExpectation}
rows="2" rows="2"
placeholder="Was hast du erwartet?" placeholder={$_('firsts.list_view.placeholder_expected')}
></textarea> ></textarea>
</label> </label>
<label class="ed-field"> <label class="ed-field">
<span class="ed-label">Es war tatsächlich...</span> <span class="ed-label">{$_('firsts.list_view.convert_reality_label')}</span>
<textarea <textarea
class="ed-textarea" class="ed-textarea"
bind:value={convertReality} bind:value={convertReality}
rows="2" rows="2"
placeholder="Wie war es wirklich?" placeholder={$_('firsts.list_view.placeholder_reality')}
></textarea> ></textarea>
</label> </label>
<label class="ed-field"> <label class="ed-field">
<span class="ed-label">Notizen</span> <span class="ed-label">{$_('firsts.list_view.convert_notes')}</span>
<textarea <textarea
class="ed-textarea" class="ed-textarea"
bind:value={convertNote} bind:value={convertNote}
rows="2" rows="2"
placeholder="Was willst du festhalten?" placeholder={$_('firsts.list_view.placeholder_notes')}
></textarea> ></textarea>
</label> </label>
<label class="ed-field"> <label class="ed-field">
<span class="ed-label">Mit wem?</span> <span class="ed-label">{$_('firsts.list_view.convert_with_whom')}</span>
<input <input
type="text" type="text"
class="ed-input-sm" class="ed-input-sm"
bind:value={convertSharedWith} bind:value={convertSharedWith}
placeholder="Alleine, mit Lisa, ..." placeholder={$_('firsts.list_view.placeholder_shared_with')}
/> />
</label> </label>
@ -405,15 +411,23 @@
class:active={convertWouldRepeat === opt} class:active={convertWouldRepeat === opt}
onclick={() => (convertWouldRepeat = convertWouldRepeat === opt ? null : opt)} onclick={() => (convertWouldRepeat = convertWouldRepeat === opt ? null : opt)}
> >
{opt === 'no' ? 'Nein' : opt === 'yes' ? 'Ja' : 'Definitiv!'} {opt === 'no'
? $_('firsts.list_view.repeat_no')
: opt === 'yes'
? $_('firsts.list_view.repeat_yes')
: $_('firsts.list_view.repeat_definitely')}
</button> </button>
{/each} {/each}
</div> </div>
</div> </div>
<div class="ed-actions"> <div class="ed-actions">
<button class="ed-btn" onclick={() => (convertingId = null)}>Abbrechen</button> <button class="ed-btn" onclick={() => (convertingId = null)}
<button class="ed-btn primary" onclick={saveConvert}>Erlebt!</button> >{$_('firsts.list_view.action_cancel')}</button
>
<button class="ed-btn primary" onclick={saveConvert}
>{$_('firsts.list_view.action_lived')}</button
>
</div> </div>
</div> </div>
{:else if editingId === first.id} {:else if editingId === first.id}
@ -431,14 +445,14 @@
class="ed-title" class="ed-title"
type="text" type="text"
bind:value={editTitle} bind:value={editTitle}
placeholder="Titel..." placeholder={$_('firsts.list_view.placeholder_title')}
autofocus autofocus
/> />
<div class="ed-row"> <div class="ed-row">
<select class="cat-select" bind:value={editCategory}> <select class="cat-select" bind:value={editCategory}>
{#each CATEGORIES as cat} {#each CATEGORIES as cat}
<option value={cat}>{CATEGORY_LABELS[cat].de}</option> <option value={cat}>{$_('firsts.categories.' + cat)}</option>
{/each} {/each}
</select> </select>
</div> </div>
@ -448,10 +462,10 @@
class="ed-textarea" class="ed-textarea"
bind:value={editMotivation} bind:value={editMotivation}
rows="2" rows="2"
placeholder="Warum will ich das erleben?" placeholder={$_('firsts.list_view.placeholder_motivation')}
></textarea> ></textarea>
<div class="ed-row"> <div class="ed-row">
<span class="ed-label">Priorität</span> <span class="ed-label">{$_('firsts.list_view.label_priority')}</span>
<div class="priority-picker"> <div class="priority-picker">
{#each [1, 2, 3] as const as p} {#each [1, 2, 3] as const as p}
<button <button
@ -459,35 +473,39 @@
class:active={editPriority === p} class:active={editPriority === p}
onclick={() => (editPriority = editPriority === p ? null : p)} onclick={() => (editPriority = editPriority === p ? null : p)}
> >
{PRIORITY_LABELS[p].de} {$_('firsts.priorities.' + p)}
</button> </button>
{/each} {/each}
</div> </div>
</div> </div>
{:else} {:else}
<label class="ed-field"> <label class="ed-field">
<span class="ed-label">Datum</span> <span class="ed-label">{$_('firsts.list_view.label_date')}</span>
<input type="date" bind:value={editDate} class="ed-input-sm" /> <input type="date" bind:value={editDate} class="ed-input-sm" />
</label> </label>
<textarea <textarea
class="ed-textarea" class="ed-textarea"
bind:value={editExpectation} bind:value={editExpectation}
rows="2" rows="2"
placeholder="Ich dachte vorher..." placeholder={$_('firsts.list_view.convert_expected_label')}
></textarea> ></textarea>
<textarea <textarea
class="ed-textarea" class="ed-textarea"
bind:value={editReality} bind:value={editReality}
rows="2" rows="2"
placeholder="Es war tatsächlich..." placeholder={$_('firsts.list_view.convert_reality_label')}
></textarea> ></textarea>
<textarea class="ed-textarea" bind:value={editNote} rows="2" placeholder="Notizen..." <textarea
class="ed-textarea"
bind:value={editNote}
rows="2"
placeholder={$_('firsts.list_view.convert_notes')}
></textarea> ></textarea>
<input <input
type="text" type="text"
class="ed-input-sm" class="ed-input-sm"
bind:value={editSharedWith} bind:value={editSharedWith}
placeholder="Mit wem?" placeholder={$_('firsts.list_view.placeholder_shared_with_short')}
/> />
<div class="ed-row"> <div class="ed-row">
@ -509,7 +527,11 @@
class:active={editWouldRepeat === opt} class:active={editWouldRepeat === opt}
onclick={() => (editWouldRepeat = editWouldRepeat === opt ? null : opt)} onclick={() => (editWouldRepeat = editWouldRepeat === opt ? null : opt)}
> >
{opt === 'no' ? 'Nein' : opt === 'yes' ? 'Ja' : 'Definitiv!'} {opt === 'no'
? $_('firsts.list_view.repeat_no')
: opt === 'yes'
? $_('firsts.list_view.repeat_yes')
: $_('firsts.list_view.repeat_definitely')}
</button> </button>
{/each} {/each}
</div> </div>
@ -517,8 +539,12 @@
{/if} {/if}
<div class="ed-actions"> <div class="ed-actions">
<button class="ed-btn danger" onclick={() => handleDelete(first.id)}>Löschen</button> <button class="ed-btn danger" onclick={() => handleDelete(first.id)}
<button class="ed-btn primary" onclick={saveEdit}>Fertig</button> >{$_('firsts.list_view.action_delete')}</button
>
<button class="ed-btn primary" onclick={saveEdit}
>{$_('firsts.list_view.action_done')}</button
>
</div> </div>
</div> </div>
{:else} {:else}
@ -563,10 +589,10 @@
<span class="dot">{'\u00b7'}</span> <span class="dot">{'\u00b7'}</span>
<span class="repeat-badge"> <span class="repeat-badge">
{first.wouldRepeat === 'definitely' {first.wouldRepeat === 'definitely'
? 'Definitiv nochmal' ? $_('firsts.list_view.repeat_definitely_again')
: first.wouldRepeat === 'yes' : first.wouldRepeat === 'yes'
? 'Nochmal' ? $_('firsts.list_view.repeat_again')
: 'Einmal reicht'} : $_('firsts.list_view.repeat_once')}
</span> </span>
{/if} {/if}
</div> </div>
@ -574,13 +600,13 @@
<div class="exp-vs-real"> <div class="exp-vs-real">
{#if first.expectation} {#if first.expectation}
<div class="exp-line"> <div class="exp-line">
<span class="exp-label">Vorher:</span> <span class="exp-label">{$_('firsts.list_view.label_before')}</span>
{first.expectation} {first.expectation}
</div> </div>
{/if} {/if}
{#if first.reality} {#if first.reality}
<div class="exp-line"> <div class="exp-line">
<span class="exp-label">Nachher:</span> <span class="exp-label">{$_('firsts.list_view.label_after')}</span>
{first.reality} {first.reality}
</div> </div>
{/if} {/if}
@ -594,7 +620,7 @@
<div class="card-meta"> <div class="card-meta">
{#if first.priority} {#if first.priority}
<span class="prio-badge prio-{first.priority}" <span class="prio-badge prio-{first.priority}"
>{PRIORITY_LABELS[first.priority].de}</span >{$_('firsts.priorities.' + first.priority)}</span
> >
{/if} {/if}
{#if first.sharedWith} {#if first.sharedWith}
@ -612,19 +638,19 @@
startConvert(first); startConvert(first);
}} }}
> >
Erlebt! {$_('firsts.list_view.action_lived')}
</button> </button>
{/if} {/if}
<span class="cat-label" style="color: {CATEGORY_COLORS[first.category]}"> <span class="cat-label" style="color: {CATEGORY_COLORS[first.category]}">
{CATEGORY_LABELS[first.category].de} {$_('firsts.categories.' + first.category)}
</span> </span>
</div> </div>
{/if} {/if}
{/each} {/each}
{#if filtered().length === 0 && allFirsts.length > 0} {#if filtered().length === 0 && allFirsts.length > 0}
<p class="empty">Keine Treffer</p> <p class="empty">{$_('firsts.list_view.empty_no_results')}</p>
{/if} {/if}
</div> </div>
{/if} {/if}
@ -635,7 +661,7 @@
{#each [3, 2, 1] as prio} {#each [3, 2, 1] as prio}
{@const group = filtered().filter((f) => (f.priority ?? 1) === prio)} {@const group = filtered().filter((f) => (f.priority ?? 1) === prio)}
{#if group.length > 0} {#if group.length > 0}
<div class="month-label">{PRIORITY_LABELS[prio as FirstPriority].de}</div> <div class="month-label">{$_('firsts.priorities.' + prio)}</div>
{#each group as first (first.id)} {#each group as first (first.id)}
<div <div
class="entry-card dream" class="entry-card dream"
@ -667,7 +693,7 @@
startConvert(first); startConvert(first);
}} }}
> >
Erlebt! {$_('firsts.list_view.action_lived')}
</button> </button>
</div> </div>
{/each} {/each}
@ -675,7 +701,7 @@
{/each} {/each}
{#if dreams.length === 0} {#if dreams.length === 0}
<p class="empty">Keine Dreams. Füge dein erstes Wunsch-Erlebnis hinzu!</p> <p class="empty">{$_('firsts.list_view.empty_no_dreams')}</p>
{/if} {/if}
</div> </div>
{/if} {/if}
@ -685,7 +711,7 @@
<div class="entry-list"> <div class="entry-list">
{#each [...personGroups.entries()] as [personKey, firsts] (personKey)} {#each [...personGroups.entries()] as [personKey, firsts] (personKey)}
<div class="month-label"> <div class="month-label">
{personKey === '__alone' ? 'Alleine' : personKey} {personKey === '__alone' ? $_('firsts.list_view.people_alone') : personKey}
<span class="group-count">({firsts.length})</span> <span class="group-count">({firsts.length})</span>
</div> </div>
{#each firsts as first (first.id)} {#each firsts as first (first.id)}
@ -707,20 +733,20 @@
{#if first.status === 'lived' && first.date} {#if first.status === 'lived' && first.date}
<span class="row-date">{formatDate(first.date)}</span> <span class="row-date">{formatDate(first.date)}</span>
{:else} {:else}
<span class="row-dream-tag">Dream</span> <span class="row-dream-tag">{$_('firsts.list_view.row_dream_tag')}</span>
{/if} {/if}
</div> </div>
{/each} {/each}
{/each} {/each}
{#if allFirsts.length === 0} {#if allFirsts.length === 0}
<p class="empty">Noch keine Einträge.</p> <p class="empty">{$_('firsts.list_view.empty_no_entries')}</p>
{/if} {/if}
</div> </div>
{/if} {/if}
{#if allFirsts.length === 0} {#if allFirsts.length === 0}
<p class="empty">Halte dein erstes "Erstes Mal" fest!</p> <p class="empty">{$_('firsts.list_view.empty_no_firsts')}</p>
{/if} {/if}
<ContextMenu <ContextMenu

View file

@ -121,7 +121,6 @@
"apps/mana/apps/web/src/lib/modules/dreams/views/SymbolDetailView.svelte": 8, "apps/mana/apps/web/src/lib/modules/dreams/views/SymbolDetailView.svelte": 8,
"apps/mana/apps/web/src/lib/modules/drink/ListView.svelte": 5, "apps/mana/apps/web/src/lib/modules/drink/ListView.svelte": 5,
"apps/mana/apps/web/src/lib/modules/finance/ListView.svelte": 6, "apps/mana/apps/web/src/lib/modules/finance/ListView.svelte": 6,
"apps/mana/apps/web/src/lib/modules/firsts/ListView.svelte": 15,
"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/guides/views/DetailView.svelte": 7, "apps/mana/apps/web/src/lib/modules/guides/views/DetailView.svelte": 7,

View file

@ -6,6 +6,7 @@
"apps/mana/apps/web/src/lib/modules/ai-agents/ListView.svelte": 2, "apps/mana/apps/web/src/lib/modules/ai-agents/ListView.svelte": 2,
"apps/mana/apps/web/src/lib/modules/ai-missions/ListView.svelte": 2, "apps/mana/apps/web/src/lib/modules/ai-missions/ListView.svelte": 2,
"apps/mana/apps/web/src/lib/modules/credits/ListView.svelte": 1, "apps/mana/apps/web/src/lib/modules/credits/ListView.svelte": 1,
"apps/mana/apps/web/src/lib/modules/firsts/ListView.svelte": 2,
"apps/mana/apps/web/src/lib/modules/invoices/components/StatusBadge.svelte": 1, "apps/mana/apps/web/src/lib/modules/invoices/components/StatusBadge.svelte": 1,
"apps/mana/apps/web/src/lib/modules/invoices/constants.ts": 1, "apps/mana/apps/web/src/lib/modules/invoices/constants.ts": 1,
"apps/mana/apps/web/src/lib/modules/invoices/ListView.svelte": 1, "apps/mana/apps/web/src/lib/modules/invoices/ListView.svelte": 1,