English fallback

Two things stay English permanently, and neither is an omission.

Error payload messages

Errors is why, error codes is what a client keys on instead, and translating in the browser is where the words a reader actually sees come from.

The message on the wire is English forever, and the server never translates it.

It is two things at once: the operator’s diagnostic in the log line and the audit row, and the fallback for every client that cannot do better. A server that translated it would lose the first to gain the second — for a caller whose locale it may not even have.

What a client gets instead

The payload carries the stable domain/reason code and structured parameters beside the English. So a translating client renders:

its own string for this code and these parameters, falling back to the English when it has none.

The code is already the translation key. There is no second identifier to keep in sync and no key that can drift from the code it describes — a locale file for the kit’s errors is, exactly, a file that covers the error codes.

This is also why a mobile app gets translated errors without shipping the i18n package at all: the payload carries everything a client needs to translate on its own.

The one exception, and why it exists

Better Auth owns every route under the auth base path and answers them in its own shape, before anything of Pithy’s sees the failure. There is no payload for a client to key on and no parameters to interpolate.

So the choice was not where to translate them but whether. Left alone they were the last English sentences on an otherwise translated screen — met by the most ordinary mistake there is: mistyping a one-time code.

So the auth capability substitutes the message server-side for a code it has words for, and keeps the English alongside — so the operator’s half survives the translation rather than being replaced by it.

It reaches a caller the client seam cannot, too: a mobile app holding a bearer token gets the reader’s language with nothing extra shipped.

Every operator surface

CLI output. The problem and action lines. Machine-readable lines. Logs. Audit rows.

Permanently English, for three reasons:

They stay greppable. Searching for an error message you saw and finding the code that produces it is a thing people do daily.

They keep matching the documentation. A page that quotes a message quotes the message.

The action line is untranslatable by definition. It names commands, repository files and binding names — none of which translate. A remedy a caller needs is a message rather than an action, and the split is exactly the client boundary: the HTTP codec strips the action and the detail, and the CLI’s own encoder keeps the action, because whoever ran the command is the operator it was written for.

What this is not

It is not a claim that English is the default language of software. It is a claim about who reads each string.

A caller’s message is for a caller and is translated by them, from a code, in their own words. An operator’s message is for the person on call at 3am, and that person is reading a log alongside a stack trace and a command they are about to type.

A stack trace in a language the responder does not read is worse than useless.

The gap that is stated rather than hidden

Per-field validation messages do not translate, though the top-level message does.

That is a reachability problem rather than a decision. The validation library has the right primitive — request-scoped, per-parse — and the kit’s surface calls it without a way to pass one.

The global slot is not the workaround, and it is banned repository-wide: a Worker isolate is reused across requests, so a locale written into a module-level map during one request is still there for the next — one reader’s error rendering in another reader’s language, silently and unreproducibly.

What you can do about the English a reader sees

Nothing, for an error message they were never meant to see. Translate it in your client, from the code.

Everything, for a message they were. A user-facing sentence belongs in a catalog under your own domain, rendered through the translator — not in an error’s message field.

If you find yourself wanting to translate an error payload’s message, the sentence is probably in the wrong place.

ESC