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": {
"today": "Heute",
"tomorrow": "Morgen",
"newEvent": "Neuer Termin",
"noEvents": "Keine Termine",
"noEventsInRange": "Keine Termine in diesem Zeitraum",
"allDay": "Ganztägig",
"myCalendars": "Meine Kalender",
"sharedCalendars": "Geteilte Kalender",
@ -58,6 +60,210 @@
"changeStartTime": "Startzeit ä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": {
"none": "Nicht wiederholen",
"daily": "Täglich",

View file

@ -29,8 +29,10 @@
},
"calendar": {
"today": "Today",
"tomorrow": "Tomorrow",
"newEvent": "New Event",
"noEvents": "No events",
"noEventsInRange": "No events in this range",
"allDay": "All day",
"myCalendars": "My Calendars",
"sharedCalendars": "Shared Calendars",
@ -58,6 +60,210 @@
"changeStartTime": "Change start 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": {
"none": "Don't repeat",
"daily": "Daily",

View file

@ -29,8 +29,10 @@
},
"calendar": {
"today": "Hoy",
"tomorrow": "Mañana",
"newEvent": "Nuevo evento",
"noEvents": "Sin eventos",
"noEventsInRange": "Sin eventos en este rango",
"allDay": "Todo el día",
"myCalendars": "Mis calendarios",
"sharedCalendars": "Calendarios compartidos",
@ -58,6 +60,210 @@
"changeStartTime": "Cambiar hora de inicio",
"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": {
"none": "No repetir",
"daily": "Diario",

View file

@ -29,8 +29,10 @@
},
"calendar": {
"today": "Aujourd'hui",
"tomorrow": "Demain",
"newEvent": "Nouvel événement",
"noEvents": "Aucun événement",
"noEventsInRange": "Aucun événement dans cette période",
"allDay": "Toute la journée",
"myCalendars": "Mes calendriers",
"sharedCalendars": "Calendriers partagés",
@ -58,6 +60,210 @@
"changeStartTime": "Changer l'heure de début",
"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": {
"none": "Ne pas répéter",
"daily": "Quotidien",

View file

@ -29,8 +29,10 @@
},
"calendar": {
"today": "Oggi",
"tomorrow": "Domani",
"newEvent": "Nuovo evento",
"noEvents": "Nessun evento",
"noEventsInRange": "Nessun evento in questo intervallo",
"allDay": "Tutto il giorno",
"myCalendars": "I miei calendari",
"sharedCalendars": "Calendari condivisi",
@ -58,6 +60,210 @@
"changeStartTime": "Cambia ora di inizio",
"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": {
"none": "Non ripetere",
"daily": "Giornaliero",

View file

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

View file

@ -11,6 +11,9 @@
in light + dark (via prefers-color-scheme in the layout).
-->
<script lang="ts">
import { _, locale } from 'svelte-i18n';
import { get } from 'svelte/store';
interface EventBlob {
title: string;
startTime: string;
@ -35,7 +38,7 @@
const end = $derived(new Date(event.endTime));
function formatDate(d: Date): string {
return new Intl.DateTimeFormat('de-DE', {
return new Intl.DateTimeFormat(get(locale) ?? 'de', {
weekday: 'long',
day: '2-digit',
month: 'long',
@ -44,7 +47,7 @@
}
function formatTime(d: Date): string {
return new Intl.DateTimeFormat('de-DE', {
return new Intl.DateTimeFormat(get(locale) ?? 'de', {
hour: '2-digit',
minute: '2-digit',
}).format(d);
@ -52,7 +55,7 @@
const dateLabel = $derived(formatDate(start));
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
@ -84,38 +87,46 @@
</svelte:head>
<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>
<dl class="event__meta">
<div class="event__row">
<dt>Wann</dt>
<dt>{$_('calendar.shared_view.label_when')}</dt>
<dd>
<div class="event__date">{dateRangeLabel}</div>
<div class="event__time">{timeLabel}</div>
{#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}
</dd>
</div>
{#if event.location}
<div class="event__row">
<dt>Wo</dt>
<dt>{$_('calendar.shared_view.label_where')}</dt>
<dd>{event.location}</dd>
</div>
{/if}
</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}
<p class="event__expiry">
Dieser Link läuft am {new Intl.DateTimeFormat('de-DE', {
day: '2-digit',
month: 'long',
year: 'numeric',
}).format(new Date(expiresAt))} ab.
{$_('calendar.shared_view.expiry', {
values: {
date: new Intl.DateTimeFormat(get(locale) ?? 'de', {
day: '2-digit',
month: 'long',
year: 'numeric',
}).format(new Date(expiresAt)),
},
})}
</p>
{/if}
</article>

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,5 @@
<script lang="ts">
import { _ } from 'svelte-i18n';
import type { CalendarEvent } from '../types';
import { eventsStore } from '../stores/events.svelte';
import {
@ -96,7 +97,7 @@
style:background-color={color}
role="button"
tabindex="0"
aria-label={event.title || 'Neuer Termin'}
aria-label={event.title || $_('calendar.event_card.new_event_label')}
onpointerdown={handlePointerDown}
onclick={handleClick}
onkeydown={handleKeydown}
@ -107,7 +108,7 @@
class="resize-handle top"
onpointerdown={handleResizeTop}
role="slider"
aria-label="Startzeit ändern"
aria-label={$_('calendar.event.changeStartTime')}
aria-valuenow={0}
tabindex="-1"
></div>
@ -130,13 +131,19 @@
<span class="event-time">{formattedTime}</span>
{#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 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}
</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}
<span class="event-location">{event.location}</span>
{/if}
@ -146,7 +153,7 @@
class="resize-handle bottom"
onpointerdown={handleResizeBottom}
role="slider"
aria-label="Endzeit ändern"
aria-label={$_('calendar.event.changeEndTime')}
aria-valuenow={0}
tabindex="-1"
></div>

View file

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

View file

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

View file

@ -1,5 +1,6 @@
<script lang="ts">
import { getDateFnsLocale } from '$lib/i18n/format';
import { _ } from 'svelte-i18n';
import {
format,
startOfMonth,
@ -32,7 +33,15 @@
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>
<div class="mini-calendar">
@ -40,7 +49,7 @@
<button
class="nav-btn"
onclick={() => (currentMonth = subMonths(currentMonth, 1))}
aria-label="Vorheriger Monat"
aria-label={$_('calendar.mini_cal.prev_aria')}
>
<CaretLeft size={16} />
</button>
@ -50,7 +59,7 @@
<button
class="nav-btn"
onclick={() => (currentMonth = addMonths(currentMonth, 1))}
aria-label="Nächster Monat"
aria-label={$_('calendar.mini_cal.next_aria')}
>
<CaretRight size={16} />
</button>

View file

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

View file

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

View file

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

View file

@ -1,4 +1,5 @@
<script lang="ts">
import { _ } from 'svelte-i18n';
import { getContext, onMount } from 'svelte';
import { dropTarget } from '@mana/shared-ui/dnd';
import type { DragPayload, TagDragData } from '@mana/shared-ui/dnd';
@ -215,7 +216,7 @@
</script>
<svelte:head>
<title>Kalender - Mana</title>
<title>{$_('calendar.detail_route.doc_title')}</title>
</svelte:head>
<RoutePage appId="calendar">
@ -272,7 +273,7 @@
role="presentation"
>
<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
mode="create"
initialStartTime={createStartTime}

View file

@ -44,13 +44,13 @@
}
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);
}
</script>
<svelte:head>
<title>Kalender verwalten - Mana</title>
<title>{$_('calendar.calendars_route.doc_title')}</title>
</svelte:head>
<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"
>
<CaretLeft size={16} />
Zurück zum Kalender
{$_('calendar.calendars_route.back_to_calendar')}
</a>
<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
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"
>
<Plus size={16} />
Neuer Kalender
{$_('calendar.calendars_route.new_calendar')}
</button>
</div>
@ -85,25 +87,28 @@
class="space-y-3"
>
<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
id="cal-name"
type="text"
bind:value={newName}
placeholder="z.B. Arbeit, Sport, Familie..."
placeholder={$_('calendar.calendars_route.placeholder_name')}
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"
/>
</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">
{#each PRESET_COLORS as color}
<button
type="button"
aria-label="Farbe wählen"
aria-label={$_('calendar.calendars_route.aria_pick_color')}
onclick={() => (newColor = color)}
class="h-8 w-8 rounded-full border-2 transition-transform hover:scale-110 {newColor ===
color
@ -121,14 +126,14 @@
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"
>
Abbrechen
{$_('common.cancel')}
</button>
<button
type="submit"
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"
>
Erstellen
{$_('calendar.calendars_route.submit_create')}
</button>
</div>
</form>
@ -138,7 +143,9 @@
<!-- Calendar List -->
<div class="space-y-2">
{#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}
{#each calendarsCtx.value as cal (cal.id)}
<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">
{cal.name}
{#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}
</div>
</div>
@ -158,7 +167,9 @@
<button
onclick={() => handleToggleVisibility(cal.id)}
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}
<Eye size={16} />
@ -170,7 +181,7 @@
<button
onclick={() => handleSetDefault(cal.id)}
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} />
</button>

View file

@ -66,7 +66,11 @@
</script>
<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>
<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"
>
<CaretLeft size={16} />
Zurück zum Kalender
{$_('calendar.detail_route.back_to_calendar')}
</button>
{#if !event}
<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">
Zurück zum Kalender
{$_('calendar.detail_route.back_to_calendar')}
</button>
</div>
{:else if isEditing}
<!-- Edit Form -->
<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
onsubmit={(e) => {
@ -101,7 +107,7 @@
>
<div>
<label for="edit-title" class="mb-1 block text-sm font-medium text-foreground"
>Titel</label
>{$_('calendar.detail_route.label_title')}</label
>
<input
id="edit-title"
@ -114,7 +120,7 @@
<div>
<label for="edit-desc" class="mb-1 block text-sm font-medium text-foreground"
>Beschreibung</label
>{$_('calendar.detail_route.label_description')}</label
>
<textarea
id="edit-desc"
@ -126,7 +132,7 @@
<div>
<label for="edit-date" class="mb-1 block text-sm font-medium text-foreground"
>Datum</label
>{$_('calendar.detail_route.label_date')}</label
>
<input
id="edit-date"
@ -139,14 +145,14 @@
<label class="flex items-center gap-2 text-sm text-foreground">
<input type="checkbox" bind:checked={editAllDay} class="rounded" />
Ganztägig
{$_('calendar.event.allDay')}
</label>
{#if !editAllDay}
<div class="grid grid-cols-2 gap-3">
<div>
<label for="edit-start" class="mb-1 block text-sm font-medium text-foreground"
>Von</label
>{$_('calendar.detail_route.label_from')}</label
>
<input
id="edit-start"
@ -157,7 +163,7 @@
</div>
<div>
<label for="edit-end" class="mb-1 block text-sm font-medium text-foreground"
>Bis</label
>{$_('calendar.detail_route.label_to')}</label
>
<input
id="edit-end"
@ -171,13 +177,13 @@
<div>
<label for="edit-location" class="mb-1 block text-sm font-medium text-foreground"
>Ort</label
>{$_('calendar.detail_route.label_location')}</label
>
<input
id="edit-location"
type="text"
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"
/>
</div>
@ -188,13 +194,13 @@
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"
>
Abbrechen
{$_('calendar.detail_route.submit_cancel')}
</button>
<button
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"
>
Speichern
{$_('calendar.detail_route.submit_save')}
</button>
</div>
</form>
@ -245,13 +251,14 @@
})}
</div>
{#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}
<div class="text-sm text-muted-foreground">
{format(new Date(event.startTime), 'HH:mm')} {format(
new Date(event.endTime),
'HH:mm'
)} Uhr
)}
{$_('calendar.detail_route.time_suffix_uhr')}
</div>
{/if}
</div>