i18n(music+profile): translate detail/hub views via $_()

- music/views/DetailView: route through music.detail.* (new namespace)
- profile/ListView: route through profile.hub.* (new sub-namespace)
  + reuse existing profile.* keys for account-section actions; TABS
  refactored from literal label → labelKey routing through $_()

Baseline 881 → 869 (-12).
This commit is contained in:
Till JS 2026-04-27 18:41:43 +02:00
parent fa401cfeec
commit a5cef980ae
3 changed files with 61 additions and 45 deletions

View file

@ -10,6 +10,7 @@
import { Heart } from '@mana/shared-icons'; import { Heart } from '@mana/shared-icons';
import type { ViewProps } from '$lib/app-registry'; import type { ViewProps } from '$lib/app-registry';
import type { LocalSong } from '../types'; import type { LocalSong } from '../types';
import { _ } from 'svelte-i18n';
let { params, goBack }: ViewProps = $props(); let { params, goBack }: ViewProps = $props();
let songId = $derived(params.songId as string); let songId = $derived(params.songId as string);
@ -38,7 +39,7 @@
async function saveField() { async function saveField() {
detail.blur(); detail.blur();
await libraryStore.updateMetadata(songId, { await libraryStore.updateMetadata(songId, {
title: editTitle.trim() || detail.entity?.title || 'Ohne Titel', title: editTitle.trim() || detail.entity?.title || $_('music.detail.title_fallback'),
artist: editArtist.trim() || undefined, artist: editArtist.trim() || undefined,
album: editAlbum.trim() || undefined, album: editAlbum.trim() || undefined,
genre: editGenre.trim() || undefined, genre: editGenre.trim() || undefined,
@ -62,14 +63,14 @@
<DetailViewShell <DetailViewShell
entity={detail.entity} entity={detail.entity}
loading={detail.loading} loading={detail.loading}
notFoundLabel="Song nicht gefunden" notFoundLabel={$_('music.detail.not_found')}
confirmDelete={detail.confirmDelete} confirmDelete={detail.confirmDelete}
onAskDelete={detail.askDelete} onAskDelete={detail.askDelete}
onCancelDelete={detail.cancelDelete} onCancelDelete={detail.cancelDelete}
confirmDeleteLabel="Song wirklich löschen?" confirmDeleteLabel={$_('music.detail.confirm_delete')}
onConfirmDelete={() => onConfirmDelete={() =>
detail.deleteWithUndo({ detail.deleteWithUndo({
label: 'Song gelöscht', label: $_('music.detail.toast_deleted'),
delete: () => libraryStore.delete(songId), delete: () => libraryStore.delete(songId),
goBack, goBack,
})} })}
@ -81,7 +82,7 @@
bind:value={editTitle} bind:value={editTitle}
onfocus={detail.focus} onfocus={detail.focus}
onblur={saveField} onblur={saveField}
placeholder="Titel..." placeholder={$_('music.detail.placeholder_title')}
/> />
<button class="fav-btn" class:active={song.favorite} onclick={toggleFavorite}> <button class="fav-btn" class:active={song.favorite} onclick={toggleFavorite}>
<Heart size={18} weight={song.favorite ? 'fill' : 'regular'} /> <Heart size={18} weight={song.favorite ? 'fill' : 'regular'} />
@ -90,18 +91,18 @@
<div class="properties"> <div class="properties">
<div class="prop-row"> <div class="prop-row">
<span class="prop-label">Künstler</span> <span class="prop-label">{$_('music.detail.prop_artist')}</span>
<input <input
class="prop-input" class="prop-input"
bind:value={editArtist} bind:value={editArtist}
onfocus={detail.focus} onfocus={detail.focus}
onblur={saveField} onblur={saveField}
placeholder="Unbekannt" placeholder={$_('music.detail.prop_artist_placeholder')}
/> />
</div> </div>
<div class="prop-row"> <div class="prop-row">
<span class="prop-label">Album</span> <span class="prop-label">{$_('music.detail.prop_album')}</span>
<input <input
class="prop-input" class="prop-input"
bind:value={editAlbum} bind:value={editAlbum}
@ -112,7 +113,7 @@
</div> </div>
<div class="prop-row"> <div class="prop-row">
<span class="prop-label">Genre</span> <span class="prop-label">{$_('music.detail.prop_genre')}</span>
<input <input
class="prop-input" class="prop-input"
bind:value={editGenre} bind:value={editGenre}
@ -123,7 +124,7 @@
</div> </div>
<div class="prop-row"> <div class="prop-row">
<span class="prop-label">Jahr</span> <span class="prop-label">{$_('music.detail.prop_year')}</span>
<input <input
type="number" type="number"
class="prop-input" class="prop-input"
@ -135,7 +136,7 @@
</div> </div>
<div class="prop-row"> <div class="prop-row">
<span class="prop-label">BPM</span> <span class="prop-label">{$_('music.detail.prop_bpm')}</span>
<input <input
type="number" type="number"
class="prop-input" class="prop-input"
@ -147,23 +148,35 @@
</div> </div>
<div class="prop-row"> <div class="prop-row">
<span class="prop-label">Dauer</span> <span class="prop-label">{$_('music.detail.prop_duration')}</span>
<span class="prop-value">{formatDuration(song.duration)}</span> <span class="prop-value">{formatDuration(song.duration)}</span>
</div> </div>
<div class="prop-row"> <div class="prop-row">
<span class="prop-label">Wiedergaben</span> <span class="prop-label">{$_('music.detail.prop_play_count')}</span>
<span class="prop-value">{song.playCount}</span> <span class="prop-value">{song.playCount}</span>
</div> </div>
</div> </div>
<div class="meta"> <div class="meta">
<span>Erstellt: {formatDate(new Date(song.createdAt ?? ''))}</span> <span
>{$_('music.detail.meta_created', {
values: { date: formatDate(new Date(song.createdAt ?? '')) },
})}</span
>
{#if song.updatedAt} {#if song.updatedAt}
<span>Bearbeitet: {formatDate(new Date(song.updatedAt))}</span> <span
>{$_('music.detail.meta_updated', {
values: { date: formatDate(new Date(song.updatedAt)) },
})}</span
>
{/if} {/if}
{#if song.lastPlayedAt} {#if song.lastPlayedAt}
<span>Zuletzt gehört: {formatDate(new Date(song.lastPlayedAt))}</span> <span
>{$_('music.detail.meta_last_played', {
values: { date: formatDate(new Date(song.lastPlayedAt)) },
})}</span
>
{/if} {/if}
</div> </div>
{/snippet} {/snippet}

View file

@ -17,6 +17,7 @@
import ContextFreeform from './ContextFreeform.svelte'; import ContextFreeform from './ContextFreeform.svelte';
import { useUserContext } from './queries'; import { useUserContext } from './queries';
import { getProgress } from './questions'; import { getProgress } from './questions';
import { _ } from 'svelte-i18n';
type Tab = 'overview' | 'interview' | 'freeform' | 'account'; type Tab = 'overview' | 'interview' | 'freeform' | 'account';
type InterviewStartMode = 'text' | 'voice' | 'conversation'; type InterviewStartMode = 'text' | 'voice' | 'conversation';
@ -44,11 +45,11 @@
} }
}); });
const TABS: { key: Tab; label: string }[] = [ const TABS: { key: Tab; labelKey: string }[] = [
{ key: 'overview', label: 'Übersicht' }, { key: 'overview', labelKey: 'profile.hub.tab_overview' },
{ key: 'interview', label: 'Interview' }, { key: 'interview', labelKey: 'profile.hub.tab_interview' },
{ key: 'freeform', label: 'Freitext' }, { key: 'freeform', labelKey: 'profile.hub.tab_freeform' },
{ key: 'account', label: 'Konto' }, { key: 'account', labelKey: 'profile.hub.tab_account' },
]; ];
function startInterview(mode: InterviewStartMode) { function startInterview(mode: InterviewStartMode) {
@ -58,15 +59,15 @@
function handleProfileUpdate(user: ApiUserProfile) { function handleProfileUpdate(user: ApiUserProfile) {
apiProfile = user; apiProfile = user;
toast.success('Profil erfolgreich aktualisiert'); toast.success($_('profile.profile_updated'));
} }
function handlePasswordChange() { function handlePasswordChange() {
toast.success('Passwort erfolgreich geändert'); toast.success($_('profile.password_changed'));
} }
async function handleAccountDeleted() { async function handleAccountDeleted() {
toast.info('Konto wird gelöscht...'); toast.info($_('profile.hub.toast_account_deleting'));
await authStore.signOut(); await authStore.signOut();
goto('/login'); goto('/login');
} }
@ -89,7 +90,7 @@
if (tab.key !== 'interview') interviewStartMode = null; if (tab.key !== 'interview') interviewStartMode = null;
}} }}
> >
{tab.label} {$_(tab.labelKey)}
</button> </button>
{/each} {/each}
</nav> </nav>
@ -102,12 +103,14 @@
<!-- Interview start hero --> <!-- Interview start hero -->
<div class="interview-hero"> <div class="interview-hero">
<div class="hero-header"> <div class="hero-header">
<h3 class="hero-title">Interview starten</h3> <h3 class="hero-title">{$_('profile.hub.hero_title')}</h3>
<p class="hero-subtitle"> <p class="hero-subtitle">
{#if progress.percent > 0} {#if progress.percent > 0}
{progress.answered} von {progress.total} Fragen beantwortet — mach weiter! {$_('profile.hub.hero_subtitle_progress', {
values: { answered: progress.answered, total: progress.total },
})}
{:else} {:else}
Erzähl Mana mehr über dich, damit die App besser zu dir passt. {$_('profile.hub.hero_subtitle_initial')}
{/if} {/if}
</p> </p>
{#if progress.percent > 0} {#if progress.percent > 0}
@ -133,8 +136,8 @@
</svg> </svg>
</span> </span>
<span class="hero-option-text"> <span class="hero-option-text">
<strong>Per Text</strong> <strong>{$_('profile.hub.option_text_title')}</strong>
<span>Fragen lesen und tippen</span> <span>{$_('profile.hub.option_text_hint')}</span>
</span> </span>
</button> </button>
<button class="hero-option voice" onclick={() => startInterview('voice')}> <button class="hero-option voice" onclick={() => startInterview('voice')}>
@ -156,8 +159,8 @@
</svg> </svg>
</span> </span>
<span class="hero-option-text"> <span class="hero-option-text">
<strong>Per Sprache</strong> <strong>{$_('profile.hub.option_voice_title')}</strong>
<span>Fragen hören und sprechen</span> <span>{$_('profile.hub.option_voice_hint')}</span>
</span> </span>
</button> </button>
<button class="hero-option conversation" onclick={() => startInterview('conversation')}> <button class="hero-option conversation" onclick={() => startInterview('conversation')}>
@ -176,8 +179,8 @@
</svg> </svg>
</span> </span>
<span class="hero-option-text"> <span class="hero-option-text">
<strong>Als Gespräch</strong> <strong>{$_('profile.hub.option_conversation_title')}</strong>
<span>Fließend — Antworten werden automatisch gespeichert</span> <span>{$_('profile.hub.option_conversation_hint')}</span>
</span> </span>
</button> </button>
</div> </div>
@ -197,7 +200,11 @@
<div class="account-card"> <div class="account-card">
<div class="account-header"> <div class="account-header">
{#if apiProfile?.image} {#if apiProfile?.image}
<img src={apiProfile.image} alt="Avatar" class="account-avatar" /> <img
src={apiProfile.image}
alt={$_('profile.hub.avatar_alt')}
class="account-avatar"
/>
{:else} {:else}
<div class="account-avatar-placeholder"> <div class="account-avatar-placeholder">
{(apiProfile?.name ?? 'U').slice(0, 2).toUpperCase()} {(apiProfile?.name ?? 'U').slice(0, 2).toUpperCase()}
@ -212,16 +219,14 @@
<div class="account-actions"> <div class="account-actions">
<button class="account-btn" onclick={() => goto('/profile/me-images')}> <button class="account-btn" onclick={() => goto('/profile/me-images')}>
Meine Bilder {$_('profile.hub.action_my_images')}
<span class="account-btn-hint"> <span class="account-btn-hint">{$_('profile.hub.action_my_images_hint')}</span>
Gesichts- und Ganzkörperbilder für KI-Bildgenerierung
</span>
</button> </button>
<button class="account-btn" onclick={() => (showEditModal = true)}> <button class="account-btn" onclick={() => (showEditModal = true)}>
Profil bearbeiten {$_('profile.edit')}
</button> </button>
<button class="account-btn" onclick={() => (showPasswordModal = true)}> <button class="account-btn" onclick={() => (showPasswordModal = true)}>
Passwort ändern {$_('profile.change_password')}
</button> </button>
<button <button
class="account-btn" class="account-btn"
@ -230,10 +235,10 @@
goto('/login'); goto('/login');
}} }}
> >
Abmelden {$_('profile.logout')}
</button> </button>
<button class="account-btn danger" onclick={() => (showDeleteModal = true)}> <button class="account-btn danger" onclick={() => (showDeleteModal = true)}>
Konto löschen {$_('profile.delete_account')}
</button> </button>
</div> </div>
</div> </div>

