i18n(gifts/redeem): translate /[code] +page.svelte via $_() — info card, redeem flow, success state

Adds gifts.redeem sub-namespace covering page back-title + page-header,
err_not_found + err_redeem_failed fallbacks, toast_received with
{credits}, success state (heading/credits-label/balance-html/2 link
buttons), gift-not-found "Anderen Code eingeben", info card (Von {name},
Du erhältst, Credits, Art/Status/Gültig bis labels, Nachricht prefix),
section_redeem heading, 3 inactive warnings (depleted/expired/other),
personalized info, action_redeeming + action_redeem, getStatusLabel and
getTypeLabel switch cases routed through $_(). Date formatter switched
to get(locale) ?? 'de'.

Baselines: hardcoded 1009 → 1001 (8 cleared from gifts) + 1 added by
parallel community-eule commit = net 1002; 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:17:27 +02:00
parent ee5bb2871c
commit de2134fb02
7 changed files with 225 additions and 37 deletions

View file

@ -65,5 +65,40 @@
"status_refunded": "Erstattet",
"type_simple_label": "Einfach",
"type_personalized_label": "Persönlich"
},
"redeem": {
"page_back_title": "Gutschein",
"page_title": "Geschenk einlösen",
"page_description": "Löse deinen Geschenk-Code ein",
"err_not_found": "Geschenk-Code nicht gefunden",
"err_redeem_failed": "Einlösen fehlgeschlagen",
"toast_received": "{credits} Credits erhalten!",
"success_heading": "Geschenk eingelöst!",
"success_credits_label": "Credits erhalten",
"success_balance_html": "Dein neuer Kontostand: <span class=\"font-semibold\">{balance} Credits</span>",
"success_link_credits": "Zu meinen Credits",
"success_link_overview": "Geschenke ansehen",
"action_other_code": "Anderen Code eingeben",
"label_from": "Von {name}",
"label_you_get": "Du erhältst",
"label_credits": "Credits",
"label_type": "Art",
"label_status": "Status",
"label_valid_until": "Gültig bis",
"label_message_prefix": "Nachricht:",
"section_redeem": "Einlösen",
"warn_depleted": "Dieses Geschenk wurde bereits eingelöst",
"warn_expired": "Dieses Geschenk ist abgelaufen",
"warn_other": "Dieses Geschenk kann nicht eingelöst werden",
"info_personalized": "Dieses Geschenk ist für eine bestimmte Person. Nur der vorgesehene Empfänger kann es einlösen.",
"action_redeeming": "Wird eingelöst...",
"action_redeem": "🎁 Geschenk einlösen",
"type_simple": "Geschenk",
"type_personalized": "Persönliches Geschenk",
"status_active": "Aktiv",
"status_depleted": "Aufgebraucht",
"status_expired": "Abgelaufen",
"status_cancelled": "Storniert",
"status_refunded": "Erstattet"
}
}

View file

@ -65,5 +65,40 @@
"status_refunded": "Refunded",
"type_simple_label": "Simple",
"type_personalized_label": "Personalized"
},
"redeem": {
"page_back_title": "Voucher",
"page_title": "Redeem gift",
"page_description": "Redeem your gift code",
"err_not_found": "Gift code not found",
"err_redeem_failed": "Redeem failed",
"toast_received": "{credits} credits received!",
"success_heading": "Gift redeemed!",
"success_credits_label": "credits received",
"success_balance_html": "Your new balance: <span class=\"font-semibold\">{balance} credits</span>",
"success_link_credits": "Go to my credits",
"success_link_overview": "View gifts",
"action_other_code": "Enter another code",
"label_from": "From {name}",
"label_you_get": "You'll receive",
"label_credits": "Credits",
"label_type": "Type",
"label_status": "Status",
"label_valid_until": "Valid until",
"label_message_prefix": "Message:",
"section_redeem": "Redeem",
"warn_depleted": "This gift has already been redeemed",
"warn_expired": "This gift has expired",
"warn_other": "This gift can't be redeemed",
"info_personalized": "This gift is for a specific person. Only the intended recipient can redeem it.",
"action_redeeming": "Redeeming…",
"action_redeem": "🎁 Redeem gift",
"type_simple": "Gift",
"type_personalized": "Personalized gift",
"status_active": "Active",
"status_depleted": "Depleted",
"status_expired": "Expired",
"status_cancelled": "Cancelled",
"status_refunded": "Refunded"
}
}

