Config
| Key | Default | |
|---|---|---|
supportedLocales | ["en"] | Every locale this project serves |
defaultLocale | "en" | Must be one of the supported ones |
messages | {} | Your catalogs, and your overrides |
queryParam | "lang" | Where an explicit choice arrives, server and browser |
cookie | The cookie the server chain’s cookie link reads | |
storageKey | "pithy.locale" | The localStorage key the browser’s storage link uses |
serverResolvers | See below | The server chain, in order |
browserResolvers | See below | The browser chain, in order |
The chains
Both are the negotiation half of the layer order — the chains pick a locale, the layers answer a key in it.
Server, in default order:
| Link | Reads |
|---|---|
param | ?lang= |
user | pithy_auth_users.locale |
cookie | The locale cookie |
header | Accept-Language, as the full q-weighted list |
default | The project default |
Browser, in default order:
| Link | Reads |
|---|---|
query | ?lang= |
account | The signed-in reader’s stored locale |
storage | This device’s remembered choice |
navigator | The reader’s own browser languages |
server | What the server declared |
default | The project default |
Reorder or shorten either. default is the last resort whether or not it is listed.
The translator
| Returns | |
|---|---|
t.t(key, params?) | The string, or the key itself on a miss |
t.maybe(key, params?) | The string, or null on a miss |
t.plural(key, count, params?) | <key>.<category>, falling back to <key>.other |
t.formatNumber(value, options?) | |
t.formatCurrency(value, currency, options?) | |
| `t.formatDate(Date | number, options?)` |
t.formatList(values, options?) | |
t.formatRelativeTime(value, unit, options?) | |
t.catalogLocale | The locale whose words answered. Falls back |
t.formattingLocale | The locale handed to Intl. Does not fall back |
t.direction | "ltr" or "rtl" |
c.var.locale carries the same three facts, and is null when nothing was negotiated.
Key grammar
<domain>/<path> — domain is a capability’s name, path is lower case with _ inside a word and . between levels.
A tag is at most 64 characters; a key at most 129. Catalog keys has the grammar and the reason for it.
A capability may declare keys only under its own name. Overriding a kit key is always allowed.
Which email templates the kit translates
Translated with the kit (7): magicLink · otp · welcome · securityAlert · invite · passwordChanged · leadCapture
Not (5), because their words arrive as payload: testerNudge · supportReply · operationalNotice · newsletter · marketingCampaign
The shell follows the job’s locale; a payload-carrying template is only as localized as your own copy.
The rendered subject comes back on the enqueue result, so nobody renders it twice — but it is the enqueue-time render. The send path renders once more at the moment the message leaves and rewrites the column from that, so a scheduled job whose catalog was retranslated in between delivers the new sentence while the recorded value keeps the old one.
The locale marker
A catalog file you wrote in a language other than English declares itself within its first 25 lines, exactly:
// LOCALE es — an unreviewed first pass. Not American English by design.Two facts, both load-bearing: the locale it is written in, and that the copy is an unreviewed first pass. Checking your coverage is what reads them.
It exempts the file’s quoted values, not the file. A catalog is not written in Spanish — its values are. Everything around them is a docblock, an import and an identifier: ordinary English prose, and as subject to a prose rule as any other file’s.
A run is one line’s worth. The census reads line by line, and a quote that never closes on its line opens no value — so the inner lines of a multi-line template literal are read as prose.