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 = { 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 Fotospiel.App!', headline: 'Elegante Erinnerungen, live erzählt.', subheadline: 'Hier beginnt euer Fotoabenteuer – gemeinsam, intuitiv und live.', 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', }, }, tasks: { page: { eyebrow: 'Aufgaben-Zentrale', title: 'Deine nächste Aufgabe', subtitle: 'Wähle eine Stimmung aus oder lass dich überraschen.', swipeHint: 'Tipp: Links wischen = neue Aufgabe · Rechts wischen = Inspiration', completedLabel: 'Schon erledigt', ctaStart: "Los geht's!", shuffleCta: 'Was Neues!', shuffleButton: 'Shuffle', inspirationTitle: 'Foto-Inspiration', inspirationLoading: 'lädt…', inspirationEmptyTitle: 'Noch kein Foto zu dieser Aufgabe', inspirationEmptyDescription: 'Sei die/der Erste und lade eins hoch', inspirationMore: 'Mehr', inspirationError: 'Fotos konnten nicht geladen werden', suggestionsEyebrow: 'Mehr Inspiration', suggestionsTitle: 'Spring direkt zur nächsten Aufgabe', noTasksAlert: 'Für dieses Event sind derzeit keine Aufgaben hinterlegt.', emptyTitle: 'Keine passende Aufgabe gefunden', emptyDescriptionWithTasks: 'Für deine aktuelle Stimmung gibt es gerade keine Aufgabe. Wähle eine andere Stimmung oder lade neue Aufgaben.', emptyDescriptionNoTasks: 'Hier sind noch keine Aufgaben hinterlegt. Bitte versuche es später erneut.', reloadButton: 'Aufgaben neu laden', filters: { none: 'Kein Filter', recentFallback: 'Stimmung wählen', showAll: 'Alle zeigen', dialogTitle: 'Alle verfügbaren Stimmungen', empty: 'Für dieses Event stehen noch keine Stimmungen bereit.', countOne: '{count} Aufgabe', countMany: '{count} Aufgaben', }, }, }, 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', }, share: { title: 'Geteiltes Foto', defaultEvent: 'Ein besonderer Moment', button: 'Teilen', copyLink: 'Link kopieren', copySuccess: 'Link kopiert!', copyError: 'Link konnte nicht kopiert werden.', manualPrompt: 'Link kopieren', openEvent: 'Event öffnen', loading: 'Moment wird geladen...', expiredTitle: 'Link abgelaufen', expiredDescription: 'Dieser Link ist nicht mehr verfügbar.', shareText: 'Schau dir diesen Moment bei Fotospiel an.', error: 'Teilen fehlgeschlagen', }, 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', }, hud: { title: 'Live-Missionen', subtitle: 'Bleib im Flow – Kamera bereithalten.', moodLabel: 'Stimmung: {mood}', moodFallback: 'Freestyle', ctaLabel: 'Inspiration öffnen', cards: { online: 'Gäste online', completed: 'Aufgaben gelöst', lastUpload: 'Letzter Upload', }, progressLabel: 'Story {count}/{target} aktiv', liveGuests: '{count} Gäste live', relative: { now: 'Gerade eben', minutes: 'vor {count} Min', hours: 'vor {count} Std', days: 'vor {count} Tagen', }, }, limitSummary: { title: 'Uploads & Slots', subtitle: 'Dein Event-Paket im Überblick', badgeLabel: 'Aktuell', cards: { photos: { title: 'Fotos insgesamt', remaining: '{remaining} von {limit} frei', unlimited: 'Unlimitierte Foto-Uploads aktiv', }, guests: { title: 'Geräte im Einsatz', remaining: '{remaining} Slots verfügbar', unlimited: 'Unlimitierte Geräte freigeschaltet', }, }, badges: { ok: 'OK', warning: 'Bald voll', limit_reached: 'Limit erreicht', unlimited: 'Unlimitiert', }, }, 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!', packageStatus: { 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.', }, help: { title: 'Hilfe & Support', description: 'Öffne das Hilfecenter mit Schritt-für-Schritt-Anleitungen.', cta: 'Hilfecenter öffnen', }, footer: { notice: 'Gastbereich - Daten werden lokal im Browser gespeichert.', }, sheet: { openLabel: 'Einstellungen öffnen', backLabel: 'Zurück', legalDescription: 'Rechtlicher Hinweis', }, }, help: { center: { title: 'Hilfe & Tipps', subtitle: 'Antworten für Gäste – nach dem ersten Laden auch offline verfügbar.', searchPlaceholder: 'Suche nach Thema oder Stichwort', offlineBadge: 'Offline-Version', offlineDescription: 'Du siehst eine zwischengespeicherte Version. Geh online für aktuelle Inhalte.', empty: 'Keine Artikel gefunden.', error: 'Hilfe konnte nicht geladen werden.', retry: 'Erneut versuchen', listTitle: 'Alle Artikel', }, article: { back: 'Zurück zur Übersicht', updated: 'Aktualisiert am {date}', relatedTitle: 'Verwandte Artikel', unavailable: 'Dieser Artikel ist nicht verfügbar.', reload: 'Neu laden', }, }, }, 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 Fotospiel.App!', headline: 'An elegant way to tell memories live.', subheadline: 'Start your collaborative photo story—intuitive, fast, live.', 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', }, }, tasks: { page: { eyebrow: 'Mission hub', title: 'Your next task', subtitle: 'Pick a mood or stay spontaneous.', swipeHint: 'Tip: Swipe left for a new task · right for inspiration', completedLabel: 'Already done', ctaStart: "Let's go!", shuffleCta: 'Something new!', shuffleButton: 'Shuffle', inspirationTitle: 'Photo inspiration', inspirationLoading: 'loading…', inspirationEmptyTitle: 'No photo for this task yet', inspirationEmptyDescription: 'Be the first one to upload!', inspirationMore: 'More', inspirationError: 'Photos could not be loaded', suggestionsEyebrow: 'More inspiration', suggestionsTitle: 'Jump straight to the next task', noTasksAlert: 'No tasks available for this event yet.', emptyTitle: 'No matching task found', emptyDescriptionWithTasks: 'No task matches this mood right now. Pick another mood or load new tasks.', emptyDescriptionNoTasks: 'No tasks are available yet. Please try again later.', reloadButton: 'Reload tasks', filters: { none: 'No filter', recentFallback: 'Select mood', showAll: 'Show all', dialogTitle: 'All available moods', empty: 'No moods are available for this event yet.', countOne: '{count} task', countMany: '{count} tasks', }, }, }, 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', }, share: { title: 'Shared photo', defaultEvent: 'A special moment', button: 'Share', copyLink: 'Copy link', copySuccess: 'Link copied!', copyError: 'Link could not be copied.', manualPrompt: 'Copy link', openEvent: 'Open event', loading: 'Loading moment...', expiredTitle: 'Link expired', expiredDescription: 'This link is no longer available.', shareText: 'Check out this moment on Fotospiel.', error: 'Share failed', }, 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', }, hud: { title: 'Live missions', subtitle: 'Stay in the flow – keep the camera ready.', moodLabel: 'Mood: {mood}', moodFallback: 'Freestyle', ctaLabel: 'Open inspiration', cards: { online: 'Guests online', completed: 'Tasks completed', lastUpload: 'Latest upload', }, progressLabel: 'Story {count}/{target} active', liveGuests: '{count} guests live', relative: { now: 'Just now', minutes: '{count} min ago', hours: '{count} h ago', days: '{count} days ago', }, }, limitSummary: { title: 'Uploads & slots', subtitle: 'Your event package overview', badgeLabel: 'Current', cards: { photos: { title: 'Photos total', remaining: '{remaining} of {limit} free', unlimited: 'Unlimited photo uploads', }, guests: { title: 'Devices in use', remaining: '{remaining} slots available', unlimited: 'Unlimited devices enabled', }, }, badges: { ok: 'OK', warning: 'Almost full', limit_reached: 'Limit reached', unlimited: 'Unlimited', }, }, 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!', packageStatus: { 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.', }, help: { title: 'Help & support', description: 'Open the help center for guides and quick answers.', cta: 'Open help center', }, footer: { notice: 'Guest area - data is stored locally in the browser.', }, sheet: { openLabel: 'Open settings', backLabel: 'Back', legalDescription: 'Legal notice', }, }, help: { center: { title: 'Help & tips', subtitle: 'Guides for guests – available offline after the first sync.', searchPlaceholder: 'Search by topic or keyword', offlineBadge: 'Offline copy', offlineDescription: 'You are viewing cached content. Go online to refresh articles.', empty: 'No articles found.', error: 'Help could not be loaded.', retry: 'Try again', listTitle: 'All articles', }, article: { back: 'Back to overview', updated: 'Updated on {date}', relatedTitle: 'Related articles', unavailable: 'This article is unavailable.', reload: 'Reload', }, }, }, }; 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); }