- Served the guest PWA at /g/{token} and introduced a mobile-friendly gallery page with lazy-loaded thumbnails, themed colors, lightbox, and download links plus new gallery data client (resources/js/guest/pages/PublicGalleryPage.tsx:1, resources/js/guest/services/galleryApi.ts:1, resources/js/guest/router.tsx:1). Added i18n strings for the public gallery experience (resources/js/guest/i18n/messages.ts:1).
- Ensured checkout step changes snap back to the progress bar on mobile via smooth scroll anchoring (resources/ js/pages/marketing/checkout/CheckoutWizard.tsx:1).
- Enabled tenant admins to export all approved event photos through a new download action that streams a ZIP archive, with translations and routing in place (app/Http/Controllers/Tenant/EventPhotoArchiveController.php:1, app/Filament/Resources/EventResource.php:1, routes/web.php:1, resources/lang/de/admin.php:1, resources/lang/en/admin.php:1).
726 lines
25 KiB
TypeScript
726 lines
25 KiB
TypeScript
export const SUPPORTED_LOCALES = [
|
|
{ code: 'de', label: 'Deutsch', flag: '🇩🇪' },
|
|
{ code: 'en', label: 'English', flag: '🇬🇧' },
|
|
] as const;
|
|
|
|
export type LocaleCode = typeof SUPPORTED_LOCALES[number]['code'];
|
|
|
|
type NestedMessages = {
|
|
[key: string]: string | NestedMessages;
|
|
};
|
|
|
|
export const DEFAULT_LOCALE: LocaleCode = 'de';
|
|
|
|
export const messages: Record<LocaleCode, NestedMessages> = {
|
|
de: {
|
|
common: {
|
|
hi: 'Hi',
|
|
actions: {
|
|
close: 'Schliessen',
|
|
loading: 'Laedt...',
|
|
},
|
|
},
|
|
navigation: {
|
|
home: 'Start',
|
|
tasks: 'Aufgaben',
|
|
achievements: 'Erfolge',
|
|
gallery: 'Galerie',
|
|
},
|
|
header: {
|
|
loading: 'Lade Event...',
|
|
stats: {
|
|
online: 'online',
|
|
tasksSolved: 'Aufgaben geloest',
|
|
},
|
|
},
|
|
eventAccess: {
|
|
loading: {
|
|
title: 'Wir pruefen deinen Zugang...',
|
|
subtitle: 'Einen Moment bitte.',
|
|
},
|
|
error: {
|
|
invalid_token: {
|
|
title: 'Zugriffscode ungueltig',
|
|
description: 'Der eingegebene Code konnte nicht verifiziert werden.',
|
|
ctaLabel: 'Neuen Code anfordern',
|
|
},
|
|
token_revoked: {
|
|
title: 'Zugriffscode deaktiviert',
|
|
description: 'Dieser Code wurde zurueckgezogen. Bitte fordere einen neuen Code an.',
|
|
ctaLabel: 'Neuen Code anfordern',
|
|
},
|
|
token_expired: {
|
|
title: 'Zugriffscode abgelaufen',
|
|
description: 'Der Code ist nicht mehr gueltig. Aktualisiere deinen Code, um fortzufahren.',
|
|
ctaLabel: 'Code aktualisieren',
|
|
},
|
|
token_rate_limited: {
|
|
title: 'Zu viele Versuche',
|
|
description: 'Es gab zu viele Eingaben in kurzer Zeit. Warte kurz und versuche es erneut.',
|
|
hint: 'Tipp: Eine erneute Eingabe ist in wenigen Minuten moeglich.',
|
|
},
|
|
event_not_public: {
|
|
title: 'Event nicht oeffentlich',
|
|
description: 'Dieses Event ist aktuell nicht oeffentlich zugaenglich.',
|
|
hint: 'Nimm Kontakt mit den Veranstalter:innen auf, um Zugang zu erhalten.',
|
|
},
|
|
network_error: {
|
|
title: 'Verbindungsproblem',
|
|
description: 'Wir konnten keine Verbindung zum Server herstellen. Pruefe deine Internetverbindung und versuche es erneut.',
|
|
},
|
|
server_error: {
|
|
title: 'Server nicht erreichbar',
|
|
description: 'Der Server reagiert derzeit nicht. Versuche es spaeter erneut.',
|
|
},
|
|
default: {
|
|
title: 'Event nicht erreichbar',
|
|
description: 'Wir konnten dein Event nicht laden. Bitte versuche es erneut.',
|
|
ctaLabel: 'Zur Code-Eingabe',
|
|
},
|
|
},
|
|
},
|
|
profileSetup: {
|
|
loading: 'Lade Event...',
|
|
error: {
|
|
default: 'Event nicht gefunden.',
|
|
backToStart: 'Zurueck zur Startseite',
|
|
},
|
|
card: {
|
|
description: 'Fange den schoensten Moment ein!',
|
|
},
|
|
form: {
|
|
label: 'Dein Name (z.B. Anna)',
|
|
placeholder: 'Dein Name',
|
|
submit: 'Los gehts!',
|
|
submitting: 'Speichere...',
|
|
},
|
|
},
|
|
landing: {
|
|
pageTitle: 'Willkommen bei der Fotobox!',
|
|
headline: 'Willkommen bei der Fotobox!',
|
|
subheadline: 'Dein Schluessel zu unvergesslichen Momenten.',
|
|
join: {
|
|
title: 'Event beitreten',
|
|
description: 'Scanne den QR-Code oder gib den Code manuell ein.',
|
|
button: 'Event beitreten',
|
|
buttonLoading: 'Pruefe...',
|
|
},
|
|
scan: {
|
|
start: 'QR-Code scannen',
|
|
stop: 'Scanner stoppen',
|
|
manualDivider: 'Oder manuell eingeben',
|
|
},
|
|
input: {
|
|
placeholder: 'Event-Code eingeben',
|
|
},
|
|
errors: {
|
|
eventClosed: 'Event nicht gefunden oder geschlossen.',
|
|
network: 'Netzwerkfehler. Bitte spaeter erneut versuchen.',
|
|
camera: 'Kamera-Zugriff fehlgeschlagen. Bitte erlaube den Zugriff und versuche es erneut.',
|
|
},
|
|
},
|
|
home: {
|
|
fallbackGuestName: 'Gast',
|
|
hero: {
|
|
subtitle: 'Willkommen zur Party',
|
|
title: 'Hey {name}!',
|
|
description: 'Du bist bereit fuer "{eventName}". Fang die Highlights des Events ein und teile sie mit allen Gaesten.',
|
|
progress: {
|
|
some: 'Schon {count} Aufgaben erledigt - weiter so!',
|
|
none: 'Starte mit deiner ersten Aufgabe - wir zaehlen auf dich!',
|
|
},
|
|
defaultEventName: 'Dein Event',
|
|
},
|
|
stats: {
|
|
online: 'Gleichzeitig online',
|
|
tasksSolved: 'Aufgaben geloest',
|
|
lastUpload: 'Letzter Upload',
|
|
completedTasks: 'Deine erledigten Aufgaben',
|
|
},
|
|
actions: {
|
|
title: 'Deine Aktionen',
|
|
subtitle: 'Waehle aus, womit du starten willst',
|
|
queueButton: 'Uploads in Warteschlange ansehen',
|
|
items: {
|
|
tasks: {
|
|
label: 'Aufgabe ziehen',
|
|
description: 'Hol dir deine naechste Challenge',
|
|
},
|
|
upload: {
|
|
label: 'Direkt hochladen',
|
|
description: 'Teile deine neuesten Fotos',
|
|
},
|
|
gallery: {
|
|
label: 'Galerie ansehen',
|
|
description: 'Lass dich von anderen inspirieren',
|
|
},
|
|
},
|
|
},
|
|
checklist: {
|
|
title: 'Dein Fortschritt',
|
|
description: 'Halte dich an diese drei kurzen Schritte fuer die besten Ergebnisse.',
|
|
steps: {
|
|
first: 'Aufgabe auswaehlen oder starten',
|
|
second: 'Emotion festhalten und Foto schiessen',
|
|
third: 'Bild hochladen und Credits sammeln',
|
|
},
|
|
},
|
|
latestUpload: {
|
|
none: 'Noch kein Upload',
|
|
invalid: 'Noch kein Upload',
|
|
justNow: 'Gerade eben',
|
|
minutes: 'vor {count} Min',
|
|
hours: 'vor {count} Std',
|
|
days: 'vor {count} Tagen',
|
|
},
|
|
},
|
|
notFound: {
|
|
title: 'Nicht gefunden',
|
|
description: 'Die Seite konnte nicht gefunden werden.',
|
|
},
|
|
galleryPublic: {
|
|
title: 'Galerie',
|
|
loading: 'Galerie wird geladen ...',
|
|
loadingMore: 'Weitere Fotos werden geladen',
|
|
loadError: 'Die Galerie konnte nicht geladen werden.',
|
|
loadMore: 'Mehr anzeigen',
|
|
download: 'Herunterladen',
|
|
expiredTitle: 'Galerie nicht verfügbar',
|
|
expiredDescription: 'Die Galerie für dieses Event ist abgelaufen.',
|
|
emptyTitle: 'Noch keine Fotos',
|
|
emptyDescription: 'Sobald Fotos freigegeben sind, erscheinen sie hier.',
|
|
lightboxGuestFallback: 'Gast',
|
|
},
|
|
uploadQueue: {
|
|
title: 'Uploads',
|
|
description: 'Warteschlange mit Fortschritt und erneuten Versuchen; Hintergrund-Sync umschalten.',
|
|
},
|
|
lightbox: {
|
|
taskLabel: 'Aufgabe',
|
|
loadingTask: 'Lade Aufgabe...',
|
|
photoAlt: 'Foto {id}{suffix}',
|
|
photoAltTaskSuffix: ' - {taskTitle}',
|
|
fallbackTitle: 'Aufgabe {id}',
|
|
unknownTitle: 'Unbekannte Aufgabe {id}',
|
|
errors: {
|
|
notFound: 'Foto nicht gefunden',
|
|
loadFailed: 'Fehler beim Laden des Fotos',
|
|
},
|
|
},
|
|
upload: {
|
|
cameraTitle: 'Kamera',
|
|
preparing: 'Aufgabe und Kamera werden vorbereitet ...',
|
|
loadError: {
|
|
title: 'Aufgabe konnte nicht geladen werden. Du kannst trotzdem ein Foto machen.',
|
|
retry: 'Nochmal versuchen',
|
|
},
|
|
primer: {
|
|
title: 'Bereit fuer dein Shooting?',
|
|
body: {
|
|
part1: 'Lass uns sicherstellen, dass alles sitzt: pruefe das Licht, wisch die Kamera sauber und richte alle Personen im Bild aus.',
|
|
part2: 'Du kannst zwischen Front- und Rueckkamera wechseln und bei Bedarf ein Raster aktivieren.',
|
|
},
|
|
dismiss: 'Verstanden',
|
|
},
|
|
cameraUnsupported: {
|
|
title: 'Kamera nicht verfuegbar',
|
|
message: 'Dein Geraet unterstuetzt keine Kameravorschau im Browser. Du kannst stattdessen Fotos aus deiner Galerie hochladen.',
|
|
openGallery: 'Foto aus Galerie waehlen',
|
|
},
|
|
cameraDenied: {
|
|
title: 'Kamera-Zugriff verweigert',
|
|
explanation: 'Du musst den Zugriff auf die Kamera erlauben, um Fotos aufnehmen zu koennen.',
|
|
reopenPrompt: 'Systemdialog erneut oeffnen',
|
|
chooseFile: 'Foto aus Galerie waehlen',
|
|
prompt: 'Wir brauchen Zugriff auf deine Kamera. Erlaube die Anfrage oder waehle alternativ ein Foto aus deiner Galerie.',
|
|
},
|
|
cameraError: {
|
|
title: 'Kamera konnte nicht gestartet werden',
|
|
explanation: 'Wir konnten keine Verbindung zur Kamera herstellen. Pruefe die Berechtigungen oder starte dein Geraet neu.',
|
|
tryAgain: 'Nochmals versuchen',
|
|
},
|
|
readyOverlay: {
|
|
title: 'Kamera bereit',
|
|
message: 'Wenn alle im Bild sind, kannst du den Countdown starten oder ein vorhandenes Foto waehlen.',
|
|
start: 'Countdown starten',
|
|
chooseFile: 'Foto auswaehlen',
|
|
},
|
|
taskInfo: {
|
|
countdown: 'Countdown',
|
|
emotion: 'Stimmung: {value}',
|
|
instructionsPrefix: 'Hinweis',
|
|
difficulty: {
|
|
easy: 'Leicht',
|
|
medium: 'Medium',
|
|
hard: 'Herausfordernd',
|
|
},
|
|
timeEstimate: '{count} Min',
|
|
fallbackTitle: 'Aufgabe {id}',
|
|
fallbackDescription: 'Halte den Moment fest und teile ihn mit allen Gaesten.',
|
|
fallbackInstructions: 'Positioniere alle im Bild, starte den Countdown und lass die Emotion wirken.',
|
|
badge: 'Aufgabe #{id}',
|
|
},
|
|
countdown: {
|
|
ready: 'Bereit machen ...',
|
|
},
|
|
review: {
|
|
retake: 'Nochmal aufnehmen',
|
|
keep: 'Foto verwenden',
|
|
readyAnnouncement: 'Foto aufgenommen. Bitte Vorschau pruefen.',
|
|
},
|
|
status: {
|
|
saving: 'Speichere Foto...',
|
|
processing: 'Verarbeite Foto...',
|
|
uploading: 'Foto wird hochgeladen...',
|
|
preparing: 'Foto wird vorbereitet...',
|
|
completed: 'Upload abgeschlossen.',
|
|
failed: 'Upload fehlgeschlagen. Bitte versuche es erneut.',
|
|
},
|
|
controls: {
|
|
toggleGrid: 'Raster umschalten',
|
|
toggleCountdown: 'Countdown umschalten',
|
|
toggleMirror: 'Spiegelung fuer Frontkamera umschalten',
|
|
toggleFlash: 'Blitzpraeferenz umschalten',
|
|
capture: 'Foto aufnehmen',
|
|
switchCamera: 'Kamera wechseln',
|
|
chooseFile: 'Foto auswaehlen',
|
|
},
|
|
limitReached: 'Upload-Limit erreicht ({used} / {max} Fotos). Bitte kontaktiere die Veranstalter fuer ein Upgrade.',
|
|
limitUnlimited: 'unbegrenzt',
|
|
cameraInactive: 'Kamera ist nicht aktiv. {hint}',
|
|
cameraInactiveHint: 'Tippe auf "{label}", um loszulegen.',
|
|
captureError: 'Foto konnte nicht erstellt werden.',
|
|
feedError: 'Kamera liefert kein Bild. Bitte starte die Kamera neu.',
|
|
canvasError: 'Canvas konnte nicht initialisiert werden.',
|
|
limitCheckError: 'Fehler beim Pruefen des Upload-Limits. Upload deaktiviert.',
|
|
galleryPickError: 'Auswahl fehlgeschlagen. Bitte versuche es erneut.',
|
|
captureButton: 'Foto aufnehmen',
|
|
galleryButton: 'Foto aus Galerie waehlen',
|
|
switchCamera: 'Kamera wechseln',
|
|
countdownLabel: 'Countdown: {seconds}s',
|
|
countdownReady: 'Bereit machen ...',
|
|
buttons: {
|
|
startCamera: 'Kamera starten',
|
|
tryAgain: 'Erneut versuchen',
|
|
},
|
|
},
|
|
settings: {
|
|
title: 'Einstellungen',
|
|
subtitle: 'Verwalte deinen Gastzugang, rechtliche Dokumente und lokale Daten.',
|
|
language: {
|
|
title: 'Sprache',
|
|
description: 'Waehle deine bevorzugte Sprache fuer diese Veranstaltung.',
|
|
activeBadge: 'aktiv',
|
|
option: {
|
|
de: 'Deutsch',
|
|
en: 'English',
|
|
},
|
|
},
|
|
name: {
|
|
title: 'Dein Name',
|
|
description: 'Passe an, wie wir dich im Event begruessen. Der Name wird nur lokal gespeichert.',
|
|
label: 'Anzeigename',
|
|
placeholder: 'z.B. Anna',
|
|
save: 'Name speichern',
|
|
saving: 'Speichere...',
|
|
reset: 'Zuruecksetzen',
|
|
saved: 'Gespeichert (ok)',
|
|
loading: 'Lade gespeicherten Namen...',
|
|
},
|
|
legal: {
|
|
title: 'Rechtliches',
|
|
description: 'Die rechtlich verbindlichen Texte sind jederzeit hier abrufbar.',
|
|
loading: 'Dokument wird geladen...',
|
|
error: 'Das Dokument konnte nicht geladen werden. Bitte versuche es spaeter erneut.',
|
|
fallbackTitle: 'Rechtlicher Hinweis',
|
|
section: {
|
|
impressum: 'Impressum',
|
|
privacy: 'Datenschutz',
|
|
terms: 'AGB',
|
|
},
|
|
},
|
|
cache: {
|
|
title: 'Offline Cache',
|
|
description: 'Loesche lokale Daten, falls Inhalte veraltet erscheinen oder Uploads haengen bleiben.',
|
|
clear: 'Cache leeren',
|
|
clearing: 'Leere Cache...',
|
|
cleared: 'Cache geloescht.',
|
|
note: 'Dies betrifft nur diesen Browser und muss pro Geraet erneut ausgefuehrt werden.',
|
|
},
|
|
footer: {
|
|
notice: 'Gastbereich - Daten werden lokal im Browser gespeichert.',
|
|
},
|
|
sheet: {
|
|
openLabel: 'Einstellungen oeffnen',
|
|
backLabel: 'Zurueck',
|
|
legalDescription: 'Rechtlicher Hinweis',
|
|
},
|
|
},
|
|
},
|
|
en: {
|
|
common: {
|
|
hi: 'Hi',
|
|
actions: {
|
|
close: 'Close',
|
|
loading: 'Loading...',
|
|
},
|
|
},
|
|
navigation: {
|
|
home: 'Home',
|
|
tasks: 'Tasks',
|
|
achievements: 'Achievements',
|
|
gallery: 'Gallery',
|
|
},
|
|
header: {
|
|
loading: 'Loading event...',
|
|
stats: {
|
|
online: 'online',
|
|
tasksSolved: 'tasks solved',
|
|
},
|
|
},
|
|
eventAccess: {
|
|
loading: {
|
|
title: 'Checking your access...',
|
|
subtitle: 'Hang tight for a moment.',
|
|
},
|
|
error: {
|
|
invalid_token: {
|
|
title: 'Access code invalid',
|
|
description: 'We could not verify the code you entered.',
|
|
ctaLabel: 'Request new code',
|
|
},
|
|
token_revoked: {
|
|
title: 'Access code revoked',
|
|
description: 'This code was revoked. Please request a new one.',
|
|
ctaLabel: 'Request new code',
|
|
},
|
|
token_expired: {
|
|
title: 'Access code expired',
|
|
description: 'The code is no longer valid. Refresh your code to continue.',
|
|
ctaLabel: 'Refresh code',
|
|
},
|
|
token_rate_limited: {
|
|
title: 'Too many attempts',
|
|
description: 'There were too many attempts in a short time. Wait a bit and try again.',
|
|
hint: 'Tip: You can retry in a few minutes.',
|
|
},
|
|
event_not_public: {
|
|
title: 'Event not public',
|
|
description: 'This event is not publicly accessible right now.',
|
|
hint: 'Contact the organizers to get access.',
|
|
},
|
|
network_error: {
|
|
title: 'Connection issue',
|
|
description: 'We could not reach the server. Check your connection and try again.',
|
|
},
|
|
server_error: {
|
|
title: 'Server unavailable',
|
|
description: 'The server is currently unavailable. Please try again later.',
|
|
},
|
|
default: {
|
|
title: 'Event unavailable',
|
|
description: 'We could not load your event. Please try again.',
|
|
ctaLabel: 'Back to code entry',
|
|
},
|
|
},
|
|
},
|
|
profileSetup: {
|
|
loading: 'Loading event...',
|
|
error: {
|
|
default: 'Event not found.',
|
|
backToStart: 'Back to start',
|
|
},
|
|
card: {
|
|
description: 'Capture the best moment!',
|
|
},
|
|
form: {
|
|
label: 'Your name (e.g. Anna)',
|
|
placeholder: 'Your name',
|
|
submit: "Let's go!",
|
|
submitting: 'Saving...',
|
|
},
|
|
},
|
|
landing: {
|
|
pageTitle: 'Welcome to the photo booth!',
|
|
headline: 'Welcome to the photo booth!',
|
|
subheadline: 'Your key to unforgettable moments.',
|
|
join: {
|
|
title: 'Join the event',
|
|
description: 'Scan the QR code or enter the code manually.',
|
|
button: 'Join event',
|
|
buttonLoading: 'Checking...',
|
|
},
|
|
scan: {
|
|
start: 'Scan QR code',
|
|
stop: 'Stop scanner',
|
|
manualDivider: 'Or enter it manually',
|
|
},
|
|
input: {
|
|
placeholder: 'Enter event code',
|
|
},
|
|
errors: {
|
|
eventClosed: 'Event not found or closed.',
|
|
network: 'Network error. Please try again later.',
|
|
camera: 'Camera access failed. Allow access and try again.',
|
|
},
|
|
},
|
|
home: {
|
|
fallbackGuestName: 'Guest',
|
|
hero: {
|
|
subtitle: 'Welcome to the party',
|
|
title: 'Hey {name}!',
|
|
description: 'You are ready for "{eventName}". Capture the highlights and share them with everyone.',
|
|
progress: {
|
|
some: 'Already {count} tasks done - keep going!',
|
|
none: 'Start with your first task - we are counting on you!',
|
|
},
|
|
defaultEventName: 'Your event',
|
|
},
|
|
stats: {
|
|
online: 'Guests online',
|
|
tasksSolved: 'Tasks completed',
|
|
lastUpload: 'Latest upload',
|
|
completedTasks: 'Your completed tasks',
|
|
},
|
|
actions: {
|
|
title: 'Your actions',
|
|
subtitle: 'Choose how you want to start',
|
|
queueButton: 'View uploads in queue',
|
|
items: {
|
|
tasks: {
|
|
label: 'Draw a task',
|
|
description: 'Grab your next challenge',
|
|
},
|
|
upload: {
|
|
label: 'Upload directly',
|
|
description: 'Share your latest photos',
|
|
},
|
|
gallery: {
|
|
label: 'Browse gallery',
|
|
description: 'Get inspired by others',
|
|
},
|
|
},
|
|
},
|
|
checklist: {
|
|
title: 'Your progress',
|
|
description: 'Follow these three quick steps for the best results.',
|
|
steps: {
|
|
first: 'Pick or start a task',
|
|
second: 'Capture the emotion and take the photo',
|
|
third: 'Upload the picture and earn credits',
|
|
},
|
|
},
|
|
latestUpload: {
|
|
none: 'No upload yet',
|
|
invalid: 'No upload yet',
|
|
justNow: 'Just now',
|
|
minutes: '{count} min ago',
|
|
hours: '{count} h ago',
|
|
days: '{count} days ago',
|
|
},
|
|
},
|
|
notFound: {
|
|
title: 'Not found',
|
|
description: 'We could not find the page you requested.',
|
|
},
|
|
galleryPublic: {
|
|
title: 'Gallery',
|
|
loading: 'Loading gallery ...',
|
|
loadingMore: 'Loading more photos',
|
|
loadError: 'The gallery could not be loaded.',
|
|
loadMore: 'Show more',
|
|
download: 'Download',
|
|
expiredTitle: 'Gallery unavailable',
|
|
expiredDescription: 'The gallery for this event has expired.',
|
|
emptyTitle: 'No photos yet',
|
|
emptyDescription: 'Once photos are approved they will appear here.',
|
|
lightboxGuestFallback: 'Guest',
|
|
},
|
|
uploadQueue: {
|
|
title: 'Uploads',
|
|
description: 'Queue with progress/retry and background sync toggle.',
|
|
},
|
|
lightbox: {
|
|
taskLabel: 'Task',
|
|
loadingTask: 'Loading task...',
|
|
photoAlt: 'Photo {id}{suffix}',
|
|
photoAltTaskSuffix: ' - {taskTitle}',
|
|
fallbackTitle: 'Task {id}',
|
|
unknownTitle: 'Unknown task {id}',
|
|
errors: {
|
|
notFound: 'Photo not found',
|
|
loadFailed: 'Failed to load photo',
|
|
},
|
|
},
|
|
upload: {
|
|
cameraTitle: 'Camera',
|
|
preparing: 'Preparing task and camera ...',
|
|
loadError: {
|
|
title: 'Task could not be loaded. You can still take a photo.',
|
|
retry: 'Try again',
|
|
},
|
|
primer: {
|
|
title: 'Ready for your shoot?',
|
|
body: {
|
|
part1: 'Make sure everything is set: check the lighting, clean the lens, and line everyone up in the frame.',
|
|
part2: 'You can switch between front and back camera and enable the grid if needed.',
|
|
},
|
|
dismiss: 'Got it',
|
|
},
|
|
cameraUnsupported: {
|
|
title: 'Camera not available',
|
|
message: 'Your device does not support live camera preview in this browser. You can upload photos from your gallery instead.',
|
|
openGallery: 'Choose photo from gallery',
|
|
},
|
|
cameraDenied: {
|
|
title: 'Camera access denied',
|
|
explanation: 'Allow camera access to capture photos.',
|
|
reopenPrompt: 'Open system dialog again',
|
|
chooseFile: 'Choose photo from gallery',
|
|
prompt: 'We need access to your camera. Allow the request or pick a photo from your gallery.',
|
|
},
|
|
cameraError: {
|
|
title: 'Camera could not be started',
|
|
explanation: 'We could not connect to the camera. Check permissions or restart your device.',
|
|
tryAgain: 'Try again',
|
|
},
|
|
readyOverlay: {
|
|
title: 'Camera ready',
|
|
message: 'Once everyone is in frame, start the countdown or pick an existing photo.',
|
|
start: 'Start countdown',
|
|
chooseFile: 'Choose photo',
|
|
},
|
|
taskInfo: {
|
|
countdown: 'Countdown',
|
|
emotion: 'Emotion: {value}',
|
|
instructionsPrefix: 'Hint',
|
|
difficulty: {
|
|
easy: 'Easy',
|
|
medium: 'Medium',
|
|
hard: 'Challenging',
|
|
},
|
|
timeEstimate: '{count} min',
|
|
fallbackTitle: 'Task {id}',
|
|
fallbackDescription: 'Capture the moment and share it with everyone.',
|
|
fallbackInstructions: 'Line everyone up, start the countdown, and let the emotion shine.',
|
|
badge: 'Task #{id}',
|
|
},
|
|
countdown: {
|
|
ready: 'Get ready ...',
|
|
},
|
|
review: {
|
|
retake: 'Retake photo',
|
|
keep: 'Use this photo',
|
|
readyAnnouncement: 'Photo captured. Please review the preview.',
|
|
},
|
|
status: {
|
|
saving: 'Saving photo...',
|
|
processing: 'Processing photo...',
|
|
uploading: 'Uploading photo...',
|
|
preparing: 'Preparing photo...',
|
|
completed: 'Upload complete.',
|
|
failed: 'Upload failed. Please try again.',
|
|
},
|
|
controls: {
|
|
toggleGrid: 'Toggle grid',
|
|
toggleCountdown: 'Toggle countdown',
|
|
toggleMirror: 'Toggle mirroring for front camera',
|
|
toggleFlash: 'Toggle flash',
|
|
capture: 'Capture photo',
|
|
switchCamera: 'Switch camera',
|
|
chooseFile: 'Choose photo',
|
|
},
|
|
limitReached: 'Upload limit reached ({used} / {max} photos). Contact the organizers for an upgrade.',
|
|
limitUnlimited: 'unlimited',
|
|
cameraInactive: 'Camera is not active. {hint}',
|
|
cameraInactiveHint: 'Tap "{label}" to get started.',
|
|
captureError: 'Photo could not be created.',
|
|
feedError: 'Camera feed not available. Please restart the camera.',
|
|
canvasError: 'Canvas could not be initialised.',
|
|
limitCheckError: 'Failed to check upload limits. Upload disabled.',
|
|
galleryPickError: 'Selection failed. Please try again.',
|
|
captureButton: 'Capture photo',
|
|
galleryButton: 'Choose from gallery',
|
|
switchCamera: 'Switch camera',
|
|
countdownLabel: 'Countdown: {seconds}s',
|
|
countdownReady: 'Get ready ...',
|
|
buttons: {
|
|
startCamera: 'Start camera',
|
|
tryAgain: 'Try again',
|
|
},
|
|
},
|
|
settings: {
|
|
title: 'Settings',
|
|
subtitle: 'Manage your guest access, legal documents, and local data.',
|
|
language: {
|
|
title: 'Language',
|
|
description: 'Choose your preferred language for this event.',
|
|
activeBadge: 'active',
|
|
option: {
|
|
de: 'German',
|
|
en: 'English',
|
|
},
|
|
},
|
|
name: {
|
|
title: 'Your name',
|
|
description: 'Update how we greet you inside the event. The name is stored locally only.',
|
|
label: 'Display name',
|
|
placeholder: 'e.g. Anna',
|
|
save: 'Save name',
|
|
saving: 'Saving...',
|
|
reset: 'Reset',
|
|
saved: 'Saved',
|
|
loading: 'Loading saved name...',
|
|
},
|
|
legal: {
|
|
title: 'Legal',
|
|
description: 'The legally binding documents are always available here.',
|
|
loading: 'Loading document...',
|
|
error: 'The document could not be loaded. Please try again later.',
|
|
fallbackTitle: 'Legal notice',
|
|
section: {
|
|
impressum: 'Imprint',
|
|
privacy: 'Privacy',
|
|
terms: 'Terms',
|
|
},
|
|
},
|
|
cache: {
|
|
title: 'Offline cache',
|
|
description: 'Clear local data if content looks outdated or uploads get stuck.',
|
|
clear: 'Clear cache',
|
|
clearing: 'Clearing cache...',
|
|
cleared: 'Cache cleared.',
|
|
note: 'This only affects this browser and must be repeated per device.',
|
|
},
|
|
footer: {
|
|
notice: 'Guest area - data is stored locally in the browser.',
|
|
},
|
|
sheet: {
|
|
openLabel: 'Open settings',
|
|
backLabel: 'Back',
|
|
legalDescription: 'Legal notice',
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
export function isLocaleCode(value: string | null | undefined): value is LocaleCode {
|
|
return SUPPORTED_LOCALES.some((item) => item.code === value);
|
|
}
|
|
|
|
export function translate(locale: LocaleCode, key: string): string | undefined {
|
|
const segments = key.split('.');
|
|
const tryLocale = (loc: LocaleCode): string | undefined => {
|
|
let current: unknown = messages[loc];
|
|
for (const segment of segments) {
|
|
if (!current || typeof current !== 'object' || !(segment in current)) {
|
|
return undefined;
|
|
}
|
|
current = (current as NestedMessages)[segment];
|
|
}
|
|
return typeof current === 'string' ? current : undefined;
|
|
};
|
|
|
|
return tryLocale(locale) ?? tryLocale(DEFAULT_LOCALE);
|
|
}
|