762 lines
27 KiB
TypeScript
762 lines
27 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: '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.',
|
||
},
|
||
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.',
|
||
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.',
|
||
},
|
||
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.',
|
||
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);
|
||
}
|