i18n(memoro): translate views/DetailView via $_() — title sources, statuses, fields, transcript

- TITLE_SOURCE_LABELS map → TITLE_SOURCE_KEYS routing through $_(memoro.detail_view.title_sources.*)
- statusLabels map → STATUS_KEYS routing through $_(memoro.detail_view.statuses.*)
- Shell labels (notFound/confirmDelete/toast_deleted)
- Title placeholder: idle vs generating variant
- 4 prop rows (Status/Dauer/Sprache/Sichtbarkeit) + lang placeholder
- Section labels (Zusammenfassung/Transkript) + transcript states (transcribing/failed/empty/source)
- Meta-row Erstellt/Bearbeitet with {date}

Baselines: hardcoded 1025 → 1017 (8 cleared); missing-keys baseline unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-27 15:07:35 +02:00
parent d391a603f7
commit 98ce33e788
2 changed files with 44 additions and 37 deletions

View file

@ -15,18 +15,16 @@
import { VisibilityPicker, type VisibilityLevel } from '@mana/shared-privacy'; import { VisibilityPicker, type VisibilityLevel } from '@mana/shared-privacy';
import type { ViewProps } from '$lib/app-registry'; import type { ViewProps } from '$lib/app-registry';
import type { LocalMemo, ProcessingStatus } from '../types'; import type { LocalMemo, ProcessingStatus } from '../types';
import { _ } from 'svelte-i18n';
// Human-readable labels for the title-source badge below the title // Map LlmTier → i18n key (mana-server keyed as 'mana_server' since dots are
// input. We use these specific strings (not @mana/shared-llm's // reserved path separators). Strings live in memoro.detail_view.title_sources.
// generic tierLabel) so we can surface the actual model family const TITLE_SOURCE_KEYS: Record<LlmTier, string> = {
// — both browser and mana-server now run Gemma 4 variants, so the none: 'memoro.detail_view.title_sources.none',
// label stays coherent across tiers. browser: 'memoro.detail_view.title_sources.browser',
const TITLE_SOURCE_LABELS: Record<LlmTier, string> = { 'mana-server': 'memoro.detail_view.title_sources.mana_server',
none: 'Lokal (regelbasiert)', byok: 'memoro.detail_view.title_sources.byok',
browser: 'Auf deinem Gerät (Gemma 4 E2B)', cloud: 'memoro.detail_view.title_sources.cloud',
'mana-server': 'Mana-Server (Gemma 4 E4B)',
byok: 'Dein API-Key',
cloud: 'Google Gemini',
}; };
function isLlmTier(value: unknown): value is LlmTier { function isLlmTier(value: unknown): value is LlmTier {
@ -84,11 +82,11 @@
return `${m}:${String(s).padStart(2, '0')}`; return `${m}:${String(s).padStart(2, '0')}`;
} }
const statusLabels: Record<ProcessingStatus, string> = { const STATUS_KEYS: Record<ProcessingStatus, string> = {
pending: 'Ausstehend', pending: 'memoro.detail_view.statuses.pending',
processing: 'Wird verarbeitet', processing: 'memoro.detail_view.statuses.processing',
completed: 'Fertig', completed: 'memoro.detail_view.statuses.completed',
failed: 'Fehlgeschlagen', failed: 'memoro.detail_view.statuses.failed',
}; };
const statusColors: Record<ProcessingStatus, string> = { const statusColors: Record<ProcessingStatus, string> = {
@ -130,21 +128,21 @@
if (detail.focused) return null; if (detail.focused) return null;
const metadata = (memo.metadata as Record<string, unknown> | null) ?? {}; const metadata = (memo.metadata as Record<string, unknown> | null) ?? {};
const source = metadata.titleSource; const source = metadata.titleSource;
return isLlmTier(source) ? TITLE_SOURCE_LABELS[source] : null; return isLlmTier(source) ? $_(TITLE_SOURCE_KEYS[source]) : null;
}); });
</script> </script>
<DetailViewShell <DetailViewShell
entity={detail.entity} entity={detail.entity}
loading={detail.loading} loading={detail.loading}
notFoundLabel="Memo nicht gefunden" notFoundLabel={$_('memoro.detail_view.not_found')}
confirmDelete={detail.confirmDelete} confirmDelete={detail.confirmDelete}
onAskDelete={detail.askDelete} onAskDelete={detail.askDelete}
onCancelDelete={detail.cancelDelete} onCancelDelete={detail.cancelDelete}
confirmDeleteLabel="Memo wirklich löschen?" confirmDeleteLabel={$_('memoro.detail_view.confirm_delete')}
onConfirmDelete={() => onConfirmDelete={() =>
detail.deleteWithUndo({ detail.deleteWithUndo({
label: 'Memo gelöscht', label: $_('memoro.detail_view.toast_deleted'),
delete: () => memosStore.delete(memoId), delete: () => memosStore.delete(memoId),
goBack, goBack,
})} })}
@ -156,7 +154,9 @@
bind:value={editTitle} bind:value={editTitle}
onfocus={detail.focus} onfocus={detail.focus}
onblur={saveField} onblur={saveField}
placeholder={titleIsGenerating && !editTitle ? 'Titel wird generiert…' : 'Titel…'} placeholder={titleIsGenerating && !editTitle
? $_('memoro.detail_view.placeholder_title_generating')
: $_('memoro.detail_view.placeholder_title_idle')}
/> />
<button class="pin-btn" class:pinned={memo.isPinned} onclick={togglePin}> <button class="pin-btn" class:pinned={memo.isPinned} onclick={togglePin}>
<PushPin size={16} /> <PushPin size={16} />
@ -169,30 +169,30 @@
<div class="properties"> <div class="properties">
<div class="prop-row"> <div class="prop-row">
<span class="prop-label">Status</span> <span class="prop-label">{$_('memoro.detail_view.label_status')}</span>
<span class="prop-value" style="color: {statusColors[memo.processingStatus]}"> <span class="prop-value" style="color: {statusColors[memo.processingStatus]}">
{statusLabels[memo.processingStatus]} {$_(STATUS_KEYS[memo.processingStatus])}
</span> </span>
</div> </div>
<div class="prop-row"> <div class="prop-row">
<span class="prop-label">Dauer</span> <span class="prop-label">{$_('memoro.detail_view.label_duration')}</span>
<span class="prop-value">{formatDuration(memo.audioDurationMs)}</span> <span class="prop-value">{formatDuration(memo.audioDurationMs)}</span>
</div> </div>
<div class="prop-row"> <div class="prop-row">
<span class="prop-label">Sprache</span> <span class="prop-label">{$_('memoro.detail_view.label_language')}</span>
<input <input
class="prop-input" class="prop-input"
bind:value={editLanguage} bind:value={editLanguage}
onfocus={detail.focus} onfocus={detail.focus}
onblur={saveField} onblur={saveField}
placeholder="z.B. de" placeholder={$_('memoro.detail_view.placeholder_language')}
/> />
</div> </div>
<div class="prop-row"> <div class="prop-row">
<span class="prop-label">Sichtbarkeit</span> <span class="prop-label">{$_('memoro.detail_view.label_visibility')}</span>
<VisibilityPicker <VisibilityPicker
level={memo.visibility ?? 'space'} level={memo.visibility ?? 'space'}
onChange={(next: VisibilityLevel) => memosStore.setVisibility(memoId, next)} onChange={(next: VisibilityLevel) => memosStore.setVisibility(memoId, next)}
@ -202,42 +202,50 @@
</div> </div>
<div class="section"> <div class="section">
<span class="section-label">Zusammenfassung</span> <span class="section-label">{$_('memoro.detail_view.section_summary')}</span>
<textarea <textarea
class="description-input" class="description-input"
bind:value={editIntro} bind:value={editIntro}
onfocus={detail.focus} onfocus={detail.focus}
onblur={saveField} onblur={saveField}
placeholder="Zusammenfassung hinzufügen..." placeholder={$_('memoro.detail_view.placeholder_summary')}
rows={2} rows={2}
></textarea> ></textarea>
</div> </div>
<div class="section"> <div class="section">
<span class="section-label">Transkript</span> <span class="section-label">{$_('memoro.detail_view.section_transcript')}</span>
{#if memo.processingStatus === 'processing'} {#if memo.processingStatus === 'processing'}
<div class="transcript transcript-loading"> <div class="transcript transcript-loading">
<span class="loading-dot"></span> <span class="loading-dot"></span>
<span class="loading-dot"></span> <span class="loading-dot"></span>
<span class="loading-dot"></span> <span class="loading-dot"></span>
<span>Wird transkribiert…</span> <span>{$_('memoro.detail_view.transcribing')}</span>
</div> </div>
{:else if memo.processingStatus === 'failed'} {:else if memo.processingStatus === 'failed'}
<div class="transcript transcript-failed"> <div class="transcript transcript-failed">
Transkription fehlgeschlagen. Versuche es erneut oder gib das Transkript manuell ein. {$_('memoro.detail_view.transcript_failed')}
</div> </div>
{:else if memo.transcript} {:else if memo.transcript}
<div class="transcript">{memo.transcript}</div> <div class="transcript">{memo.transcript}</div>
<div class="source-label">Voxtral via mana-stt</div> <div class="source-label">{$_('memoro.detail_view.transcript_source')}</div>
{:else} {:else}
<div class="transcript transcript-empty">Kein Transkript vorhanden.</div> <div class="transcript transcript-empty">{$_('memoro.detail_view.transcript_empty')}</div>
{/if} {/if}
</div> </div>
<div class="meta"> <div class="meta">
<span>Erstellt: {formatDate(new Date(memo.createdAt ?? ''))}</span> <span
>{$_('memoro.detail_view.meta_created', {
values: { date: formatDate(new Date(memo.createdAt ?? '')) },
})}</span
>
{#if memo.updatedAt} {#if memo.updatedAt}
<span>Bearbeitet: {formatDate(new Date(memo.updatedAt))}</span> <span
>{$_('memoro.detail_view.meta_updated', {
values: { date: formatDate(new Date(memo.updatedAt)) },
})}</span
>
{/if} {/if}
</div> </div>
{/snippet} {/snippet}

View file

@ -131,7 +131,6 @@
"apps/mana/apps/web/src/lib/modules/meditate/components/SessionPlayer.svelte": 3, "apps/mana/apps/web/src/lib/modules/meditate/components/SessionPlayer.svelte": 3,
"apps/mana/apps/web/src/lib/modules/meditate/components/StatsOverview.svelte": 3, "apps/mana/apps/web/src/lib/modules/meditate/components/StatsOverview.svelte": 3,
"apps/mana/apps/web/src/lib/modules/meditate/ListView.svelte": 1, "apps/mana/apps/web/src/lib/modules/meditate/ListView.svelte": 1,
"apps/mana/apps/web/src/lib/modules/memoro/views/DetailView.svelte": 8,
"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/mood/ListView.svelte": 6, "apps/mana/apps/web/src/lib/modules/mood/ListView.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,