Skip to content

fix: localise backend failures through the error contract - #125

Merged
yujiezhang-ops merged 1 commit into
mainfrom
fix/localised-error-messages
Aug 8, 2026
Merged

fix: localise backend failures through the error contract#125
yujiezhang-ops merged 1 commit into
mainfrom
fix/localised-error-messages

Conversation

@yujiezhang-ops

Copy link
Copy Markdown
Collaborator

Fixes #114.

describeError (frontend/src/backend/errors.ts:27) returned error.message verbatim, and normalizeWailsError wraps every backend failure in OneAgentApiError — so the Chinese t() 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 reported OpenAI Chat Completions connection test passed.

The ten codes in internal/errors/errors.go already 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.ts keys on error_code plus 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, and Endpoint 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, TIMEOUT and PROVIDER_UNREACHABLE were already distinct codes producing one identical Cannot reach endpoint sentence, 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_REQUEST is field-level validation text (Provider name is required); replacing it with a generic sentence would lose what the user needs.

Shape

describeFailure is additive rather than a replacement for describeError: it needs a Translate, which only components have, and some callers want only the code or the retryable flag. All 19 user-facing call sites moved over; describeError keeps its two non-display uses.

ConnectionStatus also now reads a 401/403 behind PROVIDER_UNREACHABLE as a rejected key. classifyHTTPModels only tags API_KEY_REJECTED when 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 across failureCopy.test.ts and errors.test.ts
  • Reverted both paths independently: neutering describeFailure plus ConnectionStatus's direct failureCopyFor call fails 9 tests. Worth noting — reverting only describeFailure failed 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.py
  • No duplicate i18n keys (420 total)

Remaining on #114: the three untranslated Chinese literals in settingsTransfer.ts were already fixed in #123, and Cannot reach endpoint still 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

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>
@yujiezhang-ops
yujiezhang-ops merged commit 359c448 into main Aug 8, 2026
4 checks passed
@yujiezhang-ops
yujiezhang-ops deleted the fix/localised-error-messages branch August 8, 2026 08:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

后端错误文案全部以英文原文抵达用户,错误码从未用于本地化

1 participant