feat(events): full i18n coverage across 12 files — DE/EN/ES/FR/IT

Events (party/RSVP) module had ~38 hardcoded German strings across
DetailView (13 incl. share row + map labels), SourceManager (5),
DiscoveredEventCard (3), DiscoveryTab (3), EventCard (3), RsvpSummary
(3), ListView (3), BringListEditor (2), GuestListEditor (2),
PublicRsvpList (2), DiscoverySetup (2), RegionPicker (1).

New `events` namespace with 119 keys × 5 locales:
- `list_view.*`, `detail_view.*` (incl. share/publish/map sections),
  `event_card.*` (status badges + summary), `discovered_card.*`,
  `discovery_tab.*`, `discovery_setup.*`, `region_picker.*`,
  `source_manager.*` (incl. errors_count + last_scan), `bring_list_editor.*`,
  `guest_list_editor.*` (RSVP options), `public_rsvp_list.*` (status
  labels + meta), `rsvp_summary.*` (yes/maybe/no/pending labels).
- SourceManager.formatDate now uses get(locale) instead of hardcoded
  'de-DE' for last-scan timestamps.

- Baseline ratchet: 1640 → 1602 (38 strings cleared)
- validate:i18n-parity: 42 namespaces × 5 locales — 4100 keys aligned
- svelte-check: no new errors

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-26 23:20:09 +02:00
parent c07db300b0
commit c7d80e3423
18 changed files with 975 additions and 139 deletions

View file

@ -0,0 +1,145 @@
{
"list_view": {
"doc_title": "Events - Mana",
"tab_mine": "Meine Events",
"tab_discover": "Entdecken",
"subtitle_count": "{upcoming} bevorstehend · {past} vergangen",
"new_event_btn": "+ Neues Event",
"cancel_btn": "Abbrechen",
"placeholder_title": "Worum geht's? (z. B. Geburtstag Anna)",
"placeholder_location": "Ort (optional)",
"submit_create": "Event anlegen",
"section_upcoming": "Bevorstehend",
"section_past": "Vergangen",
"empty_upcoming": "Keine bevorstehenden Events. Zeit für eine Party?"
},
"detail_view": {
"loading": "Lade Event...",
"back": "← Zurück",
"action_edit": "Bearbeiten",
"action_delete": "Löschen",
"confirm_delete": "Event \"{title}\" wirklich löschen?",
"placeholder_title": "Event-Titel",
"placeholder_description": "Beschreibung",
"placeholder_location": "Ort — tippe eine Adresse...",
"location_pinned_title": "Koordinaten gesetzt",
"label_start": "Start",
"label_end": "Ende",
"label_all_day": "Ganztägig",
"action_cancel": "Abbrechen",
"action_save": "Speichern",
"map_iframe_title": "Event-Ort auf Karte",
"map_open": "In OpenStreetMap öffnen →",
"label_visibility": "Sichtbarkeit",
"section_rsvps": "RSVPs",
"section_guests": "Gäste",
"section_bring_list": "Bring-Liste",
"section_share": "Teilen",
"action_copy_link": "Kopieren",
"action_make_private": "Privat machen",
"share_hint": "Antworten erscheinen automatisch unten in „Antworten via Link\" (Polling alle 30s).",
"action_publish": "Event veröffentlichen & Link generieren"
},
"event_card": {
"badge_draft": "Entwurf",
"badge_cancelled": "Abgesagt",
"badge_published": "Geteilt",
"all_day": "Ganztägig",
"yes_count": "{count} kommen",
"pending_count": "· {count} offen"
},
"discovered_card": {
"all_day": "Ganztag",
"source_fallback": "Quelle",
"saved_label": "Gespeichert",
"action_save": "Merken",
"action_dismiss": "Ausblenden"
},
"discovery_tab": {
"loading": "Lade...",
"action_refresh": "Aktualisieren",
"action_sources": "Quellen",
"action_sources_count": "Quellen ({count})",
"loading_events": "Lade Events...",
"empty_title": "Noch keine Events gefunden",
"empty_hint": "Füge iCal-Feeds von Venues oder Vereinen hinzu, um Events zu entdecken.",
"action_manage_sources": "Quellen verwalten",
"action_load_more": "Mehr laden"
},
"discovery_setup": {
"title": "Event-Entdeckung einrichten",
"step1_desc": "Welche Regionen sollen nach Events durchsucht werden?",
"action_next": "Weiter",
"step2_desc": "Was interessiert dich?",
"placeholder_freetext": "Weitere Interessen (kommagetrennt, z.B. Impro-Theater, Rust Meetups)",
"action_back": "Zurück",
"action_finish": "Fertig"
},
"region_picker": {
"radius_unit": "{km} km",
"add_region": "+ Region",
"placeholder_search": "Stadt oder Region suchen...",
"radius_label": "Radius: {km} km",
"searching": "Suche...",
"action_cancel": "Abbrechen"
},
"source_manager": {
"section_title": "Quellen",
"action_discover": "Automatisch finden",
"action_discovering": "Suche...",
"action_add_ical": "+ iCal-Feed",
"placeholder_name": "Name (z.B. Jazzhaus Freiburg)",
"placeholder_url": "iCal URL (.ics)",
"action_submit_add": "Hinzufügen",
"action_cancel": "Abbrechen",
"section_suggested": "Vorgeschlagene Quellen",
"action_activate": "Aktivieren",
"action_reject": "x",
"empty": "Noch keine Quellen. Nutze \"Automatisch finden\" oder füge iCal-Feeds manuell hinzu.",
"meta_last_scan": "Letzter Scan: {date}",
"never_scanned": "nie",
"errors_count": "{count} Fehler",
"action_scan": "Scannen",
"action_scan_title": "Jetzt scannen",
"action_remove_title": "Entfernen"
},
"bring_list_editor": {
"placeholder_label": "z. B. Salat, Wein, Lautsprecher",
"placeholder_quantity": "Anzahl",
"action_add": "Hinzufügen",
"option_no_one": "— Niemand —",
"action_remove_title": "Entfernen",
"empty": "Noch nichts auf der Liste.",
"claimed_via_link": "{name} (via Link)",
"unknown_assignee": "?"
},
"guest_list_editor": {
"placeholder_name": "Name",
"placeholder_email": "E-Mail (optional)",
"action_add": "Hinzufügen",
"action_remove_title": "Entfernen",
"empty": "Noch keine Gäste hinzugefügt.",
"rsvp_pending": "Offen",
"rsvp_yes": "Ja",
"rsvp_maybe": "Vielleicht",
"rsvp_no": "Nein"
},
"public_rsvp_list": {
"title": "Antworten via Link",
"action_refresh": "Neu laden",
"err_load": "Fehler beim Laden",
"empty": "Noch keine Antworten via Share-Link.",
"status_yes": "Ja",
"status_no": "Nein",
"status_maybe": "Vielleicht",
"meta_updated": "Aktualisiert um {time} · Auto-Refresh alle 30s",
"action_import_title": "Zur Gästeliste"
},
"rsvp_summary": {
"label_yes": "Ja",
"label_maybe": "Vielleicht",
"label_no": "Nein",
"label_pending": "Offen",
"attending": "kommen"
}
}

View file

