Files
familienarchiv/frontend/messages/en.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
15 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": "Annotation not found.",
"error_annotation_overlap": "The annotation overlaps an existing one.",
"annotation_outdated_notice": "Some annotations refer to an earlier file version and are not shown.",
"error_document_not_found": "Document not found.",
"error_document_no_file": "No file is associated with this document.",
"error_file_not_found": "The file could not be found in storage.",
"error_file_upload_failed": "The file could not be uploaded.",
"error_unsupported_file_type": "This file format is not supported.",
"error_user_not_found": "User not found.",
"error_import_already_running": "An import is already running. Please wait for it to finish.",
"error_unauthorized": "You are not logged in.",
"error_forbidden": "You do not have permission for this action.",
"error_validation_error": "The input is invalid.",
"error_internal_error": "An unexpected error occurred.",
"nav_documents": "Documents",
"nav_persons": "Persons",
"nav_conversations": "Conversations",
"nav_admin": "Admin",
"nav_logout": "Sign out",
"btn_save": "Save",
"btn_cancel": "Cancel",
"btn_edit": "Edit",
"btn_create": "Create",
"btn_delete": "Delete",
"doc_delete_confirm": "Really delete this document? This action cannot be undone.",
"btn_back_to_overview": "Back to overview",
"btn_back": "Back",
"btn_back_to_document": "Back to document",
"form_label_first_name": "First name",
"form_label_last_name": "Last name",
"form_label_alias": "Nickname / Alias",
"form_placeholder_alias": "e.g. Grandma Frieda, Uncle Karl…",
"form_label_date": "Date",
"form_placeholder_date": "DD.MM.YYYY",
"form_date_error": "Please enter in DD.MM.YYYY format, e.g. 20.12.2026",
"form_label_location": "Location",
"form_placeholder_location": "e.g. Berlin, Vienna…",
"form_label_sender": "Sender",
"form_label_receivers": "Recipients",
"form_label_title": "Title",
"form_label_tags": "Tags",
"form_label_content": "Content",
"form_placeholder_content": "Brief description of the content…",
"form_label_transcription": "Transcription",
"form_placeholder_transcription": "Full text of the document…",
"form_label_archive_location": "Storage location",
"form_placeholder_archive_location": "e.g. Cabinet 3, Folder B",
"form_helper_archive_location": "Where is the original document stored?",
"login_heading": "Sign in",
"login_label_username": "Username",
"login_label_password": "Password",
"login_btn_submit": "Sign in",
"docs_search_placeholder": "Search in title, content, location...",
"docs_btn_filter": "Filter",
"docs_btn_reset_title": "Reset filter",
"docs_filter_label_tags": "Tags",
"docs_filter_label_sender": "Sender",
"docs_filter_label_receivers": "Recipients",
"docs_filter_label_from": "From",
"docs_filter_label_to": "To",
"docs_btn_new": "New document",
"docs_empty_heading": "No documents found",
"docs_empty_text": "Try adjusting the filters or changing the search term.",
"docs_empty_btn_clear": "Clear all filters",
"docs_list_from": "From",
"docs_list_to": "To",
"docs_list_unknown": "Unknown",
"doc_section_who_when": "Who & When",
"doc_section_description": "Description",
"doc_section_file": "File",
"doc_file_upload_label": "Upload file",
"doc_file_upload_note": "(optional)",
"doc_file_replace_label": "Upload new file",
"doc_file_replace_note": "(replaces the current file)",
"doc_current_file_label": "Current file:",
"doc_more_details": "More details",
"doc_new_heading": "New document",
"doc_edit_heading": "Edit",
"doc_section_details": "Details",
"doc_label_document_date": "Document date",
"doc_label_creation_location": "Place of creation",
"doc_label_archive_location_original": "Storage location (original)",
"doc_section_persons": "Persons",
"doc_sender_not_specified": "Not specified",
"doc_no_receivers": "No recipients",
"doc_section_content": "Content",
"doc_label_summary": "Summary",
"doc_loading": "Loading document...",
"doc_download_link": "Try direct download",
"doc_no_scan": "No scan available",
"persons_heading": "Person directory",
"persons_subtitle": "Browse the index of all recorded persons in the family archive.",
"persons_btn_new": "New person",
"persons_search_placeholder": "Search names...",
"persons_empty_heading": "No persons found.",
"persons_empty_text": "Try a different search term.",
"persons_new_heading": "New person",
"persons_section_details": "Person details",
"person_edit_heading": "Edit person",
"person_label_full_name": "Full name",
"person_merge_heading": "Merge person",
"person_merge_description": "This person will be merged into the selected target person. All documents and links will be transferred, then this person will be deleted.",
"person_merge_target_label": "Merge with",
"person_btn_merge": "Merge",
"person_btn_merge_confirm": "Yes, merge",
"person_merge_warning": "Warning: This action cannot be undone.",
"person_label_notes": "Notes",
"person_placeholder_notes": "Biographical notes, remarks…",
"person_label_birth_year": "Birth year",
"person_label_death_year": "Death year",
"person_placeholder_year": "e.g. 1923",
"person_year_error": "Please enter a four-digit year",
"person_years_error_order": "Birth year must be before death year",
"person_docs_heading": "Sent documents",
"person_no_docs": "This person has not yet been linked as a sender.",
"person_received_docs_heading": "Received documents",
"person_no_received_docs": "This person has not yet been linked as a receiver.",
"person_role_sender": "Sent",
"person_role_receiver": "Received",
"person_co_correspondents_heading": "Frequent correspondents",
"person_show_more": "+ {count} more",
"conv_heading": "Conversations",
"conv_subtitle": "Follow the correspondence between two persons chronologically.",
"conv_label_person_a": "Person A (Sender)",
"conv_label_person_b": "Person B (Recipient)",
"conv_label_from": "Period from",
"conv_label_to": "Period to",
"conv_sort_label": "Sort:",
"conv_sort_newest": "Newest first",
"conv_sort_oldest": "Oldest first",
"conv_empty_heading": "Select two persons",
"conv_empty_text": "The correspondence will be shown here.",
"conv_no_results_heading": "No documents found.",
"conv_no_results_text": "Try adjusting the time period.",
"conv_swap_btn": "Swap persons",
"conv_summary": "{count} documents · {yearFrom}{yearTo}",
"conv_new_doc_link": "New document in this correspondence",
"admin_heading": "Admin Dashboard",
"admin_tab_users": "Users",
"admin_tab_groups": "Groups",
"admin_tab_tags": "Tags",
"admin_section_users": "User management",
"admin_col_login": "Login",
"admin_col_groups": "Groups",
"admin_col_password": "Password",
"admin_multiselect_hint": "Ctrl+Click to select",
"admin_password_placeholder": "New PW (optional)",
"admin_no_groups": "No groups",
"admin_btn_delete_user_title": "Delete user",
"admin_section_new_user": "Create new user",
"admin_multiselect_hint_multi": "Ctrl+Click for multiple",
"admin_multiselect_hint_full": "Ctrl+Click for multiple selection",
"admin_section_tags": "Tags",
"admin_tags_warning": "Warning: Renaming or deleting affects all linked documents.",
"admin_btn_edit_tag_label": "Edit tag",
"admin_tag_delete_confirm": "Really delete? The tag will be removed from all documents.",
"admin_btn_delete_tag_label": "Delete tag",
"admin_section_groups": "Group management",
"admin_col_name": "Name",
"admin_col_permissions": "Permissions",
"admin_col_actions": "Actions",
"admin_group_delete_confirm": "Really delete group?",
"admin_section_new_group": "Create new group",
"admin_group_name_placeholder": "Group name (e.g. Editors)",
"admin_user_delete_confirm": "Really delete user {username}?",
"admin_btn_new_user": "New User",
"admin_user_new_heading": "Create new user",
"admin_user_edit_heading": "Edit user: {username}",
"admin_user_created": "User has been created.",
"admin_user_updated": "Changes saved.",
"admin_col_full_name": "Name",
"admin_label_new_password_optional": "New password (optional)",
"admin_label_initial_password": "Password",
"doc_file_error_preview": "Could not load preview.",
"doc_download_title": "Download",
"doc_tag_filter_title": "Filter by {name}",
"doc_conversation_title": "Show conversation",
"doc_preview_iframe_title": "Document Preview",
"doc_image_alt": "Original scan",
"doc_no_date": "No date",
"person_merge_will_be_deleted": "will be deleted.",
"comp_typeahead_placeholder": "Type a name...",
"comp_typeahead_loading": "Searching...",
"comp_multiselect_placeholder": "Type a name...",
"comp_multiselect_remove": "Remove",
"comp_multiselect_loading": "Searching...",
"comp_taginput_placeholder_create": "Add tags...",
"comp_taginput_placeholder_filter": "Filter by tags...",
"comp_taginput_remove": "Remove tag",
"comp_taginput_create_hint": "Press Enter to create tag.",
"error_email_already_in_use": "This email address is already used by another account.",
"error_wrong_current_password": "The current password is incorrect.",
"nav_profile": "Profile",
"profile_heading": "My Profile",
"profile_section_personal": "Personal Information",
"profile_label_first_name": "First name",
"profile_label_last_name": "Last name",
"profile_label_birth_date": "Date of birth",
"profile_label_email": "Email address",
"profile_label_contact": "Contact details",
"profile_contact_placeholder": "Phone, address or other notes...",
"profile_section_password": "Change password",
"profile_label_current_password": "Current password",
"profile_label_new_password": "New password",
"profile_label_new_password_confirm": "New password (repeat)",
"profile_password_mismatch": "The new passwords do not match.",
"profile_saved": "Saved.",
"profile_password_changed": "Password changed successfully.",
"user_profile_heading": "Profile of",
"error_invalid_reset_token": "The link is invalid or has expired.",
"forgot_password_heading": "Forgot password",
"forgot_password_email_label": "Email address",
"forgot_password_submit": "Request link",
"forgot_password_success": "If an account with this email address exists, you will shortly receive an email with a link to reset your password.",
"forgot_password_back_to_login": "Back to login",
"reset_password_heading": "Set new password",
"reset_password_label": "New password",
"reset_password_confirm_label": "Confirm password",
"reset_password_submit": "Save password",
"reset_password_mismatch": "The passwords do not match.",
"reset_password_success": "Your password has been changed successfully. You can now log in.",
"login_forgot_password": "Forgot password?",
"history_section_title": "History",
"history_loading": "Loading history…",
"history_empty": "No versions yet.",
"history_version_label": "Version",
"history_compare_mode": "Compare",
"history_compare_select_a": "Version A",
"history_compare_select_b": "Version B",
"history_compare_apply": "Compare",
"history_diff_no_changes": "No changes between these versions.",
"history_field_title": "Title",
"history_field_document_date": "Date",
"history_field_location": "Location",
"history_field_document_location": "Archive location",
"history_field_transcription": "Transcription",
"history_field_summary": "Summary",
"history_field_sender": "Sender",
"history_field_receivers": "Receivers",
"history_field_tags": "Tags",
"admin_tab_system": "System",
"admin_system_backfill_heading": "Backfill history data",
"admin_system_backfill_description": "Creates an initial history entry for all documents that do not have one yet (e.g. imported documents). This ensures that future edits only highlight actually changed fields.",
"admin_system_backfill_btn": "Backfill now",
"admin_system_backfill_success": "{count} documents were backfilled.",
"admin_system_backfill_hashes_heading": "Compute file hashes",
"admin_system_backfill_hashes_description": "Computes the SHA-256 hash for all previously uploaded documents that do not have one yet. This ensures annotations are correctly linked to their file version and shown again.",
"admin_system_backfill_hashes_btn": "Compute file hashes",
"admin_system_backfill_hashes_success": "{count} documents were updated.",
"comp_expandable_show_more": "Show more",
"comp_expandable_show_less": "Show less",
"error_comment_not_found": "The comment could not be found.",
"comment_section_title": "Discussion",
"comment_placeholder": "Write a comment…",
"comment_btn_post": "Send",
"comment_btn_reply": "Reply",
"comment_edited_label": "· edited",
"comment_panel_title": "Comments",
"comment_panel_close": "Close",
"doc_panel_tab_metadata": "Metadata",
"doc_panel_tab_transcription": "Transcription",
"doc_panel_tab_discussion": "Discussion",
"doc_panel_tab_history": "History",
"doc_panel_annotate": "Annotate",
"doc_panel_annotate_stop": "Done",
"doc_panel_annotation_thread_title": "Annotation",
"doc_panel_discussion_annotation_tab": "Annotation · Page {page}",
"pdf_annotations_show": "Show annotations",
"pdf_annotations_hide": "Hide annotations",
"upload_drop_hint": "Drop one or multiple files at once",
"upload_accepted_types": "PDF, JPEG, PNG, TIFF",
"upload_filename_hint": "Tip: 2024-03-15_Mueller_Hans.pdf → date and sender pre-filled",
"upload_success": "{count} document(s) created",
"upload_duplicate": "{filename} already exists —",
"upload_duplicate_link": "View document",
"upload_invalid_type": "{filename}: unsupported file format",
"upload_error": "Error uploading {filename}",
"enrich_list_back": "Back to overview",
"enrich_list_count": "documents",
"btn_save_and_mark_reviewed": "Save & mark as reviewed",
"btn_mark_for_review": "Mark for review",
"enrich_needs_metadata_title": "Documents without metadata",
"enrich_needs_metadata_count": "{count} document(s) waiting for metadata",
"enrich_needs_metadata_cta": "Complete now",
"enrich_list_heading": "Documents without metadata",
"enrich_list_empty_heading": "All documents complete",
"enrich_list_empty_body": "There are no documents that still need metadata.",
"enrich_list_start": "Start reviewing",
"enrich_progress": "{count} remaining",
"enrich_skip": "Skip",
"enrich_done_heading": "All done!",
"enrich_done_body": "All documents have been processed.",
"enrich_back_to_list": "Back to list",
"comment_empty_hint": "No comments yet start the discussion!",
"comment_start_discussion": "Start discussion →",
"notification_bell_label": "Notifications",
"notification_bell_unread_label": "{count} unread notifications",
"notification_mark_all_read": "Mark all read",
"notification_empty": "No new notifications",
"notification_type_reply": "{actor} replied to your comment",
"notification_type_mention": "{actor} mentioned you in a comment",
"notification_prefs_heading": "Notifications",
"notification_pref_reply": "Email when someone replies to my comment",
"notification_pref_mention": "Email when someone mentions me in a comment",
"notification_unread": "unread",
"mention_btn_label": "Mention person",
"mention_popup_empty": "No users found"
}