The layer order

The five layers

For a given key, in this order:

  1. Your catalog, for the resolved locale
  2. Your catalog, for the project default
  3. The kit’s translation, for the resolved locale
  4. Every composed capability’s English, for the resolved locale
  5. That English, for the project default

Per key, never per catalog

This is the whole point. Overriding one sentence is one entry, and every key you did not mention keeps flowing from the package.

If the merge were per catalog, supplying a Spanish object would replace the kit’s Spanish object — and you would own every sentence in it forever, including the ones you never wanted to change and the ones that do not exist yet.

Per key is what makes an override a merge rather than a fork.

Two chains negotiate the locale, and they are different chains

Both chains are where a locale comes from, stated as a resolution order.

On the server, in order: an explicit ?lang=, the signed-in reader’s account, the locale cookie, Accept-Language, the project default.

In the browser, in order: ?lang=, the account, this device’s storage, the reader’s own browser languages, what the server declared, the project default.

Both are reorderable and shortenable in config. default is the last resort whether or not you list it.

Accept-Language is read as the whole list

Not as its first entry.

pt-PT;q=1.0, es;q=0.8, en;q=0.5 from a reader whose Portuguese you do not serve is a request for Spanish. Reading only the head answers English, which is the one language that reader ranked last.

Nothing in the chain is trusted before it is guarded

Every signal is caller-supplied. A *, an en_US, an empty token, or a fragment still carrying ;q=0.9 falls out of the match rather than reaching Intl.Locale and raising.

Why account sits above storage in the browser

A reader who picks Spanish on their phone must not be reading French on their laptop because that laptop’s localStorage holds an older choice.

?lang= still outranks both — it is this page, right now.

For the ordering to describe anything, a signed-in reader’s choice has to be written through to the account, not only to the device. That is the persist callback on the translator interface.

What the send Worker carries, and what travels

The send Workflow runs in its own Worker. It has no request and no access to your config, so anything it does not carry in its bundle has to be stamped in as configuration.

It is built with the kit’s own email copy, in every language the kit is written in. Adding a locale the kit ships costs you a package upgrade and no configuration at all.

What still travels is your diff against it — the email/ sentences you changed, one variable per locale, and nothing if you changed none.

Nothing else is bounded this way. Screens are negotiated in the browser and fetched one locale at a time as their own chunk; errors carry their English on the wire and are translated by the client from a catalog it already holds.

Better Auth’s refusals take a different path

Better Auth owns every route under the auth base path and answers them in its own flat shape before anything of Pithy’s sees the failure — so there is no payload for a client to key on and no params to interpolate. The choice is 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 auth composes a translation plugin that substitutes the message server-side for a code it has words for, and keeps the English on originalMessage — so the operator’s half survives the translation rather than being replaced by it. It also reaches a caller the client seam cannot: a mobile app holding a bearer token gets the reader’s language without shipping the i18n package at all.

The locale it uses is the one this project negotiated, not a second chain of its own. The plugin’s own header, cookie and session strategies are deliberately unused — two negotiations over one page is the bug where the screens answer in Spanish and the errors answer in English.

ESC