@ -0,0 +1,145 @@
{
"list_view": {
"doc_title": "Events - Mana",
"tab_mine": "My events",
"tab_discover": "Discover",
"subtitle_count": "{upcoming} upcoming · {past} past",
"new_event_btn": "+ New event",
"cancel_btn": "Cancel",
"placeholder_title": "What's it about? (e.g. Anna's birthday)",
"placeholder_location": "Location (optional)",
"submit_create": "Create event",
"section_upcoming": "Upcoming",
"section_past": "Past",
"empty_upcoming": "No upcoming events. Time for a party?"
},
"detail_view": {
"loading": "Loading event...",
"back": "← Back",
"action_edit": "Edit",
"action_delete": "Delete",
"confirm_delete": "Really delete event \"{title}\"?",
"placeholder_title": "Event title",
"placeholder_description": "Description",
"placeholder_location": "Location — type an address...",
"location_pinned_title": "Coordinates set",
"label_start": "Start",
"label_end": "End",
"label_all_day": "All day",
"action_cancel": "Cancel",
"action_save": "Save",
"map_iframe_title": "Event location on map",
"map_open": "Open in OpenStreetMap →",
"label_visibility": "Visibility",
"section_rsvps": "RSVPs",
"section_guests": "Guests",
"section_bring_list": "Bring list",
"section_share": "Share",
"action_copy_link": "Copy",
"action_make_private": "Make private",
"share_hint": "Replies appear automatically below in \"Replies via link\" (polling every 30s).",
"action_publish": "Publish event & generate link"
},
"event_card": {
"badge_draft": "Draft",
"badge_cancelled": "Cancelled",
"badge_published": "Shared",
"all_day": "All day",
"yes_count": "{count} attending",
"pending_count": "· {count} pending"
},
"discovered_card": {
"all_day": "All day",
"source_fallback": "Source",
"saved_label": "Saved",
"action_save": "Save",
"action_dismiss": "Dismiss"
},
"discovery_tab": {
"loading": "Loading...",
"action_refresh": "Refresh",
"action_sources": "Sources",
"action_sources_count": "Sources ({count})",
"loading_events": "Loading events...",
"empty_title": "No events found yet",
"empty_hint": "Add iCal feeds from venues or clubs to discover events.",
"action_manage_sources": "Manage sources",
"action_load_more": "Load more"
},
"discovery_setup": {
"title": "Set up event discovery",
"step1_desc": "Which regions should be searched for events?",
"action_next": "Next",
"step2_desc": "What interests you?",
"placeholder_freetext": "More interests (comma-separated, e.g. improv theatre, Rust meetups)",
"action_back": "Back",
"action_finish": "Done"
},
"region_picker": {
"radius_unit": "{km} km",
"add_region": "+ Region",
"placeholder_search": "Search city or region...",
"radius_label": "Radius: {km} km",
"searching": "Searching...",
"action_cancel": "Cancel"
},
"source_manager": {
"section_title": "Sources",
"action_discover": "Find automatically",
"action_discovering": "Searching...",
"action_add_ical": "+ iCal feed",
"placeholder_name": "Name (e.g. Jazzhaus Freiburg)",
"placeholder_url": "iCal URL (.ics)",
"action_submit_add": "Add",
"action_cancel": "Cancel",
"section_suggested": "Suggested sources",
"action_activate": "Activate",
"action_reject": "x",
"empty": "No sources yet. Use \"Find automatically\" or add iCal feeds manually.",
"meta_last_scan": "Last scan: {date}",
"never_scanned": "never",
"errors_count": "{count} errors",
"action_scan": "Scan",
"action_scan_title": "Scan now",
"action_remove_title": "Remove"
},
"bring_list_editor": {
"placeholder_label": "e.g. salad, wine, speaker",
"placeholder_quantity": "Count",
"action_add": "Add",
"option_no_one": "— No one —",
"action_remove_title": "Remove",
"empty": "Nothing on the list yet.",
"claimed_via_link": "{name} (via link)",
"unknown_assignee": "?"
},
"guest_list_editor": {
"placeholder_name": "Name",
"placeholder_email": "Email (optional)",
"action_add": "Add",
"action_remove_title": "Remove",
"empty": "No guests added yet.",
"rsvp_pending": "Pending",
"rsvp_yes": "Yes",
"rsvp_maybe": "Maybe",
"rsvp_no": "No"
},
"public_rsvp_list": {
"title": "Replies via link",
"action_refresh": "Reload",
"err_load": "Loading error",
"empty": "No replies via share link yet.",
"status_yes": "Yes",
"status_no": "No",
"status_maybe": "Maybe",
"meta_updated": "Updated at {time} · Auto-refresh every 30s",
"action_import_title": "Add to guest list"
},
"rsvp_summary": {
"label_yes": "Yes",
"label_maybe": "Maybe",
"label_no": "No",
"label_pending": "Pending",
"attending": "attending"
}
}

View file

@ -0,0 +1,145 @@
{
"list_view": {
"doc_title": "Eventos - Mana",
"tab_mine": "Mis eventos",
"tab_discover": "Descubrir",
"subtitle_count": "{upcoming} próximos · {past} pasados",
"new_event_btn": "+ Nuevo evento",
"cancel_btn": "Cancelar",
"placeholder_title": "¿De qué se trata? (p. ej. cumpleaños de Anna)",
"placeholder_location": "Lugar (opcional)",
"submit_create": "Crear evento",
"section_upcoming": "Próximos",
"section_past": "Pasados",
"empty_upcoming": "No hay eventos próximos. ¿Tiempo para una fiesta?"
},
"detail_view": {
"loading": "Cargando evento...",
"back": "← Atrás",
"action_edit": "Editar",
"action_delete": "Eliminar",
"confirm_delete": "¿Eliminar de verdad el evento \"{title}\"?",
"placeholder_title": "Título del evento",
"placeholder_description": "Descripción",
"placeholder_location": "Lugar — escribe una dirección...",
"location_pinned_title": "Coordenadas establecidas",
"label_start": "Inicio",
"label_end": "Fin",
"label_all_day": "Todo el día",
"action_cancel": "Cancelar",
"action_save": "Guardar",
"map_iframe_title": "Lugar del evento en el mapa",
"map_open": "Abrir en OpenStreetMap →",
"label_visibility": "Visibilidad",
"section_rsvps": "RSVPs",
"section_guests": "Invitados",
"section_bring_list": "Lista de cosas",
"section_share": "Compartir",
"action_copy_link": "Copiar",
"action_make_private": "Hacer privado",
"share_hint": "Las respuestas aparecen automáticamente abajo en «Respuestas vía enlace» (polling cada 30s).",
"action_publish": "Publicar evento y generar enlace"
},
"event_card": {
"badge_draft": "Borrador",
"badge_cancelled": "Cancelado",
"badge_published": "Compartido",
"all_day": "Todo el día",
"yes_count": "{count} asisten",
"pending_count": "· {count} pendientes"
},
"discovered_card": {
"all_day": "Todo el día",
"source_fallback": "Fuente",
"saved_label": "Guardado",
"action_save": "Guardar",
"action_dismiss": "Descartar"
},
"discovery_tab": {
"loading": "Cargando...",
"action_refresh": "Actualizar",
"action_sources": "Fuentes",
"action_sources_count": "Fuentes ({count})",
"loading_events": "Cargando eventos...",
"empty_title": "Aún no se han encontrado eventos",
"empty_hint": "Añade feeds iCal de salas o clubes para descubrir eventos.",
"action_manage_sources": "Gestionar fuentes",
"action_load_more": "Cargar más"
},
"discovery_setup": {
"title": "Configurar descubrimiento de eventos",
"step1_desc": "¿En qué regiones se deben buscar eventos?",
"action_next": "Siguiente",
"step2_desc": "¿Qué te interesa?",
"placeholder_freetext": "Más intereses (separados por coma, p. ej. teatro improv, meetups Rust)",
"action_back": "Atrás",
"action_finish": "Listo"
},
"region_picker": {
"radius_unit": "{km} km",
"add_region": "+ Región",
"placeholder_search": "Buscar ciudad o región...",
"radius_label": "Radio: {km} km",
"searching": "Buscando...",
"action_cancel": "Cancelar"
},
"source_manager": {
"section_title": "Fuentes",
"action_discover": "Buscar automáticamente",
"action_discovering": "Buscando...",
"action_add_ical": "+ Feed iCal",
"placeholder_name": "Nombre (p. ej. Jazzhaus Freiburg)",
"placeholder_url": "URL iCal (.ics)",
"action_submit_add": "Añadir",
"action_cancel": "Cancelar",
"section_suggested": "Fuentes sugeridas",
"action_activate": "Activar",
"action_reject": "x",
"empty": "Aún no hay fuentes. Usa \"Buscar automáticamente\" o añade feeds iCal manualmente.",
"meta_last_scan": "Último escaneo: {date}",
"never_scanned": "nunca",
"errors_count": "{count} errores",
"action_scan": "Escanear",
"action_scan_title": "Escanear ahora",
"action_remove_title": "Quitar"
},
"bring_list_editor": {
"placeholder_label": "p. ej. ensalada, vino, altavoz",
"placeholder_quantity": "Cantidad",
"action_add": "Añadir",
"option_no_one": "— Nadie —",
"action_remove_title": "Quitar",
"empty": "Aún no hay nada en la lista.",
"claimed_via_link": "{name} (vía enlace)",
"unknown_assignee": "?"
},
"guest_list_editor": {
"placeholder_name": "Nombre",
"placeholder_email": "Correo electrónico (opcional)",
"action_add": "Añadir",
"action_remove_title": "Quitar",
"empty": "Aún no se han añadido invitados.",
"rsvp_pending": "Pendiente",
"rsvp_yes": "Sí",
"rsvp_maybe": "Quizás",
"rsvp_no": "No"
},
"public_rsvp_list": {
"title": "Respuestas vía enlace",
"action_refresh": "Recargar",
"err_load": "Error al cargar",
"empty": "Aún no hay respuestas vía enlace.",
"status_yes": "Sí",
"status_no": "No",
"status_maybe": "Quizás",
"meta_updated": "Actualizado a las {time} · Auto-refresco cada 30s",
"action_import_title": "A la lista de invitados"
},
"rsvp_summary": {
"label_yes": "Sí",
"label_maybe": "Quizás",
"label_no": "No",
"label_pending": "Pendiente",
"attending": "asisten"
}
}