View file

@ -130,7 +130,6 @@
"apps/mana/apps/web/src/lib/modules/mood/components/QuickLog.svelte": 6, "apps/mana/apps/web/src/lib/modules/mood/components/QuickLog.svelte": 6,
"apps/mana/apps/web/src/lib/modules/moodlit/components/mood/CreateMoodDialog.svelte": 3, "apps/mana/apps/web/src/lib/modules/moodlit/components/mood/CreateMoodDialog.svelte": 3,
"apps/mana/apps/web/src/lib/modules/music/ListView.svelte": 3, "apps/mana/apps/web/src/lib/modules/music/ListView.svelte": 3,
"apps/mana/apps/web/src/lib/modules/music/views/DetailView.svelte": 6,
"apps/mana/apps/web/src/lib/modules/news-research/ListView.svelte": 3, "apps/mana/apps/web/src/lib/modules/news-research/ListView.svelte": 3,
"apps/mana/apps/web/src/lib/modules/news/widgets/NewsUnreadWidget.svelte": 2, "apps/mana/apps/web/src/lib/modules/news/widgets/NewsUnreadWidget.svelte": 2,
"apps/mana/apps/web/src/lib/modules/notes/ListView.svelte": 4, "apps/mana/apps/web/src/lib/modules/notes/ListView.svelte": 4,
@ -147,7 +146,6 @@
"apps/mana/apps/web/src/lib/modules/profile/components/MeImageSlotCard.svelte": 3, "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/components/MeImageTile.svelte": 3,
"apps/mana/apps/web/src/lib/modules/profile/ContextInterview.svelte": 6, "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, "apps/mana/apps/web/src/lib/modules/profile/MeImagesView.svelte": 2,
"apps/mana/apps/web/src/lib/modules/questions/ListView.svelte": 1, "apps/mana/apps/web/src/lib/modules/questions/ListView.svelte": 1,
"apps/mana/apps/web/src/lib/modules/questions/views/DetailView.svelte": 6, "apps/mana/apps/web/src/lib/modules/questions/views/DetailView.svelte": 6,