Module

i18n

One piece of middleware answers one question — what language is this reader in? — and hands every route, every screen and every email a translator that already knows the answer.

@pithy-sh/i18n

terminal
pithy add i18n
Read the docs Source on GitHub

What it is for

Serving readers in more than one language, without every screen, error and email growing its own answer to where the words come from.

What it sits on

A seam in core, which is always there.

The translator lives on the request context whether or not you compose this. With nothing installed it is a translator over the English each capability contributed, so a capability writes its key with no null check and no config.

Composing i18n replaces that translator with one that negotiated the reader's locale and merged the catalogs behind it. A project that never composes it is byte-identical to one from before any of this landed.

License

MIT, like the rest of the kit.

MIT

Read it, run it, change it, ship it in your product. The translations ship with the package rather than being copied into your repository, so a typo fix or a new locale reaches you with the next release instead of stopping at the day you scaffolded.

The honest bit

Two locales, and only one of them falls back.

The reader's tag does two different jobs, and collapsing them is the bug where an Argentine reads Spanish and sees 1,234.56. The catalog locale answers the words, and it falls back — an es-AR reader gets es, because es is what somebody wrote. The formatting locale goes to Intl, and it does not — that reader gets es-AR, which Intl supports whether or not a translator ever did.

So a reader in Buenos Aires gets Spanish sentences and Argentine dates, numbers and currency, from one translator, with nobody writing an es-AR catalog.

The kit ships Spanish today, and no English catalog. That is not an omission: English is the source. An error carries its English message on the wire, a capability contributes its English, and a copied screen carries the English it was scaffolded with. A second copy would be a second place for one sentence to drift.

If you build it yourself

The library is the easy part.

Every i18n library translates a string. What costs you a fortnight is everything around it.

Negotiation on both sides

localStorage is absent from a Worker and navigator.language inside workerd is the constant en, so the server and the browser need different chains to reach the same answer.

Accept-Language as a whole list

pt-PT;q=1.0, es;q=0.8 from a project with no Portuguese is a request for Spanish. Reading only the first entry answers English.

An override that is a merge, not a fork

Lookup walks layers per key, so changing one sentence is one entry and every key you did not mention keeps arriving with each release.

Errors that stay English

The payload is both the operator's diagnostic and the fallback for every client that does not translate, so it carries a code and params and the client renders the words.

A coverage check that fails the build

A missing translation is not a runtime surprise; it is a doctor finding that names the locale and the keys.

The account outranking the device

Someone who picks Spanish on their phone should not read French on their laptop, which means writing the choice through to their account rather than to storage.

None of that is hard. All of it is a fortnight, and it is a fortnight you spend again the first time a second client appears.

terminal
pithy add i18n

In the dashboard

Administer it from a browser, without building the screen.

Which locales you serve, and which messages have no translation yet in each — the same finding pithy doctor fails on, without having to run it.

The dashboard is the admin interface for your own application: whatever this capability writes, your team can look up, check and act on from a browser. Reading is free, forever, with no time limit. Connect the project when you deploy.

Add it, and get on with the feature.

One command installs the package, wires its bindings into every environment your project declares, and runs its migrations.