201 lines
6.4 KiB
TypeScript
201 lines
6.4 KiB
TypeScript
/**
|
|
* Maps raw errors to user-friendly Swedish messages.
|
|
*
|
|
* Priority chain:
|
|
* 1. Zod validation field errors
|
|
* 2. Postgres error code map
|
|
* 3. HTTP status code map
|
|
* 4. Context-specific fallback
|
|
* 5. Generic fallback
|
|
*/
|
|
|
|
type ErrorContext =
|
|
| 'invoice'
|
|
| 'supplier_invoice'
|
|
| 'customer'
|
|
| 'supplier'
|
|
| 'transaction'
|
|
| 'journal_entry'
|
|
| 'settings'
|
|
| 'auth'
|
|
|
|
interface GetErrorMessageOptions {
|
|
context?: ErrorContext
|
|
statusCode?: number
|
|
}
|
|
|
|
// Postgres error codes -> Swedish messages
|
|
const POSTGRES_ERROR_MAP: Record<string, string> = {
|
|
'23505': 'En post med samma uppgifter finns redan.',
|
|
'23503': 'Posten kan inte ändras eftersom den refereras av annan data.',
|
|
'23502': 'Ett obligatoriskt fält saknas.',
|
|
'42501': 'Du har inte behörighet att utföra denna åtgärd.',
|
|
'42P01': 'Resursen kunde inte hittas.',
|
|
'23514': 'Värdet uppfyller inte de tillåtna kraven.',
|
|
'40001': 'En annan ändring pågick samtidigt. Försök igen.',
|
|
'40P01': 'En konflikt uppstod. Försök igen.',
|
|
'22P02': 'Ogiltigt värde angavs.',
|
|
'22003': 'Värdet är utanför tillåtet intervall.',
|
|
}
|
|
|
|
// HTTP status codes -> Swedish messages
|
|
const HTTP_STATUS_MAP: Record<number, string> = {
|
|
400: 'Förfrågan innehåller ogiltiga uppgifter.',
|
|
401: 'Din session har gått ut. Logga in igen.',
|
|
403: 'Du har inte behörighet att utföra denna åtgärd.',
|
|
404: 'Resursen kunde inte hittas.',
|
|
409: 'En konflikt uppstod. Ladda om sidan och försök igen.',
|
|
422: 'Uppgifterna kunde inte bearbetas. Kontrollera fälten och försök igen.',
|
|
429: 'För många förfrågningar. Vänta en stund och försök igen.',
|
|
500: 'Ett oväntat serverfel uppstod. Försök igen senare.',
|
|
502: 'Servern är tillfälligt otillgänglig. Försök igen om en stund.',
|
|
503: 'Tjänsten är tillfälligt otillgänglig. Försök igen om en stund.',
|
|
}
|
|
|
|
// Context-specific fallbacks
|
|
const CONTEXT_FALLBACKS: Record<ErrorContext, string> = {
|
|
invoice: 'Kunde inte hantera fakturan. Försök igen.',
|
|
supplier_invoice: 'Kunde inte hantera leverantörsfakturan. Försök igen.',
|
|
customer: 'Kunde inte hantera kunden. Försök igen.',
|
|
supplier: 'Kunde inte hantera leverantören. Försök igen.',
|
|
transaction: 'Kunde inte hantera transaktionen. Försök igen.',
|
|
journal_entry: 'Kunde inte hantera verifikationen. Försök igen.',
|
|
settings: 'Kunde inte spara inställningarna. Försök igen.',
|
|
auth: 'Ett fel uppstod vid inloggningen. Försök igen.',
|
|
}
|
|
|
|
const GENERIC_FALLBACK = 'Något gick fel. Försök igen.'
|
|
|
|
/**
|
|
* Simple heuristic to detect already-translated Swedish messages.
|
|
* If the message contains common Swedish words/patterns, pass it through.
|
|
*/
|
|
function isSwedishUserMessage(message: string): boolean {
|
|
const swedishPatterns = [
|
|
/kunde inte/i,
|
|
/försök igen/i,
|
|
/ogiltigt?/i,
|
|
/saknas/i,
|
|
/måste/i,
|
|
/redan finns/i,
|
|
/gick fel/i,
|
|
/behörighet/i,
|
|
/session/i,
|
|
/förfrågan/i,
|
|
/obligatorisk/i,
|
|
]
|
|
return swedishPatterns.some((p) => p.test(message))
|
|
}
|
|
|
|
/**
|
|
* Extract a user-friendly message from a Zod validation error shape.
|
|
* Returns null if the error is not a Zod error.
|
|
*/
|
|
function tryParseZodErrors(error: unknown): string | null {
|
|
if (typeof error !== 'object' || error === null) return null
|
|
|
|
const obj = error as Record<string, unknown>
|
|
|
|
// Check for Zod-style field errors: { fieldName: ["message"] } or { issues: [...] }
|
|
if (Array.isArray(obj.issues)) {
|
|
const issues = obj.issues as Array<{ message?: string; path?: string[] }>
|
|
const messages = issues
|
|
.slice(0, 3)
|
|
.map((issue) => {
|
|
const field = issue.path?.join('.') || ''
|
|
const msg = issue.message || 'ogiltigt värde'
|
|
return field ? `${field}: ${msg}` : msg
|
|
})
|
|
if (messages.length > 0) return messages.join('. ')
|
|
}
|
|
|
|
// Check for { errors: { field: ["msg"] } } shape from validateBody
|
|
if (typeof obj.errors === 'object' && obj.errors !== null) {
|
|
const fieldErrors = obj.errors as Record<string, string[]>
|
|
const messages: string[] = []
|
|
for (const [field, msgs] of Object.entries(fieldErrors)) {
|
|
if (Array.isArray(msgs) && msgs.length > 0) {
|
|
messages.push(`${field}: ${msgs[0]}`)
|
|
}
|
|
if (messages.length >= 3) break
|
|
}
|
|
if (messages.length > 0) return messages.join('. ')
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
/**
|
|
* Get a user-friendly Swedish error message from a raw error.
|
|
*
|
|
* @param error - The raw error. Can be an API response body (object), Error instance, string, or unknown.
|
|
* @param options - Optional context and HTTP status code.
|
|
*/
|
|
export function getErrorMessage(
|
|
error: unknown,
|
|
options: GetErrorMessageOptions = {}
|
|
): string {
|
|
const { context, statusCode } = options
|
|
|
|
// 1. If it's a string, check if it's already Swedish
|
|
if (typeof error === 'string' && error.trim()) {
|
|
if (isSwedishUserMessage(error)) return error
|
|
}
|
|
|
|
// 2. If it's an object, try various parsing strategies
|
|
if (typeof error === 'object' && error !== null) {
|
|
const obj = error as Record<string, unknown>
|
|
|
|
// Try Zod validation errors
|
|
const zodMessage = tryParseZodErrors(obj)
|
|
if (zodMessage) return zodMessage
|
|
|
|
// Try Postgres error code
|
|
if (typeof obj.code === 'string' && POSTGRES_ERROR_MAP[obj.code]) {
|
|
return POSTGRES_ERROR_MAP[obj.code]
|
|
}
|
|
|
|
// Try error.message if it's already a good Swedish message
|
|
if (typeof obj.error === 'string' && obj.error.trim()) {
|
|
if (isSwedishUserMessage(obj.error)) return obj.error
|
|
}
|
|
|
|
if (typeof obj.message === 'string' && obj.message.trim()) {
|
|
if (isSwedishUserMessage(obj.message)) return obj.message
|
|
}
|
|
}
|
|
|
|
// 3. Error instance
|
|
if (error instanceof Error && error.message.trim()) {
|
|
if (isSwedishUserMessage(error.message)) return error.message
|
|
}
|
|
|
|
// 4. HTTP status code map
|
|
if (statusCode && HTTP_STATUS_MAP[statusCode]) {
|
|
return HTTP_STATUS_MAP[statusCode]
|
|
}
|
|
|
|
// 5. Context-specific fallback
|
|
if (context && CONTEXT_FALLBACKS[context]) {
|
|
return CONTEXT_FALLBACKS[context]
|
|
}
|
|
|
|
// 6. Generic fallback
|
|
return GENERIC_FALLBACK
|
|
}
|
|
|
|
/**
|
|
* Helper that parses a Response body and returns a user-friendly error message.
|
|
*/
|
|
export async function getResponseErrorMessage(
|
|
response: Response,
|
|
context?: ErrorContext
|
|
): Promise<string> {
|
|
try {
|
|
const body = await response.json()
|
|
return getErrorMessage(body, { context, statusCode: response.status })
|
|
} catch {
|
|
return getErrorMessage(null, { context, statusCode: response.status })
|
|
}
|
|
}
|