Guest PWA vollständig lokalisiert

This commit is contained in:
Codex Agent
2025-10-17 15:00:07 +02:00
parent bd38decc23
commit 25e8f0511b
26 changed files with 1464 additions and 588 deletions

View File

@@ -22,6 +22,9 @@ import SlideshowPage from './pages/SlideshowPage';
import SettingsPage from './pages/SettingsPage';
import LegalPage from './pages/LegalPage';
import NotFoundPage from './pages/NotFoundPage';
import { LocaleProvider } from './i18n/LocaleContext';
import { DEFAULT_LOCALE, isLocaleCode } from './i18n/messages';
import { useTranslation, type TranslateFn } from './i18n/useTranslation';
function HomeLayout() {
const { token } = useParams();
@@ -85,41 +88,52 @@ function EventBoundary({ token }: { token: string }) {
return <EventErrorView code={errorCode} message={error} />;
}
const eventLocale = isLocaleCode(event.default_locale) ? event.default_locale : DEFAULT_LOCALE;
const localeStorageKey = `guestLocale_event_${event.id ?? token}`;
return (
<EventStatsProvider eventKey={token}>
<div className="pb-16">
<Header slug={token} />
<div className="px-4 py-3">
<Outlet />
<LocaleProvider defaultLocale={eventLocale} storageKey={localeStorageKey}>
<EventStatsProvider eventKey={token}>
<div className="pb-16">
<Header slug={token} />
<div className="px-4 py-3">
<Outlet />
</div>
<BottomNav />
</div>
<BottomNav />
</div>
</EventStatsProvider>
</EventStatsProvider>
</LocaleProvider>
);
}
function SetupLayout() {
const { token } = useParams<{ token: string }>();
if (!token) return null;
const { event } = useEventData();
const eventLocale = event && isLocaleCode(event.default_locale) ? event.default_locale : DEFAULT_LOCALE;
const localeStorageKey = event ? `guestLocale_event_${event.id}` : `guestLocale_event_${token}`;
return (
<GuestIdentityProvider eventKey={token}>
<EventStatsProvider eventKey={token}>
<div className="pb-0">
<Header slug={token} />
<Outlet />
</div>
</EventStatsProvider>
<LocaleProvider defaultLocale={eventLocale} storageKey={localeStorageKey}>
<EventStatsProvider eventKey={token}>
<div className="pb-0">
<Header slug={token} />
<Outlet />
</div>
</EventStatsProvider>
</LocaleProvider>
</GuestIdentityProvider>
);
}
function EventLoadingView() {
const { t } = useTranslation();
return (
<div className="flex min-h-screen flex-col items-center justify-center gap-4 px-6 text-center">
<Loader2 className="h-10 w-10 animate-spin text-muted-foreground" aria-hidden />
<div className="space-y-1">
<p className="text-lg font-semibold text-foreground">Wir prüfen deinen Zugang...</p>
<p className="text-sm text-muted-foreground">Einen Moment bitte.</p>
<p className="text-lg font-semibold text-foreground">{t('eventAccess.loading.title')}</p>
<p className="text-sm text-muted-foreground">{t('eventAccess.loading.subtitle')}</p>
</div>
</div>
);
@@ -131,7 +145,8 @@ interface EventErrorViewProps {
}
function EventErrorView({ code, message }: EventErrorViewProps) {
const content = getErrorContent(code, message);
const { t } = useTranslation();
const content = getErrorContent(t, code, message);
return (
<div className="flex min-h-screen flex-col items-center justify-center gap-6 px-6 text-center">
@@ -155,50 +170,39 @@ function EventErrorView({ code, message }: EventErrorViewProps) {
}
function getErrorContent(
t: TranslateFn,
code: FetchEventErrorCode | null,
message: string | null,
) {
const base = (fallbackTitle: string, fallbackDescription: string, options?: { ctaLabel?: string; ctaHref?: string; hint?: string }) => ({
title: fallbackTitle,
description: message ?? fallbackDescription,
ctaLabel: options?.ctaLabel,
ctaHref: options?.ctaHref,
hint: options?.hint ?? null,
});
const build = (key: string, options?: { ctaHref?: string }) => {
const ctaLabel = t(`eventAccess.error.${key}.ctaLabel`, '');
const hint = t(`eventAccess.error.${key}.hint`, '');
return {
title: t(`eventAccess.error.${key}.title`),
description: message ?? t(`eventAccess.error.${key}.description`),
ctaLabel: ctaLabel.trim().length > 0 ? ctaLabel : undefined,
ctaHref: options?.ctaHref,
hint: hint.trim().length > 0 ? hint : null,
};
};
switch (code) {
case 'invalid_token':
return base('Zugriffscode ungültig', 'Der eingegebene Code konnte nicht verifiziert werden.', {
ctaLabel: 'Neuen Code anfordern',
ctaHref: '/event',
});
return build('invalid_token', { ctaHref: '/event' });
case 'token_revoked':
return base('Zugriffscode deaktiviert', 'Dieser Code wurde zurückgezogen. Bitte fordere einen neuen Code an.', {
ctaLabel: 'Neuen Code anfordern',
ctaHref: '/event',
});
return build('token_revoked', { ctaHref: '/event' });
case 'token_expired':
return base('Zugriffscode abgelaufen', 'Der Code ist nicht mehr gültig. Aktualisiere deinen Code, um fortzufahren.', {
ctaLabel: 'Code aktualisieren',
ctaHref: '/event',
});
return build('token_expired', { ctaHref: '/event' });
case 'token_rate_limited':
return base('Zu viele Versuche', 'Es gab zu viele Eingaben in kurzer Zeit. Warte kurz und versuche es erneut.', {
hint: 'Tipp: Eine erneute Eingabe ist in wenigen Minuten möglich.',
});
return build('token_rate_limited');
case 'event_not_public':
return base('Event nicht öffentlich', 'Dieses Event ist aktuell nicht öffentlich zugänglich.', {
hint: 'Nimm Kontakt mit den Veranstalter:innen auf, um Zugang zu erhalten.',
});
return build('event_not_public');
case 'network_error':
return base('Verbindungsproblem', 'Wir konnten keine Verbindung zum Server herstellen. Prüfe deine Internetverbindung und versuche es erneut.');
return build('network_error');
case 'server_error':
return base('Server nicht erreichbar', 'Der Server reagiert derzeit nicht. Versuche es später erneut.');
return build('server_error');
default:
return base('Event nicht erreichbar', 'Wir konnten dein Event nicht laden. Bitte versuche es erneut.', {
ctaLabel: 'Zur Code-Eingabe',
ctaHref: '/event',
});
return build('default', { ctaHref: '/event' });
}
}