feat(geschichten): frontend foundation — canBlogWrite, sanitize util, nav, i18n

- Derives canBlogWrite in +layout.server.ts the same way as canAnnotate.
- Adds Geschichten link to AppNav (desktop + mobile, between Stammbaum and Admin).
- Adds error_geschichte_not_found mapping to errors.ts and translation keys
  for the Geschichten index, detail, editor, and confirmation copy in
  de/en/es.
- Adds isomorphic-dompurify-backed safeHtml() helper with allow-list
  matching the backend OWASP policy (p/br/strong/em/h2/h3/ul/ol/li),
  plus Vitest spec.
- Updates legacy spec test data so the new required canBlogWrite layout
  prop type-checks.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-05-02 17:43:29 +02:00
parent afd6d0b20d
commit 9e7861fa03
18 changed files with 998 additions and 28 deletions

View File

@@ -919,6 +919,56 @@
"bulk_edit_count_pill": "{count} werden bearbeitet",
"nav_stammbaum": "Stammbaum",
"nav_geschichten": "Geschichten",
"error_geschichte_not_found": "Die Geschichte wurde nicht gefunden.",
"geschichten_index_title": "Geschichten",
"geschichten_new_button": "Neue Geschichte",
"geschichten_filter_all_pill": "Alle",
"geschichten_filter_choose_person": "Person wählen",
"geschichten_filter_aria_label": "Person filtern",
"geschichten_empty_for_person": "Keine Geschichten für {name} gefunden.",
"geschichten_empty_no_filter": "Es gibt noch keine veröffentlichten Geschichten.",
"geschichten_back_to_index": "Zurück zu Geschichten",
"geschichten_published_on": "veröffentlicht am {date}",
"geschichten_persons_section": "Personen in dieser Geschichte",
"geschichten_documents_section": "Erwähnte Dokumente",
"geschichten_card_heading": "Geschichten",
"geschichten_card_write_action": "+ Geschichte schreiben",
"geschichten_card_attach_action": "+ Geschichte anhängen",
"geschichten_card_show_all_for_person": "Alle Geschichten zu {name}",
"geschichten_card_show_all": "Alle anzeigen",
"geschichte_editor_title_placeholder": "Titel der Geschichte",
"geschichte_editor_body_placeholder": "Schreibe hier deine Geschichte…",
"geschichte_editor_status_draft": "ENTWURF",
"geschichte_editor_status_published": "VERÖFFENTLICHT",
"geschichte_editor_status_draft_hint": "Noch nicht öffentlich sichtbar.",
"geschichte_editor_status_published_hint": "Öffentlich sichtbar für alle Leser.",
"geschichte_editor_save_hint_draft": "Alle Änderungen werden als Entwurf gespeichert.",
"geschichte_editor_save_hint_published": "Änderungen sind sofort live.",
"geschichte_editor_save_draft": "Entwurf speichern",
"geschichte_editor_publish": "Veröffentlichen",
"geschichte_editor_save": "Speichern",
"geschichte_editor_unpublish": "Zurück zu Entwurf",
"geschichte_editor_title_required": "Bitte gib einen Titel ein.",
"geschichte_editor_unsaved_changes": "Du hast ungespeicherte Änderungen — wirklich verlassen?",
"geschichte_editor_personen_heading": "Personen",
"geschichte_editor_personen_hint": "Welche historischen Personen kommen in dieser Geschichte vor?",
"geschichte_editor_dokumente_heading": "Dokumente",
"geschichte_editor_dokumente_hint": "Welche Briefe oder Dokumente sind Teil dieser Geschichte?",
"geschichte_editor_search_person": "Person suchen…",
"geschichte_editor_search_document": "Dokument suchen…",
"geschichte_editor_toolbar_bold": "Fett (Strg+B)",
"geschichte_editor_toolbar_italic": "Kursiv (Strg+I)",
"geschichte_editor_toolbar_h2": "Überschrift",
"geschichte_editor_toolbar_h3": "Unterüberschrift",
"geschichte_editor_toolbar_ul": "Aufzählung",
"geschichte_editor_toolbar_ol": "Nummerierte Liste",
"geschichte_delete_confirm_title": "Geschichte löschen?",
"geschichte_delete_confirm_body": "Diese Aktion kann nicht rückgängig gemacht werden. Die Geschichte wird dauerhaft gelöscht und aus allen verlinkten Personen- und Dokumentseiten entfernt.",
"error_relationship_not_found": "Die Beziehung wurde nicht gefunden.",
"error_circular_relationship": "Diese Beziehung würde einen Kreis erzeugen.",

View File

