You already have an i18n stack, a translation workflow, and translators who work in a tool. You should keep all of it.
The rule that makes this possible
The translator interface is the surface you are replacing, i18n is the seam it resolves, and writing a locale is the path most projects take instead.
None of those libraries is a dependency of this package, and none can become one.
Adapters exist for the common ones, and they adapt rather than depend. Compose yours, or use the built-in translator, and nothing in the kit cares which.
That constraint is why the seam is shaped the way it is: a translator is an interface, not an implementation, and the capability’s job is to negotiate a locale and hand something that satisfies it to every capability on the request.
What an adapter has to satisfy
The interface a translator implements. Look up a key, render a plural with a count, format a number, a date and a currency, and answer what locale it resolved.
Your library already does all of that. An adapter is the translation of your library’s names into those.
Writing one
The type lives in core and you can annotate against it whether or not you ever compose the capability:
import type { Translator } from "@pithy-sh/core/src/i18n/translator";Then it is one object with the interface’s methods, closing over your library’s instance for the negotiated locale.
Two things worth getting right:
Handle a missing key the way the built-in one does — fall back through the layers rather than rendering the key itself. A screen showing auth.sign_in.title is worse than one showing English.
Do not translate error payloads. Those carry a code and parameters for the client to render, and the English on the wire is the operator’s diagnostic. An adapter that translated them server-side would lose that.
What you keep, and what you inherit
You keep your catalogs, your file format, your translation tooling, your review workflow and your translators’ habits.
You inherit the negotiation chain, the two-locale split, the per-key layer walk over the kit’s own catalogs, and the fact that every capability’s strings resolve through the same seam.
The last one is the point. The kit’s own sentences are still translated by the kit — you did not sign up to translate auth’s screens by adopting a backend.
The keys you will meet
The kit’s are <domain>/<path>, and your library may prefer another shape.
An adapter is free to map: hold the kit’s keys in one namespace of your own catalogs and translate the shape at the boundary. Keep the domains intact, because they are what stops contributions colliding.
When not to bother
If you have no existing stack, use the built-in translator. It negotiates, it walks layers, it formats through the runtime’s own ICU, and it costs you no dependency.
The adapters exist for people who already have a workflow — not as an admission that the built-in one is insufficient.
Check it worked
- A kit screen renders in your second language
- A missing key falls back rather than showing the key
- Plurals render the right category at 0, 1 and many
- Error payloads still carry English and a code
- Your own translation workflow is unchanged