View file

@ -0,0 +1,145 @@
{
"list_view": {
"doc_title": "Événements - Mana",
"tab_mine": "Mes événements",
"tab_discover": "Découvrir",
"subtitle_count": "{upcoming} à venir · {past} passés",
"new_event_btn": "+ Nouvel événement",
"cancel_btn": "Annuler",
"placeholder_title": "De quoi s'agit-il ? (p. ex. anniversaire d'Anna)",
"placeholder_location": "Lieu (optionnel)",
"submit_create": "Créer l'événement",
"section_upcoming": "À venir",
"section_past": "Passés",
"empty_upcoming": "Aucun événement à venir. C'est l'heure d'une fête ?"
},
"detail_view": {
"loading": "Chargement de l'événement...",
"back": "← Retour",
"action_edit": "Modifier",
"action_delete": "Supprimer",
"confirm_delete": "Vraiment supprimer l'événement « {title} » ?",
"placeholder_title": "Titre de l'événement",
"placeholder_description": "Description",
"placeholder_location": "Lieu — saisis une adresse...",
"location_pinned_title": "Coordonnées définies",
"label_start": "Début",
"label_end": "Fin",
"label_all_day": "Toute la journée",
"action_cancel": "Annuler",
"action_save": "Enregistrer",
"map_iframe_title": "Lieu de l'événement sur la carte",
"map_open": "Ouvrir dans OpenStreetMap →",
"label_visibility": "Visibilité",
"section_rsvps": "RSVPs",
"section_guests": "Invités",
"section_bring_list": "Liste à apporter",
"section_share": "Partager",
"action_copy_link": "Copier",
"action_make_private": "Rendre privé",
"share_hint": "Les réponses apparaissent automatiquement ci-dessous dans « Réponses via le lien » (polling toutes les 30s).",
"action_publish": "Publier l'événement & générer le lien"
},
"event_card": {
"badge_draft": "Brouillon",
"badge_cancelled": "Annulé",
"badge_published": "Partagé",
"all_day": "Toute la journée",
"yes_count": "{count} viennent",
"pending_count": "· {count} en attente"
},
"discovered_card": {
"all_day": "Toute la journée",
"source_fallback": "Source",
"saved_label": "Enregistré",
"action_save": "Enregistrer",
"action_dismiss": "Masquer"
},
"discovery_tab": {
"loading": "Chargement...",
"action_refresh": "Actualiser",
"action_sources": "Sources",
"action_sources_count": "Sources ({count})",
"loading_events": "Chargement des événements...",
"empty_title": "Aucun événement trouvé pour l'instant",
"empty_hint": "Ajoute des flux iCal de salles ou d'associations pour découvrir des événements.",
"action_manage_sources": "Gérer les sources",
"action_load_more": "Charger plus"
},
"discovery_setup": {
"title": "Configurer la découverte d'événements",
"step1_desc": "Quelles régions doivent être recherchées ?",
"action_next": "Suivant",
"step2_desc": "Qu'est-ce qui t'intéresse ?",
"placeholder_freetext": "Autres centres d'intérêt (séparés par virgule, p. ex. impro-théâtre, meetups Rust)",
"action_back": "Retour",
"action_finish": "Terminé"
},
"region_picker": {
"radius_unit": "{km} km",
"add_region": "+ Région",
"placeholder_search": "Rechercher une ville ou région...",
"radius_label": "Rayon : {km} km",
"searching": "Recherche...",
"action_cancel": "Annuler"
},
"source_manager": {
"section_title": "Sources",
"action_discover": "Trouver automatiquement",
"action_discovering": "Recherche...",
"action_add_ical": "+ Flux iCal",
"placeholder_name": "Nom (p. ex. Jazzhaus Freiburg)",
"placeholder_url": "URL iCal (.ics)",
"action_submit_add": "Ajouter",
"action_cancel": "Annuler",
"section_suggested": "Sources suggérées",
"action_activate": "Activer",
"action_reject": "x",
"empty": "Pas encore de sources. Utilise « Trouver automatiquement » ou ajoute des flux iCal manuellement.",
"meta_last_scan": "Dernier scan : {date}",
"never_scanned": "jamais",
"errors_count": "{count} erreurs",
"action_scan": "Scanner",
"action_scan_title": "Scanner maintenant",
"action_remove_title": "Retirer"
},
"bring_list_editor": {
"placeholder_label": "p. ex. salade, vin, enceinte",
"placeholder_quantity": "Quantité",
"action_add": "Ajouter",
"option_no_one": "— Personne —",
"action_remove_title": "Retirer",
"empty": "Rien sur la liste pour l'instant.",
"claimed_via_link": "{name} (via lien)",
"unknown_assignee": "?"
},
"guest_list_editor": {
"placeholder_name": "Nom",
"placeholder_email": "E-mail (optionnel)",
"action_add": "Ajouter",
"action_remove_title": "Retirer",
"empty": "Aucun invité ajouté pour l'instant.",
"rsvp_pending": "En attente",
"rsvp_yes": "Oui",
"rsvp_maybe": "Peut-être",
"rsvp_no": "Non"
},
"public_rsvp_list": {
"title": "Réponses via lien",
"action_refresh": "Recharger",
"err_load": "Erreur de chargement",
"empty": "Pas encore de réponses via lien.",
"status_yes": "Oui",
"status_no": "Non",
"status_maybe": "Peut-être",
"meta_updated": "Mis à jour à {time} · Auto-rafraîchissement toutes les 30s",
"action_import_title": "Vers la liste d'invités"
},
"rsvp_summary": {
"label_yes": "Oui",
"label_maybe": "Peut-être",
"label_no": "Non",
"label_pending": "En attente",
"attending": "viennent"
}
}

View file

