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

Calendar already had a `calendar` namespace, but ~70 strings were
hardcoded across EventForm, EventDetailModal, CustomRecurrenceBuilder,
CalendarHeader (15 block-type filter chips), QuickEventPopover, AgendaView,
EventCard, SlotSuggestions, MiniCalendar, DateStrip, ListView,
SharedEventView, the inline DetailView, and 3 routes.

- Extended namespace with `event_form.*`, `event_card.*`, `event_modal.*`,
  `agenda.*`, `recurrence.*` (custom builder + preview format),
  `weekday_short.*` / `weekday_long.*`, `header.*` (15 block-type labels +
  4 ARIA), `date_strip.*`, `mini_cal.*`, `slots.*`, `quick_event.*`,
  `list_view.*`, `detail_route.*`, `detail_view.*`, `calendars_route.*`,
  `shared_view.*` — ~172 new keys × 5 locales = ~860 translations.
- Recurrence preview formatters in EventForm + EventDetailModal +
  CustomRecurrenceBuilder all rebuilt around `recurrence.every_n_unit` /
  `weekly_with_days` / weekday-short maps.
- Locale-aware Intl.DateTimeFormat in SharedEventView (was hardcoded
  'de-DE').
- Baseline ratchet: 1753 → 1687 (66 calendar strings cleared, 16 files
  fully clean).

- validate:i18n-parity: 40 namespaces × 5 locales — 3768 keys aligned
- svelte-check: 0 new errors from i18n changes (pre-existing drift
  in unrelated modules unchanged)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-26 22:17:34 +02:00
parent 723a64808c
commit 4e31c8d736
22 changed files with 1429 additions and 277 deletions

View file

