fix(activities): i18n keys + retry useOwnedTickets after transient failure

i18n: add the missing keys the ticket purchase + owned-tickets
surfaces use across en/es/fr — activities.detail.{buyTicket,
buyAnotherTicket, viewMyTickets, ticketsOwned, unlimitedTickets}
and activities.filters.myTickets. Without these the runtime fell
back to the literal key strings + spammed [intlify] warnings; the
filter chip rendered the bare key text on logged-in sessions.

ticketsOwned uses i18n pluralization so "You have 1 ticket" vs
"You have 5 tickets" both come out correct.

useOwnedTickets: the hasAutoLoaded guard prevented retries after
a transient backend failure (e.g. an LNbits restart mid-fetch).
The composable would stay stuck with tickets = [] forever, so the
buyer landing on a fresh detail page right after a transient error
saw no badges anywhere. Detect the "previous load didn't actually
hydrate" state (lastLoadedUserId still null while authenticated)
and retry on the next useOwnedTickets() call.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Padreug 2026-05-23 21:11:05 +02:00
commit 4dcee143fd
5 changed files with 37 additions and 2 deletions

View file

@ -57,6 +57,7 @@ const messages: LocaleMessages = {
tomorrow: 'Tomorrow', tomorrow: 'Tomorrow',
thisWeek: 'This Week', thisWeek: 'This Week',
thisMonth: 'This Month', thisMonth: 'This Month',
myTickets: 'My tickets',
}, },
categories: { categories: {
concert: 'Concert', concert: 'Concert',
@ -96,6 +97,11 @@ const messages: LocaleMessages = {
when: 'When', when: 'When',
tickets: 'Tickets', tickets: 'Tickets',
ticketsAvailable: '{count} tickets available', ticketsAvailable: '{count} tickets available',
ticketsOwned: 'You have {count} ticket | You have {count} tickets',
unlimitedTickets: 'Unlimited tickets',
buyTicket: 'Buy ticket',
buyAnotherTicket: 'Buy another ticket',
viewMyTickets: 'View in My Tickets',
soldOut: 'Sold Out', soldOut: 'Sold Out',
free: 'Free', free: 'Free',
}, },

View file

@ -57,6 +57,7 @@ const messages: LocaleMessages = {
tomorrow: 'Mañana', tomorrow: 'Mañana',
thisWeek: 'Esta semana', thisWeek: 'Esta semana',
thisMonth: 'Este mes', thisMonth: 'Este mes',
myTickets: 'Mis boletos',
}, },
categories: { categories: {
concert: 'Concierto', concert: 'Concierto',
@ -96,6 +97,11 @@ const messages: LocaleMessages = {
when: 'Cuándo', when: 'Cuándo',
tickets: 'Boletos', tickets: 'Boletos',
ticketsAvailable: '{count} boletos disponibles', ticketsAvailable: '{count} boletos disponibles',
ticketsOwned: 'Tienes {count} boleto | Tienes {count} boletos',
unlimitedTickets: 'Boletos ilimitados',
buyTicket: 'Comprar boleto',
buyAnotherTicket: 'Comprar otro boleto',
viewMyTickets: 'Ver en Mis boletos',
soldOut: 'Agotado', soldOut: 'Agotado',
free: 'Gratis', free: 'Gratis',
}, },

View file

@ -57,6 +57,7 @@ const messages: LocaleMessages = {
tomorrow: 'Demain', tomorrow: 'Demain',
thisWeek: 'Cette semaine', thisWeek: 'Cette semaine',
thisMonth: 'Ce mois-ci', thisMonth: 'Ce mois-ci',
myTickets: 'Mes billets',
}, },
categories: { categories: {
concert: 'Concert', concert: 'Concert',
@ -96,6 +97,11 @@ const messages: LocaleMessages = {
when: 'Quand', when: 'Quand',
tickets: 'Billets', tickets: 'Billets',
ticketsAvailable: '{count} billets disponibles', ticketsAvailable: '{count} billets disponibles',
ticketsOwned: 'Vous avez {count} billet | Vous avez {count} billets',
unlimitedTickets: 'Billets illimités',
buyTicket: 'Acheter un billet',
buyAnotherTicket: 'Acheter un autre billet',
viewMyTickets: 'Voir dans Mes billets',
soldOut: 'Épuisé', soldOut: 'Épuisé',
free: 'Gratuit', free: 'Gratuit',
}, },

View file

@ -58,6 +58,7 @@ export interface LocaleMessages {
tomorrow: string tomorrow: string
thisWeek: string thisWeek: string
thisMonth: string thisMonth: string
myTickets: string
} }
categories: Record<string, string> categories: Record<string, string>
detail: { detail: {
@ -71,6 +72,11 @@ export interface LocaleMessages {
when: string when: string
tickets: string tickets: string
ticketsAvailable: string ticketsAvailable: string
ticketsOwned: string
unlimitedTickets: string
buyTicket: string
buyAnotherTicket: string
viewMyTickets: string
soldOut: string soldOut: string
free: string free: string
} }

View file

@ -81,8 +81,8 @@ function paidCount(activityId: string): number {
export function useOwnedTickets() { export function useOwnedTickets() {
const { isAuthenticated, currentUser } = useAuth() const { isAuthenticated, currentUser } = useAuth()
// First call kicks off the initial load. Subsequent calls just // First call kicks off the initial load + sets up the auth-change
// attach to the shared state. // watcher. Subsequent calls attach to the shared state.
if (!hasAutoLoaded) { if (!hasAutoLoaded) {
hasAutoLoaded = true hasAutoLoaded = true
fetchTickets() fetchTickets()
@ -97,6 +97,17 @@ export function useOwnedTickets() {
if (id !== lastLoadedUserId) fetchTickets() if (id !== lastLoadedUserId) fetchTickets()
}, },
) )
} else if (
!isLoading.value &&
isAuthenticated.value &&
currentUser.value &&
lastLoadedUserId !== currentUser.value.id
) {
// A previous load failed (lastLoadedUserId stayed null) or the
// user changed identity while the singleton was idle. Retry —
// the buyer landing on a fresh detail page after a transient
// backend hiccup shouldn't be stuck with empty tickets.
fetchTickets()
} }
return { return {