Files
fotospiel-app/resources/js/guest/i18n/messages.ts
Codex Agent 79b209de9a Limit-Status im Upload-Flow anzeigen (Warnbanner + Sperrzustände).
Upload-Fehlercodes auswerten und freundliche Dialoge zeigen.
2025-11-01 19:50:17 +01:00

892 lines
32 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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: 'Schließen',
loading: 'Lädt...',
},
},
navigation: {
home: 'Start',
tasks: 'Aufgaben',
achievements: 'Erfolge',
gallery: 'Galerie',
},
header: {
loading: 'Lade Event...',
stats: {
online: 'online',
tasksSolved: 'Aufgaben gelöst',
},
},
eventAccess: {
loading: {
title: 'Wir prüfen deinen Zugang...',
subtitle: 'Einen Moment bitte.',
},
error: {
invalid_token: {
title: 'Zugriffscode ungültig',
description: 'Der eingegebene Code konnte nicht verifiziert werden.',
ctaLabel: 'Neuen Code anfordern',
},
token_revoked: {
title: 'Zugriffscode deaktiviert',
description: 'Dieser Code wurde zurückgezogen. Bitte fordere einen neuen Code an.',
ctaLabel: 'Neuen Code anfordern',
},
token_expired: {
title: 'Zugriffscode abgelaufen',
description: 'Der Code ist nicht mehr gültig. 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 möglich.',
},
access_rate_limited: {
title: 'Zu viele Aufrufe',
description: 'Es gab sehr viele Aufrufe in kurzer Zeit. Warte kurz und versuche es erneut.',
hint: 'Tipp: Du kannst es gleich noch einmal versuchen.',
},
gallery_expired: {
title: 'Galerie nicht mehr verfügbar',
description: 'Die Galerie zu diesem Event ist nicht mehr zugänglich.',
ctaLabel: 'Neuen Code anfordern',
},
event_not_public: {
title: 'Event nicht öffentlich',
description: 'Dieses Event ist aktuell nicht öffentlich zugänglich.',
hint: 'Nimm Kontakt mit den Veranstalter:innen auf, um Zugang zu erhalten.',
},
network_error: {
title: 'Verbindungsproblem',
description: 'Wir konnten keine Verbindung zum Server herstellen. Prüfe deine Internetverbindung und versuche es erneut.',
},
server_error: {
title: 'Server nicht erreichbar',
description: 'Der Server reagiert derzeit nicht. Versuche es später 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: 'Zurück zur Startseite',
},
card: {
description: 'Fange den schönsten 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 Schlüssel zu unvergesslichen Momenten.',
join: {
title: 'Event beitreten',
description: 'Scanne den QR-Code oder gib den Code manuell ein.',
button: 'Event beitreten',
buttonLoading: 'Prüfe...',
},
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 später 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 für "{eventName}". Fang die Highlights des Events ein und teile sie mit allen Gästen.',
progress: {
some: 'Schon {count} Aufgaben erledigt weiter so!',
none: 'Starte mit deiner ersten Aufgabe wir zählen auf dich!',
},
defaultEventName: 'Dein Event',
},
stats: {
online: 'Gleichzeitig online',
tasksSolved: 'Aufgaben gelöst',
lastUpload: 'Letzter Upload',
completedTasks: 'Deine erledigten Aufgaben',
},
actions: {
title: 'Deine Aktionen',
subtitle: 'Wähle aus, womit du starten willst',
queueButton: 'Uploads in Warteschlange ansehen',
items: {
tasks: {
label: 'Aufgabe ziehen',
description: 'Hol dir deine nächste 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 für die besten Ergebnisse.',
steps: {
first: 'Aufgabe auswählen oder starten',
second: 'Emotion festhalten und Foto schießen',
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.',
},
galleryCountdown: {
expiresIn: 'Noch {days} Tage online',
expiresToday: 'Letzter Tag!',
expired: 'Galerie abgelaufen',
description: 'Sichere jetzt deine Lieblingsfotos die Galerie verschwindet bald.',
expiredDescription: 'Nur die Veranstalter:innen können die Galerie jetzt noch verlängern.',
ctaUpload: 'Letzte Fotos hochladen',
},
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 für dein Shooting?',
body: {
part1: 'Lass uns sicherstellen, dass alles sitzt: prüfe das Licht, wisch die Kamera sauber und richte alle Personen im Bild aus.',
part2: 'Du kannst zwischen Front- und Rückkamera wechseln und bei Bedarf ein Raster aktivieren.',
},
dismiss: 'Verstanden',
},
cameraUnsupported: {
title: 'Kamera nicht verfügbar',
message: 'Dein Gerät unterstützt keine Kameravorschau im Browser. Du kannst stattdessen Fotos aus deiner Galerie hochladen.',
openGallery: 'Foto aus Galerie wählen',
},
cameraDenied: {
title: 'Kamera-Zugriff verweigert',
explanation: 'Du musst den Zugriff auf die Kamera erlauben, um Fotos aufnehmen zu können.',
reopenPrompt: 'Systemdialog erneut öffnen',
chooseFile: 'Foto aus Galerie wählen',
prompt: 'Wir brauchen Zugriff auf deine Kamera. Erlaube die Anfrage oder wähle alternativ ein Foto aus deiner Galerie.',
},
cameraError: {
title: 'Kamera konnte nicht gestartet werden',
explanation: 'Wir konnten keine Verbindung zur Kamera herstellen. Prüfe die Berechtigungen oder starte dein Gerät neu.',
tryAgain: 'Nochmals versuchen',
},
readyOverlay: {
title: 'Kamera bereit',
message: 'Wenn alle im Bild sind, kannst du den Countdown starten oder ein vorhandenes Foto wählen.',
start: 'Countdown starten',
chooseFile: 'Foto auswählen',
},
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 Gästen.',
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 prüfen.',
},
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 für Frontkamera umschalten',
toggleFlash: 'Blitzpräferenz umschalten',
capture: 'Foto aufnehmen',
switchCamera: 'Kamera wechseln',
chooseFile: 'Foto auswählen',
},
limitReached: 'Upload-Limit erreicht ({used} / {max} Fotos). Bitte kontaktiere die Veranstalter für ein Upgrade.',
limitUnlimited: 'unbegrenzt',
limitWarning: 'Nur noch {remaining} von {max} Fotos möglich. Bitte kontaktiere die Veranstalter für ein Upgrade.',
galleryWarningDay: 'Galerie läuft in {days} Tag ab. Teile deine Fotos rechtzeitig!',
galleryWarningDays: 'Galerie läuft in {days} Tagen ab. Teile deine Fotos rechtzeitig!',
status: {
title: 'Dein Paketstatus',
subtitle: 'Behalte deine Kontingente im Blick, bevor es eng wird.',
badges: {
ok: 'Bereit',
warning: 'Hinweis',
limit_reached: 'Limit erreicht',
unlimited: 'Unbegrenzt',
},
cards: {
photos: {
title: 'Fotos',
remaining: 'Nur noch {remaining} von {limit} Fotos möglich',
unlimited: 'Unbegrenzte Foto-Uploads inklusive',
},
guests: {
title: 'Gäste',
remaining: '{remaining} Gäste frei (max. {limit})',
unlimited: 'Beliebig viele Gäste erlaubt',
},
},
},
dialogs: {
close: 'Verstanden',
photoLimit: {
title: 'Upload-Limit erreicht',
description: 'Es wurden {used} von {limit} Fotos hochgeladen. Bitte kontaktiere das Team für ein Upgrade.',
hint: 'Tipp: Größere Pakete schalten sofort wieder Uploads frei.',
},
deviceLimit: {
title: 'Gerätelimit erreicht',
description: 'Dieses Gerät hat sein Upload-Kontingent ausgeschöpft.',
hint: 'Nutze ein anderes Gerät oder sprich die Veranstalter:innen an.',
},
packageMissing: {
title: 'Event pausiert Uploads',
description: 'Für dieses Event sind aktuell keine Uploads freigeschaltet.',
hint: 'Bitte kontaktiere die Veranstalter:innen für weitere Informationen.',
},
galleryExpired: {
title: 'Galerie abgelaufen',
description: 'Die Galerie ist geschlossen neue Uploads sind nicht mehr möglich.',
hint: 'Nur die Veranstalter:innen können die Galerie verlängern.',
},
csrf: {
title: 'Sitzung abgelaufen',
description: 'Bitte lade die Seite neu und versuche den Upload anschließend erneut.',
hint: 'Aktualisiere die Seite, um eine neue Sitzung zu starten.',
},
generic: {
title: 'Upload fehlgeschlagen',
description: 'Der Upload konnte nicht abgeschlossen werden. Bitte versuche es später erneut.',
hint: 'Bleibt das Problem bestehen, kontaktiere die Veranstalter:innen.',
},
},
errors: {
photoLimit: 'Upload-Limit erreicht. Bitte kontaktiere die Veranstalter für ein Upgrade.',
deviceLimit: 'Dieses Gerät hat das Upload-Limit erreicht. Bitte wende dich an die Veranstalter.',
packageMissing: 'Dieses Event akzeptiert derzeit keine Uploads.',
galleryExpired: 'Die Galerie ist abgelaufen. Uploads sind nicht mehr möglich.',
generic: 'Upload fehlgeschlagen. Bitte versuche es erneut.',
},
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 Prüfen des Upload-Limits. Upload deaktiviert.',
galleryPickError: 'Auswahl fehlgeschlagen. Bitte versuche es erneut.',
captureButton: 'Foto aufnehmen',
galleryButton: 'Foto aus Galerie wählen',
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: 'Wähle deine bevorzugte Sprache für diese Veranstaltung.',
activeBadge: 'aktiv',
option: {
de: 'Deutsch',
en: 'English',
},
},
name: {
title: 'Dein Name',
description: 'Passe an, wie wir dich im Event begrüßen. Der Name wird nur lokal gespeichert.',
label: 'Anzeigename',
placeholder: 'z.B. Anna',
save: 'Name speichern',
saving: 'Speichere...',
reset: 'Zurücksetzen',
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 später erneut.',
fallbackTitle: 'Rechtlicher Hinweis',
section: {
impressum: 'Impressum',
privacy: 'Datenschutz',
terms: 'AGB',
},
},
cache: {
title: 'Offline-Cache',
description: 'Lösche lokale Daten, falls Inhalte veraltet erscheinen oder Uploads hängen bleiben.',
clear: 'Cache leeren',
clearing: 'Leere Cache...',
cleared: 'Cache gelöscht.',
note: 'Dies betrifft nur diesen Browser und muss pro Gerät erneut ausgeführt werden.',
},
footer: {
notice: 'Gastbereich - Daten werden lokal im Browser gespeichert.',
},
sheet: {
openLabel: 'Einstellungen öffnen',
backLabel: 'Zurück',
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.',
},
access_rate_limited: {
title: 'Too many requests',
description: 'There were too many requests in a short time. Please wait a moment and try again.',
hint: 'Tip: You can retry shortly.',
},
gallery_expired: {
title: 'Gallery unavailable',
description: 'The gallery for this event is no longer accessible.',
ctaLabel: 'Request new code',
},
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.',
},
galleryCountdown: {
expiresIn: '{days} days remaining',
expiresToday: 'Final day!',
expired: 'Gallery expired',
description: 'Save your favourite photos before the gallery goes offline.',
expiredDescription: 'Only the organizers can extend the gallery now.',
ctaUpload: 'Share last photos',
},
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',
limitWarning: 'Only {remaining} of {max} photos left. Please contact the organizers for an upgrade.',
galleryWarningDay: 'Gallery expires in {days} day. Upload your photos soon!',
galleryWarningDays: 'Gallery expires in {days} days. Upload your photos soon!',
status: {
title: 'Your package status',
subtitle: 'Keep an eye on your remaining allowances.',
badges: {
ok: 'Ready',
warning: 'Heads-up',
limit_reached: 'Limit reached',
unlimited: 'Unlimited',
},
cards: {
photos: {
title: 'Photos',
remaining: '{remaining} of {limit} photo slots left',
unlimited: 'Unlimited photo uploads included',
},
guests: {
title: 'Guests',
remaining: '{remaining} guest slots free (max {limit})',
unlimited: 'Unlimited guests allowed',
},
},
},
dialogs: {
close: 'Got it',
photoLimit: {
title: 'Upload limit reached',
description: '{used} of {limit} photos are already uploaded. Please reach out to upgrade your package.',
hint: 'Tip: Upgrading the package re-enables uploads instantly.',
},
deviceLimit: {
title: 'Device limit reached',
description: 'This device has used all available upload slots.',
hint: 'Try a different device or contact the organizers.',
},
packageMissing: {
title: 'Uploads paused',
description: 'This event is currently not accepting new uploads.',
hint: 'Check in with the organizers for the latest status.',
},
galleryExpired: {
title: 'Gallery closed',
description: 'The gallery has expired and no new uploads can be added.',
hint: 'Only the organizers can extend the gallery window.',
},
csrf: {
title: 'Session expired',
description: 'Refresh the page and try the upload again.',
hint: 'Reload the page to start a new session.',
},
generic: {
title: 'Upload failed',
description: 'We could not complete the upload. Please try again later.',
hint: 'If it keeps happening, reach out to the organizers.',
},
},
errors: {
photoLimit: 'Upload limit reached. Contact the organizers for an upgrade.',
deviceLimit: 'This device reached its upload limit. Please contact the organizers.',
packageMissing: 'This event is not accepting uploads right now.',
galleryExpired: 'The gallery has expired. Uploads are no longer possible.',
generic: 'Upload failed. Please try again.',
},
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);
}