feat: centralize API error localization
Replace the err instanceof ApiError ? err.message : t(fallback) anti-
pattern -- which showed raw English backend text for the common case and
only used the localized fallback for the rare network-failure case -- at
all 13 call sites across 7 files.
New frontend/src/api/errorMessages.ts (describeApiError) resolves a
caught error to a localized {title, explanation, nextStep?, technical}
by checking the 32 known AppError codes first, then known HTTP statuses
(401/403/404/409/422/500), then a fully generic fallback. New
ApiErrorNotice (PageChrome.tsx) renders title/explanation/nextStep with
the raw text demoted to a "Technical details"/"Details techniques"
disclosure -- never shown as the primary message.
ApiError itself is split out of client.ts into a standalone
api/apiError.ts with no import.meta.env dependency, so errorMessages.ts
(and its tests) can be loaded outside a Vite/browser context.
This commit is contained in:
@@ -1,8 +1,180 @@
|
||||
{
|
||||
"generic": "Something went wrong. Please try again.",
|
||||
"workspaceLoadFailed": "We couldn't load this workspace.",
|
||||
"unauthorized": "Your session has expired. Please log in again.",
|
||||
"forbidden": "You don't have access to this section.",
|
||||
"notFound": "This record could not be found.",
|
||||
"networkUnavailable": "The connection to the server is currently unavailable."
|
||||
"generic": {
|
||||
"title": "Something went wrong",
|
||||
"explanation": "This action could not be completed. Please try again."
|
||||
},
|
||||
"codes": {
|
||||
"VEHICLE_NOT_FOUND": {
|
||||
"title": "Vehicle not found",
|
||||
"explanation": "This vehicle could not be found. It may have been removed or the reference may be incorrect."
|
||||
},
|
||||
"BOOKING_NOT_FOUND": {
|
||||
"title": "Booking not found",
|
||||
"explanation": "This booking could not be found. It may have been removed or the reference may be incorrect."
|
||||
},
|
||||
"CUSTOMER_NOT_FOUND": {
|
||||
"title": "Customer not found",
|
||||
"explanation": "This customer record could not be found."
|
||||
},
|
||||
"ENTITY_NOT_FOUND": {
|
||||
"title": "Record not found",
|
||||
"explanation": "The underlying record for this action could not be found."
|
||||
},
|
||||
"EVENT_NOT_FOUND": {
|
||||
"title": "Workflow event not found",
|
||||
"explanation": "This automation event could not be found."
|
||||
},
|
||||
"ISSUE_NOT_FOUND": {
|
||||
"title": "Data-quality issue not found",
|
||||
"explanation": "This data-quality issue could not be found."
|
||||
},
|
||||
"ISSUE_NOT_OPEN": {
|
||||
"title": "Issue is no longer open",
|
||||
"explanation": "This issue has already been resolved, deferred or rejected.",
|
||||
"nextStep": "Refresh the page to see its current state."
|
||||
},
|
||||
"BOOKING_NOT_ACTIVE": {
|
||||
"title": "Booking is not active",
|
||||
"explanation": "Only a reserved or active booking can be used for this action."
|
||||
},
|
||||
"INVALID_BOOKING_STATE": {
|
||||
"title": "Booking is in the wrong state",
|
||||
"explanation": "This booking's current state does not allow this action."
|
||||
},
|
||||
"NOT_RETRYABLE": {
|
||||
"title": "This event cannot be retried",
|
||||
"explanation": "Only a failed delivery can be retried."
|
||||
},
|
||||
"CONFLICT_STILL_PRESENT": {
|
||||
"title": "Conflict was not resolved",
|
||||
"explanation": "Applying this change did not resolve the underlying conflict.",
|
||||
"nextStep": "Review the recommendation again before retrying."
|
||||
},
|
||||
"OVERLAP_STILL_PRESENT": {
|
||||
"title": "Overlap was not resolved",
|
||||
"explanation": "Blocking this booking did not remove the overlap; another commitment remains."
|
||||
},
|
||||
"EMPTY_VALUE": {
|
||||
"title": "A required field is blank",
|
||||
"explanation": "This field cannot be left blank.",
|
||||
"nextStep": "Enter a value and try again."
|
||||
},
|
||||
"NO_FIELDS_PROVIDED": {
|
||||
"title": "No changes provided",
|
||||
"explanation": "At least one field must be filled in to continue."
|
||||
},
|
||||
"INVALID_FIELD": {
|
||||
"title": "Field not allowed here",
|
||||
"explanation": "One of the fields provided is not permitted for this action."
|
||||
},
|
||||
"INVALID_FIELD_OVERRIDE": {
|
||||
"title": "Field cannot be merged",
|
||||
"explanation": "One of the selected fields cannot be used in this merge."
|
||||
},
|
||||
"INVALID_SURVIVOR": {
|
||||
"title": "Invalid selection",
|
||||
"explanation": "The record you selected to keep must be one of the two records being compared."
|
||||
},
|
||||
"INVALID_BOOKING_REFERENCE": {
|
||||
"title": "Booking reference not valid here",
|
||||
"explanation": "The selected booking is not one of the bookings related to this issue."
|
||||
},
|
||||
"INVALID_EVENT_ID": {
|
||||
"title": "Invalid event reference",
|
||||
"explanation": "This automation event reference is not valid."
|
||||
},
|
||||
"INVALID_IDEMPOTENCY_KEY": {
|
||||
"title": "Request could not be repeated safely",
|
||||
"explanation": "This request's tracking key is not valid.",
|
||||
"nextStep": "Reload the page and try again."
|
||||
},
|
||||
"IDEMPOTENCY_KEY_REUSED": {
|
||||
"title": "This action was already submitted",
|
||||
"explanation": "An identical request was already processed with a different outcome.",
|
||||
"nextStep": "Reload the page to see the current state before retrying."
|
||||
},
|
||||
"CORRECTED_VALUE_REQUIRED": {
|
||||
"title": "A corrected value is required",
|
||||
"explanation": "Choose \"correct the reading\" requires entering the corrected value."
|
||||
},
|
||||
"CORRECTION_BELOW_CANONICAL": {
|
||||
"title": "Correction is below the confirmed reading",
|
||||
"explanation": "A corrected odometer reading can never be lower than the last confirmed reading."
|
||||
},
|
||||
"NOT_A_DUPLICATE_ISSUE": {
|
||||
"title": "Wrong issue type",
|
||||
"explanation": "This action only applies to possible-duplicate-customer issues."
|
||||
},
|
||||
"NOT_A_MISSING_FIELD_ISSUE": {
|
||||
"title": "Wrong issue type",
|
||||
"explanation": "This action only applies to missing-required-field issues."
|
||||
},
|
||||
"NOT_AN_ODOMETER_ISSUE": {
|
||||
"title": "Wrong issue type",
|
||||
"explanation": "This action only applies to odometer-regression issues."
|
||||
},
|
||||
"NOT_AN_OVERLAP_ISSUE": {
|
||||
"title": "Wrong issue type",
|
||||
"explanation": "This action only applies to booking-overlap issues."
|
||||
},
|
||||
"NOT_A_STATUS_CONFLICT_ISSUE": {
|
||||
"title": "Wrong issue type",
|
||||
"explanation": "This action only applies to vehicle-status-conflict issues."
|
||||
},
|
||||
"UNSUPPORTED_ENTITY": {
|
||||
"title": "Not supported for this record type",
|
||||
"explanation": "This action is not available for this kind of record."
|
||||
},
|
||||
"MANUAL_REVIEW_REQUIRED": {
|
||||
"title": "Manual review required",
|
||||
"explanation": "The facts for this vehicle contradict each other, so no automatic change is safe.",
|
||||
"nextStep": "Defer or reject this issue, or investigate manually."
|
||||
},
|
||||
"NO_CONFLICT_DETECTED": {
|
||||
"title": "Nothing to apply",
|
||||
"explanation": "The current state no longer conflicts, so there is nothing left to apply."
|
||||
},
|
||||
"RECOMMENDATION_STALE": {
|
||||
"title": "The situation has changed",
|
||||
"explanation": "The underlying facts changed since this recommendation was shown.",
|
||||
"nextStep": "Review the recommendation again before applying it."
|
||||
},
|
||||
"UNAUTHORIZED_SERVICE": {
|
||||
"title": "Service authorisation failed",
|
||||
"explanation": "This automated request could not be authorised."
|
||||
}
|
||||
},
|
||||
"http": {
|
||||
"401": {
|
||||
"title": "Session expired",
|
||||
"explanation": "Your session has expired.",
|
||||
"nextStep": "Please log in again."
|
||||
},
|
||||
"403": {
|
||||
"title": "Permission denied",
|
||||
"explanation": "You don't have permission to perform this action."
|
||||
},
|
||||
"404": {
|
||||
"title": "Not found",
|
||||
"explanation": "This record could not be found."
|
||||
},
|
||||
"409": {
|
||||
"title": "This action is no longer possible",
|
||||
"explanation": "The underlying state has changed since this page was loaded.",
|
||||
"nextStep": "Refresh the page and try again."
|
||||
},
|
||||
"422": {
|
||||
"title": "Validation failed",
|
||||
"explanation": "The information provided is not valid."
|
||||
},
|
||||
"500": {
|
||||
"title": "Server error",
|
||||
"explanation": "Something went wrong on our side."
|
||||
},
|
||||
"network": {
|
||||
"title": "Connection unavailable",
|
||||
"explanation": "The connection to the server is currently unavailable.",
|
||||
"nextStep": "Check your connection and try again."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user