Files
familienarchiv/frontend/messages/es.json
Marcel dc6ea080c4 fix(#71-#73): address all review findings from Markus and Sara
BLOCKERs:
- Remove direct AppUserRepository/CommentRepository access from CommentService and
  NotificationService — replaced with UserService.findAllById() and UserService
  (fixes layering contract from CLAUDE.md)
- Switch Optional<JavaMailSender> constructor injection — removes @Autowired(required=false)
  field and ReflectionTestUtils hack in tests
- Add @RequirePermission(READ_ALL) to UserSearchController — prevents user enumeration
  without read access

Data bug:
- Promote actorName from @Transient to persisted VARCHAR column (V18 migration)
- Set actorName in notifyReply and notifyMentions from comment.getAuthorName()

Architecture:
- Add @RequirePermission(READ_ALL) to NotificationController
- Introduce NotificationDTO — controller returns DTO instead of Notification entity,
  eliminating lazy-load N+1 and AppUser field leakage
- Change mentions FetchType to EAGER — fixes LazyInitializationException outside transaction
- Add @Transactional(propagation=REQUIRES_NEW) to notifyReply/notifyMentions so a
  notification failure cannot roll back the parent comment
- N+1 fix: replace per-ID findById loops with single findAllById bulk fetch
- Move collectParticipantIds to CommentService; notifyReply accepts Set<UUID> directly

Security:
- Escape displayName before injecting into renderBody HTML span
- Replace <a href="#"> with <span class="mention"> — no profile page to link to, and
  the anchor's scroll-to-top behaviour is harmful

Tests added/fixed:
- markRead_throwsNotFound, markAllRead_delegatesToRepository, countUnread_delegatesToRepository
- markOneRead_returns401, @RequirePermission 403 coverage for both controllers
- postComment/replyToComment_triggersNotifyMentions_whenMentionedUserIdsProvided
- search_returnsAtMostTenResults now asserts $.length() <= 10
- XSS regression test for escaped displayName in mention.spec.ts

Frontend minors:
- relativeTime() uses Intl.RelativeTimeFormat (locale-aware, not German-hardcoded)
- aria-label uses m.notification_unread() Paraglide key (de/en/es added)
- <div role="button"> replaced with <button> (native Enter+Space handling)
- onDestroy clears debounceTimer in MentionEditor
- setTimeout(100) replaced with await tick() + requestAnimationFrame in CommentThread
- Notification prefs form uses checkbox name attributes + formData.has() pattern

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 00:31:38 +01:00

311 lines
16 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"$schema": "https://inlang.com/schema/inlang-message-format",
"error_annotation_not_found": "Anotación no encontrada.",
"error_annotation_overlap": "La anotación se superpone con una existente.",
"annotation_outdated_notice": "Algunas anotaciones hacen referencia a una versión anterior del archivo y no se muestran.",
"error_document_not_found": "Documento no encontrado.",
"error_document_no_file": "No hay ningún archivo asociado a este documento.",
"error_file_not_found": "El archivo no pudo encontrarse en el almacenamiento.",
"error_file_upload_failed": "No se pudo subir el archivo.",
"error_unsupported_file_type": "Este formato de archivo no está admitido.",
"error_user_not_found": "Usuario no encontrado.",
"error_import_already_running": "Ya hay una importación en curso. Por favor, espere a que finalice.",
"error_unauthorized": "No ha iniciado sesión.",
"error_forbidden": "No tiene permiso para realizar esta acción.",
"error_validation_error": "La entrada no es válida.",
"error_internal_error": "Se ha producido un error inesperado.",
"nav_documents": "Documentos",
"nav_persons": "Personas",
"nav_conversations": "Conversaciones",
"nav_admin": "Admin",
"nav_logout": "Cerrar sesión",
"btn_save": "Guardar",
"btn_cancel": "Cancelar",
"btn_edit": "Editar",
"btn_create": "Crear",
"btn_delete": "Eliminar",
"doc_delete_confirm": "¿Realmente eliminar este documento? Esta acción no se puede deshacer.",
"btn_back_to_overview": "Volver al resumen",
"btn_back": "Volver",
"btn_back_to_document": "Volver al documento",
"form_label_first_name": "Nombre",
"form_label_last_name": "Apellido",
"form_label_alias": "Apodo / Alias",
"form_placeholder_alias": "p.ej. Abuela Frieda, Tío Karl…",
"form_label_date": "Fecha",
"form_placeholder_date": "DD.MM.AAAA",
"form_date_error": "Introduzca en formato DD.MM.AAAA, p.ej. 20.12.2026",
"form_label_location": "Lugar",
"form_placeholder_location": "p.ej. Berlín, Viena…",
"form_label_sender": "Remitente",
"form_label_receivers": "Destinatarios",
"form_label_title": "Título",
"form_label_tags": "Etiquetas",
"form_label_content": "Contenido",
"form_placeholder_content": "Breve descripción del contenido…",
"form_label_transcription": "Transcripción",
"form_placeholder_transcription": "Texto completo del documento…",
"form_label_archive_location": "Ubicación de almacenamiento",
"form_placeholder_archive_location": "p.ej. Armario 3, Carpeta B",
"form_helper_archive_location": "¿Dónde se encuentra el documento original?",
"login_heading": "Iniciar sesión",
"login_label_username": "Usuario",
"login_label_password": "Contraseña",
"login_btn_submit": "Iniciar sesión",
"docs_search_placeholder": "Buscar en título, contenido, lugar...",
"docs_btn_filter": "Filtrar",
"docs_btn_reset_title": "Restablecer filtro",
"docs_filter_label_tags": "Etiquetas",
"docs_filter_label_sender": "Remitente",
"docs_filter_label_receivers": "Destinatarios",
"docs_filter_label_from": "Desde",
"docs_filter_label_to": "Hasta",
"docs_btn_new": "Nuevo documento",
"docs_empty_heading": "No se encontraron documentos",
"docs_empty_text": "Intente ajustar los filtros o cambiar el término de búsqueda.",
"docs_empty_btn_clear": "Borrar todos los filtros",
"docs_list_from": "De",
"docs_list_to": "Para",
"docs_list_unknown": "Desconocido",
"doc_section_who_when": "Quién & Cuándo",
"doc_section_description": "Descripción",
"doc_section_file": "Archivo",
"doc_file_upload_label": "Subir archivo",
"doc_file_upload_note": "(opcional)",
"doc_file_replace_label": "Subir nuevo archivo",
"doc_file_replace_note": "(reemplaza el archivo actual)",
"doc_current_file_label": "Archivo actual:",
"doc_more_details": "Más detalles",
"doc_new_heading": "Nuevo documento",
"doc_edit_heading": "Editar",
"doc_section_details": "Detalles",
"doc_label_document_date": "Fecha del documento",
"doc_label_creation_location": "Lugar de creación",
"doc_label_archive_location_original": "Ubicación de almacenamiento (original)",
"doc_section_persons": "Personas",
"doc_sender_not_specified": "No especificado",
"doc_no_receivers": "Sin destinatarios",
"doc_section_content": "Contenido",
"doc_label_summary": "Resumen",
"doc_loading": "Cargando documento...",
"doc_download_link": "Intentar descarga directa",
"doc_no_scan": "No hay escaneo disponible",
"persons_heading": "Directorio de personas",
"persons_subtitle": "Explore el índice de todas las personas registradas en el archivo familiar.",
"persons_btn_new": "Nueva persona",
"persons_search_placeholder": "Buscar nombres...",
"persons_empty_heading": "No se encontraron personas.",
"persons_empty_text": "Pruebe con otro término de búsqueda.",
"persons_new_heading": "Nueva persona",
"persons_section_details": "Datos de la persona",
"person_edit_heading": "Editar persona",
"person_label_full_name": "Nombre completo",
"person_merge_heading": "Fusionar persona",
"person_merge_description": "Esta persona se fusionará con la persona de destino seleccionada. Todos los documentos y enlaces se transferirán y esta persona será eliminada.",
"person_merge_target_label": "Fusionar con",
"person_btn_merge": "Fusionar",
"person_btn_merge_confirm": "Sí, fusionar",
"person_merge_warning": "Atención: Esta acción no se puede deshacer.",
"person_label_notes": "Notas",
"person_placeholder_notes": "Notas biográficas, observaciones…",
"person_label_birth_year": "Año de nacimiento",
"person_label_death_year": "Año de fallecimiento",
"person_placeholder_year": "p.ej. 1923",
"person_year_error": "Introduzca un año de cuatro dígitos",
"person_years_error_order": "El año de nacimiento debe ser anterior al año de fallecimiento",
"person_docs_heading": "Documentos enviados",
"person_no_docs": "Esta persona aún no está vinculada como remitente.",
"person_received_docs_heading": "Documentos recibidos",
"person_no_received_docs": "Esta persona aún no está vinculada como receptor.",
"person_role_sender": "Enviado",
"person_role_receiver": "Recibido",
"person_co_correspondents_heading": "Corresponsales frecuentes",
"person_show_more": "+ {count} más",
"conv_heading": "Conversaciones",
"conv_subtitle": "Siga la correspondencia entre dos personas cronológicamente.",
"conv_label_person_a": "Persona A (Remitente)",
"conv_label_person_b": "Persona B (Destinatario)",
"conv_label_from": "Período desde",
"conv_label_to": "Período hasta",
"conv_sort_label": "Ordenar:",
"conv_sort_newest": "Más reciente primero",
"conv_sort_oldest": "Más antiguo primero",
"conv_empty_heading": "Seleccione dos personas",
"conv_empty_text": "La correspondencia se mostrará aquí.",
"conv_no_results_heading": "No se encontraron documentos.",
"conv_no_results_text": "Intente ajustar el período de tiempo.",
"conv_swap_btn": "Intercambiar personas",
"conv_summary": "{count} documentos · {yearFrom}{yearTo}",
"conv_new_doc_link": "Nuevo documento en esta correspondencia",
"admin_heading": "Panel de administración",
"admin_tab_users": "Usuarios",
"admin_tab_groups": "Grupos",
"admin_tab_tags": "Etiquetas",
"admin_section_users": "Gestión de usuarios",
"admin_col_login": "Login",
"admin_col_groups": "Grupos",
"admin_col_password": "Contraseña",
"admin_multiselect_hint": "Ctrl+Clic para seleccionar",
"admin_password_placeholder": "Nueva contraseña (opcional)",
"admin_no_groups": "Sin grupos",
"admin_btn_delete_user_title": "Eliminar usuario",
"admin_section_new_user": "Crear nuevo usuario",
"admin_multiselect_hint_multi": "Ctrl+Clic para varios",
"admin_multiselect_hint_full": "Ctrl+Clic para selección múltiple",
"admin_section_tags": "Etiquetas",
"admin_tags_warning": "Advertencia: Renombrar o eliminar afecta a todos los documentos vinculados.",
"admin_btn_edit_tag_label": "Editar etiqueta",
"admin_tag_delete_confirm": "¿Realmente eliminar? La etiqueta se eliminará de todos los documentos.",
"admin_btn_delete_tag_label": "Eliminar etiqueta",
"admin_section_groups": "Gestión de grupos",
"admin_col_name": "Nombre",
"admin_col_permissions": "Permisos",
"admin_col_actions": "Acciones",
"admin_group_delete_confirm": "¿Realmente eliminar el grupo?",
"admin_section_new_group": "Crear nuevo grupo",
"admin_group_name_placeholder": "Nombre del grupo (p.ej. Editores)",
"admin_user_delete_confirm": "¿Realmente eliminar al usuario {username}?",
"admin_btn_new_user": "Nuevo usuario",
"admin_user_new_heading": "Crear nuevo usuario",
"admin_user_edit_heading": "Editar usuario: {username}",
"admin_user_created": "Usuario creado.",
"admin_user_updated": "Cambios guardados.",
"admin_col_full_name": "Nombre",
"admin_label_new_password_optional": "Nueva contraseña (opcional)",
"admin_label_initial_password": "Contraseña",
"doc_file_error_preview": "No se pudo cargar la vista previa.",
"doc_download_title": "Descargar",
"doc_tag_filter_title": "Filtrar por {name}",
"doc_conversation_title": "Ver conversación",
"doc_preview_iframe_title": "Vista previa del documento",
"doc_image_alt": "Escaneado original",
"doc_no_date": "Sin fecha",
"person_merge_will_be_deleted": "será eliminado.",
"comp_typeahead_placeholder": "Escriba un nombre...",
"comp_typeahead_loading": "Buscando...",
"comp_multiselect_placeholder": "Escriba un nombre...",
"comp_multiselect_remove": "Eliminar",
"comp_multiselect_loading": "Buscando...",
"comp_taginput_placeholder_create": "Añadir etiquetas...",
"comp_taginput_placeholder_filter": "Filtrar por etiquetas...",
"comp_taginput_remove": "Eliminar etiqueta",
"comp_taginput_create_hint": "Pulse Enter para crear etiqueta.",
"error_email_already_in_use": "Esta dirección de correo ya está en uso por otra cuenta.",
"error_wrong_current_password": "La contraseña actual es incorrecta.",
"nav_profile": "Perfil",
"profile_heading": "Mi perfil",
"profile_section_personal": "Información personal",
"profile_label_first_name": "Nombre",
"profile_label_last_name": "Apellido",
"profile_label_birth_date": "Fecha de nacimiento",
"profile_label_email": "Correo electrónico",
"profile_label_contact": "Datos de contacto",
"profile_contact_placeholder": "Teléfono, dirección u otras notas...",
"profile_section_password": "Cambiar contraseña",
"profile_label_current_password": "Contraseña actual",
"profile_label_new_password": "Nueva contraseña",
"profile_label_new_password_confirm": "Nueva contraseña (repetir)",
"profile_password_mismatch": "Las nuevas contraseñas no coinciden.",
"profile_saved": "Guardado.",
"profile_password_changed": "Contraseña cambiada con éxito.",
"user_profile_heading": "Perfil de",
"error_invalid_reset_token": "El enlace no es válido o ha expirado.",
"forgot_password_heading": "Contraseña olvidada",
"forgot_password_email_label": "Correo electrónico",
"forgot_password_submit": "Solicitar enlace",
"forgot_password_success": "Si existe una cuenta con esta dirección de correo electrónico, recibirá en breve un correo con un enlace para restablecer su contraseña.",
"forgot_password_back_to_login": "Volver al inicio de sesión",
"reset_password_heading": "Establecer nueva contraseña",
"reset_password_label": "Nueva contraseña",
"reset_password_confirm_label": "Confirmar contraseña",
"reset_password_submit": "Guardar contraseña",
"reset_password_mismatch": "Las contraseñas no coinciden.",
"reset_password_success": "Su contraseña ha sido cambiada con éxito. Ahora puede iniciar sesión.",
"login_forgot_password": "¿Olvidó su contraseña?",
"history_section_title": "Historial",
"history_loading": "Cargando historial…",
"history_empty": "Aún no hay versiones.",
"history_version_label": "Versión",
"history_compare_mode": "Comparar",
"history_compare_select_a": "Versión A",
"history_compare_select_b": "Versión B",
"history_compare_apply": "Comparar",
"history_diff_no_changes": "No hay cambios entre estas versiones.",
"history_field_title": "Título",
"history_field_document_date": "Fecha",
"history_field_location": "Lugar",
"history_field_document_location": "Ubicación en archivo",
"history_field_transcription": "Transcripción",
"history_field_summary": "Resumen",
"history_field_sender": "Remitente",
"history_field_receivers": "Destinatarios",
"history_field_tags": "Etiquetas",
"admin_tab_system": "Sistema",
"admin_system_backfill_heading": "Completar datos de historial",
"admin_system_backfill_description": "Crea una entrada de historial inicial para todos los documentos que aún no tienen ninguna (p.ej. documentos importados). Así, en la próxima edición solo se resaltarán los campos realmente modificados.",
"admin_system_backfill_btn": "Completar ahora",
"admin_system_backfill_success": "{count} documentos fueron completados.",
"admin_system_backfill_hashes_heading": "Calcular hashes de archivo",
"admin_system_backfill_hashes_description": "Calcula el hash SHA-256 para todos los documentos ya subidos que aún no tienen uno. Así las anotaciones se vinculan correctamente a su versión del archivo y vuelven a mostrarse.",
"admin_system_backfill_hashes_btn": "Calcular hashes de archivo",
"admin_system_backfill_hashes_success": "{count} documentos fueron actualizados.",
"comp_expandable_show_more": "Mostrar más",
"comp_expandable_show_less": "Mostrar menos",
"error_comment_not_found": "El comentario no pudo encontrarse.",
"comment_section_title": "Discusión",
"comment_placeholder": "Escribe un comentario…",
"comment_btn_post": "Enviar",
"comment_btn_reply": "Responder",
"comment_edited_label": "· editado",
"comment_panel_title": "Comentarios",
"comment_panel_close": "Cerrar",
"doc_panel_tab_metadata": "Metadatos",
"doc_panel_tab_transcription": "Transcripción",
"doc_panel_tab_discussion": "Discusión",
"doc_panel_tab_history": "Historial",
"doc_panel_annotate": "Anotar",
"doc_panel_annotate_stop": "Listo",
"doc_panel_annotation_thread_title": "Anotación",
"doc_panel_discussion_annotation_tab": "Anotación · Página {page}",
"pdf_annotations_show": "Mostrar anotaciones",
"pdf_annotations_hide": "Ocultar anotaciones",
"upload_drop_hint": "Uno o varios archivos a la vez",
"upload_accepted_types": "PDF, JPEG, PNG, TIFF",
"upload_filename_hint": "Consejo: 2024-03-15_Mueller_Hans.pdf → fecha y remitente prellenados",
"upload_success": "{count} documento(s) creado(s)",
"upload_duplicate": "{filename} ya existe —",
"upload_duplicate_link": "Ver documento",
"upload_invalid_type": "{filename}: formato de archivo no admitido",
"upload_error": "Error al subir {filename}",
"enrich_list_back": "Volver a la vista general",
"enrich_list_count": "documentos",
"btn_save_and_mark_reviewed": "Guardar y marcar como revisado",
"btn_mark_for_review": "Marcar para revisión",
"enrich_needs_metadata_title": "Documentos sin metadatos",
"enrich_needs_metadata_count": "{count} documento(s) esperando metadatos",
"enrich_needs_metadata_cta": "Completar ahora",
"enrich_list_heading": "Documentos sin metadatos",
"enrich_list_empty_heading": "Todos los documentos completos",
"enrich_list_empty_body": "No hay documentos que necesiten metadatos.",
"enrich_list_start": "Comenzar revisión",
"enrich_progress": "{count} restante(s)",
"enrich_skip": "Omitir",
"enrich_done_heading": "¡Todo listo!",
"enrich_done_body": "Todos los documentos han sido procesados.",
"enrich_back_to_list": "Volver a la lista",
"comment_empty_hint": "Aún no hay comentarios ¡inicia la discusión!",
"comment_start_discussion": "Iniciar discusión →",
"notification_bell_label": "Notificaciones",
"notification_bell_unread_label": "{count} notificaciones sin leer",
"notification_mark_all_read": "Marcar todo como leído",
"notification_empty": "No hay notificaciones nuevas",
"notification_type_reply": "{actor} respondió a tu comentario",
"notification_type_mention": "{actor} te mencionó en un comentario",
"notification_prefs_heading": "Notificaciones",
"notification_pref_reply": "Correo cuando alguien responde a mi comentario",
"notification_pref_mention": "Correo cuando alguien me menciona en un comentario",
"notification_unread": "no leído",
"mention_btn_label": "Mencionar persona",
"mention_popup_empty": "No se encontraron usuarios"
}