fix: localise backend failures through the error contract - #125
Merged
Conversation
describeError returned error.message verbatim, and normalizeWailsError wraps every backend failure in OneAgentApiError, so the Chinese t() fallbacks the 19 call sites passed almost never fired. Users read English inside a Chinese UI, including on success: a passing probe reported "OpenAI Chat Completions connection test passed." The ten codes in internal/errors already exist for this. They were being used for styling and the retry flag but never to choose copy, and there was no error-code-to-message table anywhere in the frontend. failureCopy.ts adds one, keyed on error_code plus the HTTP status a probe carries. Not on the message text: that would break the moment Go rewords anything, and the codes exist so the UI does not have to read prose. The status is needed because one code is too coarse -- 402, 429 and 503 all arrive as PROVIDER_UNREACHABLE, and "Endpoint returned HTTP 429." never said whether the account or the configuration was at fault. TIMEOUT and PROVIDER_UNREACHABLE were already separate codes producing one identical sentence, so a hung request and a mistyped hostname looked the same. Each mapped code also carries a hint, because a message that only restates the problem leaves the user where they started. The English message is dropped rather than appended: it is written for a maintainer reading a log -- "Cannot reach endpoint: dial tcp: lookup api.example.com: no such host" -- and showing both would leave the user to decide which half to trust. Where a code adds nothing the raw message stays; INVALID_REQUEST is field-level validation text a generic sentence would lose. describeFailure is separate from describeError rather than replacing it: it needs a Translate, which only components have, and some callers want just the code or the flag. Refs #114 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Closed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #114.
describeError(frontend/src/backend/errors.ts:27) returnederror.messageverbatim, andnormalizeWailsErrorwraps every backend failure inOneAgentApiError— so the Chineset()fallbacks the 19 call sites carefully passed almost never fired. They looked like localisation and were dead code. Users read English inside a Chinese UI, including on success: a passing probe reportedOpenAI Chat Completions connection test passed.The ten codes in
internal/errors/errors.goalready exist for exactly this. They were driving styling and the retry flag but were never used to choose copy, and there was no error-code-to-message table anywhere in the frontend.The mapping
failureCopy.tskeys onerror_codeplus the HTTP status a probe carries.Not on message text — that breaks the moment Go rewords anything, and the codes exist precisely so the UI doesn't have to read prose.
The status is load-bearing because one code is too coarse. 402, 429 and 503 all arrive as
PROVIDER_UNREACHABLE, andEndpoint returned HTTP 429.never told the user whether their account or their configuration was at fault — these are the two failures a new user with a fresh key hits most. Separately,TIMEOUTandPROVIDER_UNREACHABLEwere already distinct codes producing one identicalCannot reach endpointsentence, so a hung request and a mistyped hostname were indistinguishable.Every mapped code also carries a
hint. A message that only restates the problem leaves the user where they started.What is dropped and what is kept
The English message is replaced, not appended. It is written for a maintainer reading a log —
Cannot reach endpoint: Post "https://...": dial tcp: lookup api.example.com: no such host— and showing both halves would leave the user deciding which to trust.Where a code adds nothing, the raw message stays.
INVALID_REQUESTis field-level validation text (Provider name is required); replacing it with a generic sentence would lose what the user needs.Shape
describeFailureis additive rather than a replacement fordescribeError: it needs aTranslate, which only components have, and some callers want only the code or the retryable flag. All 19 user-facing call sites moved over;describeErrorkeeps its two non-display uses.ConnectionStatusalso now reads a 401/403 behindPROVIDER_UNREACHABLEas a rejected key.classifyHTTPModelsonly tagsAPI_KEY_REJECTEDwhen the models call is what failed, so the same rejection rendered as a hard error on one path and a warning on the other.Verification
pnpm run test— 299 passed (39 files), 14 new acrossfailureCopy.test.tsanderrors.test.tsdescribeFailureplusConnectionStatus's directfailureCopyForcall fails 9 tests. Worth noting — reverting onlydescribeFailurefailed nothing, which is how I found that the probe path needed its own coverage rather than assuming one test covered both.pnpm run build,pnpm run test:e2e(6 passed),go test ./...,scripts/check-docs.pyRemaining on #114: the three untranslated Chinese literals in
settingsTransfer.tswere already fixed in #123, andCannot reach endpointstill embeds the raw Go error in the backend message — this change stops it reaching the user, but trimming it at the source is a separate Go-side edit.🤖 Generated with Claude Code