Files
familienarchiv/frontend/src/lib/errors.ts
Marcel 8016cb6905
Some checks failed
CI / Unit & Component Tests (push) Has been cancelled
CI / OCR Service Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
feat(stammbaum): add i18n keys (de/en/es) + mirror error codes
In each of de/en/es:
- nav_stammbaum
- 9 relation_<type>_of keys for the stored relation types
- 17 relation_inferred_<label> keys covering everything LABEL_MAP emits
  (parent/child/spouse/sibling, grand*, great-grand*, uncle/aunt,
  niece/nephew, in-laws, cousin, distant)
- doc_details_field_relationship — badge label "Verwandtschaft"
- stammbaum_empty_*, stammbaum_panel_*, stammbaum_zoom_*,
  stammbaum_generations
- relation_error_* (inline form errors), relation_year_error_*,
  relation_label_*, relation_btn_*
- person_relationships_heading + person_relationships_empty
- error_relationship_not_found / error_circular_relationship /
  error_duplicate_relationship for the centralised error mapper

frontend/src/lib/errors.ts mirrors the backend's three new ErrorCodes
and routes them through getErrorMessage().

Refs #358.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 14:40:25 +02:00

164 lines
4.9 KiB
TypeScript

import * as m from '$lib/paraglide/messages.js';
/**
* Mirror of the backend ErrorCode enum.
* Keep in sync with backend/src/main/java/org/raddatz/familienarchiv/exception/ErrorCode.java
*/
export type ErrorCode =
| 'PERSON_NOT_FOUND'
| 'ALIAS_NOT_FOUND'
| 'INVALID_PERSON_TYPE'
| 'DOCUMENT_NOT_FOUND'
| 'DOCUMENT_NO_FILE'
| 'FILE_NOT_FOUND'
| 'FILE_UPLOAD_FAILED'
| 'UNSUPPORTED_FILE_TYPE'
| 'USER_NOT_FOUND'
| 'EMAIL_ALREADY_IN_USE'
| 'WRONG_CURRENT_PASSWORD'
| 'IMPORT_ALREADY_RUNNING'
| 'INVALID_RESET_TOKEN'
| 'INVITE_NOT_FOUND'
| 'INVITE_EXHAUSTED'
| 'INVITE_REVOKED'
| 'INVITE_EXPIRED'
| 'ANNOTATION_NOT_FOUND'
| 'ANNOTATION_UPDATE_FAILED'
| 'TRANSCRIPTION_BLOCK_NOT_FOUND'
| 'TRANSCRIPTION_BLOCK_CONFLICT'
| 'COMMENT_NOT_FOUND'
| 'OCR_SERVICE_UNAVAILABLE'
| 'OCR_JOB_NOT_FOUND'
| 'OCR_DOCUMENT_NOT_UPLOADED'
| 'OCR_PROCESSING_FAILED'
| 'TRAINING_ALREADY_RUNNING'
| 'OCR_TRAINING_CONFLICT'
| 'INVALID_TAG_COLOR'
| 'TAG_CYCLE_DETECTED'
| 'TAG_NOT_FOUND'
| 'TAG_MERGE_SELF'
| 'TAG_MERGE_INVALID_TARGET'
| 'RELATIONSHIP_NOT_FOUND'
| 'CIRCULAR_RELATIONSHIP'
| 'DUPLICATE_RELATIONSHIP'
| 'MISSING_CREDENTIALS'
| 'UNAUTHORIZED'
| 'FORBIDDEN'
| 'VALIDATION_ERROR'
| 'BATCH_TOO_LARGE'
| 'BULK_EDIT_TOO_MANY_IDS'
| 'INTERNAL_ERROR';
export interface BackendError {
code: ErrorCode;
message: string; // English developer message — not shown to users
}
/**
* Attempts to parse a backend ErrorResponse from a failed fetch response.
* Returns null if the body is not valid JSON or does not contain a code field.
*/
export async function parseBackendError(res: Response): Promise<BackendError | null> {
try {
const body = await res.json();
if (body && typeof body.code === 'string') {
return body as BackendError;
}
} catch {
// Body was not JSON
}
return null;
}
/** Returns a localised message for the given error code via Paraglide. Falls back to INTERNAL_ERROR. */
export function getErrorMessage(code: ErrorCode | string | undefined): string {
switch (code) {
case 'PERSON_NOT_FOUND':
return m.error_person_not_found();
case 'ALIAS_NOT_FOUND':
return m.error_alias_not_found();
case 'INVALID_PERSON_TYPE':
return m.error_invalid_person_type();
case 'DOCUMENT_NOT_FOUND':
return m.error_document_not_found();
case 'DOCUMENT_NO_FILE':
return m.error_document_no_file();
case 'FILE_NOT_FOUND':
return m.error_file_not_found();
case 'FILE_UPLOAD_FAILED':
return m.error_file_upload_failed();
case 'UNSUPPORTED_FILE_TYPE':
return m.error_unsupported_file_type();
case 'USER_NOT_FOUND':
return m.error_user_not_found();
case 'EMAIL_ALREADY_IN_USE':
return m.error_email_already_in_use();
case 'WRONG_CURRENT_PASSWORD':
return m.error_wrong_current_password();
case 'IMPORT_ALREADY_RUNNING':
return m.error_import_already_running();
case 'INVALID_RESET_TOKEN':
return m.error_invalid_reset_token();
case 'INVITE_NOT_FOUND':
return m.error_invite_not_found();
case 'INVITE_EXHAUSTED':
return m.error_invite_exhausted();
case 'INVITE_REVOKED':
return m.error_invite_revoked();
case 'INVITE_EXPIRED':
return m.error_invite_expired();
case 'ANNOTATION_NOT_FOUND':
return m.error_annotation_not_found();
case 'ANNOTATION_UPDATE_FAILED':
return m.error_annotation_update_failed();
case 'TRANSCRIPTION_BLOCK_NOT_FOUND':
return m.error_transcription_block_not_found();
case 'TRANSCRIPTION_BLOCK_CONFLICT':
return m.error_transcription_block_conflict();
case 'COMMENT_NOT_FOUND':
return m.error_comment_not_found();
case 'OCR_SERVICE_UNAVAILABLE':
return m.error_ocr_service_unavailable();
case 'OCR_JOB_NOT_FOUND':
return m.error_ocr_job_not_found();
case 'OCR_DOCUMENT_NOT_UPLOADED':
return m.error_ocr_document_not_uploaded();
case 'OCR_PROCESSING_FAILED':
return m.error_ocr_processing_failed();
case 'TRAINING_ALREADY_RUNNING':
return m.error_training_already_running();
case 'OCR_TRAINING_CONFLICT':
return m.error_internal_error();
case 'INVALID_TAG_COLOR':
return m.error_invalid_tag_color();
case 'TAG_CYCLE_DETECTED':
return m.error_tag_cycle_detected();
case 'TAG_NOT_FOUND':
return m.error_tag_not_found();
case 'TAG_MERGE_SELF':
return m.error_tag_merge_self();
case 'TAG_MERGE_INVALID_TARGET':
return m.error_tag_merge_invalid_target();
case 'RELATIONSHIP_NOT_FOUND':
return m.error_relationship_not_found();
case 'CIRCULAR_RELATIONSHIP':
return m.error_circular_relationship();
case 'DUPLICATE_RELATIONSHIP':
return m.error_duplicate_relationship();
case 'MISSING_CREDENTIALS':
return m.login_error_missing_credentials();
case 'UNAUTHORIZED':
return m.error_unauthorized();
case 'FORBIDDEN':
return m.error_forbidden();
case 'VALIDATION_ERROR':
return m.error_validation_error();
case 'BATCH_TOO_LARGE':
return m.error_batch_too_large();
case 'BULK_EDIT_TOO_MANY_IDS':
return m.error_bulk_edit_too_many_ids();
default:
return m.error_internal_error();
}
}