@@ -919,6 +919,56 @@
"bulk_edit_count_pill": "{count} will be edited",
"nav_stammbaum": "Family tree",
"nav_geschichten": "Stories",
"error_geschichte_not_found": "The story was not found.",
"geschichten_index_title": "Stories",
"geschichten_new_button": "New story",
"geschichten_filter_all_pill": "All",
"geschichten_filter_choose_person": "Choose person",
"geschichten_filter_aria_label": "Filter by person",
"geschichten_empty_for_person": "No stories found for {name}.",
"geschichten_empty_no_filter": "There are no published stories yet.",
"geschichten_back_to_index": "Back to stories",
"geschichten_published_on": "published on {date}",
"geschichten_persons_section": "People in this story",
"geschichten_documents_section": "Referenced documents",
"geschichten_card_heading": "Stories",
"geschichten_card_write_action": "+ Write a story",
"geschichten_card_attach_action": "+ Attach a story",
"geschichten_card_show_all_for_person": "All stories about {name}",
"geschichten_card_show_all": "Show all",
"geschichte_editor_title_placeholder": "Story title",
"geschichte_editor_body_placeholder": "Write your story here…",
"geschichte_editor_status_draft": "DRAFT",
"geschichte_editor_status_published": "PUBLISHED",
"geschichte_editor_status_draft_hint": "Not yet visible to readers.",
"geschichte_editor_status_published_hint": "Visible to all readers.",
"geschichte_editor_save_hint_draft": "All changes are saved as a draft.",
"geschichte_editor_save_hint_published": "Changes go live immediately.",
"geschichte_editor_save_draft": "Save draft",
"geschichte_editor_publish": "Publish",
"geschichte_editor_save": "Save",
"geschichte_editor_unpublish": "Back to draft",
"geschichte_editor_title_required": "Please enter a title.",
"geschichte_editor_unsaved_changes": "You have unsaved changes — leave anyway?",
"geschichte_editor_personen_heading": "People",
"geschichte_editor_personen_hint": "Which historical persons appear in this story?",
"geschichte_editor_dokumente_heading": "Documents",
"geschichte_editor_dokumente_hint": "Which letters or documents are part of this story?",
"geschichte_editor_search_person": "Search person…",
"geschichte_editor_search_document": "Search document…",
"geschichte_editor_toolbar_bold": "Bold (Ctrl+B)",
"geschichte_editor_toolbar_italic": "Italic (Ctrl+I)",
"geschichte_editor_toolbar_h2": "Heading",
"geschichte_editor_toolbar_h3": "Subheading",
"geschichte_editor_toolbar_ul": "Bulleted list",
"geschichte_editor_toolbar_ol": "Numbered list",
"geschichte_delete_confirm_title": "Delete story?",
"geschichte_delete_confirm_body": "This action cannot be undone. The story will be permanently deleted and removed from all linked person and document pages.",
"error_relationship_not_found": "Relationship not found.",
"error_circular_relationship": "This relationship would form a cycle.",

View File

@@ -919,6 +919,56 @@
"bulk_edit_count_pill": "Se editarán {count}",
"nav_stammbaum": "Árbol genealógico",
"nav_geschichten": "Historias",
"error_geschichte_not_found": "No se encontró la historia.",
"geschichten_index_title": "Historias",
"geschichten_new_button": "Nueva historia",
"geschichten_filter_all_pill": "Todas",
"geschichten_filter_choose_person": "Elegir persona",
"geschichten_filter_aria_label": "Filtrar por persona",
"geschichten_empty_for_person": "No hay historias para {name}.",
"geschichten_empty_no_filter": "Aún no hay historias publicadas.",
"geschichten_back_to_index": "Volver a Historias",
"geschichten_published_on": "publicada el {date}",
"geschichten_persons_section": "Personas en esta historia",
"geschichten_documents_section": "Documentos mencionados",
"geschichten_card_heading": "Historias",
"geschichten_card_write_action": "+ Escribir historia",
"geschichten_card_attach_action": "+ Adjuntar historia",
"geschichten_card_show_all_for_person": "Todas las historias sobre {name}",
"geschichten_card_show_all": "Mostrar todas",
"geschichte_editor_title_placeholder": "Título de la historia",
"geschichte_editor_body_placeholder": "Escribe tu historia aquí…",
"geschichte_editor_status_draft": "BORRADOR",
"geschichte_editor_status_published": "PUBLICADA",
"geschichte_editor_status_draft_hint": "Aún no visible para lectores.",
"geschichte_editor_status_published_hint": "Visible para todos los lectores.",
"geschichte_editor_save_hint_draft": "Los cambios se guardan como borrador.",
"geschichte_editor_save_hint_published": "Los cambios se publican inmediatamente.",
"geschichte_editor_save_draft": "Guardar borrador",
"geschichte_editor_publish": "Publicar",
"geschichte_editor_save": "Guardar",
"geschichte_editor_unpublish": "Volver a borrador",
"geschichte_editor_title_required": "Por favor ingresa un título.",
"geschichte_editor_unsaved_changes": "Tienes cambios no guardados — ¿salir igualmente?",
"geschichte_editor_personen_heading": "Personas",
"geschichte_editor_personen_hint": "¿Qué personas históricas aparecen en esta historia?",
"geschichte_editor_dokumente_heading": "Documentos",
"geschichte_editor_dokumente_hint": "¿Qué cartas o documentos forman parte de esta historia?",
"geschichte_editor_search_person": "Buscar persona…",
"geschichte_editor_search_document": "Buscar documento…",
"geschichte_editor_toolbar_bold": "Negrita (Ctrl+B)",
"geschichte_editor_toolbar_italic": "Cursiva (Ctrl+I)",
"geschichte_editor_toolbar_h2": "Encabezado",
"geschichte_editor_toolbar_h3": "Subencabezado",
"geschichte_editor_toolbar_ul": "Lista con viñetas",
"geschichte_editor_toolbar_ol": "Lista numerada",
"geschichte_delete_confirm_title": "¿Eliminar historia?",
"geschichte_delete_confirm_body": "Esta acción no se puede deshacer. La historia se eliminará permanentemente y se quitará de todas las páginas de personas y documentos vinculados.",
"error_relationship_not_found": "La relación no fue encontrada.",
"error_circular_relationship": "Esta relación crearía un ciclo.",