@ -0,0 +1,145 @@
{
"list_view": {
"doc_title": "Eventi - Mana",
"tab_mine": "I miei eventi",
"tab_discover": "Scopri",
"subtitle_count": "{upcoming} in arrivo · {past} passati",
"new_event_btn": "+ Nuovo evento",
"cancel_btn": "Annulla",
"placeholder_title": "Di cosa si tratta? (es. compleanno di Anna)",
"placeholder_location": "Luogo (opzionale)",
"submit_create": "Crea evento",
"section_upcoming": "In arrivo",
"section_past": "Passati",
"empty_upcoming": "Nessun evento in arrivo. Tempo di una festa?"
},
"detail_view": {
"loading": "Caricamento evento...",
"back": "← Indietro",
"action_edit": "Modifica",
"action_delete": "Elimina",
"confirm_delete": "Eliminare davvero l'evento \"{title}\"?",
"placeholder_title": "Titolo dell'evento",
"placeholder_description": "Descrizione",
"placeholder_location": "Luogo — digita un indirizzo...",
"location_pinned_title": "Coordinate impostate",
"label_start": "Inizio",
"label_end": "Fine",
"label_all_day": "Tutto il giorno",
"action_cancel": "Annulla",
"action_save": "Salva",
"map_iframe_title": "Luogo dell'evento sulla mappa",
"map_open": "Apri in OpenStreetMap →",
"label_visibility": "Visibilità",
"section_rsvps": "RSVPs",
"section_guests": "Ospiti",
"section_bring_list": "Lista da portare",
"section_share": "Condividi",
"action_copy_link": "Copia",
"action_make_private": "Rendi privato",
"share_hint": "Le risposte appaiono automaticamente sotto in «Risposte via link» (polling ogni 30s).",
"action_publish": "Pubblica evento & genera link"
},
"event_card": {
"badge_draft": "Bozza",
"badge_cancelled": "Annullato",
"badge_published": "Condiviso",
"all_day": "Tutto il giorno",
"yes_count": "{count} vengono",
"pending_count": "· {count} in sospeso"
},
"discovered_card": {
"all_day": "Tutto il giorno",
"source_fallback": "Fonte",
"saved_label": "Salvato",
"action_save": "Salva",
"action_dismiss": "Nascondi"
},
"discovery_tab": {
"loading": "Caricamento...",
"action_refresh": "Aggiorna",
"action_sources": "Fonti",
"action_sources_count": "Fonti ({count})",
"loading_events": "Caricamento eventi...",
"empty_title": "Ancora nessun evento trovato",
"empty_hint": "Aggiungi feed iCal di locali o associazioni per scoprire eventi.",
"action_manage_sources": "Gestisci fonti",
"action_load_more": "Carica altri"
},
"discovery_setup": {
"title": "Imposta scoperta eventi",
"step1_desc": "In quali regioni cercare eventi?",
"action_next": "Avanti",
"step2_desc": "Cosa ti interessa?",
"placeholder_freetext": "Altri interessi (separati da virgola, es. teatro improv, meetup Rust)",
"action_back": "Indietro",
"action_finish": "Fatto"
},
"region_picker": {
"radius_unit": "{km} km",
"add_region": "+ Regione",
"placeholder_search": "Cerca città o regione...",
"radius_label": "Raggio: {km} km",
"searching": "Ricerca...",
"action_cancel": "Annulla"
},
"source_manager": {
"section_title": "Fonti",
"action_discover": "Trova automaticamente",
"action_discovering": "Ricerca...",
"action_add_ical": "+ Feed iCal",
"placeholder_name": "Nome (es. Jazzhaus Freiburg)",
"placeholder_url": "URL iCal (.ics)",
"action_submit_add": "Aggiungi",
"action_cancel": "Annulla",
"section_suggested": "Fonti suggerite",
"action_activate": "Attiva",
"action_reject": "x",
"empty": "Ancora nessuna fonte. Usa \"Trova automaticamente\" o aggiungi feed iCal manualmente.",
"meta_last_scan": "Ultima scansione: {date}",
"never_scanned": "mai",
"errors_count": "{count} errori",
"action_scan": "Scansiona",
"action_scan_title": "Scansiona ora",
"action_remove_title": "Rimuovi"
},
"bring_list_editor": {
"placeholder_label": "es. insalata, vino, casse",
"placeholder_quantity": "Quantità",
"action_add": "Aggiungi",
"option_no_one": "— Nessuno —",
"action_remove_title": "Rimuovi",
"empty": "Ancora niente nella lista.",
"claimed_via_link": "{name} (via link)",
"unknown_assignee": "?"
},
"guest_list_editor": {
"placeholder_name": "Nome",
"placeholder_email": "E-mail (opzionale)",
"action_add": "Aggiungi",
"action_remove_title": "Rimuovi",
"empty": "Ancora nessun ospite aggiunto.",
"rsvp_pending": "In sospeso",
"rsvp_yes": "Sì",
"rsvp_maybe": "Forse",
"rsvp_no": "No"
},
"public_rsvp_list": {
"title": "Risposte via link",
"action_refresh": "Ricarica",
"err_load": "Errore di caricamento",
"empty": "Ancora nessuna risposta via link.",
"status_yes": "Sì",
"status_no": "No",
"status_maybe": "Forse",
"meta_updated": "Aggiornato alle {time} · Auto-refresh ogni 30s",
"action_import_title": "Alla lista ospiti"
},
"rsvp_summary": {
"label_yes": "Sì",
"label_maybe": "Forse",
"label_no": "No",
"label_pending": "In sospeso",
"attending": "vengono"
}
}

View file