View file

@ -65,5 +65,40 @@
"status_refunded": "Reembolsado",
"type_simple_label": "Simple",
"type_personalized_label": "Personalizado"
},
"redeem": {
"page_back_title": "Vale",
"page_title": "Canjear regalo",
"page_description": "Canjea tu código de regalo",
"err_not_found": "Código de regalo no encontrado",
"err_redeem_failed": "Canje fallido",
"toast_received": "¡{credits} credits recibidos!",
"success_heading": "¡Regalo canjeado!",
"success_credits_label": "credits recibidos",
"success_balance_html": "Nuevo saldo: <span class=\"font-semibold\">{balance} credits</span>",
"success_link_credits": "A mis credits",
"success_link_overview": "Ver regalos",
"action_other_code": "Introducir otro código",
"label_from": "De {name}",
"label_you_get": "Recibes",
"label_credits": "Credits",
"label_type": "Tipo",
"label_status": "Estado",
"label_valid_until": "Válido hasta",
"label_message_prefix": "Mensaje:",
"section_redeem": "Canjear",
"warn_depleted": "Este regalo ya ha sido canjeado",
"warn_expired": "Este regalo ha expirado",
"warn_other": "Este regalo no puede canjearse",
"info_personalized": "Este regalo es para una persona concreta. Solo el destinatario puede canjearlo.",
"action_redeeming": "Canjeando…",
"action_redeem": "🎁 Canjear regalo",
"type_simple": "Regalo",
"type_personalized": "Regalo personalizado",
"status_active": "Activo",
"status_depleted": "Agotado",
"status_expired": "Expirado",
"status_cancelled": "Cancelado",
"status_refunded": "Reembolsado"
}
}

View file

@ -65,5 +65,40 @@
"status_refunded": "Remboursé",
"type_simple_label": "Simple",
"type_personalized_label": "Personnalisé"
},
"redeem": {
"page_back_title": "Bon",
"page_title": "Utiliser le cadeau",
"page_description": "Utilise ton code cadeau",
"err_not_found": "Code cadeau introuvable",
"err_redeem_failed": "Échec de l'utilisation",
"toast_received": "{credits} crédits reçus !",
"success_heading": "Cadeau utilisé !",
"success_credits_label": "crédits reçus",
"success_balance_html": "Nouveau solde : <span class=\"font-semibold\">{balance} crédits</span>",
"success_link_credits": "Voir mes crédits",
"success_link_overview": "Voir les cadeaux",
"action_other_code": "Saisir un autre code",
"label_from": "De {name}",
"label_you_get": "Tu reçois",
"label_credits": "Crédits",
"label_type": "Type",
"label_status": "Statut",
"label_valid_until": "Valide jusqu'au",
"label_message_prefix": "Message :",
"section_redeem": "Utiliser",
"warn_depleted": "Ce cadeau a déjà été utilisé",
"warn_expired": "Ce cadeau a expiré",
"warn_other": "Ce cadeau ne peut pas être utilisé",
"info_personalized": "Ce cadeau est destiné à une personne précise. Seul le destinataire prévu peut l'utiliser.",
"action_redeeming": "Utilisation…",
"action_redeem": "🎁 Utiliser le cadeau",
"type_simple": "Cadeau",
"type_personalized": "Cadeau personnalisé",
"status_active": "Actif",
"status_depleted": "Épuisé",
"status_expired": "Expiré",
"status_cancelled": "Annulé",
"status_refunded": "Remboursé"
}
}

View file

