Answer user-facing API failures with a machine-readable error code

The dashboard now speaks seven languages but every failure path still showed
the API's raw English text ("insufficient balance", "current password is
incorrect"), which is the most frequent and least forgiving part of the UI to
leave untranslated.

Rather than teach the API about locales, it keeps answering in one language
and hands the client something to translate: `detail` becomes
{code, message, params}, where message stays English for non-dashboard
consumers (curl, tests) and code maps onto `error.<code>` in i18n.js. An
unknown code falls back to message, so a client older or newer than the server
degrades to English instead of a blank toast.

Domain exceptions (BetError, WithdrawalError) subclass the new ApiError and
carry the code from where the failure actually happens; str(exc) is still the
English message, so existing tests keep matching on it. Interpolated values
travel in params rather than baked into the English sentence — amounts as
*_sats, from which the frontend derives a *_plm sibling, so each language can
place them wherever its grammar wants.

admin.js reads detail.message defensively: the admin endpoints still return a
bare string, but the shared auth dependencies now return the structured form.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-26 21:44:39 +02:00
co-authored by Claude Opus 5
parent 7048fe7ea6
commit 0cf35147ad
13 changed files with 254 additions and 31 deletions
+2
View File
@@ -138,6 +138,8 @@ So worst case (last bet confirms right at the deadline) is ~3 block times end-to
- **Every language must have exactly the same key set.** There is no fallback beyond `en`, and a missing key renders as the raw key string.
- `/admin` is intentionally **not** translated (operator-facing, Italian only), and neither is `/guida` (serves `docs/guida-utente.md`).
**API error contract** (`app/api/errors.py`): the API is single-language by design. User-facing failures answer with a structured `detail``{"code", "message", "params"}` — where `message` is English for non-dashboard consumers and `code` is what the frontend maps onto `error.<code>` in `i18n.js` (falling back to `message` for an unknown code). Domain exceptions (`BetError`, `WithdrawalError`) subclass `ApiError` and carry the code from where the failure actually happens; `str(exc)` is still the English message. When adding a user-facing error: give it a code, add `error.<code>` to all 7 languages, and pass interpolated values through `params` (amounts as `*_sats` — the frontend derives a `*_plm` sibling automatically) rather than baking them into the English text.
## Admin dashboard and test UI
Two static single-page apps, served directly by FastAPI (`app/main.py` mounts `app/static/` and adds a dedicated `GET /admin` route) — no build step, no framework. Each page's HTML/CSS/JS are separate files (`index.html`/`style.css`/`app.js`, `admin.html`/`admin.css`/`admin.js`), served as plain static files (no bundler):