@ -29,8 +29,10 @@
}, },
"calendar": { "calendar": {
"today": "Heute", "today": "Heute",
"tomorrow": "Morgen",
"newEvent": "Neuer Termin", "newEvent": "Neuer Termin",
"noEvents": "Keine Termine", "noEvents": "Keine Termine",
"noEventsInRange": "Keine Termine in diesem Zeitraum",
"allDay": "Ganztägig", "allDay": "Ganztägig",
"myCalendars": "Meine Kalender", "myCalendars": "Meine Kalender",
"sharedCalendars": "Geteilte Kalender", "sharedCalendars": "Geteilte Kalender",
@ -58,6 +60,210 @@
"changeStartTime": "Startzeit ändern", "changeStartTime": "Startzeit ändern",
"changeEndTime": "Endzeit ändern" "changeEndTime": "Endzeit ändern"
}, },
"event_form": {
"aria_create": "Termin erstellen",
"aria_edit": "Termin bearbeiten",
"label_title_required": "Titel *",
"placeholder_title": "Terminname eingeben",
"label_time": "Uhrzeit",
"label_recurrence": "Wiederholung",
"placeholder_location": "Ort eingeben...",
"placeholder_description": "Beschreibung hinzufügen",
"label_tags": "Tags",
"recur_edit_suffix": "{preview} — Bearbeiten"
},
"event_card": {
"new_event_label": "Neuer Termin",
"recurring_title": "Wiederkehrend",
"linked_title": "Durchgeführt"
},
"event_modal": {
"editing_title": "Termin bearbeiten",
"copy_title": "Kopieren",
"label_visibility": "Sichtbarkeit",
"label_share_link": "Link",
"created_at": "Erstellt: {date}",
"updated_at": "· Bearbeitet: {date}",
"confirm_delete_single": "Diesen Termin löschen?",
"clipboard_location_prefix": "Ort: {location}",
"recur_edit_title": "Wiederkehrenden Termin bearbeiten",
"recur_edit_text": "Möchtest du nur diesen Termin oder alle zukünftigen bearbeiten?",
"recur_edit_only_this": "Nur diesen Termin",
"recur_edit_all_future": "Alle zukünftigen Termine",
"recur_delete_title": "Wiederkehrenden Termin löschen",
"recur_delete_text": "Möchtest du nur diesen Termin oder die gesamte Serie löschen?",
"recur_delete_only_this": "Nur diesen Termin",
"recur_delete_all": "Alle Termine der Serie"
},
"agenda": {
"empty": "Keine Termine in diesem Zeitraum",
"open_details_aria": "Details öffnen"
},
"recurrence": {
"none": "Keine Wiederholung",
"daily": "Täglich",
"weekly": "Wöchentlich",
"monthly": "Monatlich",
"yearly": "Jährlich",
"every_2_weeks": "Alle 2 Wochen",
"custom": "Benutzerdefiniert...",
"recurring_fallback": "Wiederkehrend",
"weekly_with_days": "Wöchentlich ({days})",
"every_n_unit": "Alle {n} {unit}",
"every_unit": "Alle {unit}",
"unit_days": "Tage",
"unit_weeks": "Wochen",
"unit_months": "Monate",
"unit_years": "Jahre",
"unit_freq_daily": "Tag(e)",
"unit_freq_weekly": "Woche(n)",
"unit_freq_monthly": "Monat(e)",
"unit_freq_yearly": "Jahr(e)",
"preview_at_days": " an {days}",
"preview_count_suffix": ", {count}x",
"preview_until_suffix": " bis {date}",
"builder_title": "Benutzerdefinierte Wiederholung",
"builder_every_label": "Alle",
"builder_weekdays_label": "Wochentage",
"builder_end_label": "Endet",
"builder_end_never": "Nie",
"builder_end_after": "Nach",
"builder_end_after_unit": "Terminen",
"builder_end_until": "Am",
"builder_apply": "Übernehmen"
},
"weekday_short": {
"mon": "Mo",
"tue": "Di",
"wed": "Mi",
"thu": "Do",
"fri": "Fr",
"sat": "Sa",
"sun": "So"
},
"weekday_long": {
"sun": "Sonntag",
"mon": "Montag",
"tue": "Dienstag",
"wed": "Mittwoch",
"thu": "Donnerstag",
"fri": "Freitag",
"sat": "Samstag"
},
"header": {
"today": "Heute",
"new_event": "Termin",
"aria_prev": "Zurück",
"aria_next": "Weiter",
"aria_filter": "Filter",
"aria_export": "Exportieren",
"block_event": "Termine",
"block_task": "Aufgaben",
"block_time_entry": "Zeiten",
"block_habit": "Habits",
"block_body": "Training",
"block_watering": "Gießen",
"block_sleep": "Schlaf",
"block_practice": "Übung",
"block_period": "Periode",
"block_guide": "Guides",
"block_visit": "Besuche",
"block_study": "Lernen",
"block_listening": "Musik",
"block_mood": "Stimmung",
"block_rehearsal": "Probe"
},
"date_strip": {
"today_aria": "Zum heutigen Tag",
"today_label": "Heute"
},
"mini_cal": {
"prev_aria": "Vorheriger Monat",
"next_aria": "Nächster Monat"
},
"slots": {
"loading": "Suche freie Zeiten...",
"empty": "Keine freien Slots gefunden",
"label": "Freie Zeiten",
"duration_suffix": "{n}min"
},
"quick_event": {
"aria_dialog": "Termin erstellen",
"header_new": "Neuer Termin",
"placeholder_title": "Titel hinzufügen",
"type_event": "Termin",
"type_time_entry": "Zeiterfassung",
"type_habit": "Habit",
"placeholder_location": "Ort hinzufügen",
"placeholder_description": "Beschreibung"
},
"list_view": {
"placeholder_new_event": "Neuer Termin...",
"voice_reason": "Termine werden verschlüsselt gespeichert. Dafür brauchst du ein Mana-Konto.",
"empty_label": "Termine",
"empty_text": "Keine Termine",
"open_action": "Öffnen",
"delete_action": "Löschen"
},
"detail_route": {
"doc_title": "Kalender - Mana",
"create_modal_title": "Neuer Termin",
"event_doc_title": "{title} - Kalender - Mana",
"event_doc_title_fallback": "Termin",
"back_to_calendar": "Zurück zum Kalender",
"not_found": "Termin nicht gefunden",
"edit_title": "Termin bearbeiten",
"label_title": "Titel",
"label_description": "Beschreibung",
"label_date": "Datum",
"label_from": "Von",
"label_to": "Bis",
"label_location": "Ort",
"placeholder_location": "Ort eingeben...",
"submit_save": "Speichern",
"submit_cancel": "Abbrechen",
"time_suffix_uhr": "Uhr"
},
"detail_view": {
"toast_deleted": "Termin gelöscht",
"not_found": "Termin nicht gefunden",
"confirm_delete": "Termin wirklich löschen?",
"placeholder_title": "Titel...",
"label_visibility": "Sichtbarkeit",
"label_allday": "Ganztägig",
"placeholder_location": "Ort hinzufügen...",
"section_tags": "Tags",
"section_description": "Beschreibung",
"placeholder_description": "Beschreibung hinzufügen...",
"meta_created": "Erstellt: {date}",
"meta_updated": "Bearbeitet: {date}",
"untitled_fallback": "Untitled"
},
"calendars_route": {
"doc_title": "Kalender verwalten - Mana",
"back_to_calendar": "Zurück zum Kalender",
"page_title": "Meine Kalender",
"new_calendar": "Neuer Kalender",
"label_name": "Name",
"placeholder_name": "z.B. Arbeit, Sport, Familie...",
"label_color": "Farbe",
"aria_pick_color": "Farbe wählen",
"submit_create": "Erstellen",
"empty": "Noch keine Kalender vorhanden.",
"badge_default": "(Standard)",
"aria_hide": "Ausblenden",
"aria_show": "Einblenden",
"aria_set_default": "Als Standard setzen",
"confirm_delete": "Kalender wirklich löschen? Alle zugehörigen Termine gehen verloren."
},
"shared_view": {
"kind": "Termin",
"label_when": "Wann",
"label_where": "Wo",
"timezone_prefix": "Zeitzone: {tz}",
"add_to_calendar": "📅 Zum eigenen Kalender hinzufügen",
"expiry": "Dieser Link läuft am {date} ab."
},
"repeat": { "repeat": {
"none": "Nicht wiederholen", "none": "Nicht wiederholen",
"daily": "Täglich", "daily": "Täglich",

View file

@ -29,8 +29,10 @@
}, },
"calendar": { "calendar": {
"today": "Today", "today": "Today",
"tomorrow": "Tomorrow",
"newEvent": "New Event", "newEvent": "New Event",
"noEvents": "No events", "noEvents": "No events",
"noEventsInRange": "No events in this range",
"allDay": "All day", "allDay": "All day",
"myCalendars": "My Calendars", "myCalendars": "My Calendars",
"sharedCalendars": "Shared Calendars", "sharedCalendars": "Shared Calendars",
@ -58,6 +60,210 @@
"changeStartTime": "Change start time", "changeStartTime": "Change start time",
"changeEndTime": "Change end time" "changeEndTime": "Change end time"
}, },
"event_form": {
"aria_create": "Create event",
"aria_edit": "Edit event",
"label_title_required": "Title *",
"placeholder_title": "Enter event name",
"label_time": "Time",
"label_recurrence": "Recurrence",
"placeholder_location": "Enter location...",
"placeholder_description": "Add a description",
"label_tags": "Tags",
"recur_edit_suffix": "{preview} — Edit"
},
"event_card": {
"new_event_label": "New event",
"recurring_title": "Recurring",
"linked_title": "Done"
},
"event_modal": {
"editing_title": "Edit event",
"copy_title": "Copy",
"label_visibility": "Visibility",
"label_share_link": "Link",
"created_at": "Created: {date}",
"updated_at": "· Edited: {date}",
"confirm_delete_single": "Delete this event?",
"clipboard_location_prefix": "Location: {location}",
"recur_edit_title": "Edit recurring event",
"recur_edit_text": "Edit only this event or all future ones?",
"recur_edit_only_this": "Only this event",
"recur_edit_all_future": "All future events",
"recur_delete_title": "Delete recurring event",
"recur_delete_text": "Delete only this event or the entire series?",
"recur_delete_only_this": "Only this event",
"recur_delete_all": "All events in the series"
},
"agenda": {
"empty": "No events in this range",
"open_details_aria": "Open details"
},
"recurrence": {
"none": "No recurrence",
"daily": "Daily",
"weekly": "Weekly",
"monthly": "Monthly",
"yearly": "Yearly",
"every_2_weeks": "Every 2 weeks",
"custom": "Custom...",
"recurring_fallback": "Recurring",
"weekly_with_days": "Weekly ({days})",
"every_n_unit": "Every {n} {unit}",
"every_unit": "Every {unit}",
"unit_days": "days",
"unit_weeks": "weeks",
"unit_months": "months",
"unit_years": "years",
"unit_freq_daily": "day(s)",
"unit_freq_weekly": "week(s)",
"unit_freq_monthly": "month(s)",
"unit_freq_yearly": "year(s)",
"preview_at_days": " on {days}",
"preview_count_suffix": ", {count}x",
"preview_until_suffix": " until {date}",
"builder_title": "Custom recurrence",
"builder_every_label": "Every",
"builder_weekdays_label": "Weekdays",
"builder_end_label": "Ends",
"builder_end_never": "Never",
"builder_end_after": "After",
"builder_end_after_unit": "events",
"builder_end_until": "On",
"builder_apply": "Apply"
},
"weekday_short": {
"mon": "Mon",
"tue": "Tue",
"wed": "Wed",
"thu": "Thu",
"fri": "Fri",
"sat": "Sat",
"sun": "Sun"
},
"weekday_long": {
"sun": "Sunday",
"mon": "Monday",
"tue": "Tuesday",
"wed": "Wednesday",
"thu": "Thursday",
"fri": "Friday",
"sat": "Saturday"
},
"header": {
"today": "Today",
"new_event": "Event",
"aria_prev": "Previous",
"aria_next": "Next",
"aria_filter": "Filter",
"aria_export": "Export",
"block_event": "Events",
"block_task": "Tasks",
"block_time_entry": "Time",
"block_habit": "Habits",
"block_body": "Training",
"block_watering": "Watering",
"block_sleep": "Sleep",
"block_practice": "Practice",
"block_period": "Period",
"block_guide": "Guides",
"block_visit": "Visits",
"block_study": "Study",
"block_listening": "Music",
"block_mood": "Mood",
"block_rehearsal": "Rehearsal"
},
"date_strip": {
"today_aria": "Go to today",
"today_label": "Today"
},
"mini_cal": {
"prev_aria": "Previous month",
"next_aria": "Next month"
},
"slots": {
"loading": "Searching free time...",
"empty": "No free slots found",
"label": "Free slots",
"duration_suffix": "{n}min"
},
"quick_event": {
"aria_dialog": "Create event",
"header_new": "New event",
"placeholder_title": "Add a title",
"type_event": "Event",
"type_time_entry": "Time entry",
"type_habit": "Habit",
"placeholder_location": "Add location",
"placeholder_description": "Description"
},
"list_view": {
"placeholder_new_event": "New event...",
"voice_reason": "Events are stored encrypted. You need a Mana account for that.",
"empty_label": "Events",
"empty_text": "No events",
"open_action": "Open",
"delete_action": "Delete"
},
"detail_route": {
"doc_title": "Calendar - Mana",
"create_modal_title": "New event",
"event_doc_title": "{title} - Calendar - Mana",
"event_doc_title_fallback": "Event",
"back_to_calendar": "Back to calendar",
"not_found": "Event not found",
"edit_title": "Edit event",
"label_title": "Title",
"label_description": "Description",
"label_date": "Date",
"label_from": "From",
"label_to": "To",
"label_location": "Location",
"placeholder_location": "Enter location...",
"submit_save": "Save",
"submit_cancel": "Cancel",
"time_suffix_uhr": ""
},
"detail_view": {
"toast_deleted": "Event deleted",
"not_found": "Event not found",
"confirm_delete": "Really delete event?",
"placeholder_title": "Title...",
"label_visibility": "Visibility",
"label_allday": "All day",
"placeholder_location": "Add location...",
"section_tags": "Tags",
"section_description": "Description",
"placeholder_description": "Add description...",
"meta_created": "Created: {date}",
"meta_updated": "Edited: {date}",
"untitled_fallback": "Untitled"
},
"calendars_route": {
"doc_title": "Manage calendars - Mana",
"back_to_calendar": "Back to calendar",
"page_title": "My calendars",
"new_calendar": "New calendar",
"label_name": "Name",
"placeholder_name": "e.g. Work, Sport, Family...",
"label_color": "Color",
"aria_pick_color": "Pick color",
"submit_create": "Create",
"empty": "No calendars yet.",
"badge_default": "(Default)",
"aria_hide": "Hide",
"aria_show": "Show",
"aria_set_default": "Set as default",
"confirm_delete": "Really delete calendar? All associated events will be lost."
},
"shared_view": {
"kind": "Event",
"label_when": "When",
"label_where": "Where",
"timezone_prefix": "Time zone: {tz}",
"add_to_calendar": "📅 Add to my calendar",
"expiry": "This link expires on {date}."
},
"repeat": { "repeat": {
"none": "Don't repeat", "none": "Don't repeat",
"daily": "Daily", "daily": "Daily",

View file

@ -29,8 +29,10 @@
}, },
"calendar": { "calendar": {
"today": "Hoy", "today": "Hoy",
"tomorrow": "Mañana",
"newEvent": "Nuevo evento", "newEvent": "Nuevo evento",
"noEvents": "Sin eventos", "noEvents": "Sin eventos",
"noEventsInRange": "Sin eventos en este rango",
"allDay": "Todo el día", "allDay": "Todo el día",
"myCalendars": "Mis calendarios", "myCalendars": "Mis calendarios",
"sharedCalendars": "Calendarios compartidos", "sharedCalendars": "Calendarios compartidos",
@ -58,6 +60,210 @@
"changeStartTime": "Cambiar hora de inicio", "changeStartTime": "Cambiar hora de inicio",
"changeEndTime": "Cambiar hora de fin" "changeEndTime": "Cambiar hora de fin"
}, },
"event_form": {
"aria_create": "Crear evento",
"aria_edit": "Editar evento",
"label_title_required": "Título *",
"placeholder_title": "Introduce el nombre del evento",
"label_time": "Hora",
"label_recurrence": "Repetición",
"placeholder_location": "Introduce la ubicación...",
"placeholder_description": "Añade una descripción",
"label_tags": "Etiquetas",
"recur_edit_suffix": "{preview} — Editar"
},
"event_card": {
"new_event_label": "Nuevo evento",
"recurring_title": "Recurrente",
"linked_title": "Realizado"
},
"event_modal": {
"editing_title": "Editar evento",
"copy_title": "Copiar",
"label_visibility": "Visibilidad",
"label_share_link": "Enlace",
"created_at": "Creado: {date}",
"updated_at": "· Editado: {date}",
"confirm_delete_single": "¿Eliminar este evento?",
"clipboard_location_prefix": "Ubicación: {location}",
"recur_edit_title": "Editar evento recurrente",
"recur_edit_text": "¿Editar solo este evento o todos los futuros?",
"recur_edit_only_this": "Solo este evento",
"recur_edit_all_future": "Todos los eventos futuros",
"recur_delete_title": "Eliminar evento recurrente",
"recur_delete_text": "¿Eliminar solo este evento o toda la serie?",
"recur_delete_only_this": "Solo este evento",
"recur_delete_all": "Todos los eventos de la serie"
},
"agenda": {
"empty": "Sin eventos en este rango",
"open_details_aria": "Abrir detalles"
},
"recurrence": {
"none": "Sin repetición",
"daily": "Diario",
"weekly": "Semanal",
"monthly": "Mensual",
"yearly": "Anual",
"every_2_weeks": "Cada 2 semanas",
"custom": "Personalizado...",
"recurring_fallback": "Recurrente",
"weekly_with_days": "Semanal ({days})",
"every_n_unit": "Cada {n} {unit}",
"every_unit": "Cada {unit}",
"unit_days": "días",
"unit_weeks": "semanas",
"unit_months": "meses",
"unit_years": "años",
"unit_freq_daily": "día(s)",
"unit_freq_weekly": "semana(s)",
"unit_freq_monthly": "mes(es)",
"unit_freq_yearly": "año(s)",
"preview_at_days": " los {days}",
"preview_count_suffix": ", {count}x",
"preview_until_suffix": " hasta {date}",
"builder_title": "Repetición personalizada",
"builder_every_label": "Cada",
"builder_weekdays_label": "Días de la semana",
"builder_end_label": "Termina",
"builder_end_never": "Nunca",
"builder_end_after": "Tras",
"builder_end_after_unit": "eventos",
"builder_end_until": "El",
"builder_apply": "Aplicar"
},
"weekday_short": {
"mon": "Lun",
"tue": "Mar",
"wed": "Mié",
"thu": "Jue",
"fri": "Vie",
"sat": "Sáb",
"sun": "Dom"
},
"weekday_long": {
"sun": "Domingo",
"mon": "Lunes",
"tue": "Martes",
"wed": "Miércoles",
"thu": "Jueves",
"fri": "Viernes",
"sat": "Sábado"
},
"header": {
"today": "Hoy",
"new_event": "Evento",
"aria_prev": "Anterior",
"aria_next": "Siguiente",
"aria_filter": "Filtro",
"aria_export": "Exportar",
"block_event": "Eventos",
"block_task": "Tareas",
"block_time_entry": "Tiempos",
"block_habit": "Hábitos",
"block_body": "Entrenamiento",
"block_watering": "Riego",
"block_sleep": "Sueño",
"block_practice": "Práctica",
"block_period": "Periodo",
"block_guide": "Guías",
"block_visit": "Visitas",
"block_study": "Estudio",
"block_listening": "Música",
"block_mood": "Estado",
"block_rehearsal": "Ensayo"
},
"date_strip": {
"today_aria": "Ir a hoy",
"today_label": "Hoy"
},
"mini_cal": {
"prev_aria": "Mes anterior",
"next_aria": "Mes siguiente"
},
"slots": {
"loading": "Buscando huecos libres...",
"empty": "No se han encontrado huecos libres",
"label": "Huecos libres",
"duration_suffix": "{n}min"
},
"quick_event": {
"aria_dialog": "Crear evento",
"header_new": "Nuevo evento",
"placeholder_title": "Añadir un título",
"type_event": "Evento",
"type_time_entry": "Tiempo",
"type_habit": "Hábito",
"placeholder_location": "Añadir ubicación",
"placeholder_description": "Descripción"
},
"list_view": {
"placeholder_new_event": "Nuevo evento...",
"voice_reason": "Los eventos se guardan cifrados. Necesitas una cuenta Mana para esto.",
"empty_label": "Eventos",
"empty_text": "Sin eventos",
"open_action": "Abrir",
"delete_action": "Eliminar"
},
"detail_route": {
"doc_title": "Calendario - Mana",
"create_modal_title": "Nuevo evento",
"event_doc_title": "{title} - Calendario - Mana",
"event_doc_title_fallback": "Evento",
"back_to_calendar": "Volver al calendario",
"not_found": "Evento no encontrado",
"edit_title": "Editar evento",
"label_title": "Título",
"label_description": "Descripción",
"label_date": "Fecha",
"label_from": "Desde",
"label_to": "Hasta",
"label_location": "Ubicación",
"placeholder_location": "Introduce la ubicación...",
"submit_save": "Guardar",
"submit_cancel": "Cancelar",
"time_suffix_uhr": ""
},
"detail_view": {
"toast_deleted": "Evento eliminado",
"not_found": "Evento no encontrado",
"confirm_delete": "¿Eliminar el evento de verdad?",
"placeholder_title": "Título...",
"label_visibility": "Visibilidad",
"label_allday": "Todo el día",
"placeholder_location": "Añadir ubicación...",
"section_tags": "Etiquetas",
"section_description": "Descripción",
"placeholder_description": "Añadir descripción...",
"meta_created": "Creado: {date}",
"meta_updated": "Editado: {date}",
"untitled_fallback": "Sin título"
},
"calendars_route": {
"doc_title": "Gestionar calendarios - Mana",
"back_to_calendar": "Volver al calendario",
"page_title": "Mis calendarios",
"new_calendar": "Nuevo calendario",
"label_name": "Nombre",
"placeholder_name": "p. ej. Trabajo, Deporte, Familia...",
"label_color": "Color",
"aria_pick_color": "Elegir color",
"submit_create": "Crear",
"empty": "Aún no hay calendarios.",
"badge_default": "(Predeterminado)",
"aria_hide": "Ocultar",
"aria_show": "Mostrar",
"aria_set_default": "Establecer como predeterminado",
"confirm_delete": "¿Eliminar el calendario de verdad? Se perderán todos los eventos asociados."
},
"shared_view": {
"kind": "Evento",
"label_when": "Cuándo",
"label_where": "Dónde",
"timezone_prefix": "Zona horaria: {tz}",
"add_to_calendar": "📅 Añadir a mi calendario",
"expiry": "Este enlace caduca el {date}."
},
"repeat": { "repeat": {
"none": "No repetir", "none": "No repetir",
"daily": "Diario", "daily": "Diario",

View file

@ -29,8 +29,10 @@
}, },
"calendar": { "calendar": {
"today": "Aujourd'hui", "today": "Aujourd'hui",
"tomorrow": "Demain",
"newEvent": "Nouvel événement", "newEvent": "Nouvel événement",
"noEvents": "Aucun événement", "noEvents": "Aucun événement",
"noEventsInRange": "Aucun événement dans cette période",
"allDay": "Toute la journée", "allDay": "Toute la journée",
"myCalendars": "Mes calendriers", "myCalendars": "Mes calendriers",
"sharedCalendars": "Calendriers partagés", "sharedCalendars": "Calendriers partagés",
@ -58,6 +60,210 @@
"changeStartTime": "Changer l'heure de début", "changeStartTime": "Changer l'heure de début",
"changeEndTime": "Changer l'heure de fin" "changeEndTime": "Changer l'heure de fin"
}, },
"event_form": {
"aria_create": "Créer un événement",
"aria_edit": "Modifier l'événement",
"label_title_required": "Titre *",
"placeholder_title": "Saisis le nom de l'événement",
"label_time": "Heure",
"label_recurrence": "Répétition",
"placeholder_location": "Saisis le lieu...",
"placeholder_description": "Ajoute une description",
"label_tags": "Tags",
"recur_edit_suffix": "{preview} — Modifier"
},
"event_card": {
"new_event_label": "Nouvel événement",
"recurring_title": "Récurrent",
"linked_title": "Réalisé"
},
"event_modal": {
"editing_title": "Modifier l'événement",
"copy_title": "Copier",
"label_visibility": "Visibilité",
"label_share_link": "Lien",
"created_at": "Créé : {date}",
"updated_at": "· Modifié : {date}",
"confirm_delete_single": "Supprimer cet événement ?",
"clipboard_location_prefix": "Lieu : {location}",
"recur_edit_title": "Modifier l'événement récurrent",
"recur_edit_text": "Modifier seulement cet événement ou tous les futurs ?",
"recur_edit_only_this": "Seulement cet événement",
"recur_edit_all_future": "Tous les événements futurs",
"recur_delete_title": "Supprimer l'événement récurrent",
"recur_delete_text": "Supprimer seulement cet événement ou toute la série ?",
"recur_delete_only_this": "Seulement cet événement",
"recur_delete_all": "Tous les événements de la série"
},
"agenda": {
"empty": "Aucun événement dans cette période",
"open_details_aria": "Ouvrir les détails"
},
"recurrence": {
"none": "Pas de répétition",
"daily": "Quotidien",
"weekly": "Hebdomadaire",
"monthly": "Mensuel",
"yearly": "Annuel",
"every_2_weeks": "Toutes les 2 semaines",
"custom": "Personnalisé...",
"recurring_fallback": "Récurrent",
"weekly_with_days": "Hebdomadaire ({days})",
"every_n_unit": "Tous les {n} {unit}",
"every_unit": "Tous les {unit}",
"unit_days": "jours",
"unit_weeks": "semaines",
"unit_months": "mois",
"unit_years": "ans",
"unit_freq_daily": "jour(s)",
"unit_freq_weekly": "semaine(s)",
"unit_freq_monthly": "mois",
"unit_freq_yearly": "an(s)",
"preview_at_days": " le(s) {days}",
"preview_count_suffix": ", {count}x",
"preview_until_suffix": " jusqu'au {date}",
"builder_title": "Répétition personnalisée",
"builder_every_label": "Tous les",
"builder_weekdays_label": "Jours de la semaine",
"builder_end_label": "Se termine",
"builder_end_never": "Jamais",
"builder_end_after": "Après",
"builder_end_after_unit": "événements",
"builder_end_until": "Le",
"builder_apply": "Appliquer"
},
"weekday_short": {
"mon": "Lun",
"tue": "Mar",
"wed": "Mer",
"thu": "Jeu",
"fri": "Ven",
"sat": "Sam",
"sun": "Dim"
},
"weekday_long": {
"sun": "Dimanche",
"mon": "Lundi",
"tue": "Mardi",
"wed": "Mercredi",
"thu": "Jeudi",
"fri": "Vendredi",
"sat": "Samedi"
},
"header": {
"today": "Aujourd'hui",
"new_event": "Événement",
"aria_prev": "Précédent",
"aria_next": "Suivant",
"aria_filter": "Filtre",
"aria_export": "Exporter",
"block_event": "Événements",
"block_task": "Tâches",
"block_time_entry": "Temps",
"block_habit": "Habitudes",
"block_body": "Entraînement",
"block_watering": "Arrosage",
"block_sleep": "Sommeil",
"block_practice": "Pratique",
"block_period": "Cycle",
"block_guide": "Guides",
"block_visit": "Visites",
"block_study": "Étude",
"block_listening": "Musique",
"block_mood": "Humeur",
"block_rehearsal": "Répétition"
},
"date_strip": {
"today_aria": "Aller à aujourd'hui",
"today_label": "Aujourd'hui"
},
"mini_cal": {
"prev_aria": "Mois précédent",
"next_aria": "Mois suivant"
},
"slots": {
"loading": "Recherche de créneaux libres...",
"empty": "Aucun créneau libre trouvé",
"label": "Créneaux libres",
"duration_suffix": "{n}min"
},
"quick_event": {
"aria_dialog": "Créer un événement",
"header_new": "Nouvel événement",
"placeholder_title": "Ajouter un titre",
"type_event": "Événement",
"type_time_entry": "Temps",
"type_habit": "Habitude",
"placeholder_location": "Ajouter un lieu",
"placeholder_description": "Description"
},
"list_view": {
"placeholder_new_event": "Nouvel événement...",
"voice_reason": "Les événements sont stockés chiffrés. Tu as besoin d'un compte Mana pour ça.",
"empty_label": "Événements",
"empty_text": "Aucun événement",
"open_action": "Ouvrir",
"delete_action": "Supprimer"
},
"detail_route": {
"doc_title": "Calendrier - Mana",
"create_modal_title": "Nouvel événement",
"event_doc_title": "{title} - Calendrier - Mana",
"event_doc_title_fallback": "Événement",
"back_to_calendar": "Retour au calendrier",
"not_found": "Événement introuvable",
"edit_title": "Modifier l'événement",
"label_title": "Titre",
"label_description": "Description",
"label_date": "Date",
"label_from": "De",
"label_to": "À",
"label_location": "Lieu",
"placeholder_location": "Saisis le lieu...",
"submit_save": "Enregistrer",
"submit_cancel": "Annuler",
"time_suffix_uhr": ""
},
"detail_view": {
"toast_deleted": "Événement supprimé",
"not_found": "Événement introuvable",
"confirm_delete": "Vraiment supprimer l'événement ?",
"placeholder_title": "Titre...",
"label_visibility": "Visibilité",
"label_allday": "Toute la journée",
"placeholder_location": "Ajouter un lieu...",
"section_tags": "Tags",
"section_description": "Description",
"placeholder_description": "Ajouter une description...",
"meta_created": "Créé : {date}",
"meta_updated": "Modifié : {date}",
"untitled_fallback": "Sans titre"
},
"calendars_route": {
"doc_title": "Gérer les calendriers - Mana",
"back_to_calendar": "Retour au calendrier",
"page_title": "Mes calendriers",
"new_calendar": "Nouveau calendrier",
"label_name": "Nom",
"placeholder_name": "p. ex. Travail, Sport, Famille...",
"label_color": "Couleur",
"aria_pick_color": "Choisir la couleur",
"submit_create": "Créer",
"empty": "Pas encore de calendriers.",
"badge_default": "(Par défaut)",
"aria_hide": "Masquer",
"aria_show": "Afficher",
"aria_set_default": "Définir comme défaut",
"confirm_delete": "Vraiment supprimer le calendrier ? Tous les événements associés seront perdus."
},
"shared_view": {
"kind": "Événement",
"label_when": "Quand",
"label_where": "Où",
"timezone_prefix": "Fuseau horaire : {tz}",
"add_to_calendar": "📅 Ajouter à mon calendrier",
"expiry": "Ce lien expire le {date}."
},
"repeat": { "repeat": {
"none": "Ne pas répéter", "none": "Ne pas répéter",
"daily": "Quotidien", "daily": "Quotidien",

View file

@ -29,8 +29,10 @@
}, },
"calendar": { "calendar": {
"today": "Oggi", "today": "Oggi",
"tomorrow": "Domani",
"newEvent": "Nuovo evento", "newEvent": "Nuovo evento",
"noEvents": "Nessun evento", "noEvents": "Nessun evento",
"noEventsInRange": "Nessun evento in questo intervallo",
"allDay": "Tutto il giorno", "allDay": "Tutto il giorno",
"myCalendars": "I miei calendari", "myCalendars": "I miei calendari",
"sharedCalendars": "Calendari condivisi", "sharedCalendars": "Calendari condivisi",
@ -58,6 +60,210 @@
"changeStartTime": "Cambia ora di inizio", "changeStartTime": "Cambia ora di inizio",
"changeEndTime": "Cambia ora di fine" "changeEndTime": "Cambia ora di fine"
}, },
"event_form": {
"aria_create": "Crea evento",
"aria_edit": "Modifica evento",
"label_title_required": "Titolo *",
"placeholder_title": "Inserisci il nome dell'evento",
"label_time": "Ora",
"label_recurrence": "Ripetizione",
"placeholder_location": "Inserisci il luogo...",
"placeholder_description": "Aggiungi una descrizione",
"label_tags": "Tag",
"recur_edit_suffix": "{preview} — Modifica"
},
"event_card": {
"new_event_label": "Nuovo evento",
"recurring_title": "Ricorrente",
"linked_title": "Eseguito"
},
"event_modal": {
"editing_title": "Modifica evento",
"copy_title": "Copia",
"label_visibility": "Visibilità",
"label_share_link": "Link",
"created_at": "Creato: {date}",
"updated_at": "· Modificato: {date}",
"confirm_delete_single": "Eliminare questo evento?",
"clipboard_location_prefix": "Luogo: {location}",
"recur_edit_title": "Modifica evento ricorrente",
"recur_edit_text": "Modificare solo questo evento o tutti quelli futuri?",
"recur_edit_only_this": "Solo questo evento",
"recur_edit_all_future": "Tutti gli eventi futuri",
"recur_delete_title": "Elimina evento ricorrente",
"recur_delete_text": "Eliminare solo questo evento o tutta la serie?",
"recur_delete_only_this": "Solo questo evento",
"recur_delete_all": "Tutti gli eventi della serie"
},
"agenda": {
"empty": "Nessun evento in questo intervallo",
"open_details_aria": "Apri dettagli"
},
"recurrence": {
"none": "Nessuna ripetizione",
"daily": "Giornaliero",
"weekly": "Settimanale",
"monthly": "Mensile",
"yearly": "Annuale",
"every_2_weeks": "Ogni 2 settimane",
"custom": "Personalizzato...",
"recurring_fallback": "Ricorrente",
"weekly_with_days": "Settimanale ({days})",
"every_n_unit": "Ogni {n} {unit}",
"every_unit": "Ogni {unit}",
"unit_days": "giorni",
"unit_weeks": "settimane",
"unit_months": "mesi",
"unit_years": "anni",
"unit_freq_daily": "giorno/i",
"unit_freq_weekly": "settimana/e",
"unit_freq_monthly": "mese/i",
"unit_freq_yearly": "anno/i",
"preview_at_days": " il {days}",
"preview_count_suffix": ", {count}x",
"preview_until_suffix": " fino al {date}",
"builder_title": "Ripetizione personalizzata",
"builder_every_label": "Ogni",
"builder_weekdays_label": "Giorni della settimana",
"builder_end_label": "Termina",
"builder_end_never": "Mai",
"builder_end_after": "Dopo",
"builder_end_after_unit": "eventi",
"builder_end_until": "Il",
"builder_apply": "Applica"
},
"weekday_short": {
"mon": "Lun",
"tue": "Mar",
"wed": "Mer",
"thu": "Gio",
"fri": "Ven",
"sat": "Sab",
"sun": "Dom"
},
"weekday_long": {
"sun": "Domenica",
"mon": "Lunedì",
"tue": "Martedì",
"wed": "Mercoledì",
"thu": "Giovedì",
"fri": "Venerdì",
"sat": "Sabato"
},
"header": {
"today": "Oggi",
"new_event": "Evento",
"aria_prev": "Indietro",
"aria_next": "Avanti",
"aria_filter": "Filtro",
"aria_export": "Esporta",
"block_event": "Eventi",
"block_task": "Attività",
"block_time_entry": "Tempi",
"block_habit": "Abitudini",
"block_body": "Allenamento",
"block_watering": "Annaffiature",
"block_sleep": "Sonno",
"block_practice": "Esercizio",
"block_period": "Ciclo",
"block_guide": "Guide",
"block_visit": "Visite",
"block_study": "Studio",
"block_listening": "Musica",
"block_mood": "Umore",
"block_rehearsal": "Prova"
},
"date_strip": {
"today_aria": "Vai a oggi",
"today_label": "Oggi"
},
"mini_cal": {
"prev_aria": "Mese precedente",
"next_aria": "Mese successivo"
},
"slots": {
"loading": "Ricerca slot liberi...",
"empty": "Nessuno slot libero trovato",
"label": "Slot liberi",
"duration_suffix": "{n}min"
},
"quick_event": {
"aria_dialog": "Crea evento",
"header_new": "Nuovo evento",
"placeholder_title": "Aggiungi un titolo",
"type_event": "Evento",
"type_time_entry": "Tempo",
"type_habit": "Abitudine",
"placeholder_location": "Aggiungi luogo",
"placeholder_description": "Descrizione"
},
"list_view": {
"placeholder_new_event": "Nuovo evento...",
"voice_reason": "Gli eventi sono salvati cifrati. Per questo serve un account Mana.",
"empty_label": "Eventi",
"empty_text": "Nessun evento",
"open_action": "Apri",
"delete_action": "Elimina"
},
"detail_route": {
"doc_title": "Calendario - Mana",
"create_modal_title": "Nuovo evento",
"event_doc_title": "{title} - Calendario - Mana",
"event_doc_title_fallback": "Evento",
"back_to_calendar": "Torna al calendario",
"not_found": "Evento non trovato",
"edit_title": "Modifica evento",
"label_title": "Titolo",
"label_description": "Descrizione",
"label_date": "Data",
"label_from": "Da",
"label_to": "A",
"label_location": "Luogo",
"placeholder_location": "Inserisci il luogo...",
"submit_save": "Salva",
"submit_cancel": "Annulla",
"time_suffix_uhr": ""
},
"detail_view": {
"toast_deleted": "Evento eliminato",
"not_found": "Evento non trovato",
"confirm_delete": "Eliminare davvero l'evento?",
"placeholder_title": "Titolo...",
"label_visibility": "Visibilità",
"label_allday": "Tutto il giorno",
"placeholder_location": "Aggiungi luogo...",
"section_tags": "Tag",
"section_description": "Descrizione",
"placeholder_description": "Aggiungi descrizione...",
"meta_created": "Creato: {date}",
"meta_updated": "Modificato: {date}",
"untitled_fallback": "Senza titolo"
},
"calendars_route": {
"doc_title": "Gestisci calendari - Mana",
"back_to_calendar": "Torna al calendario",
"page_title": "I miei calendari",
"new_calendar": "Nuovo calendario",
"label_name": "Nome",
"placeholder_name": "es. Lavoro, Sport, Famiglia...",
"label_color": "Colore",
"aria_pick_color": "Scegli colore",
"submit_create": "Crea",
"empty": "Ancora nessun calendario.",
"badge_default": "(Predefinito)",
"aria_hide": "Nascondi",
"aria_show": "Mostra",
"aria_set_default": "Imposta come predefinito",
"confirm_delete": "Eliminare davvero il calendario? Tutti gli eventi associati andranno persi."
},
"shared_view": {
"kind": "Evento",
"label_when": "Quando",
"label_where": "Dove",
"timezone_prefix": "Fuso orario: {tz}",
"add_to_calendar": "📅 Aggiungi al mio calendario",
"expiry": "Questo link scade il {date}."
},
"repeat": { "repeat": {
"none": "Non ripetere", "none": "Non ripetere",
"daily": "Giornaliero", "daily": "Giornaliero",

View file

@ -3,6 +3,7 @@
Mini week strip + today's events. Floating input at bottom. Mini week strip + today's events. Floating input at bottom.
--> -->
<script lang="ts"> <script lang="ts">
import { _ } from 'svelte-i18n';
import { db } from '$lib/data/database'; import { db } from '$lib/data/database';
import { eventsStore } from './stores/events.svelte'; import { eventsStore } from './stores/events.svelte';
import { useAllCalendarItems } from './queries'; import { useAllCalendarItems } from './queries';
@ -61,16 +62,24 @@
return new Date(iso).toLocaleTimeString('de', { hour: '2-digit', minute: '2-digit' }); return new Date(iso).toLocaleTimeString('de', { hour: '2-digit', minute: '2-digit' });
} }
const WEEKDAYS = [ const WEEKDAYS = $derived([
'Sonntag', $_('calendar.weekday_long.sun'),
'Montag', $_('calendar.weekday_long.mon'),
'Dienstag', $_('calendar.weekday_long.tue'),
'Mittwoch', $_('calendar.weekday_long.wed'),
'Donnerstag', $_('calendar.weekday_long.thu'),
'Freitag', $_('calendar.weekday_long.fri'),
'Samstag', $_('calendar.weekday_long.sat'),
]; ]);
const dayNames = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So']; const dayNames = $derived([
$_('calendar.weekday_short.mon'),
$_('calendar.weekday_short.tue'),
$_('calendar.weekday_short.wed'),
$_('calendar.weekday_short.thu'),
$_('calendar.weekday_short.fri'),
$_('calendar.weekday_short.sat'),
$_('calendar.weekday_short.sun'),
]);
function formatDateLabel(iso: string): string { function formatDateLabel(iso: string): string {
const date = new Date(iso); const date = new Date(iso);
@ -78,8 +87,9 @@
const todayDate = new Date(now); const todayDate = new Date(now);
const tomorrowDate = new Date(todayDate); const tomorrowDate = new Date(todayDate);
tomorrowDate.setDate(tomorrowDate.getDate() + 1); tomorrowDate.setDate(tomorrowDate.getDate() + 1);
if (dateStr === todayStr) return 'Heute'; if (dateStr === todayStr) return $_('calendar.calendar.today');
if (dateStr === tomorrowDate.toISOString().split('T')[0]) return 'Morgen'; if (dateStr === tomorrowDate.toISOString().split('T')[0])
return $_('calendar.calendar.tomorrow');
// Within 7 days: show weekday name // Within 7 days: show weekday name
const diffMs = date.getTime() - todayDate.getTime(); const diffMs = date.getTime() - todayDate.getTime();
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
@ -94,7 +104,7 @@
? [ ? [
{ {
id: 'open', id: 'open',
label: 'Öffnen', label: $_('calendar.list_view.open_action'),
icon: PencilSimple, icon: PencilSimple,
action: () => { action: () => {
const target = ctxMenu.state.target; const target = ctxMenu.state.target;
@ -104,7 +114,7 @@
{ id: 'div', label: '', type: 'divider' as const }, { id: 'div', label: '', type: 'divider' as const },
{ {
id: 'delete', id: 'delete',
label: 'Löschen', label: $_('calendar.list_view.delete_action'),
icon: Trash, icon: Trash,
variant: 'danger' as const, variant: 'danger' as const,
action: () => { action: () => {
@ -211,7 +221,7 @@
<span class="event-date">{formatDateLabel(event.startTime)}</span> <span class="event-date">{formatDateLabel(event.startTime)}</span>
<span class="event-time"> <span class="event-time">
{#if event.isAllDay} {#if event.isAllDay}
Ganztägig {$_('calendar.event.allDay')}
{:else} {:else}
{formatTime(event.startTime)} {formatTime(event.startTime)}
{/if} {/if}
@ -222,20 +232,20 @@
{#if upcomingEvents.length === 0} {#if upcomingEvents.length === 0}
{#if hasActiveSceneScope()} {#if hasActiveSceneScope()}
<ScopeEmptyState label="Termine" /> <ScopeEmptyState label={$_('calendar.list_view.empty_label')} />
{:else} {:else}
<p class="empty">Keine Termine</p> <p class="empty">{$_('calendar.list_view.empty_text')}</p>
{/if} {/if}
{/if} {/if}
</div> </div>
<FloatingInputBar <FloatingInputBar
bind:value={newTitle} bind:value={newTitle}
placeholder="Neuer Termin..." placeholder={$_('calendar.list_view.placeholder_new_event')}
onSubmit={createEvent} onSubmit={createEvent}
voice voice
voiceFeature="calendar-voice-capture" voiceFeature="calendar-voice-capture"
voiceReason="Termine werden verschlüsselt gespeichert. Dafür brauchst du ein Mana-Konto." voiceReason={$_('calendar.list_view.voice_reason')}
onVoiceComplete={handleVoiceComplete} onVoiceComplete={handleVoiceComplete}
/> />

View file

@ -11,6 +11,9 @@
in light + dark (via prefers-color-scheme in the layout). in light + dark (via prefers-color-scheme in the layout).
--> -->
<script lang="ts"> <script lang="ts">
import { _, locale } from 'svelte-i18n';
import { get } from 'svelte/store';
interface EventBlob { interface EventBlob {
title: string; title: string;
startTime: string; startTime: string;
@ -35,7 +38,7 @@
const end = $derived(new Date(event.endTime)); const end = $derived(new Date(event.endTime));
function formatDate(d: Date): string { function formatDate(d: Date): string {
return new Intl.DateTimeFormat('de-DE', { return new Intl.DateTimeFormat(get(locale) ?? 'de', {
weekday: 'long', weekday: 'long',
day: '2-digit', day: '2-digit',
month: 'long', month: 'long',
@ -44,7 +47,7 @@
} }
function formatTime(d: Date): string { function formatTime(d: Date): string {
return new Intl.DateTimeFormat('de-DE', { return new Intl.DateTimeFormat(get(locale) ?? 'de', {
hour: '2-digit', hour: '2-digit',
minute: '2-digit', minute: '2-digit',
}).format(d); }).format(d);
@ -52,7 +55,7 @@
const dateLabel = $derived(formatDate(start)); const dateLabel = $derived(formatDate(start));
const timeLabel = $derived( const timeLabel = $derived(
event.isAllDay ? 'Ganztägig' : `${formatTime(start)} ${formatTime(end)}` event.isAllDay ? $_('calendar.event.allDay') : `${formatTime(start)} ${formatTime(end)}`
); );
// Same-day range = compact; otherwise show two dates // Same-day range = compact; otherwise show two dates
@ -84,38 +87,46 @@
</svelte:head> </svelte:head>
<article class="event"> <article class="event">
<span class="event__kind">Termin</span> <span class="event__kind">{$_('calendar.shared_view.kind')}</span>
<h1 class="event__title">{event.title}</h1> <h1 class="event__title">{event.title}</h1>
<dl class="event__meta"> <dl class="event__meta">
<div class="event__row"> <div class="event__row">
<dt>Wann</dt> <dt>{$_('calendar.shared_view.label_when')}</dt>
<dd> <dd>
<div class="event__date">{dateRangeLabel}</div> <div class="event__date">{dateRangeLabel}</div>
<div class="event__time">{timeLabel}</div> <div class="event__time">{timeLabel}</div>
{#if event.timezone} {#if event.timezone}
<div class="event__tz">Zeitzone: {event.timezone}</div> <div class="event__tz">
{$_('calendar.shared_view.timezone_prefix', { values: { tz: event.timezone } })}
</div>
{/if} {/if}
</dd> </dd>
</div> </div>
{#if event.location} {#if event.location}
<div class="event__row"> <div class="event__row">
<dt>Wo</dt> <dt>{$_('calendar.shared_view.label_where')}</dt>
<dd>{event.location}</dd> <dd>{event.location}</dd>
</div> </div>
{/if} {/if}
</dl> </dl>
<a class="event__ics" href={icsUrl} download="event.ics">📅 Zum eigenen Kalender hinzufügen</a> <a class="event__ics" href={icsUrl} download="event.ics"
>{$_('calendar.shared_view.add_to_calendar')}</a
>
{#if expiresAt} {#if expiresAt}
<p class="event__expiry"> <p class="event__expiry">
Dieser Link läuft am {new Intl.DateTimeFormat('de-DE', { {$_('calendar.shared_view.expiry', {
day: '2-digit', values: {
month: 'long', date: new Intl.DateTimeFormat(get(locale) ?? 'de', {
year: 'numeric', day: '2-digit',
}).format(new Date(expiresAt))} ab. month: 'long',
year: 'numeric',
}).format(new Date(expiresAt)),
},
})}
</p> </p>
{/if} {/if}
</article> </article>

View file

@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import { getDateFnsLocale } from '$lib/i18n/format'; import { getDateFnsLocale } from '$lib/i18n/format';
import { _ } from 'svelte-i18n';
import { getContext } from 'svelte'; import { getContext } from 'svelte';
import { calendarViewStore } from '../stores/view.svelte'; import { calendarViewStore } from '../stores/view.svelte';
import { eventsStore } from '../stores/events.svelte'; import { eventsStore } from '../stores/events.svelte';
@ -73,8 +74,8 @@
}); });
function formatDateHeader(date: Date) { function formatDateHeader(date: Date) {
if (isToday(date)) return 'Heute'; if (isToday(date)) return $_('calendar.calendar.today');
if (isTomorrow(date)) return 'Morgen'; if (isTomorrow(date)) return $_('calendar.calendar.tomorrow');
return format(date, 'EEEE, d. MMMM', { locale: getDateFnsLocale() }); return format(date, 'EEEE, d. MMMM', { locale: getDateFnsLocale() });
} }
@ -108,7 +109,7 @@
{#if groupedEvents.length === 0} {#if groupedEvents.length === 0}
<div class="empty-state"> <div class="empty-state">
<CalendarBlank size={64} /> <CalendarBlank size={64} />
<p>Keine Termine in diesem Zeitraum</p> <p>{$_('calendar.agenda.empty')}</p>
</div> </div>
{:else} {:else}
<div class="event-list"> <div class="event-list">
@ -125,7 +126,7 @@
<div class="event-content"> <div class="event-content">
<div class="event-time"> <div class="event-time">
{#if event.isAllDay} {#if event.isAllDay}
Ganztägig {$_('calendar.event.allDay')}
{:else} {:else}
{format(toDate(event.startTime), 'HH:mm')} - {format( {format(toDate(event.startTime), 'HH:mm')} - {format(
toDate(event.endTime), toDate(event.endTime),
@ -155,8 +156,8 @@
<button <button
class="expand-btn" class="expand-btn"
onclick={() => handleEventClick(event)} onclick={() => handleEventClick(event)}
title="Details öffnen" title={$_('calendar.agenda.open_details_aria')}
aria-label="Details öffnen" aria-label={$_('calendar.agenda.open_details_aria')}
> >
<CaretRight size={16} /> <CaretRight size={16} />
</button> </button>

View file

@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import { getDateFnsLocale } from '$lib/i18n/format'; import { getDateFnsLocale } from '$lib/i18n/format';
import { _ } from 'svelte-i18n';
import { calendarViewStore } from '../stores/view.svelte'; import { calendarViewStore } from '../stores/view.svelte';
import type { CalendarViewType } from '../types'; import type { CalendarViewType } from '../types';
import type { TimeBlockType } from '$lib/data/time-blocks/types'; import type { TimeBlockType } from '$lib/data/time-blocks/types';
@ -39,23 +40,25 @@
let showFilters = $state(false); let showFilters = $state(false);
const blockTypeConfig: { type: TimeBlockType; label: string; icon: typeof CalendarBlank }[] = [ const blockTypeConfig = $derived<
{ type: 'event', label: 'Termine', icon: CalendarBlank }, { type: TimeBlockType; label: string; icon: typeof CalendarBlank }[]
{ type: 'task', label: 'Aufgaben', icon: CheckSquare }, >([
{ type: 'timeEntry', label: 'Zeiten', icon: Timer }, { type: 'event', label: $_('calendar.header.block_event'), icon: CalendarBlank },
{ type: 'habit', label: 'Habits', icon: Heart }, { type: 'task', label: $_('calendar.header.block_task'), icon: CheckSquare },
{ type: 'body', label: 'Training', icon: Barbell }, { type: 'timeEntry', label: $_('calendar.header.block_time_entry'), icon: Timer },
{ type: 'watering', label: 'Gießen', icon: Drop }, { type: 'habit', label: $_('calendar.header.block_habit'), icon: Heart },
{ type: 'sleep', label: 'Schlaf', icon: Moon }, { type: 'body', label: $_('calendar.header.block_body'), icon: Barbell },
{ type: 'practice', label: 'Übung', icon: GraduationCap }, { type: 'watering', label: $_('calendar.header.block_watering'), icon: Drop },
{ type: 'period', label: 'Periode', icon: FlowerLotus }, { type: 'sleep', label: $_('calendar.header.block_sleep'), icon: Moon },
{ type: 'guide', label: 'Guides', icon: Compass }, { type: 'practice', label: $_('calendar.header.block_practice'), icon: GraduationCap },
{ type: 'visit', label: 'Besuche', icon: MapPin }, { type: 'period', label: $_('calendar.header.block_period'), icon: FlowerLotus },
{ type: 'study', label: 'Lernen', icon: BookOpen }, { type: 'guide', label: $_('calendar.header.block_guide'), icon: Compass },
{ type: 'listening', label: 'Musik', icon: MusicNote }, { type: 'visit', label: $_('calendar.header.block_visit'), icon: MapPin },
{ type: 'mood', label: 'Stimmung', icon: SunHorizon }, { type: 'study', label: $_('calendar.header.block_study'), icon: BookOpen },
{ type: 'rehearsal', label: 'Probe', icon: Presentation }, { type: 'listening', label: $_('calendar.header.block_listening'), icon: MusicNote },
]; { type: 'mood', label: $_('calendar.header.block_mood'), icon: SunHorizon },
{ type: 'rehearsal', label: $_('calendar.header.block_rehearsal'), icon: Presentation },
]);
let allActive = $derived( let allActive = $derived(
blockTypeConfig.every((c) => calendarViewStore.visibleBlockTypes.has(c.type)) blockTypeConfig.every((c) => calendarViewStore.visibleBlockTypes.has(c.type))
@ -82,22 +85,32 @@
}); });
}); });
const viewLabels: Record<CalendarViewType, string> = { const viewLabels = $derived<Record<CalendarViewType, string>>({
week: 'Woche', week: $_('calendar.views.week'),
month: 'Monat', month: $_('calendar.views.month'),
agenda: 'Agenda', agenda: $_('calendar.views.agenda'),
}; });
</script> </script>
<header class="calendar-header"> <header class="calendar-header">
<div class="header-left"> <div class="header-left">
<h1 class="header-label">{headerLabel}</h1> <h1 class="header-label">{headerLabel}</h1>
<div class="nav-buttons"> <div class="nav-buttons">
<button onclick={() => calendarViewStore.goToPrevious()} class="nav-btn" aria-label="Zurück"> <button
onclick={() => calendarViewStore.goToPrevious()}
class="nav-btn"
aria-label={$_('calendar.header.aria_prev')}
>
<CaretLeft size={18} /> <CaretLeft size={18} />
</button> </button>
<button onclick={() => calendarViewStore.goToToday()} class="today-btn"> Heute </button> <button onclick={() => calendarViewStore.goToToday()} class="today-btn">
<button onclick={() => calendarViewStore.goToNext()} class="nav-btn" aria-label="Weiter"> {$_('calendar.header.today')}
</button>
<button
onclick={() => calendarViewStore.goToNext()}
class="nav-btn"
aria-label={$_('calendar.header.aria_next')}
>
<CaretRight size={18} /> <CaretRight size={18} />
</button> </button>
</div> </div>
@ -120,18 +133,22 @@
onclick={() => (showFilters = !showFilters)} onclick={() => (showFilters = !showFilters)}
class="filter-btn" class="filter-btn"
class:active={!allActive} class:active={!allActive}
aria-label="Filter" aria-label={$_('calendar.header.aria_filter')}
> >
<Funnel size={16} /> <Funnel size={16} />
</button> </button>
<button class="filter-btn" onclick={handleExport} aria-label="Exportieren"> <button
class="filter-btn"
onclick={handleExport}
aria-label={$_('calendar.header.aria_export')}
>
<Export size={16} /> <Export size={16} />
</button> </button>
<button onclick={onNewEvent} class="new-event-btn"> <button onclick={onNewEvent} class="new-event-btn">
<Plus size={16} /> <Plus size={16} />
Termin {$_('calendar.header.new_event')}
</button> </button>
</div> </div>

View file

@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { _ } from 'svelte-i18n';
import { RRule } from 'rrule'; import { RRule } from 'rrule';
interface Props { interface Props {
@ -20,22 +21,22 @@
let count = $state(parsed?.count ?? 10); let count = $state(parsed?.count ?? 10);
let untilDate = $state(parsed?.until ?? ''); let untilDate = $state(parsed?.until ?? '');
const DAYS = [ const DAYS = $derived([
{ value: 0, short: 'So', rrule: 'SU' }, { value: 0, short: $_('calendar.weekday_short.sun'), rrule: 'SU' },
{ value: 1, short: 'Mo', rrule: 'MO' }, { value: 1, short: $_('calendar.weekday_short.mon'), rrule: 'MO' },
{ value: 2, short: 'Di', rrule: 'TU' }, { value: 2, short: $_('calendar.weekday_short.tue'), rrule: 'TU' },
{ value: 3, short: 'Mi', rrule: 'WE' }, { value: 3, short: $_('calendar.weekday_short.wed'), rrule: 'WE' },
{ value: 4, short: 'Do', rrule: 'TH' }, { value: 4, short: $_('calendar.weekday_short.thu'), rrule: 'TH' },
{ value: 5, short: 'Fr', rrule: 'FR' }, { value: 5, short: $_('calendar.weekday_short.fri'), rrule: 'FR' },
{ value: 6, short: 'Sa', rrule: 'SA' }, { value: 6, short: $_('calendar.weekday_short.sat'), rrule: 'SA' },
]; ]);
const FREQ_LABELS: Record<string, string> = { const FREQ_LABELS = $derived<Record<string, string>>({
DAILY: 'Tag(e)', DAILY: $_('calendar.recurrence.unit_freq_daily'),
WEEKLY: 'Woche(n)', WEEKLY: $_('calendar.recurrence.unit_freq_weekly'),
MONTHLY: 'Monat(e)', MONTHLY: $_('calendar.recurrence.unit_freq_monthly'),
YEARLY: 'Jahr(e)', YEARLY: $_('calendar.recurrence.unit_freq_yearly'),
}; });
function toggleDay(day: number) { function toggleDay(day: number) {
if (selectedDays.includes(day)) { if (selectedDays.includes(day)) {
@ -112,35 +113,43 @@
// Preview text // Preview text
let preview = $derived.by(() => { let preview = $derived.by(() => {
let text = `Alle ${interval > 1 ? interval + ' ' : ''}${FREQ_LABELS[freq]}`; const unit = FREQ_LABELS[freq];
let text =
interval > 1
? $_('calendar.recurrence.every_n_unit', { values: { n: interval, unit } })
: $_('calendar.recurrence.every_unit', { values: { unit } });
if (freq === 'WEEKLY' && selectedDays.length > 0 && selectedDays.length < 7) { if (freq === 'WEEKLY' && selectedDays.length > 0 && selectedDays.length < 7) {
text += ` an ${selectedDays.map((d) => DAYS[d].short).join(', ')}`; text += $_('calendar.recurrence.preview_at_days', {
values: { days: selectedDays.map((d) => DAYS[d].short).join(', ') },
});
} }
if (endType === 'count') text += `, ${count}x`; if (endType === 'count')
else if (endType === 'until' && untilDate) text += ` bis ${untilDate}`; text += $_('calendar.recurrence.preview_count_suffix', { values: { count } });
else if (endType === 'until' && untilDate)
text += $_('calendar.recurrence.preview_until_suffix', { values: { date: untilDate } });
return text; return text;
}); });
</script> </script>
<div class="recurrence-builder"> <div class="recurrence-builder">
<div class="builder-header">Benutzerdefinierte Wiederholung</div> <div class="builder-header">{$_('calendar.recurrence.builder_title')}</div>
<!-- Frequency + Interval --> <!-- Frequency + Interval -->
<div class="builder-row"> <div class="builder-row">
<span class="row-label">Alle</span> <span class="row-label">{$_('calendar.recurrence.builder_every_label')}</span>
<input type="number" class="interval-input" bind:value={interval} min="1" max="99" /> <input type="number" class="interval-input" bind:value={interval} min="1" max="99" />
<select class="freq-select" bind:value={freq}> <select class="freq-select" bind:value={freq}>
<option value="DAILY">Tag(e)</option> <option value="DAILY">{$_('calendar.recurrence.unit_freq_daily')}</option>
<option value="WEEKLY">Woche(n)</option> <option value="WEEKLY">{$_('calendar.recurrence.unit_freq_weekly')}</option>
<option value="MONTHLY">Monat(e)</option> <option value="MONTHLY">{$_('calendar.recurrence.unit_freq_monthly')}</option>
<option value="YEARLY">Jahr(e)</option> <option value="YEARLY">{$_('calendar.recurrence.unit_freq_yearly')}</option>
</select> </select>
</div> </div>
<!-- Weekday picker (only for WEEKLY) --> <!-- Weekday picker (only for WEEKLY) -->
{#if freq === 'WEEKLY'} {#if freq === 'WEEKLY'}
<div class="builder-section"> <div class="builder-section">
<span class="section-label">Wochentage</span> <span class="section-label">{$_('calendar.recurrence.builder_weekdays_label')}</span>
<div class="day-picker"> <div class="day-picker">
{#each DAYS as day} {#each DAYS as day}
<button <button
@ -158,23 +167,23 @@
<!-- End condition --> <!-- End condition -->
<div class="builder-section"> <div class="builder-section">
<span class="section-label">Endet</span> <span class="section-label">{$_('calendar.recurrence.builder_end_label')}</span>
<div class="end-options"> <div class="end-options">
<label class="radio-label"> <label class="radio-label">
<input type="radio" bind:group={endType} value="never" /> <input type="radio" bind:group={endType} value="never" />
<span>Nie</span> <span>{$_('calendar.recurrence.builder_end_never')}</span>
</label> </label>
<label class="radio-label"> <label class="radio-label">
<input type="radio" bind:group={endType} value="count" /> <input type="radio" bind:group={endType} value="count" />
<span>Nach</span> <span>{$_('calendar.recurrence.builder_end_after')}</span>
{#if endType === 'count'} {#if endType === 'count'}
<input type="number" class="count-input" bind:value={count} min="1" max="999" /> <input type="number" class="count-input" bind:value={count} min="1" max="999" />
<span>Terminen</span> <span>{$_('calendar.recurrence.builder_end_after_unit')}</span>
{/if} {/if}
</label> </label>
<label class="radio-label"> <label class="radio-label">
<input type="radio" bind:group={endType} value="until" /> <input type="radio" bind:group={endType} value="until" />
<span>Am</span> <span>{$_('calendar.recurrence.builder_end_until')}</span>
{#if endType === 'until'} {#if endType === 'until'}
<input type="date" class="until-input" bind:value={untilDate} /> <input type="date" class="until-input" bind:value={untilDate} />
{/if} {/if}
@ -187,8 +196,11 @@
<!-- Actions --> <!-- Actions -->
<div class="builder-actions"> <div class="builder-actions">
<button type="button" class="btn btn-secondary" onclick={onCancel}>Abbrechen</button> <button type="button" class="btn btn-secondary" onclick={onCancel}>{$_('common.cancel')}</button
<button type="button" class="btn btn-primary" onclick={handleApply}>Übernehmen</button> >
<button type="button" class="btn btn-primary" onclick={handleApply}
>{$_('calendar.recurrence.builder_apply')}</button
>
</div> </div>
</div> </div>

View file

@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import { getDateFnsLocale } from '$lib/i18n/format'; import { getDateFnsLocale } from '$lib/i18n/format';
import { _ } from 'svelte-i18n';
import { getContext } from 'svelte'; import { getContext } from 'svelte';
import { calendarViewStore } from '../stores/view.svelte'; import { calendarViewStore } from '../stores/view.svelte';
import { getEventsForDay } from '../queries'; import { getEventsForDay } from '../queries';
@ -209,8 +210,12 @@
<div class="month-header"> <div class="month-header">
<span class="month-label"> <span class="month-label">
{#if !isTodayVisible} {#if !isTodayVisible}
<button onclick={goToToday} title="Zum heutigen Tag" class="today-button"> <button
<span class="today-label">Heute</span> onclick={goToToday}
title={$_('calendar.date_strip.today_aria')}
class="today-button"
>
<span class="today-label">{$_('calendar.date_strip.today_label')}</span>
<span class="today-date" <span class="today-date"
>{format(new Date(), 'd. MMM', { locale: getDateFnsLocale() })}</span >{format(new Date(), 'd. MMM', { locale: getDateFnsLocale() })}</span
> >

View file

@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { _ } from 'svelte-i18n';
import type { CalendarEvent } from '../types'; import type { CalendarEvent } from '../types';
import { eventsStore } from '../stores/events.svelte'; import { eventsStore } from '../stores/events.svelte';
import { import {
@ -96,7 +97,7 @@
style:background-color={color} style:background-color={color}
role="button" role="button"
tabindex="0" tabindex="0"
aria-label={event.title || 'Neuer Termin'} aria-label={event.title || $_('calendar.event_card.new_event_label')}
onpointerdown={handlePointerDown} onpointerdown={handlePointerDown}
onclick={handleClick} onclick={handleClick}
onkeydown={handleKeydown} onkeydown={handleKeydown}
@ -107,7 +108,7 @@
class="resize-handle top" class="resize-handle top"
onpointerdown={handleResizeTop} onpointerdown={handleResizeTop}
role="slider" role="slider"
aria-label="Startzeit ändern" aria-label={$_('calendar.event.changeStartTime')}
aria-valuenow={0} aria-valuenow={0}
tabindex="-1" tabindex="-1"
></div> ></div>
@ -130,13 +131,19 @@
<span class="event-time">{formattedTime}</span> <span class="event-time">{formattedTime}</span>
{#if event.parentBlockId} {#if event.parentBlockId}
<span class="repeat-icon" title="Wiederkehrend"><ArrowsClockwise size={9} /></span> <span class="repeat-icon" title={$_('calendar.event_card.recurring_title')}
><ArrowsClockwise size={9} /></span
>
{/if} {/if}
{#if event.linkedBlockId} {#if event.linkedBlockId}
<span class="linked-badge" title="Durchgeführt"><CheckCircle size={10} weight="fill" /></span> <span class="linked-badge" title={$_('calendar.event_card.linked_title')}
><CheckCircle size={10} weight="fill" /></span
>
{/if} {/if}
</div> </div>
<span class="event-title">{event.title || (isDraft ? 'Neuer Termin' : '')}</span> <span class="event-title"
>{event.title || (isDraft ? $_('calendar.event_card.new_event_label') : '')}</span
>
{#if event.location} {#if event.location}
<span class="event-location">{event.location}</span> <span class="event-location">{event.location}</span>
{/if} {/if}
@ -146,7 +153,7 @@
class="resize-handle bottom" class="resize-handle bottom"
onpointerdown={handleResizeBottom} onpointerdown={handleResizeBottom}
role="slider" role="slider"
aria-label="Endzeit ändern" aria-label={$_('calendar.event.changeEndTime')}
aria-valuenow={0} aria-valuenow={0}
tabindex="-1" tabindex="-1"
></div> ></div>

View file

@ -79,7 +79,7 @@
// Format time display // Format time display
function formatEventTime(ev: CalendarEvent): string { function formatEventTime(ev: CalendarEvent): string {
if (ev.isAllDay) return 'Ganztägig'; if (ev.isAllDay) return $_('calendar.event.allDay');
const start = toDate(ev.startTime); const start = toDate(ev.startTime);
const end = toDate(ev.endTime); const end = toDate(ev.endTime);
const dateStr = format(start, 'EEEE, d. MMMM yyyy', { locale: getDateFnsLocale() }); const dateStr = format(start, 'EEEE, d. MMMM yyyy', { locale: getDateFnsLocale() });
@ -102,32 +102,36 @@
function formatRecurrence(rule: string): string { function formatRecurrence(rule: string): string {
if (!rule) return ''; if (!rule) return '';
if (rule.includes('FREQ=DAILY')) return 'Täglich'; if (rule.includes('FREQ=DAILY')) return $_('calendar.recurrence.daily');
if (rule.includes('FREQ=WEEKLY')) { if (rule.includes('FREQ=WEEKLY')) {
if (rule.includes('INTERVAL=2')) return 'Alle 2 Wochen'; if (rule.includes('INTERVAL=2')) return $_('calendar.recurrence.every_2_weeks');
if (rule.includes('BYDAY=')) { if (rule.includes('BYDAY=')) {
const days = rule.match(/BYDAY=([A-Z,]+)/)?.[1]; const days = rule.match(/BYDAY=([A-Z,]+)/)?.[1];
if (days) { if (days) {
const dayMap: Record<string, string> = { const dayMap: Record<string, string> = {
MO: 'Mo', MO: $_('calendar.weekday_short.mon'),
TU: 'Di', TU: $_('calendar.weekday_short.tue'),
WE: 'Mi', WE: $_('calendar.weekday_short.wed'),
TH: 'Do', TH: $_('calendar.weekday_short.thu'),
FR: 'Fr', FR: $_('calendar.weekday_short.fri'),
SA: 'Sa', SA: $_('calendar.weekday_short.sat'),
SU: 'So', SU: $_('calendar.weekday_short.sun'),
}; };
return `Wöchentlich (${days return $_('calendar.recurrence.weekly_with_days', {
.split(',') values: {
.map((d) => dayMap[d] || d) days: days
.join(', ')})`; .split(',')
.map((d) => dayMap[d] || d)
.join(', '),
},
});
} }
} }
return 'Wöchentlich'; return $_('calendar.recurrence.weekly');
} }
if (rule.includes('FREQ=MONTHLY')) return 'Monatlich'; if (rule.includes('FREQ=MONTHLY')) return $_('calendar.recurrence.monthly');
if (rule.includes('FREQ=YEARLY')) return 'Jährlich'; if (rule.includes('FREQ=YEARLY')) return $_('calendar.recurrence.yearly');
return 'Wiederkehrend'; return $_('calendar.recurrence.recurring_fallback');
} }
async function handleSave(data: Parameters<typeof eventsStore.updateEvent>[1]) { async function handleSave(data: Parameters<typeof eventsStore.updateEvent>[1]) {
@ -170,7 +174,7 @@
if (isRecurring || hasParent) { if (isRecurring || hasParent) {
showDeleteOptions = true; showDeleteOptions = true;
} else { } else {
if (confirm('Diesen Termin löschen?')) { if (confirm($_('calendar.event_modal.confirm_delete_single'))) {
eventsStore.deleteEvent(event.id); eventsStore.deleteEvent(event.id);
onClose(); onClose();
} }
@ -181,8 +185,12 @@
const start = toDate(event.startTime); const start = toDate(event.startTime);
const text = [ const text = [
event.title, event.title,
event.isAllDay ? 'Ganztägig' : `${format(start, 'dd.MM.yyyy HH:mm')}`, event.isAllDay ? $_('calendar.event.allDay') : `${format(start, 'dd.MM.yyyy HH:mm')}`,
event.location ? `Ort: ${event.location}` : '', event.location
? $_('calendar.event_modal.clipboard_location_prefix', {
values: { location: event.location },
})
: '',
event.description || '', event.description || '',
] ]
.filter(Boolean) .filter(Boolean)
@ -222,7 +230,7 @@
<div class="modal-header"> <div class="modal-header">
<div class="header-left"> <div class="header-left">
<h2 id="modal-title" class="modal-title"> <h2 id="modal-title" class="modal-title">
{isEditing ? 'Termin bearbeiten' : event.title} {isEditing ? $_('calendar.event_modal.editing_title') : event.title}
</h2> </h2>
{#if !isEditing && calendarName} {#if !isEditing && calendarName}
<span class="calendar-badge"> <span class="calendar-badge">
@ -233,7 +241,11 @@
</div> </div>
<div class="modal-actions"> <div class="modal-actions">
{#if !isEditing} {#if !isEditing}
<button class="btn btn-ghost" onclick={copyToClipboard} title="Kopieren"> <button
class="btn btn-ghost"
onclick={copyToClipboard}
title={$_('calendar.event_modal.copy_title')}
>
{#if copied}<Check size={16} />{:else}<Copy size={16} />{/if} {#if copied}<Check size={16} />{:else}<Copy size={16} />{/if}
</button> </button>
<button class="btn btn-ghost" onclick={handleEditClick} title={$_('common.edit')}> <button class="btn btn-ghost" onclick={handleEditClick} title={$_('common.edit')}>
@ -266,7 +278,7 @@
<div class="event-details"> <div class="event-details">
<!-- Visibility --> <!-- Visibility -->
<div class="detail-row"> <div class="detail-row">
<span class="detail-label">Sichtbarkeit</span> <span class="detail-label">{$_('calendar.event_modal.label_visibility')}</span>
<div class="detail-content"> <div class="detail-content">
<VisibilityPicker level={event.visibility} onChange={handleVisibilityChange} /> <VisibilityPicker level={event.visibility} onChange={handleVisibilityChange} />
</div> </div>
@ -275,7 +287,7 @@
<!-- Share link (only when visibility = unlisted) --> <!-- Share link (only when visibility = unlisted) -->
{#if event.visibility === 'unlisted' && event.unlistedToken && shareUrl} {#if event.visibility === 'unlisted' && event.unlistedToken && shareUrl}
<div class="detail-row"> <div class="detail-row">
<span class="detail-label">Link</span> <span class="detail-label">{$_('calendar.event_modal.label_share_link')}</span>
<div class="detail-content"> <div class="detail-content">
<SharedLinkControls <SharedLinkControls
token={event.unlistedToken} token={event.unlistedToken}
@ -347,14 +359,22 @@
<!-- Metadata --> <!-- Metadata -->
<div class="detail-meta-row"> <div class="detail-meta-row">
<span <span
>Erstellt: {format(new Date(event.createdAt), 'dd. MMM yyyy', { >{$_('calendar.event_modal.created_at', {
locale: getDateFnsLocale(), values: {
date: format(new Date(event.createdAt), 'dd. MMM yyyy', {
locale: getDateFnsLocale(),
}),
},
})}</span })}</span
> >
{#if event.updatedAt !== event.createdAt} {#if event.updatedAt !== event.createdAt}
<span <span
>· Bearbeitet: {format(new Date(event.updatedAt), 'dd. MMM yyyy', { >{$_('calendar.event_modal.updated_at', {
locale: getDateFnsLocale(), values: {
date: format(new Date(event.updatedAt), 'dd. MMM yyyy', {
locale: getDateFnsLocale(),
}),
},
})}</span })}</span
> >
{/if} {/if}
@ -377,14 +397,14 @@
<!-- svelte-ignore a11y_interactive_supports_focus --> <!-- svelte-ignore a11y_interactive_supports_focus -->
<!-- svelte-ignore a11y_click_events_have_key_events --> <!-- svelte-ignore a11y_click_events_have_key_events -->
<div class="delete-dialog" role="dialog" aria-modal="true" onclick={(e) => e.stopPropagation()}> <div class="delete-dialog" role="dialog" aria-modal="true" onclick={(e) => e.stopPropagation()}>
<h3 class="delete-title">Wiederkehrenden Termin bearbeiten</h3> <h3 class="delete-title">{$_('calendar.event_modal.recur_edit_title')}</h3>
<p class="delete-text">Möchtest du nur diesen Termin oder alle zukünftigen bearbeiten?</p> <p class="delete-text">{$_('calendar.event_modal.recur_edit_text')}</p>
<div class="delete-actions"> <div class="delete-actions">
<button class="btn btn-outline" onclick={() => startEdit('single')}> <button class="btn btn-outline" onclick={() => startEdit('single')}>
Nur diesen Termin {$_('calendar.event_modal.recur_edit_only_this')}
</button> </button>
<button class="btn btn-primary-full" onclick={() => startEdit('all')}> <button class="btn btn-primary-full" onclick={() => startEdit('all')}>
Alle zukünftigen Termine {$_('calendar.event_modal.recur_edit_all_future')}
</button> </button>
<button class="btn btn-ghost" onclick={() => (showEditOptions = false)}> <button class="btn btn-ghost" onclick={() => (showEditOptions = false)}>
{$_('common.cancel')} {$_('common.cancel')}
@ -405,14 +425,14 @@
> >
<!-- svelte-ignore a11y_click_events_have_key_events --> <!-- svelte-ignore a11y_click_events_have_key_events -->
<div class="delete-dialog" role="dialog" aria-modal="true" onclick={(e) => e.stopPropagation()}> <div class="delete-dialog" role="dialog" aria-modal="true" onclick={(e) => e.stopPropagation()}>
<h3 class="delete-title">Wiederkehrenden Termin löschen</h3> <h3 class="delete-title">{$_('calendar.event_modal.recur_delete_title')}</h3>
<p class="delete-text">Möchtest du nur diesen Termin oder die gesamte Serie löschen?</p> <p class="delete-text">{$_('calendar.event_modal.recur_delete_text')}</p>
<div class="delete-actions"> <div class="delete-actions">
<button class="btn btn-outline" onclick={() => handleDelete('this')}> <button class="btn btn-outline" onclick={() => handleDelete('this')}>
Nur diesen Termin {$_('calendar.event_modal.recur_delete_only_this')}
</button> </button>
<button class="btn btn-destructive" onclick={() => handleDelete('all')}> <button class="btn btn-destructive" onclick={() => handleDelete('all')}>
Alle Termine der Serie {$_('calendar.event_modal.recur_delete_all')}
</button> </button>
<button class="btn btn-ghost" onclick={() => (showDeleteOptions = false)}> <button class="btn btn-ghost" onclick={() => (showDeleteOptions = false)}>
{$_('common.cancel')} {$_('common.cancel')}

View file

@ -112,14 +112,14 @@
// Recurrence options // Recurrence options
const CUSTOM_VALUE = '__custom__'; const CUSTOM_VALUE = '__custom__';
const recurrenceOptions = [ const recurrenceOptions = $derived([
{ value: '', label: 'Keine Wiederholung' }, { value: '', label: $_('calendar.recurrence.none') },
{ value: 'FREQ=DAILY', label: 'Täglich' }, { value: 'FREQ=DAILY', label: $_('calendar.recurrence.daily') },
{ value: 'FREQ=WEEKLY', label: 'Wöchentlich' }, { value: 'FREQ=WEEKLY', label: $_('calendar.recurrence.weekly') },
{ value: 'FREQ=MONTHLY', label: 'Monatlich' }, { value: 'FREQ=MONTHLY', label: $_('calendar.recurrence.monthly') },
{ value: 'FREQ=YEARLY', label: 'Jährlich' }, { value: 'FREQ=YEARLY', label: $_('calendar.recurrence.yearly') },
{ value: CUSTOM_VALUE, label: 'Benutzerdefiniert...' }, { value: CUSTOM_VALUE, label: $_('calendar.recurrence.custom') },
]; ]);
let showCustomBuilder = $state(false); let showCustomBuilder = $state(false);
@ -156,30 +156,32 @@
.map((p) => p.split('=')) .map((p) => p.split('='))
); );
const freqMap: Record<string, string> = { const freqMap: Record<string, string> = {
DAILY: 'Täglich', DAILY: $_('calendar.recurrence.daily'),
WEEKLY: 'Wöchentlich', WEEKLY: $_('calendar.recurrence.weekly'),
MONTHLY: 'Monatlich', MONTHLY: $_('calendar.recurrence.monthly'),
YEARLY: 'Jährlich', YEARLY: $_('calendar.recurrence.yearly'),
}; };
let text = freqMap[parts.FREQ] ?? 'Wiederkehrend'; let text = freqMap[parts.FREQ] ?? $_('calendar.recurrence.recurring_fallback');
if (parts.INTERVAL && parseInt(parts.INTERVAL) > 1) { if (parts.INTERVAL && parseInt(parts.INTERVAL) > 1) {
const unitMap: Record<string, string> = { const unitMap: Record<string, string> = {
DAILY: 'Tage', DAILY: $_('calendar.recurrence.unit_days'),
WEEKLY: 'Wochen', WEEKLY: $_('calendar.recurrence.unit_weeks'),
MONTHLY: 'Monate', MONTHLY: $_('calendar.recurrence.unit_months'),
YEARLY: 'Jahre', YEARLY: $_('calendar.recurrence.unit_years'),
}; };
text = `Alle ${parts.INTERVAL} ${unitMap[parts.FREQ] ?? ''}`; text = $_('calendar.recurrence.every_n_unit', {
values: { n: parts.INTERVAL, unit: unitMap[parts.FREQ] ?? '' },
});
} }
if (parts.BYDAY) { if (parts.BYDAY) {
const dayMap: Record<string, string> = { const dayMap: Record<string, string> = {
MO: 'Mo', MO: $_('calendar.weekday_short.mon'),
TU: 'Di', TU: $_('calendar.weekday_short.tue'),
WE: 'Mi', WE: $_('calendar.weekday_short.wed'),
TH: 'Do', TH: $_('calendar.weekday_short.thu'),
FR: 'Fr', FR: $_('calendar.weekday_short.fri'),
SA: 'Sa', SA: $_('calendar.weekday_short.sat'),
SU: 'So', SU: $_('calendar.weekday_short.sun'),
}; };
text += ` (${parts.BYDAY.split(',') text += ` (${parts.BYDAY.split(',')
.map((d: string) => dayMap[d] || d) .map((d: string) => dayMap[d] || d)
@ -192,23 +194,25 @@
<form <form
onsubmit={handleSubmit} onsubmit={handleSubmit}
class="event-form" class="event-form"
aria-label={mode === 'create' ? 'Termin erstellen' : 'Termin bearbeiten'} aria-label={mode === 'create'
? $_('calendar.event_form.aria_create')
: $_('calendar.event_form.aria_edit')}
> >
<div class="field"> <div class="field">
<label for="title" class="label">Titel *</label> <label for="title" class="label">{$_('calendar.event_form.label_title_required')}</label>
<input <input
type="text" type="text"
id="title" id="title"
class="input" class="input"
bind:value={title} bind:value={title}
placeholder="Terminname eingeben" placeholder={$_('calendar.event_form.placeholder_title')}
required required
/> />
</div> </div>
{#if mode === 'create' && calendarOptions.length > 1} {#if mode === 'create' && calendarOptions.length > 1}
<div class="field"> <div class="field">
<label for="calendar" class="label">Kalender</label> <label for="calendar" class="label">{$_('calendar.event.calendar')}</label>
<select id="calendar" class="input" bind:value={calendarId}> <select id="calendar" class="input" bind:value={calendarId}>
{#each calendarOptions as cal} {#each calendarOptions as cal}
<option value={cal.id}>{cal.name}</option> <option value={cal.id}>{cal.name}</option>
@ -220,18 +224,18 @@
<div class="field"> <div class="field">
<label class="checkbox-label"> <label class="checkbox-label">
<input type="checkbox" bind:checked={isAllDay} class="checkbox" /> <input type="checkbox" bind:checked={isAllDay} class="checkbox" />
<span>Ganztägig</span> <span>{$_('calendar.event.allDay')}</span>
</label> </label>
</div> </div>
<div class="field-row"> <div class="field-row">
<div class="field flex-1"> <div class="field flex-1">
<label for="startDate" class="label">Beginn</label> <label for="startDate" class="label">{$_('calendar.event.start')}</label>
<input type="date" id="startDate" class="input" bind:value={startDate} required /> <input type="date" id="startDate" class="input" bind:value={startDate} required />
</div> </div>
{#if !isAllDay} {#if !isAllDay}
<div class="field flex-1"> <div class="field flex-1">
<label for="startTime" class="label">Uhrzeit</label> <label for="startTime" class="label">{$_('calendar.event_form.label_time')}</label>
<input type="time" id="startTime" class="input" bind:value={startTime} required /> <input type="time" id="startTime" class="input" bind:value={startTime} required />
</div> </div>
{/if} {/if}
@ -239,12 +243,12 @@
<div class="field-row"> <div class="field-row">
<div class="field flex-1"> <div class="field flex-1">
<label for="endDate" class="label">Ende</label> <label for="endDate" class="label">{$_('calendar.event.end')}</label>
<input type="date" id="endDate" class="input" bind:value={endDate} required /> <input type="date" id="endDate" class="input" bind:value={endDate} required />
</div> </div>
{#if !isAllDay} {#if !isAllDay}
<div class="field flex-1"> <div class="field flex-1">
<label for="endTime" class="label">Uhrzeit</label> <label for="endTime" class="label">{$_('calendar.event_form.label_time')}</label>
<input type="time" id="endTime" class="input" bind:value={endTime} required /> <input type="time" id="endTime" class="input" bind:value={endTime} required />
</div> </div>
{/if} {/if}
@ -257,7 +261,7 @@
{/if} {/if}
<div class="field"> <div class="field">
<label for="recurrence" class="label">Wiederholung</label> <label for="recurrence" class="label">{$_('calendar.event_form.label_recurrence')}</label>
<select id="recurrence" class="input" value={selectValue} onchange={handleRecurrenceChange}> <select id="recurrence" class="input" value={selectValue} onchange={handleRecurrenceChange}>
{#each recurrenceOptions as opt} {#each recurrenceOptions as opt}
<option value={opt.value}>{opt.label}</option> <option value={opt.value}>{opt.label}</option>
@ -265,7 +269,9 @@
</select> </select>
{#if isCustomRule && !showCustomBuilder} {#if isCustomRule && !showCustomBuilder}
<button type="button" class="custom-rule-preview" onclick={() => (showCustomBuilder = true)}> <button type="button" class="custom-rule-preview" onclick={() => (showCustomBuilder = true)}>
{formatCustomPreview(recurrenceRule)} — Bearbeiten {$_('calendar.event_form.recur_edit_suffix', {
values: { preview: formatCustomPreview(recurrenceRule) },
})}
</button> </button>
{/if} {/if}
</div> </div>
@ -279,29 +285,29 @@
{/if} {/if}
<div class="field"> <div class="field">
<label for="location" class="label">Ort</label> <label for="location" class="label">{$_('calendar.event.location')}</label>
<input <input
type="text" type="text"
id="location" id="location"
class="input" class="input"
bind:value={location} bind:value={location}
placeholder="Ort eingeben..." placeholder={$_('calendar.event_form.placeholder_location')}
/> />
</div> </div>
<div class="field"> <div class="field">
<label for="description" class="label">Beschreibung</label> <label for="description" class="label">{$_('calendar.event.description')}</label>
<textarea <textarea
id="description" id="description"
class="input textarea" class="input textarea"
rows="3" rows="3"
bind:value={description} bind:value={description}
placeholder="Beschreibung hinzufügen" placeholder={$_('calendar.event_form.placeholder_description')}
></textarea> ></textarea>
</div> </div>
<div class="field"> <div class="field">
<span class="label">Tags</span> <span class="label">{$_('calendar.event_form.label_tags')}</span>
<TagField <TagField
tags={allTags.value} tags={allTags.value}
selectedIds={selectedTagIds} selectedIds={selectedTagIds}

View file

@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import { getDateFnsLocale } from '$lib/i18n/format'; import { getDateFnsLocale } from '$lib/i18n/format';
import { _ } from 'svelte-i18n';
import { import {
format, format,
startOfMonth, startOfMonth,
@ -32,7 +33,15 @@
return eachDayOfInterval({ start: calendarStart, end: calendarEnd }); return eachDayOfInterval({ start: calendarStart, end: calendarEnd });
}); });
const weekDays = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So']; const weekDays = $derived([
$_('calendar.weekday_short.mon'),
$_('calendar.weekday_short.tue'),
$_('calendar.weekday_short.wed'),
$_('calendar.weekday_short.thu'),
$_('calendar.weekday_short.fri'),
$_('calendar.weekday_short.sat'),
$_('calendar.weekday_short.sun'),
]);
</script> </script>
<div class="mini-calendar"> <div class="mini-calendar">
@ -40,7 +49,7 @@
<button <button
class="nav-btn" class="nav-btn"
onclick={() => (currentMonth = subMonths(currentMonth, 1))} onclick={() => (currentMonth = subMonths(currentMonth, 1))}
aria-label="Vorheriger Monat" aria-label={$_('calendar.mini_cal.prev_aria')}
> >
<CaretLeft size={16} /> <CaretLeft size={16} />
</button> </button>
@ -50,7 +59,7 @@
<button <button
class="nav-btn" class="nav-btn"
onclick={() => (currentMonth = addMonths(currentMonth, 1))} onclick={() => (currentMonth = addMonths(currentMonth, 1))}
aria-label="Nächster Monat" aria-label={$_('calendar.mini_cal.next_aria')}
> >
<CaretRight size={16} /> <CaretRight size={16} />
</button> </button>

View file

@ -72,14 +72,14 @@
const calendarColor = $derived(getCalendarColor(calendarsCtx.value, calendarId || '')); const calendarColor = $derived(getCalendarColor(calendarsCtx.value, calendarId || ''));
const RECURRENCE_OPTIONS = [ const RECURRENCE_OPTIONS = $derived([
{ value: '', label: 'Keine Wiederholung' }, { value: '', label: $_('calendar.recurrence.none') },
{ value: 'FREQ=DAILY', label: 'Täglich' }, { value: 'FREQ=DAILY', label: $_('calendar.recurrence.daily') },
{ value: 'FREQ=WEEKLY', label: 'Wöchentlich' }, { value: 'FREQ=WEEKLY', label: $_('calendar.recurrence.weekly') },
{ value: 'FREQ=WEEKLY;INTERVAL=2', label: 'Alle 2 Wochen' }, { value: 'FREQ=WEEKLY;INTERVAL=2', label: $_('calendar.recurrence.every_2_weeks') },
{ value: 'FREQ=MONTHLY', label: 'Monatlich' }, { value: 'FREQ=MONTHLY', label: $_('calendar.recurrence.monthly') },
{ value: 'FREQ=YEARLY', label: 'Jährlich' }, { value: 'FREQ=YEARLY', label: $_('calendar.recurrence.yearly') },
]; ]);
function handleSubmit(e: Event) { function handleSubmit(e: Event) {
e.preventDefault(); e.preventDefault();
@ -143,7 +143,7 @@
class="popover" class="popover"
style="top: {popoverPos.top}px; left: {popoverPos.left}px;" style="top: {popoverPos.top}px; left: {popoverPos.left}px;"
role="dialog" role="dialog"
aria-label="Termin erstellen" aria-label={$_('calendar.quick_event.aria_dialog')}
> >
<!-- Color accent bar --> <!-- Color accent bar -->
<div class="accent-bar" style="background-color: {calendarColor};"></div> <div class="accent-bar" style="background-color: {calendarColor};"></div>
@ -151,7 +151,7 @@
<form onsubmit={handleSubmit}> <form onsubmit={handleSubmit}>
<!-- Header --> <!-- Header -->
<div class="popover-header"> <div class="popover-header">
<span class="header-title">Neuer Termin</span> <span class="header-title">{$_('calendar.quick_event.header_new')}</span>
<button type="button" class="close-btn" onclick={onClose} aria-label={$_('common.close')}> <button type="button" class="close-btn" onclick={onClose} aria-label={$_('common.close')}>
<X size={16} /> <X size={16} />
</button> </button>
@ -164,7 +164,7 @@
bind:this={titleInput} bind:this={titleInput}
bind:value={title} bind:value={title}
type="text" type="text"
placeholder="Titel hinzufügen" placeholder={$_('calendar.quick_event.placeholder_title')}
class="title-input" class="title-input"
required required
/> />
@ -177,7 +177,8 @@
class:active={blockType === 'event'} class:active={blockType === 'event'}
onclick={() => (blockType = 'event')} onclick={() => (blockType = 'event')}
> >
<CalendarBlank size={12} /> Termin <CalendarBlank size={12} />
{$_('calendar.quick_event.type_event')}
</button> </button>
<button <button
type="button" type="button"
@ -185,7 +186,8 @@
class:active={blockType === 'timeEntry'} class:active={blockType === 'timeEntry'}
onclick={() => (blockType = 'timeEntry')} onclick={() => (blockType = 'timeEntry')}
> >
<Timer size={12} /> Zeiterfassung <Timer size={12} />
{$_('calendar.quick_event.type_time_entry')}
</button> </button>
<button <button
type="button" type="button"
@ -193,7 +195,8 @@
class:active={blockType === 'habit'} class:active={blockType === 'habit'}
onclick={() => (blockType = 'habit')} onclick={() => (blockType = 'habit')}
> >
<Heart size={12} /> Habit <Heart size={12} />
{$_('calendar.quick_event.type_habit')}
</button> </button>
</div> </div>
@ -217,7 +220,7 @@
<!-- All-day toggle --> <!-- All-day toggle -->
<label class="form-row clickable"> <label class="form-row clickable">
<CalendarBlank size={16} class="row-icon-el" /> <CalendarBlank size={16} class="row-icon-el" />
<span class="row-label">Ganztägig</span> <span class="row-label">{$_('calendar.event.allDay')}</span>
<input type="checkbox" bind:checked={isAllDay} class="toggle-cb" /> <input type="checkbox" bind:checked={isAllDay} class="toggle-cb" />
</label> </label>
@ -226,14 +229,14 @@
<Clock size={16} class="row-icon-el" /> <Clock size={16} class="row-icon-el" />
<div class="datetime-fields"> <div class="datetime-fields">
<div class="dt-group"> <div class="dt-group">
<span class="dt-label">Beginn</span> <span class="dt-label">{$_('calendar.event.start')}</span>
<input type="date" bind:value={startDateStr} class="dt-input" /> <input type="date" bind:value={startDateStr} class="dt-input" />
{#if !isAllDay} {#if !isAllDay}
<input type="time" bind:value={startTimeStr} class="dt-input time" /> <input type="time" bind:value={startTimeStr} class="dt-input time" />
{/if} {/if}
</div> </div>
<div class="dt-group"> <div class="dt-group">
<span class="dt-label">Ende</span> <span class="dt-label">{$_('calendar.event.end')}</span>
<input type="date" bind:value={endDateStr} class="dt-input" /> <input type="date" bind:value={endDateStr} class="dt-input" />
{#if !isAllDay} {#if !isAllDay}
<input type="time" bind:value={endTimeStr} class="dt-input time" /> <input type="time" bind:value={endTimeStr} class="dt-input time" />
@ -269,7 +272,12 @@
<!-- Location --> <!-- Location -->
<div class="form-row"> <div class="form-row">
<MapPin size={16} class="row-icon-el" /> <MapPin size={16} class="row-icon-el" />
<input bind:value={location} type="text" placeholder="Ort hinzufügen" class="field-input" /> <input
bind:value={location}
type="text"
placeholder={$_('calendar.quick_event.placeholder_location')}
class="field-input"
/>
</div> </div>
<!-- Description --> <!-- Description -->
@ -277,7 +285,7 @@
<TextAlignLeft size={16} class="row-icon-el" /> <TextAlignLeft size={16} class="row-icon-el" />
<textarea <textarea
bind:value={description} bind:value={description}
placeholder="Beschreibung" placeholder={$_('calendar.quick_event.placeholder_description')}
rows="2" rows="2"
class="field-input field-textarea" class="field-input field-textarea"
></textarea> ></textarea>

View file

@ -4,6 +4,7 @@
--> -->
<script lang="ts"> <script lang="ts">
import { getDateFnsLocale } from '$lib/i18n/format'; import { getDateFnsLocale } from '$lib/i18n/format';
import { _ } from 'svelte-i18n';
import { db } from '$lib/data/database'; import { db } from '$lib/data/database';
import type { LocalTimeBlock } from '$lib/data/time-blocks/types'; import type { LocalTimeBlock } from '$lib/data/time-blocks/types';
import { toTimeBlock, findFreeSlots } from '$lib/data/time-blocks/queries'; import { toTimeBlock, findFreeSlots } from '$lib/data/time-blocks/queries';
@ -49,19 +50,19 @@
} }
function formatDayLabel(date: Date): string { function formatDayLabel(date: Date): string {
if (isToday(date)) return 'Heute'; if (isToday(date)) return $_('calendar.calendar.today');
if (isTomorrow(date)) return 'Morgen'; if (isTomorrow(date)) return $_('calendar.calendar.tomorrow');
return format(date, 'EEE, d. MMM', { locale: getDateFnsLocale() }); return format(date, 'EEE, d. MMM', { locale: getDateFnsLocale() });
} }
</script> </script>
{#if loading} {#if loading}
<div class="slot-loading">Suche freie Zeiten...</div> <div class="slot-loading">{$_('calendar.slots.loading')}</div>
{:else if slots.length === 0} {:else if slots.length === 0}
<div class="slot-empty">Keine freien Slots gefunden</div> <div class="slot-empty">{$_('calendar.slots.empty')}</div>
{:else} {:else}
<div class="slot-list"> <div class="slot-list">
<span class="slot-label">Freie Zeiten</span> <span class="slot-label">{$_('calendar.slots.label')}</span>
{#each slots as slot} {#each slots as slot}
<button class="slot-btn" onclick={() => onSelect(slot.start, slot.end)}> <button class="slot-btn" onclick={() => onSelect(slot.start, slot.end)}>
<CalendarBlank size={12} /> <CalendarBlank size={12} />
@ -69,7 +70,9 @@
<span class="slot-time"> <span class="slot-time">
{format(slot.start, 'HH:mm')}{format(slot.end, 'HH:mm')} {format(slot.start, 'HH:mm')}{format(slot.end, 'HH:mm')}
</span> </span>
<span class="slot-duration">{slot.durationMinutes}min</span> <span class="slot-duration"
>{$_('calendar.slots.duration_suffix', { values: { n: slot.durationMinutes } })}</span
>
</button> </button>
{/each} {/each}
</div> </div>

View file

@ -3,6 +3,7 @@
All fields are always editable. Changes auto-save on blur. All fields are always editable. Changes auto-save on blur.
--> -->
<script lang="ts"> <script lang="ts">
import { _ } from 'svelte-i18n';
import { formatDate } from '$lib/i18n/format'; import { formatDate } from '$lib/i18n/format';
import { db } from '$lib/data/database'; import { db } from '$lib/data/database';
import { decryptRecord } from '$lib/data/crypto'; import { decryptRecord } from '$lib/data/crypto';
@ -86,7 +87,8 @@
const startTime = editAllDay ? `${editDate}T00:00:00` : `${editDate}T${editStartTime}:00`; const startTime = editAllDay ? `${editDate}T00:00:00` : `${editDate}T${editStartTime}:00`;
const endTime = editAllDay ? `${editDate}T23:59:59` : `${editDate}T${editEndTime}:00`; const endTime = editAllDay ? `${editDate}T23:59:59` : `${editDate}T${editEndTime}:00`;
await eventsStore.updateEvent(eventId, { await eventsStore.updateEvent(eventId, {
title: editTitle.trim() || detail.entity?.title || 'Untitled', title:
editTitle.trim() || detail.entity?.title || $_('calendar.detail_view.untitled_fallback'),
startTime, startTime,
endTime, endTime,
isAllDay: editAllDay, isAllDay: editAllDay,
@ -126,8 +128,8 @@
const id = eventId; const id = eventId;
await eventsStore.deleteEvent(id); await eventsStore.deleteEvent(id);
goBack(); goBack();
toastStore.undo('Termin gelöscht', () => { toastStore.undo($_('calendar.detail_view.toast_deleted'), () => {
db.table('events').update(id, { deletedAt: undefined, updatedAt: new Date().toISOString() }); db.table('events').update(id, { deletedAt: undefined });
}); });
} }
</script> </script>
@ -135,11 +137,11 @@
<DetailViewShell <DetailViewShell
entity={detail.entity} entity={detail.entity}
loading={detail.loading} loading={detail.loading}
notFoundLabel="Termin nicht gefunden" notFoundLabel={$_('calendar.detail_view.not_found')}
confirmDelete={detail.confirmDelete} confirmDelete={detail.confirmDelete}
onAskDelete={detail.askDelete} onAskDelete={detail.askDelete}
onCancelDelete={detail.cancelDelete} onCancelDelete={detail.cancelDelete}
confirmDeleteLabel="Termin wirklich löschen?" confirmDeleteLabel={$_('calendar.detail_view.confirm_delete')}
onConfirmDelete={deleteEvent} onConfirmDelete={deleteEvent}
> >
{#snippet body(event)} {#snippet body(event)}
@ -148,12 +150,12 @@
bind:value={editTitle} bind:value={editTitle}
onfocus={detail.focus} onfocus={detail.focus}
onblur={saveField} onblur={saveField}
placeholder="Titel..." placeholder={$_('calendar.detail_view.placeholder_title')}
/> />
<div class="properties"> <div class="properties">
<div class="prop-row prop-row--labeled"> <div class="prop-row prop-row--labeled">
<span class="prop-label">Sichtbarkeit</span> <span class="prop-label">{$_('calendar.detail_view.label_visibility')}</span>
<VisibilityPicker level={event.visibility ?? 'private'} onChange={handleVisibilityChange} /> <VisibilityPicker level={event.visibility ?? 'private'} onChange={handleVisibilityChange} />
</div> </div>
@ -201,7 +203,7 @@
{/if} {/if}
<label class="allday-label"> <label class="allday-label">
<input type="checkbox" bind:checked={editAllDay} onchange={handleAllDayChange} /> <input type="checkbox" bind:checked={editAllDay} onchange={handleAllDayChange} />
Ganztägig {$_('calendar.detail_view.label_allday')}
</label> </label>
</div> </div>
</div> </div>
@ -213,7 +215,7 @@
bind:value={editLocation} bind:value={editLocation}
onfocus={detail.focus} onfocus={detail.focus}
onblur={saveField} onblur={saveField}
placeholder="Ort hinzufügen..." placeholder={$_('calendar.detail_view.placeholder_location')}
/> />
</div> </div>
@ -227,7 +229,7 @@
{#if eventTags.length > 0} {#if eventTags.length > 0}
<div class="section"> <div class="section">
<span class="section-label">Tags</span> <span class="section-label">{$_('calendar.detail_view.section_tags')}</span>
<div class="tags-list"> <div class="tags-list">
{#each eventTags as tag (tag.id)} {#each eventTags as tag (tag.id)}
<button <button
@ -247,23 +249,31 @@
<LinkedItems recordRef={{ app: 'calendar', collection: 'events', id: eventId }} {navigate} /> <LinkedItems recordRef={{ app: 'calendar', collection: 'events', id: eventId }} {navigate} />
<div class="section"> <div class="section">
<span class="section-label">Beschreibung</span> <span class="section-label">{$_('calendar.detail_view.section_description')}</span>
<textarea <textarea
class="description-input" class="description-input"
bind:value={editDescription} bind:value={editDescription}
onfocus={detail.focus} onfocus={detail.focus}
onblur={saveField} onblur={saveField}
placeholder="Beschreibung hinzufügen..." placeholder={$_('calendar.detail_view.placeholder_description')}
rows={3} rows={3}
></textarea> ></textarea>
</div> </div>
<div class="meta"> <div class="meta">
{#if event.createdAt} {#if event.createdAt}
<span>Erstellt: {formatDate(new Date(event.createdAt))}</span> <span
>{$_('calendar.detail_view.meta_created', {
values: { date: formatDate(new Date(event.createdAt)) },
})}</span
>
{/if} {/if}
{#if event.updatedAt} {#if event.updatedAt}
<span>Bearbeitet: {formatDate(new Date(event.updatedAt))}</span> <span
>{$_('calendar.detail_view.meta_updated', {
values: { date: formatDate(new Date(event.updatedAt)) },
})}</span
>
{/if} {/if}
</div> </div>
{/snippet} {/snippet}

View file

@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { _ } from 'svelte-i18n';
import { getContext, onMount } from 'svelte'; import { getContext, onMount } from 'svelte';
import { dropTarget } from '@mana/shared-ui/dnd'; import { dropTarget } from '@mana/shared-ui/dnd';
import type { DragPayload, TagDragData } from '@mana/shared-ui/dnd'; import type { DragPayload, TagDragData } from '@mana/shared-ui/dnd';
@ -215,7 +216,7 @@
</script> </script>
<svelte:head> <svelte:head>
<title>Kalender - Mana</title> <title>{$_('calendar.detail_route.doc_title')}</title>
</svelte:head> </svelte:head>
<RoutePage appId="calendar"> <RoutePage appId="calendar">
@ -272,7 +273,7 @@
role="presentation" role="presentation"
> >
<div class="modal-container" role="dialog" aria-modal="true"> <div class="modal-container" role="dialog" aria-modal="true">
<h2 class="modal-title">Neuer Termin</h2> <h2 class="modal-title">{$_('calendar.detail_route.create_modal_title')}</h2>
<EventForm <EventForm
mode="create" mode="create"
initialStartTime={createStartTime} initialStartTime={createStartTime}

View file

@ -44,13 +44,13 @@
} }
async function handleDelete(id: string) { async function handleDelete(id: string) {
if (!confirm('Kalender wirklich löschen? Alle zugehörigen Termine gehen verloren.')) return; if (!confirm($_('calendar.calendars_route.confirm_delete'))) return;
await calendarsStore.deleteCalendar(id); await calendarsStore.deleteCalendar(id);
} }
</script> </script>
<svelte:head> <svelte:head>
<title>Kalender verwalten - Mana</title> <title>{$_('calendar.calendars_route.doc_title')}</title>
</svelte:head> </svelte:head>
<RoutePage appId="calendar" backHref="/calendar"> <RoutePage appId="calendar" backHref="/calendar">
@ -60,17 +60,19 @@
class="mb-4 inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground transition-colors" class="mb-4 inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground transition-colors"
> >
<CaretLeft size={16} /> <CaretLeft size={16} />
Zurück zum Kalender {$_('calendar.calendars_route.back_to_calendar')}
</a> </a>
<div class="mb-6 flex items-center justify-between"> <div class="mb-6 flex items-center justify-between">
<h1 class="text-2xl font-bold text-foreground">Meine Kalender</h1> <h1 class="text-2xl font-bold text-foreground">
{$_('calendar.calendars_route.page_title')}
</h1>
<button <button
onclick={() => (showCreateForm = !showCreateForm)} onclick={() => (showCreateForm = !showCreateForm)}
class="flex items-center gap-1.5 rounded-lg bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors" class="flex items-center gap-1.5 rounded-lg bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors"
> >
<Plus size={16} /> <Plus size={16} />
Neuer Kalender {$_('calendar.calendars_route.new_calendar')}
</button> </button>
</div> </div>
@ -85,25 +87,28 @@
class="space-y-3" class="space-y-3"
> >
<div> <div>
<label for="cal-name" class="mb-1 block text-sm font-medium text-foreground">Name</label <label for="cal-name" class="mb-1 block text-sm font-medium text-foreground"
>{$_('calendar.calendars_route.label_name')}</label
> >
<input <input
id="cal-name" id="cal-name"
type="text" type="text"
bind:value={newName} bind:value={newName}
placeholder="z.B. Arbeit, Sport, Familie..." placeholder={$_('calendar.calendars_route.placeholder_name')}
required required
class="w-full rounded-lg border border-border bg-background px-3 py-2 text-foreground placeholder:text-muted-foreground focus:border-primary focus:ring-1 focus:ring-primary" class="w-full rounded-lg border border-border bg-background px-3 py-2 text-foreground placeholder:text-muted-foreground focus:border-primary focus:ring-1 focus:ring-primary"
/> />
</div> </div>
<div> <div>
<span class="mb-2 block text-sm font-medium text-foreground">Farbe</span> <span class="mb-2 block text-sm font-medium text-foreground"
>{$_('calendar.calendars_route.label_color')}</span
>
<div class="flex gap-2"> <div class="flex gap-2">
{#each PRESET_COLORS as color} {#each PRESET_COLORS as color}
<button <button
type="button" type="button"
aria-label="Farbe wählen" aria-label={$_('calendar.calendars_route.aria_pick_color')}
onclick={() => (newColor = color)} onclick={() => (newColor = color)}
class="h-8 w-8 rounded-full border-2 transition-transform hover:scale-110 {newColor === class="h-8 w-8 rounded-full border-2 transition-transform hover:scale-110 {newColor ===
color color
@ -121,14 +126,14 @@
onclick={() => (showCreateForm = false)} onclick={() => (showCreateForm = false)}
class="flex-1 rounded-lg border border-border px-3 py-2 text-sm font-medium text-foreground hover:bg-muted transition-colors" class="flex-1 rounded-lg border border-border px-3 py-2 text-sm font-medium text-foreground hover:bg-muted transition-colors"
> >
Abbrechen {$_('common.cancel')}
</button> </button>
<button <button
type="submit" type="submit"
disabled={!newName.trim()} disabled={!newName.trim()}
class="flex-1 rounded-lg bg-primary px-3 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors disabled:opacity-50" class="flex-1 rounded-lg bg-primary px-3 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors disabled:opacity-50"
> >
Erstellen {$_('calendar.calendars_route.submit_create')}
</button> </button>
</div> </div>
</form> </form>
@ -138,7 +143,9 @@
<!-- Calendar List --> <!-- Calendar List -->
<div class="space-y-2"> <div class="space-y-2">
{#if calendarsCtx.value.length === 0} {#if calendarsCtx.value.length === 0}
<div class="py-12 text-center text-muted-foreground">Noch keine Kalender vorhanden.</div> <div class="py-12 text-center text-muted-foreground">
{$_('calendar.calendars_route.empty')}
</div>
{:else} {:else}
{#each calendarsCtx.value as cal (cal.id)} {#each calendarsCtx.value as cal (cal.id)}
<div class="flex items-center gap-3 rounded-lg border border-border bg-card p-3"> <div class="flex items-center gap-3 rounded-lg border border-border bg-card p-3">
@ -150,7 +157,9 @@
<div class="font-medium text-foreground"> <div class="font-medium text-foreground">
{cal.name} {cal.name}
{#if cal.isDefault} {#if cal.isDefault}
<span class="ml-1 text-xs text-primary">(Standard)</span> <span class="ml-1 text-xs text-primary"
>{$_('calendar.calendars_route.badge_default')}</span
>
{/if} {/if}
</div> </div>
</div> </div>
@ -158,7 +167,9 @@
<button <button
onclick={() => handleToggleVisibility(cal.id)} onclick={() => handleToggleVisibility(cal.id)}
class="rounded-lg p-1.5 text-muted-foreground hover:text-foreground transition-colors" class="rounded-lg p-1.5 text-muted-foreground hover:text-foreground transition-colors"
title={cal.isVisible ? 'Ausblenden' : 'Einblenden'} title={cal.isVisible
? $_('calendar.calendars_route.aria_hide')
: $_('calendar.calendars_route.aria_show')}
> >
{#if cal.isVisible} {#if cal.isVisible}
<Eye size={16} /> <Eye size={16} />
@ -170,7 +181,7 @@
<button <button
onclick={() => handleSetDefault(cal.id)} onclick={() => handleSetDefault(cal.id)}
class="rounded-lg p-1.5 text-muted-foreground hover:text-amber-500 transition-colors" class="rounded-lg p-1.5 text-muted-foreground hover:text-amber-500 transition-colors"
title="Als Standard setzen" title={$_('calendar.calendars_route.aria_set_default')}
> >
<Star size={16} /> <Star size={16} />
</button> </button>

View file

@ -66,7 +66,11 @@
</script> </script>
<svelte:head> <svelte:head>
<title>{event?.title ?? 'Termin'} - Kalender - Mana</title> <title
>{$_('calendar.detail_route.event_doc_title', {
values: { title: event?.title ?? $_('calendar.detail_route.event_doc_title_fallback') },
})}</title
>
</svelte:head> </svelte:head>
<RoutePage appId="calendar" backHref="/calendar" title="Event"> <RoutePage appId="calendar" backHref="/calendar" title="Event">
@ -77,20 +81,22 @@
class="mb-4 flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground transition-colors" class="mb-4 flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground transition-colors"
> >
<CaretLeft size={16} /> <CaretLeft size={16} />
Zurück zum Kalender {$_('calendar.detail_route.back_to_calendar')}
</button> </button>
{#if !event} {#if !event}
<div class="py-16 text-center"> <div class="py-16 text-center">
<p class="text-lg text-muted-foreground">Termin nicht gefunden</p> <p class="text-lg text-muted-foreground">{$_('calendar.detail_route.not_found')}</p>
<button onclick={() => goto('/calendar')} class="mt-4 text-sm text-primary hover:underline"> <button onclick={() => goto('/calendar')} class="mt-4 text-sm text-primary hover:underline">
Zurück zum Kalender {$_('calendar.detail_route.back_to_calendar')}
</button> </button>
</div> </div>
{:else if isEditing} {:else if isEditing}
<!-- Edit Form --> <!-- Edit Form -->
<div class="rounded-xl border border-border bg-card p-6"> <div class="rounded-xl border border-border bg-card p-6">
<h2 class="mb-4 text-xl font-semibold text-foreground">Termin bearbeiten</h2> <h2 class="mb-4 text-xl font-semibold text-foreground">
{$_('calendar.detail_route.edit_title')}
</h2>
<form <form
onsubmit={(e) => { onsubmit={(e) => {
@ -101,7 +107,7 @@
> >
<div> <div>
<label for="edit-title" class="mb-1 block text-sm font-medium text-foreground" <label for="edit-title" class="mb-1 block text-sm font-medium text-foreground"
>Titel</label >{$_('calendar.detail_route.label_title')}</label
> >
<input <input
id="edit-title" id="edit-title"
@ -114,7 +120,7 @@
<div> <div>
<label for="edit-desc" class="mb-1 block text-sm font-medium text-foreground" <label for="edit-desc" class="mb-1 block text-sm font-medium text-foreground"
>Beschreibung</label >{$_('calendar.detail_route.label_description')}</label
> >
<textarea <textarea
id="edit-desc" id="edit-desc"
@ -126,7 +132,7 @@
<div> <div>
<label for="edit-date" class="mb-1 block text-sm font-medium text-foreground" <label for="edit-date" class="mb-1 block text-sm font-medium text-foreground"
>Datum</label >{$_('calendar.detail_route.label_date')}</label
> >
<input <input
id="edit-date" id="edit-date"
@ -139,14 +145,14 @@
<label class="flex items-center gap-2 text-sm text-foreground"> <label class="flex items-center gap-2 text-sm text-foreground">
<input type="checkbox" bind:checked={editAllDay} class="rounded" /> <input type="checkbox" bind:checked={editAllDay} class="rounded" />
Ganztägig {$_('calendar.event.allDay')}
</label> </label>
{#if !editAllDay} {#if !editAllDay}
<div class="grid grid-cols-2 gap-3"> <div class="grid grid-cols-2 gap-3">
<div> <div>
<label for="edit-start" class="mb-1 block text-sm font-medium text-foreground" <label for="edit-start" class="mb-1 block text-sm font-medium text-foreground"
>Von</label >{$_('calendar.detail_route.label_from')}</label
> >
<input <input
id="edit-start" id="edit-start"
@ -157,7 +163,7 @@
</div> </div>
<div> <div>
<label for="edit-end" class="mb-1 block text-sm font-medium text-foreground" <label for="edit-end" class="mb-1 block text-sm font-medium text-foreground"
>Bis</label >{$_('calendar.detail_route.label_to')}</label
> >
<input <input
id="edit-end" id="edit-end"
@ -171,13 +177,13 @@
<div> <div>
<label for="edit-location" class="mb-1 block text-sm font-medium text-foreground" <label for="edit-location" class="mb-1 block text-sm font-medium text-foreground"
>Ort</label >{$_('calendar.detail_route.label_location')}</label
> >
<input <input
id="edit-location" id="edit-location"
type="text" type="text"
bind:value={editLocation} bind:value={editLocation}
placeholder="Ort eingeben..." placeholder={$_('calendar.detail_route.placeholder_location')}
class="w-full rounded-lg border border-border bg-background px-3 py-2 text-foreground placeholder:text-muted-foreground focus:border-primary focus:ring-1 focus:ring-primary" class="w-full rounded-lg border border-border bg-background px-3 py-2 text-foreground placeholder:text-muted-foreground focus:border-primary focus:ring-1 focus:ring-primary"
/> />
</div> </div>
@ -188,13 +194,13 @@
onclick={() => (isEditing = false)} onclick={() => (isEditing = false)}
class="flex-1 rounded-lg border border-border px-4 py-2 text-sm font-medium text-foreground hover:bg-muted transition-colors" class="flex-1 rounded-lg border border-border px-4 py-2 text-sm font-medium text-foreground hover:bg-muted transition-colors"
> >
Abbrechen {$_('calendar.detail_route.submit_cancel')}
</button> </button>
<button <button
type="submit" type="submit"
class="flex-1 rounded-lg bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors" class="flex-1 rounded-lg bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors"
> >
Speichern {$_('calendar.detail_route.submit_save')}
</button> </button>
</div> </div>
</form> </form>
@ -245,13 +251,14 @@
})} })}
</div> </div>
{#if event.isAllDay} {#if event.isAllDay}
<div class="text-sm text-muted-foreground">Ganztägig</div> <div class="text-sm text-muted-foreground">{$_('calendar.event.allDay')}</div>
{:else} {:else}
<div class="text-sm text-muted-foreground"> <div class="text-sm text-muted-foreground">
{format(new Date(event.startTime), 'HH:mm')} {format( {format(new Date(event.startTime), 'HH:mm')} {format(
new Date(event.endTime), new Date(event.endTime),
'HH:mm' 'HH:mm'
)} Uhr )}
{$_('calendar.detail_route.time_suffix_uhr')}
</div> </div>
{/if} {/if}
</div> </div>

View file

@ -74,19 +74,6 @@
"apps/mana/apps/web/src/lib/modules/broadcast/views/DetailView.svelte": 9, "apps/mana/apps/web/src/lib/modules/broadcast/views/DetailView.svelte": 9,
"apps/mana/apps/web/src/lib/modules/broadcast/widgets/BroadcastsWidget.svelte": 3, "apps/mana/apps/web/src/lib/modules/broadcast/widgets/BroadcastsWidget.svelte": 3,
"apps/mana/apps/web/src/lib/modules/calc/ListView.svelte": 1, "apps/mana/apps/web/src/lib/modules/calc/ListView.svelte": 1,
"apps/mana/apps/web/src/lib/modules/calendar/components/AgendaView.svelte": 4,
"apps/mana/apps/web/src/lib/modules/calendar/components/CalendarHeader.svelte": 2,
"apps/mana/apps/web/src/lib/modules/calendar/components/CustomRecurrenceBuilder.svelte": 8,
"apps/mana/apps/web/src/lib/modules/calendar/components/DateStrip.svelte": 1,
"apps/mana/apps/web/src/lib/modules/calendar/components/EventCard.svelte": 5,
"apps/mana/apps/web/src/lib/modules/calendar/components/EventDetailModal.svelte": 4,
"apps/mana/apps/web/src/lib/modules/calendar/components/EventForm.svelte": 10,
"apps/mana/apps/web/src/lib/modules/calendar/components/MiniCalendar.svelte": 2,
"apps/mana/apps/web/src/lib/modules/calendar/components/QuickEventPopover.svelte": 6,
"apps/mana/apps/web/src/lib/modules/calendar/components/SlotSuggestions.svelte": 3,
"apps/mana/apps/web/src/lib/modules/calendar/ListView.svelte": 1,
"apps/mana/apps/web/src/lib/modules/calendar/SharedEventView.svelte": 2,
"apps/mana/apps/web/src/lib/modules/calendar/views/DetailView.svelte": 5,
"apps/mana/apps/web/src/lib/modules/cards/components/CreateDeckModal.svelte": 3, "apps/mana/apps/web/src/lib/modules/cards/components/CreateDeckModal.svelte": 3,
"apps/mana/apps/web/src/lib/modules/cards/views/DetailView.svelte": 6, "apps/mana/apps/web/src/lib/modules/cards/views/DetailView.svelte": 6,
"apps/mana/apps/web/src/lib/modules/chat/ListView.svelte": 1, "apps/mana/apps/web/src/lib/modules/chat/ListView.svelte": 1,
@ -295,9 +282,6 @@
"apps/mana/apps/web/src/routes/(app)/broadcasts/new/+page.svelte": 1, "apps/mana/apps/web/src/routes/(app)/broadcasts/new/+page.svelte": 1,
"apps/mana/apps/web/src/routes/(app)/calc/+page.svelte": 1, "apps/mana/apps/web/src/routes/(app)/calc/+page.svelte": 1,
"apps/mana/apps/web/src/routes/(app)/calc/standard/+page.svelte": 2, "apps/mana/apps/web/src/routes/(app)/calc/standard/+page.svelte": 2,
"apps/mana/apps/web/src/routes/(app)/calendar/+page.svelte": 1,
"apps/mana/apps/web/src/routes/(app)/calendar/calendars/+page.svelte": 6,
"apps/mana/apps/web/src/routes/(app)/calendar/event/[id]/+page.svelte": 6,
"apps/mana/apps/web/src/routes/(app)/cards/+page.svelte": 2, "apps/mana/apps/web/src/routes/(app)/cards/+page.svelte": 2,
"apps/mana/apps/web/src/routes/(app)/cards/decks/[id]/+page.svelte": 6, "apps/mana/apps/web/src/routes/(app)/cards/decks/[id]/+page.svelte": 6,
"apps/mana/apps/web/src/routes/(app)/cards/decks/+page.svelte": 3, "apps/mana/apps/web/src/routes/(app)/cards/decks/+page.svelte": 3,