@ -65,5 +65,40 @@
"status_refunded": "Rimborsato",
"type_simple_label": "Semplice",
"type_personalized_label": "Personalizzato"
},
"redeem": {
"page_back_title": "Buono",
"page_title": "Riscatta regalo",
"page_description": "Riscatta il tuo codice regalo",
"err_not_found": "Codice regalo non trovato",
"err_redeem_failed": "Riscatto non riuscito",
"toast_received": "{credits} credits ricevuti!",
"success_heading": "Regalo riscattato!",
"success_credits_label": "credits ricevuti",
"success_balance_html": "Nuovo saldo: <span class=\"font-semibold\">{balance} credits</span>",
"success_link_credits": "Ai miei credits",
"success_link_overview": "Vedi regali",
"action_other_code": "Inserisci un altro codice",
"label_from": "Da {name}",
"label_you_get": "Ricevi",
"label_credits": "Credits",
"label_type": "Tipo",
"label_status": "Stato",
"label_valid_until": "Valido fino a",
"label_message_prefix": "Messaggio:",
"section_redeem": "Riscatta",
"warn_depleted": "Questo regalo è già stato riscattato",
"warn_expired": "Questo regalo è scaduto",
"warn_other": "Questo regalo non può essere riscattato",
"info_personalized": "Questo regalo è per una persona specifica. Solo il destinatario può riscattarlo.",
"action_redeeming": "Riscatto in corso…",
"action_redeem": "🎁 Riscatta regalo",
"type_simple": "Regalo",
"type_personalized": "Regalo personalizzato",
"status_active": "Attivo",
"status_depleted": "Esaurito",
"status_expired": "Scaduto",
"status_cancelled": "Annullato",
"status_refunded": "Rimborsato"
}
}

View file

