fix(errors): show a route's Swedish message even without a keyword match (#2086) (#2253)

* fix(errors): show a route's Swedish message even without a keyword match (#2086)

getErrorMessage passed a route's free-text `error` / `message` through
only if it contained one of ~30 keywords ("kunde inte", "saknas", ...).
"Inget skattekonto är registrerat hos Skatteverket." has none, so the
skattekonto sync replaced the one sentence that would have helped with
"Ett oväntat serverfel uppstod. Försök igen senare.", which is wrong
advice for a company without a skattekonto. 155 of the 631 message_sv
strings in structured-errors.ts failed the same keyword test.

A second way in: looksLikeUserFacingSwedish accepts a string that reads
as Swedish (å/ä/ö, a strong Swedish word, or two weak function words)
and shows no sign of a technical leak (stack frames, file:line, JS/Node
error vocabulary, Postgres/PostgREST/SQL fragments, JSON, URLs). The
keyword list stays as the first way in. English framework text still
falls through to the status/context fallback, and a registry-wide test
pins that every message_sv now passes.

Closes #2086

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VnConrmMCxJRQ5kfiPPWyy

* test(errors): pin the two call sites whose Swedish messages now pass through (#2086)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VnConrmMCxJRQ5kfiPPWyy

---------

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-09-03 18:55:33 +02:00
committed by GitHub
co-authored by Claude Fable 5.1 Jakob Wennberg
parent 5070041028
commit c3b7e79af8
4 changed files with 180 additions and 11 deletions
+70 -2
View File
@@ -1,6 +1,11 @@
import { describe, it, expect } from 'vitest'
import { getErrorMessage, getProviderResourceForbiddenMessage } from '../get-error-message'
import { getErrorEntry } from '../structured-errors'
import {
getErrorMessage,
getProviderResourceForbiddenMessage,
isSwedishUserMessage,
looksLikeUserFacingSwedish,
} from '../get-error-message'
import { getErrorEntry, listErrorCodes } from '../structured-errors'
import {
AccountsNotInChartError,
BookkeepingDatabaseError,
@@ -601,3 +606,66 @@ describe('getErrorMessage: INVOICE_SEND_PAYMENT_ACCOUNT_MISSING (#2126)', () =>
expect(unknown).toBe(getErrorEntry('INVOICE_SEND_PAYMENT_ACCOUNT_MISSING')!.message_sv)
})
})
// Issue #2086: the keyword heuristic dropped correct Swedish route messages
// that happened to lack one of its ~30 keywords and replaced them with generic
// HTTP text whose advice was sometimes wrong. A sentence that reads as Swedish
// and shows no sign of a technical leak now passes through.
describe('getErrorMessage: Swedish route messages without a keyword pass through (#2086)', () => {
it('shows the skattekonto sync reason instead of the generic 500 text', () => {
const msg = getErrorMessage(
{ error: 'Inget skattekonto är registrerat hos Skatteverket.' },
{ statusCode: 500 },
)
expect(msg).toBe('Inget skattekonto är registrerat hos Skatteverket.')
})
it.each([
'Datumet ligger utanför det valda räkenskapsåret.',
'Rättelsen motsvarar ingen ekonomisk händelse: det finns inget att rätta.',
'Funktionen är inte implementerad ännu.',
])('passes through %s', (text) => {
expect(getErrorMessage({ error: text }, { statusCode: 400 })).toBe(text)
expect(getErrorMessage({ message: text }, { statusCode: 400 })).toBe(text)
})
it('still hides English and technical text behind the status/context fallback', () => {
expect(getErrorMessage({ error: 'Failed to fetch customer' }, { statusCode: 500 })).toBe(
'Ett oväntat serverfel uppstod. Försök igen senare.',
)
expect(
getErrorMessage({ error: 'TypeError: Cannot read properties of undefined (reading "id")' }, { statusCode: 500 }),
).toBe('Ett oväntat serverfel uppstod. Försök igen senare.')
// Swedish words next to a leak are still a leak.
expect(
getErrorMessage({ error: 'Kontot är trasigt: TypeError: x is not a function' }, { statusCode: 500 }),
).toBe('Ett oväntat serverfel uppstod. Försök igen senare.')
expect(
getErrorMessage({ error: 'Det gick inte att läsa relation "public.invoices"' }, { statusCode: 500 }),
).toBe('Ett oväntat serverfel uppstod. Försök igen senare.')
})
it('looksLikeUserFacingSwedish scores on å/ä/ö, a strong Swedish word, or two weak ones, never on English', () => {
expect(looksLikeUserFacingSwedish('Inget skattekonto är registrerat hos Skatteverket.')).toBe(true)
expect(looksLikeUserFacingSwedish('Det finns inget att rätta.')).toBe(true)
expect(looksLikeUserFacingSwedish('Kopplingen misslyckades.')).toBe(true)
expect(looksLikeUserFacingSwedish('Ingen fil bifogad.')).toBe(true)
expect(looksLikeUserFacingSwedish('Kan hittas med den.')).toBe(true) // two weak words
expect(looksLikeUserFacingSwedish('Not found')).toBe(false)
expect(looksLikeUserFacingSwedish('Request failed with status 500')).toBe(false)
expect(looksLikeUserFacingSwedish('Invalid input: expected string, received undefined')).toBe(false)
expect(looksLikeUserFacingSwedish('')).toBe(false)
// A lone weak word is not enough ("till" is also English).
expect(looksLikeUserFacingSwedish('Redirect till /login')).toBe(false)
expect(looksLikeUserFacingSwedish('Set the value for det')).toBe(false)
})
it('every message_sv in the structured-error registry passes the combined test', () => {
const failing = listErrorCodes().filter((code) => {
const entry = getErrorEntry(code)
return entry ? !isSwedishUserMessage(entry.message_sv) : false
})
expect(failing).toEqual([])
})
})