@ -1,5 +1,6 @@
<script lang="ts">
import { onMount } from 'svelte';
import { _ } from 'svelte-i18n';
import { useUpcomingEvents, usePastEvents, useGuestsByEvent, summarizeRsvps } from './queries';
import { eventsStore } from './stores/events.svelte';
import { drainTombstones } from './tombstones';
@ -59,30 +60,35 @@
</script>
<svelte:head>
<title>Events - Mana</title>
<title>{$_('events.list_view.doc_title')}</title>
</svelte:head>
<div class="events-page">
<div class="tab-bar">
<button class="tab" class:active={activeTab === 'mine'} onclick={() => (activeTab = 'mine')}>
Meine Events
{$_('events.list_view.tab_mine')}
</button>
<button
class="tab"
class:active={activeTab === 'discover'}
onclick={() => (activeTab = 'discover')}
>
Entdecken
{$_('events.list_view.tab_discover')}
</button>
</div>
{#if activeTab === 'mine'}
<header class="events-header">
<p class="page-subtitle">
{(upcoming.value ?? []).length} bevorstehend · {(past.value ?? []).length} vergangen
{$_('events.list_view.subtitle_count', {
values: {
upcoming: (upcoming.value ?? []).length,
past: (past.value ?? []).length,
},
})}
</p>
<button class="new-btn" onclick={() => (showCreate = !showCreate)}>
{showCreate ? 'Abbrechen' : '+ Neues Event'}
{showCreate ? $_('events.list_view.cancel_btn') : $_('events.list_view.new_event_btn')}
</button>
</header>
@ -91,22 +97,28 @@
<input
class="input"
bind:value={newTitle}
placeholder="Worum geht's? (z. B. Geburtstag Anna)"
placeholder={$_('events.list_view.placeholder_title')}
required
/>
<div class="form-row">
<input class="input" type="date" bind:value={newDate} required />
<input class="input" type="time" bind:value={newTime} />
<input class="input" bind:value={newLocation} placeholder="Ort (optional)" />
<input
class="input"
bind:value={newLocation}
placeholder={$_('events.list_view.placeholder_location')}
/>
</div>
<button type="submit" class="action-btn primary">Event anlegen</button>
<button type="submit" class="action-btn primary">
{$_('events.list_view.submit_create')}
</button>
</form>
{/if}
<section class="event-section">
<h2 class="section-title">Bevorstehend</h2>
<h2 class="section-title">{$_('events.list_view.section_upcoming')}</h2>
{#if (upcoming.value ?? []).length === 0}
<p class="empty">Keine bevorstehenden Events. Zeit fur eine Party?</p>
<p class="empty">{$_('events.list_view.empty_upcoming')}</p>
{:else}
<div class="event-list">
{#each upcoming.value ?? [] as event (event.id)}
@ -119,7 +131,7 @@
{#if (past.value ?? []).length > 0}
<section class="event-section">
<h2 class="section-title">Vergangen</h2>
<h2 class="section-title">{$_('events.list_view.section_past')}</h2>
<div class="event-list">
{#each past.value ?? [] as event (event.id)}
<EventCard {event} onclick={() => open(event)} />

View file

@ -1,4 +1,5 @@
<script lang="ts">
import { _ } from 'svelte-i18n';
import { useEventItems, useEventGuests } from '../queries';
import { eventItemsStore } from '../stores/items.svelte';
import { eventsStore } from '../stores/events.svelte';
@ -20,10 +21,14 @@
function assigneeLabel(item: EventItem): string | null {
if (item.assignedGuestId) {
return guestNameById.get(item.assignedGuestId) ?? '?';
return (
guestNameById.get(item.assignedGuestId) ?? $_('events.bring_list_editor.unknown_assignee')
);
}
if (item.claimedByName) {
return `${item.claimedByName} (via Link)`;
return $_('events.bring_list_editor.claimed_via_link', {
values: { name: item.claimedByName },
});
}
return null;
}
@ -50,7 +55,7 @@
<input
type="text"
bind:value={newLabel}
placeholder="z. B. Salat, Wein, Lautsprecher"
placeholder={$_('events.bring_list_editor.placeholder_label')}
class="input label-input"
required
/>
@ -58,11 +63,11 @@
type="number"
min="1"
max="999"
placeholder="Anzahl"
placeholder={$_('events.bring_list_editor.placeholder_quantity')}
bind:value={newQuantity}
class="input qty-input"
/>
<button type="submit" class="add-btn">Hinzufügen</button>
<button type="submit" class="add-btn">{$_('events.bring_list_editor.action_add')}</button>
</form>
<ul class="item-list">
@ -94,7 +99,7 @@
value={item.assignedGuestId ?? ''}
onchange={(e) => eventItemsStore.assign(item.id, e.currentTarget.value || null)}
>
<option value="">— Niemand —</option>
<option value="">{$_('events.bring_list_editor.option_no_one')}</option>
{#each guests.value ?? [] as g (g.id)}
<option value={g.id}>{g.name}</option>
{/each}
@ -103,7 +108,7 @@
<button
class="remove-btn"
onclick={() => eventItemsStore.deleteItem(item.id)}
title="Entfernen"
title={$_('events.bring_list_editor.action_remove_title')}
>
×
</button>
@ -111,7 +116,7 @@
{/each}
{#if (items.value ?? []).length === 0}
<li class="empty">Noch nichts auf der Liste.</li>
<li class="empty">{$_('events.bring_list_editor.empty')}</li>
{/if}
</ul>
</div>

View file

@ -1,5 +1,6 @@
<script lang="ts">
import { formatDate, formatTime } from '$lib/i18n/format';
import { _ } from 'svelte-i18n';
import type { DiscoveredEvent } from '../discovery/types';
interface Props {
@ -19,7 +20,9 @@
})
);
const timeLabel = $derived(
event.allDay ? 'Ganztag' : formatTime(startDate, { hour: '2-digit', minute: '2-digit' })
event.allDay
? $_('events.discovered_card.all_day')
: formatTime(startDate, { hour: '2-digit', minute: '2-digit' })
);
const isSaved = $derived(event.userAction === 'save');
@ -54,14 +57,20 @@
{event.sourceName}
</a>
{:else}
<a class="source-link" href={event.sourceUrl} target="_blank" rel="noopener"> Quelle </a>
<a class="source-link" href={event.sourceUrl} target="_blank" rel="noopener">
{$_('events.discovered_card.source_fallback')}
</a>
{/if}
<div class="actions">
{#if isSaved}
<span class="saved-label">Gespeichert</span>
<span class="saved-label">{$_('events.discovered_card.saved_label')}</span>
{:else}
<button class="action-btn save" onclick={onSave}>Merken</button>
<button class="action-btn dismiss" onclick={onDismiss}>Ausblenden</button>
<button class="action-btn save" onclick={onSave}>
{$_('events.discovered_card.action_save')}
</button>
<button class="action-btn dismiss" onclick={onDismiss}>
{$_('events.discovered_card.action_dismiss')}
</button>
{/if}
</div>
</div>

View file

@ -1,4 +1,5 @@
<script lang="ts">
import { _ } from 'svelte-i18n';
import { discoveryStore } from '../discovery/store.svelte';
import { EVENT_CATEGORIES } from '../discovery/types';
import RegionPicker from './RegionPicker.svelte';
@ -44,17 +45,19 @@
</script>
<div class="setup">
<h2 class="setup-title">Event-Entdeckung einrichten</h2>
<h2 class="setup-title">{$_('events.discovery_setup.title')}</h2>
{#if step === 1}
<div class="step">
<p class="step-desc">Welche Regionen sollen nach Events durchsucht werden?</p>
<p class="step-desc">{$_('events.discovery_setup.step1_desc')}</p>
<RegionPicker regions={discoveryStore.regions} />
<button class="next-btn" disabled={!canProceed} onclick={() => (step = 2)}> Weiter </button>
<button class="next-btn" disabled={!canProceed} onclick={() => (step = 2)}>
{$_('events.discovery_setup.action_next')}
</button>
</div>
{:else}
<div class="step">
<p class="step-desc">Was interessiert dich?</p>
<p class="step-desc">{$_('events.discovery_setup.step2_desc')}</p>
<div class="category-grid">
{#each EVENT_CATEGORIES as cat}
<button
@ -69,11 +72,15 @@
<input
class="input"
bind:value={freetext}
placeholder="Weitere Interessen (kommagetrennt, z.B. Impro-Theater, Rust Meetups)"
placeholder={$_('events.discovery_setup.placeholder_freetext')}
/>
<div class="step-actions">
<button class="back-btn" onclick={() => (step = 1)}>Zuruck</button>
<button class="next-btn" disabled={!canFinish} onclick={finishSetup}> Fertig </button>
<button class="back-btn" onclick={() => (step = 1)}>
{$_('events.discovery_setup.action_back')}
</button>
<button class="next-btn" disabled={!canFinish} onclick={finishSetup}>
{$_('events.discovery_setup.action_finish')}
</button>
</div>
</div>
{/if}

View file

@ -1,5 +1,6 @@
<script lang="ts">
import { onMount } from 'svelte';
import { _ } from 'svelte-i18n';
import { discoveryStore } from '../discovery/store.svelte';
import DiscoverySetup from './DiscoverySetup.svelte';
import DiscoveredEventCard from './DiscoveredEventCard.svelte';
@ -29,7 +30,7 @@
<div class="discovery-tab">
{#if !initialized}
<p class="loading">Lade...</p>
<p class="loading">{$_('events.discovery_tab.loading')}</p>
{:else if !discoveryStore.isSetUp}
<DiscoverySetup onComplete={handleSetupComplete} />
{:else}
@ -37,14 +38,18 @@
<RegionPicker regions={discoveryStore.regions} />
<div class="control-row">
<button class="control-btn" onclick={() => discoveryStore.refreshFeed()}>
Aktualisieren
{$_('events.discovery_tab.action_refresh')}
</button>
<button
class="control-btn"
class:active={showSources}
onclick={() => (showSources = !showSources)}
>
Quellen {discoveryStore.sources.length > 0 ? `(${discoveryStore.sources.length})` : ''}
{discoveryStore.sources.length > 0
? $_('events.discovery_tab.action_sources_count', {
values: { count: discoveryStore.sources.length },
})
: $_('events.discovery_tab.action_sources')}
</button>
</div>
</div>
@ -54,18 +59,16 @@
{/if}
{#if discoveryStore.loading}
<p class="loading">Lade Events...</p>
<p class="loading">{$_('events.discovery_tab.loading_events')}</p>
{:else if discoveryStore.error}
<p class="error-msg">{discoveryStore.error}</p>
{:else if discoveryStore.feed.length === 0}
<div class="empty">
<p class="empty-title">Noch keine Events gefunden</p>
<p class="empty-hint">
Fuge iCal-Feeds von Venues oder Vereinen hinzu, um Events zu entdecken.
</p>
<p class="empty-title">{$_('events.discovery_tab.empty_title')}</p>
<p class="empty-hint">{$_('events.discovery_tab.empty_hint')}</p>
{#if !showSources}
<button class="action-btn" onclick={() => (showSources = true)}>
Quellen verwalten
{$_('events.discovery_tab.action_manage_sources')}
</button>
{/if}
</div>
@ -83,7 +86,7 @@
class="load-more"
onclick={() => discoveryStore.refreshFeed({ offset: discoveryStore.feed.length })}
>
Mehr laden
{$_('events.discovery_tab.action_load_more')}
</button>
{/if}
</div>

View file

@ -1,5 +1,6 @@
<script lang="ts">
import { formatDate, formatTime } from '$lib/i18n/format';
import { _ } from 'svelte-i18n';
import type { SocialEvent, RsvpSummary } from '../types';
interface Props {
@ -19,7 +20,9 @@
})
);
const timeLabel = $derived(
event.isAllDay ? 'Ganztägig' : formatTime(startDate, { hour: '2-digit', minute: '2-digit' })
event.isAllDay
? $_('events.event_card.all_day')
: formatTime(startDate, { hour: '2-digit', minute: '2-digit' })
);
</script>
@ -32,11 +35,11 @@
<div class="title-row">
<h3 class="title">{event.title}</h3>
{#if event.status === 'draft'}
<span class="status-badge draft">Entwurf</span>
<span class="status-badge draft">{$_('events.event_card.badge_draft')}</span>
{:else if event.status === 'cancelled'}
<span class="status-badge cancelled">Abgesagt</span>
<span class="status-badge cancelled">{$_('events.event_card.badge_cancelled')}</span>
{:else if event.isPublished}
<span class="status-badge published">Geteilt</span>
<span class="status-badge published">{$_('events.event_card.badge_published')}</span>
{/if}
</div>
{#if event.location}
@ -44,9 +47,17 @@
{/if}
{#if summary}
<div class="summary-row">
<span class="yes-count">{summary.totalAttending} kommen</span>
<span class="yes-count"
>{$_('events.event_card.yes_count', {
values: { count: summary.totalAttending },
})}</span
>
{#if summary.pending > 0}
<span class="pending-count">· {summary.pending} offen</span>
<span class="pending-count"
>{$_('events.event_card.pending_count', {
values: { count: summary.pending },
})}</span
>
{/if}
</div>
{/if}

View file

@ -1,4 +1,5 @@
<script lang="ts">
import { _ } from 'svelte-i18n';
import { eventGuestsStore } from '../stores/guests.svelte';
import { useEventGuests } from '../queries';
import type { RsvpStatus } from '../types';
@ -14,12 +15,12 @@
let newName = $state('');
let newEmail = $state('');
const RSVP_OPTIONS: { value: RsvpStatus; label: string }[] = [
{ value: 'pending', label: 'Offen' },
{ value: 'yes', label: 'Ja' },
{ value: 'maybe', label: 'Vielleicht' },
{ value: 'no', label: 'Nein' },
];
const RSVP_OPTIONS = $derived<{ value: RsvpStatus; label: string }[]>([
{ value: 'pending', label: $_('events.guest_list_editor.rsvp_pending') },
{ value: 'yes', label: $_('events.guest_list_editor.rsvp_yes') },
{ value: 'maybe', label: $_('events.guest_list_editor.rsvp_maybe') },
{ value: 'no', label: $_('events.guest_list_editor.rsvp_no') },
]);
async function handleAdd(e: SubmitEvent) {
e.preventDefault();
@ -37,14 +38,20 @@
<div class="guest-editor">
<form class="add-row" onsubmit={handleAdd}>
<input type="text" bind:value={newName} placeholder="Name" class="input name-input" required />
<input
type="text"
bind:value={newName}
placeholder={$_('events.guest_list_editor.placeholder_name')}
class="input name-input"
required
/>
<input
type="email"
bind:value={newEmail}
placeholder="E-Mail (optional)"
placeholder={$_('events.guest_list_editor.placeholder_email')}
class="input email-input"
/>
<button type="submit" class="add-btn">Hinzufügen</button>
<button type="submit" class="add-btn">{$_('events.guest_list_editor.action_add')}</button>
</form>
<ul class="guest-list">
@ -86,7 +93,7 @@
<button
class="remove-btn"
onclick={() => eventGuestsStore.deleteGuest(guest.id)}
title="Entfernen"
title={$_('events.guest_list_editor.action_remove_title')}
>
×
</button>
@ -95,7 +102,7 @@
{/each}
{#if (guests.value ?? []).length === 0}
<li class="empty">Noch keine Gäste hinzugefügt.</li>
<li class="empty">{$_('events.guest_list_editor.empty')}</li>
{/if}
</ul>
</div>

View file

@ -1,5 +1,6 @@
<script lang="ts">
import { formatTime } from '$lib/i18n/format';
import { _ } from 'svelte-i18n';
import { eventsApi, type PublicRsvpRecord } from '../api';
import { eventGuestsStore } from '../stores/guests.svelte';
@ -30,7 +31,7 @@
consecutiveFailures = 0;
lastFetchedAt = new Date();
} catch (e) {
lastErrorMessage = e instanceof Error ? e.message : 'Fehler beim Laden';
lastErrorMessage = e instanceof Error ? e.message : $_('events.public_rsvp_list.err_load');
consecutiveFailures++;
} finally {
loading = false;
@ -64,16 +65,16 @@
{#if isPublished}
<div class="public-rsvps">
<div class="header-row">
<h3>Antworten via Link</h3>
<h3>{$_('events.public_rsvp_list.title')}</h3>
<button class="refresh" onclick={fetchRsvps} disabled={loading}>
{loading ? '…' : 'Neu laden'}
{loading ? '…' : $_('events.public_rsvp_list.action_refresh')}
</button>
</div>
{#if showError}
<p class="error">{lastErrorMessage}</p>
{:else if rsvps.length === 0 && !loading}
<p class="empty">Noch keine Antworten via Share-Link.</p>
<p class="empty">{$_('events.public_rsvp_list.empty')}</p>
{:else}
<ul class="rsvp-list">
{#each rsvps as r (r.id)}
@ -82,7 +83,11 @@
<div class="name-row">
<span class="name">{r.name}</span>
<span class="status status-{r.status}">
{r.status === 'yes' ? 'Ja' : r.status === 'no' ? 'Nein' : 'Vielleicht'}
{r.status === 'yes'
? $_('events.public_rsvp_list.status_yes')
: r.status === 'no'
? $_('events.public_rsvp_list.status_no')
: $_('events.public_rsvp_list.status_maybe')}
</span>
{#if r.plusOnes > 0}
<span class="plus">+{r.plusOnes}</span>
@ -95,7 +100,11 @@
<div class="note">{r.note}</div>
{/if}
</div>
<button class="import-btn" onclick={() => importToGuestList(r)} title="Zur Gästeliste">
<button
class="import-btn"
onclick={() => importToGuestList(r)}
title={$_('events.public_rsvp_list.action_import_title')}
>
</button>
</li>
@ -105,11 +114,15 @@
{#if lastFetchedAt}
<div class="meta">
Aktualisiert um {formatTime(lastFetchedAt, {
{$_('events.public_rsvp_list.meta_updated', {
values: {
time: formatTime(lastFetchedAt, {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
})} · Auto-Refresh alle 30s
}),
},
})}
</div>
{/if}
</div>

View file

@ -1,4 +1,5 @@
<script lang="ts">
import { _ } from 'svelte-i18n';
import type { DiscoveryRegion } from '../discovery/types';
import { discoveryStore } from '../discovery/store.svelte';
@ -70,12 +71,16 @@
{#each regions as region (region.id)}
<div class="region-chip">
<span class="region-label">{region.label}</span>
<span class="region-radius">{region.radiusKm} km</span>
<span class="region-radius"
>{$_('events.region_picker.radius_unit', { values: { km: region.radiusKm } })}</span
>
<button class="remove-btn" onclick={() => removeRegion(region.id)}>x</button>
</div>
{/each}
{#if !showForm}
<button class="add-btn" onclick={() => (showForm = true)}>+ Region</button>
<button class="add-btn" onclick={() => (showForm = true)}
>{$_('events.region_picker.add_region')}</button
>
{/if}
</div>
@ -85,10 +90,12 @@
class="input"
bind:value={searchQuery}
oninput={onSearchInput}
placeholder="Stadt oder Region suchen..."
placeholder={$_('events.region_picker.placeholder_search')}
/>
<div class="radius-row">
<label class="radius-label" for="region-radius">Radius: {radiusKm} km</label>
<label class="radius-label" for="region-radius"
>{$_('events.region_picker.radius_label', { values: { km: radiusKm } })}</label
>
<input id="region-radius" type="range" min="5" max="100" step="5" bind:value={radiusKm} />
</div>
{#if suggestions.length > 0}
@ -103,7 +110,7 @@
</ul>
{/if}
{#if searching}
<p class="searching-hint">Suche...</p>
<p class="searching-hint">{$_('events.region_picker.searching')}</p>
{/if}
<button
class="cancel-btn"
@ -113,7 +120,7 @@
suggestions = [];
}}
>
Abbrechen
{$_('events.region_picker.action_cancel')}
</button>
</div>
{/if}

View file

@ -1,4 +1,5 @@
<script lang="ts">
import { _ } from 'svelte-i18n';
import type { RsvpSummary } from '../types';
interface Props {
@ -12,26 +13,26 @@
<div class="rsvp-summary">
<div class="badge yes">
<span class="count">{summary.yes}</span>
<span class="label">Ja</span>
<span class="label">{$_('events.rsvp_summary.label_yes')}</span>
</div>
<div class="badge maybe">
<span class="count">{summary.maybe}</span>
<span class="label">Vielleicht</span>
<span class="label">{$_('events.rsvp_summary.label_maybe')}</span>
</div>
<div class="badge no">
<span class="count">{summary.no}</span>
<span class="label">Nein</span>
<span class="label">{$_('events.rsvp_summary.label_no')}</span>
</div>
<div class="badge pending">
<span class="count">{summary.pending}</span>
<span class="label">Offen</span>
<span class="label">{$_('events.rsvp_summary.label_pending')}</span>
</div>
<div class="total">
<strong>{summary.totalAttending}</strong>
{#if capacity}
<span class="muted">/ {capacity}</span>
{/if}
<span class="muted">kommen</span>
<span class="muted">{$_('events.rsvp_summary.attending')}</span>
</div>
</div>

View file

@ -1,4 +1,6 @@
<script lang="ts">
import { _, locale } from 'svelte-i18n';
import { get } from 'svelte/store';
import type { DiscoverySource, DiscoveryRegion } from '../discovery/types';
import { discoveryStore } from '../discovery/store.svelte';
@ -61,8 +63,8 @@
}
function formatDate(iso: string | null): string {
if (!iso) return 'nie';
return new Date(iso).toLocaleString('de-DE', {
if (!iso) return $_('events.source_manager.never_scanned');
return new Date(iso).toLocaleString(get(locale) ?? 'de', {
day: '2-digit',
month: '2-digit',
hour: '2-digit',
@ -73,17 +75,21 @@
<div class="source-manager">
<div class="header">
<h3 class="section-title">Quellen</h3>
<h3 class="section-title">{$_('events.source_manager.section_title')}</h3>
<div class="header-actions">
<button
class="discover-btn"
onclick={handleDiscover}
disabled={discovering || regions.length === 0}
>
{discovering ? 'Suche...' : 'Automatisch finden'}
{discovering
? $_('events.source_manager.action_discovering')
: $_('events.source_manager.action_discover')}
</button>
{#if !showForm}
<button class="add-btn" onclick={() => (showForm = true)}>+ iCal-Feed</button>
<button class="add-btn" onclick={() => (showForm = true)}
>{$_('events.source_manager.action_add_ical')}</button
>
{/if}
</div>
</div>
@ -93,10 +99,16 @@
<input
class="input"
bind:value={newName}
placeholder="Name (z.B. Jazzhaus Freiburg)"
placeholder={$_('events.source_manager.placeholder_name')}
required
/>
<input
class="input"
bind:value={newUrl}
placeholder={$_('events.source_manager.placeholder_url')}
type="url"
required
/>
<input class="input" bind:value={newUrl} placeholder="iCal URL (.ics)" type="url" required />
{#if regions.length > 1}
<select class="input" bind:value={newRegionId}>
{#each regions as r (r.id)}
@ -105,9 +117,11 @@
</select>
{/if}
<div class="form-actions">
<button type="submit" class="action-btn primary">Hinzufugen</button>
<button type="submit" class="action-btn primary"
>{$_('events.source_manager.action_submit_add')}</button
>
<button type="button" class="action-btn" onclick={() => (showForm = false)}
>Abbrechen</button
>{$_('events.source_manager.action_cancel')}</button
>
</div>
</form>
@ -115,7 +129,7 @@
{#if suggestedSources.length > 0}
<div class="suggestions-section">
<h4 class="sub-title">Vorgeschlagene Quellen</h4>
<h4 class="sub-title">{$_('events.source_manager.section_suggested')}</h4>
<div class="source-list">
{#each suggestedSources as source (source.id)}
<div class="source-item suggested">
@ -132,9 +146,11 @@
</div>
<div class="source-actions">
<button class="icon-btn activate" onclick={() => handleActivate(source.id)}
>Aktivieren</button
>{$_('events.source_manager.action_activate')}</button
>
<button class="icon-btn danger" onclick={() => handleReject(source.id)}
>{$_('events.source_manager.action_reject')}</button
>
<button class="icon-btn danger" onclick={() => handleReject(source.id)}>x</button>
</div>
</div>
{/each}
@ -143,9 +159,7 @@
{/if}
{#if activeSources.length === 0 && suggestedSources.length === 0}
<p class="empty">
Noch keine Quellen. Nutze "Automatisch finden" oder fuge iCal-Feeds manuell hinzu.
</p>
<p class="empty">{$_('events.source_manager.empty')}</p>
{:else if activeSources.length > 0}
<div class="source-list">
{#each activeSources as source (source.id)}
@ -153,9 +167,15 @@
<div class="source-info">
<div class="source-name">{source.name}</div>
<div class="source-meta">
{source.type.toUpperCase()} · Letzter Scan: {formatDate(source.lastCrawledAt)}
{source.type.toUpperCase()} · {$_('events.source_manager.meta_last_scan', {
values: { date: formatDate(source.lastCrawledAt) },
})}
{#if source.errorCount > 0}
<span class="error-badge">{source.errorCount} Fehler</span>
<span class="error-badge"
>{$_('events.source_manager.errors_count', {
values: { count: source.errorCount },
})}</span
>
{/if}
</div>
{#if source.lastError}
@ -163,13 +183,17 @@
{/if}
</div>
<div class="source-actions">
<button class="icon-btn" onclick={() => handleCrawl(source.id)} title="Jetzt scannen">
Scannen
<button
class="icon-btn"
onclick={() => handleCrawl(source.id)}
title={$_('events.source_manager.action_scan_title')}
>
{$_('events.source_manager.action_scan')}
</button>
<button
class="icon-btn danger"
onclick={() => handleRemove(source.id)}
title="Entfernen"
title={$_('events.source_manager.action_remove_title')}
>
x
</button>

View file

@ -1,5 +1,6 @@
<script lang="ts">
import { formatDateTime } from '$lib/i18n/format';
import { _ } from 'svelte-i18n';
import { useEvent, useEventGuests, summarizeRsvps } from '../queries';
import { eventsStore } from '../stores/events.svelte';
import GuestListEditor from '../components/GuestListEditor.svelte';
@ -136,7 +137,8 @@
async function handleDelete() {
if (!event) return;
if (!confirm(`Event "${event.title}" wirklich löschen?`)) return;
if (!confirm($_('events.detail_view.confirm_delete', { values: { title: event.title } })))
return;
await eventsStore.deleteEvent(event.id);
goBack();
}
@ -154,21 +156,33 @@
</script>
{#if !event}
<div class="loading">Lade Event...</div>
<div class="loading">{$_('events.detail_view.loading')}</div>
{:else}
<div class="detail">
<header class="detail-header">
<button class="back-btn" onclick={goBack}> Zurück</button>
<button class="back-btn" onclick={goBack}>{$_('events.detail_view.back')}</button>
<div class="header-actions">
<button class="action-btn" onclick={startEdit}>Bearbeiten</button>
<button class="action-btn danger" onclick={handleDelete}>Löschen</button>
<button class="action-btn" onclick={startEdit}>
{$_('events.detail_view.action_edit')}
</button>
<button class="action-btn danger" onclick={handleDelete}>
{$_('events.detail_view.action_delete')}
</button>
</div>
</header>
{#if editing}
<div class="edit-form">
<input class="title-input" bind:value={titleDraft} placeholder="Event-Titel" />
<textarea class="desc-input" bind:value={descDraft} rows="3" placeholder="Beschreibung"
<input
class="title-input"
bind:value={titleDraft}
placeholder={$_('events.detail_view.placeholder_title')}
/>
<textarea
class="desc-input"
bind:value={descDraft}
rows="3"
placeholder={$_('events.detail_view.placeholder_description')}
></textarea>
<div class="loc-wrapper">
<input
@ -179,10 +193,10 @@
onfocus={() => {
if (addressSuggestions.length > 0) showAddressSuggestions = true;
}}
placeholder="Ort — tippe eine Adresse..."
placeholder={$_('events.detail_view.placeholder_location')}
/>
{#if locationLatDraft && locationLonDraft}
<span class="loc-pinned" title="Koordinaten gesetzt">
<span class="loc-pinned" title={$_('events.detail_view.location_pinned_title')}>
<MapPin size={12} weight="fill" />
</span>
{/if}
@ -206,21 +220,25 @@
</div>
<div class="time-row">
<label>
<span>Start</span>
<span>{$_('events.detail_view.label_start')}</span>
<input type="datetime-local" bind:value={startDraft} />
</label>
<label>
<span>Ende</span>
<span>{$_('events.detail_view.label_end')}</span>
<input type="datetime-local" bind:value={endDraft} />
</label>
<label class="all-day">
<input type="checkbox" bind:checked={allDayDraft} />
<span>Ganztägig</span>
<span>{$_('events.detail_view.label_all_day')}</span>
</label>
</div>
<div class="form-actions">
<button class="action-btn" onclick={() => (editing = false)}>Abbrechen</button>
<button class="action-btn primary" onclick={saveEdit}>Speichern</button>
<button class="action-btn" onclick={() => (editing = false)}>
{$_('events.detail_view.action_cancel')}
</button>
<button class="action-btn primary" onclick={saveEdit}>
{$_('events.detail_view.action_save')}
</button>
</div>
</div>
{:else}
@ -247,7 +265,7 @@
{#if mapUrl}
<div class="event-map">
<iframe
title="Event-Ort auf Karte"
title={$_('events.detail_view.map_iframe_title')}
src={mapUrl}
width="100%"
height="180"
@ -260,7 +278,7 @@
target="_blank"
rel="noopener noreferrer"
>
In OpenStreetMap öffnen →
{$_('events.detail_view.map_open')}
</a>
</div>
{/if}
@ -269,7 +287,7 @@
<section class="section">
<div class="visibility-row">
<span class="visibility-label">Sichtbarkeit</span>
<span class="visibility-label">{$_('events.detail_view.label_visibility')}</span>
<VisibilityPicker
level={event.visibility ?? 'space'}
onChange={handleVisibilityChange}
@ -279,17 +297,17 @@
</section>
<section class="section">
<h2>RSVPs</h2>
<h2>{$_('events.detail_view.section_rsvps')}</h2>
<RsvpSummaryView {summary} capacity={event.capacity} />
</section>
<section class="section">
<h2>Gäste</h2>
<h2>{$_('events.detail_view.section_guests')}</h2>
<GuestListEditor eventId={event.id} />
</section>
<section class="section">
<h2>Bring-Liste</h2>
<h2>{$_('events.detail_view.section_bring_list')}</h2>
<BringListEditor eventId={event.id} />
</section>
@ -300,19 +318,21 @@
{/if}
<section class="section">
<h2>Teilen</h2>
<h2>{$_('events.detail_view.section_share')}</h2>
{#if event.isPublished && event.publicToken}
<div class="share-row">
<code class="share-link">{window.location.origin}/rsvp/{event.publicToken}</code>
<button class="action-btn" onclick={copyShareLink}>Kopieren</button>
<button class="action-btn" onclick={handlePublish}>Privat machen</button>
<button class="action-btn" onclick={copyShareLink}>
{$_('events.detail_view.action_copy_link')}
</button>
<button class="action-btn" onclick={handlePublish}>
{$_('events.detail_view.action_make_private')}
</button>
</div>
<p class="share-hint">
Antworten erscheinen automatisch unten in „Antworten via Link“ (Polling alle 30s).
</p>
<p class="share-hint">{$_('events.detail_view.share_hint')}</p>
{:else}
<button class="action-btn primary" onclick={handlePublish}>
Event veröffentlichen & Link generieren
{$_('events.detail_view.action_publish')}
</button>
{/if}
</section>

View file

@ -97,6 +97,10 @@
"apps/mana/apps/web/src/lib/modules/comic/views/DetailCharacterView.svelte": 6,
"apps/mana/apps/web/src/lib/modules/comic/views/DetailView.svelte": 7,
"apps/mana/apps/web/src/lib/modules/comic/views/ListView.svelte": 2,
"apps/mana/apps/web/src/lib/modules/community/components/ItemCard.svelte": 1,
"apps/mana/apps/web/src/lib/modules/community/views/DetailView.svelte": 1,
"apps/mana/apps/web/src/lib/modules/community/views/ListView.svelte": 1,
"apps/mana/apps/web/src/lib/modules/community/views/RoadmapView.svelte": 1,
"apps/mana/apps/web/src/lib/modules/companion/components/CompanionChat.svelte": 1,
"apps/mana/apps/web/src/lib/modules/companion/components/RitualRunner.svelte": 5,
"apps/mana/apps/web/src/lib/modules/companion/ListView.svelte": 1,
@ -117,18 +121,6 @@
"apps/mana/apps/web/src/lib/modules/dreams/ListView.svelte": 12,
"apps/mana/apps/web/src/lib/modules/dreams/views/SymbolDetailView.svelte": 8,
"apps/mana/apps/web/src/lib/modules/drink/ListView.svelte": 5,
"apps/mana/apps/web/src/lib/modules/events/components/BringListEditor.svelte": 2,
"apps/mana/apps/web/src/lib/modules/events/components/DiscoveredEventCard.svelte": 3,
"apps/mana/apps/web/src/lib/modules/events/components/DiscoverySetup.svelte": 2,
"apps/mana/apps/web/src/lib/modules/events/components/DiscoveryTab.svelte": 3,
"apps/mana/apps/web/src/lib/modules/events/components/EventCard.svelte": 3,
"apps/mana/apps/web/src/lib/modules/events/components/GuestListEditor.svelte": 2,
"apps/mana/apps/web/src/lib/modules/events/components/PublicRsvpList.svelte": 2,
"apps/mana/apps/web/src/lib/modules/events/components/RegionPicker.svelte": 1,
"apps/mana/apps/web/src/lib/modules/events/components/RsvpSummary.svelte": 3,
"apps/mana/apps/web/src/lib/modules/events/components/SourceManager.svelte": 5,
"apps/mana/apps/web/src/lib/modules/events/ListView.svelte": 3,
"apps/mana/apps/web/src/lib/modules/events/views/DetailView.svelte": 13,
"apps/mana/apps/web/src/lib/modules/finance/ListView.svelte": 6,
"apps/mana/apps/web/src/lib/modules/firsts/ListView.svelte": 15,
"apps/mana/apps/web/src/lib/modules/goals/GoalEditor.svelte": 15,