@ -6,6 +6,8 @@
import { toast } from '$lib/stores/toast.svelte';
import { giftsService, type GiftCodeInfo } from '$lib/api/gifts';
import { RoutePage } from '$lib/components/shell';
import { _, locale } from 'svelte-i18n';
import { get } from 'svelte/store';
let code = $derived($page.params.code ?? '');
let giftInfo = $state<GiftCodeInfo | null>(null);
@ -29,7 +31,7 @@
try {
giftInfo = await giftsService.getGiftInfo(code);
} catch (e) {
error = e instanceof Error ? e.message : 'Geschenk-Code nicht gefunden';
error = e instanceof Error ? e.message : $_('gifts.redeem.err_not_found');
console.error('Failed to load gift info:', e);
} finally {
loading = false;
@ -49,13 +51,13 @@
success = true;
receivedCredits = result.credits || 0;
newBalance = result.newBalance || 0;
toast.success(`${receivedCredits} Credits erhalten!`);
toast.success($_('gifts.redeem.toast_received', { values: { credits: receivedCredits } }));
} else {
error = result.error || 'Einlösen fehlgeschlagen';
error = result.error || $_('gifts.redeem.err_redeem_failed');
toast.error(error);
}
} catch (e) {
error = e instanceof Error ? e.message : 'Einlösen fehlgeschlagen';
error = e instanceof Error ? e.message : $_('gifts.redeem.err_redeem_failed');
toast.error(error);
console.error('Failed to redeem gift:', e);
} finally {
@ -64,7 +66,7 @@
}
function formatDate(dateStr: string): string {
return new Date(dateStr).toLocaleDateString('de-DE', {
return new Date(dateStr).toLocaleDateString(get(locale) ?? 'de', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
@ -74,15 +76,15 @@
function getStatusLabel(status: string): string {
switch (status) {
case 'active':
return 'Aktiv';
return $_('gifts.redeem.status_active');
case 'depleted':
return 'Aufgebraucht';
return $_('gifts.redeem.status_depleted');
case 'expired':
return 'Abgelaufen';
return $_('gifts.redeem.status_expired');
case 'cancelled':
return 'Storniert';
return $_('gifts.redeem.status_cancelled');
case 'refunded':
return 'Erstattet';
return $_('gifts.redeem.status_refunded');
default:
return status;
}
@ -91,18 +93,22 @@
function getTypeLabel(type: string): string {
switch (type) {
case 'simple':
return 'Geschenk';
return $_('gifts.redeem.type_simple');
case 'personalized':
return 'Persönliches Geschenk';
return $_('gifts.redeem.type_personalized');
default:
return type;
}
}
</script>
<RoutePage appId="gifts" backHref="/gifts" title="Gutschein">
<RoutePage appId="gifts" backHref="/gifts" title={$_('gifts.redeem.page_back_title')}>
<div>
<PageHeader title="Geschenk einlösen" description="Löse deinen Geschenk-Code ein" size="lg" />
<PageHeader
title={$_('gifts.redeem.page_title')}
description={$_('gifts.redeem.page_description')}
size="lg"
/>
{#if loading}
<div class="flex items-center justify-center py-12">
@ -119,24 +125,26 @@
>
<span class="text-5xl">🎉</span>
</div>
<h2 class="text-2xl font-bold text-foreground">Geschenk eingelöst!</h2>
<h2 class="text-2xl font-bold text-foreground">{$_('gifts.redeem.success_heading')}</h2>
<p class="mt-2 text-5xl font-bold text-primary">+{receivedCredits}</p>
<p class="text-lg text-muted-foreground">Credits erhalten</p>
<p class="text-lg text-muted-foreground">{$_('gifts.redeem.success_credits_label')}</p>
<p class="mt-4 text-muted-foreground">
Dein neuer Kontostand: <span class="font-semibold">{newBalance} Credits</span>
{@html $_('gifts.redeem.success_balance_html', {
values: { balance: newBalance },
})}
</p>
<div class="mt-8 flex justify-center gap-4">
<a
href="/?app=credits"
class="rounded-lg bg-primary px-6 py-2 font-medium text-primary-foreground hover:bg-primary/90"
>
Zu meinen Credits
{$_('gifts.redeem.success_link_credits')}
</a>
<a
href="/gifts"
class="rounded-lg bg-surface px-6 py-2 font-medium text-foreground hover:bg-surface-hover"
>
Geschenke ansehen
{$_('gifts.redeem.success_link_overview')}
</a>
</div>
</div>
@ -155,7 +163,7 @@
href="/gifts/redeem"
class="inline-block rounded-lg bg-primary px-6 py-2 text-primary-foreground hover:bg-primary/90"
>
Anderen Code eingeben
{$_('gifts.redeem.action_other_code')}
</a>
</div>
</Card>
@ -171,23 +179,27 @@
</div>
<p class="font-mono text-lg font-bold text-primary">{giftInfo.code}</p>
{#if giftInfo.creatorName}
<p class="mt-1 text-sm text-muted-foreground">Von {giftInfo.creatorName}</p>
<p class="mt-1 text-sm text-muted-foreground">
{$_('gifts.redeem.label_from', {
values: { name: giftInfo.creatorName },
})}
</p>
{/if}
</div>
<div class="mt-6 text-center">
<p class="text-sm text-muted-foreground">Du erhältst</p>
<p class="text-sm text-muted-foreground">{$_('gifts.redeem.label_you_get')}</p>
<p class="text-4xl font-bold text-primary">{giftInfo.totalCredits}</p>
<p class="text-muted-foreground">Credits</p>
<p class="text-muted-foreground">{$_('gifts.redeem.label_credits')}</p>
</div>
<div class="mt-6 space-y-3 rounded-lg bg-surface p-4">
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">Art</span>
<span class="text-muted-foreground">{$_('gifts.redeem.label_type')}</span>
<span class="font-medium">{getTypeLabel(giftInfo.type)}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">Status</span>
<span class="text-muted-foreground">{$_('gifts.redeem.label_status')}</span>
<span
class="font-medium {giftInfo.status === 'active'
? 'text-green-600 dark:text-green-400'
@ -198,7 +210,7 @@
</div>
{#if giftInfo.expiresAt}
<div class="flex justify-between text-sm">
<span class="text-muted-foreground">Gültig bis</span>
<span class="text-muted-foreground">{$_('gifts.redeem.label_valid_until')}</span>
<span class="font-medium">{formatDate(giftInfo.expiresAt)}</span>
</div>
{/if}
@ -206,7 +218,9 @@
{#if giftInfo.message}
<div class="mt-6 rounded-lg border border-border p-4">
<p class="text-sm text-muted-foreground mb-1">Nachricht:</p>
<p class="text-sm text-muted-foreground mb-1">
{$_('gifts.redeem.label_message_prefix')}
</p>
<p class="italic text-foreground">"{giftInfo.message}"</p>
</div>
{/if}
@ -214,17 +228,17 @@
<!-- Redemption card -->
<Card>
<h3 class="text-lg font-semibold mb-4">Einlösen</h3>
<h3 class="text-lg font-semibold mb-4">{$_('gifts.redeem.section_redeem')}</h3>
{#if giftInfo.status !== 'active'}
<div class="rounded-lg bg-amber-50 dark:bg-amber-900/20 p-4 text-center">
<p class="font-medium text-amber-800 dark:text-amber-200">
{#if giftInfo.status === 'depleted'}
Dieses Geschenk wurde bereits eingelöst
{$_('gifts.redeem.warn_depleted')}
{:else if giftInfo.status === 'expired'}
Dieses Geschenk ist abgelaufen
{$_('gifts.redeem.warn_expired')}
{:else}
Dieses Geschenk kann nicht eingelöst werden
{$_('gifts.redeem.warn_other')}
{/if}
</p>
</div>
@ -234,8 +248,7 @@
<div class="flex items-center gap-2">
<span class="text-xl">👤</span>
<p class="text-sm text-blue-800 dark:text-blue-200">
Dieses Geschenk ist für eine bestimmte Person. Nur der vorgesehene Empfänger
kann es einlösen.
{$_('gifts.redeem.info_personalized')}
</p>
</div>
</div>
@ -268,16 +281,16 @@
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
Wird eingelöst...
{$_('gifts.redeem.action_redeeming')}
{:else}
🎁 Geschenk einlösen
{$_('gifts.redeem.action_redeem')}
{/if}
</button>
{/if}
<div class="mt-6 text-center">
<a href="/gifts/redeem" class="text-sm text-primary hover:underline">
Anderen Code eingeben
{$_('gifts.redeem.action_other_code')}
</a>
</div>
</Card>

View file

@ -230,7 +230,6 @@
"apps/mana/apps/web/src/routes/(app)/food/+page.svelte": 7,
"apps/mana/apps/web/src/routes/(app)/food/add/+page.svelte": 2,
"apps/mana/apps/web/src/routes/(app)/food/goals/+page.svelte": 1,
"apps/mana/apps/web/src/routes/(app)/gifts/redeem/[code]/+page.svelte": 8,
"apps/mana/apps/web/src/routes/(app)/gifts/redeem/+page.svelte": 1,
"apps/mana/apps/web/src/routes/(app)/guides/+page.svelte": 2,
"apps/mana/apps/web/src/routes/(app)/habits/[id]/+page.svelte": 3,
@ -317,6 +316,7 @@
"apps/mana/apps/web/src/routes/community/+layout.svelte": 5,
"apps/mana/apps/web/src/routes/community/+page.svelte": 1,
"apps/mana/apps/web/src/routes/community/admin/+page.svelte": 6,
"apps/mana/apps/web/src/routes/community/eule/[hash]/+page.svelte": 1,
"apps/mana/apps/web/src/routes/community/roadmap/+page.svelte": 1,
"apps/mana/apps/web/src/routes/g/[code]/+page.svelte": 6,
"apps/mana/apps/web/src/routes/rsvp/[token]/+page.svelte": 1,