Skip to content

Locales & the contribution model ​

A Turkish name should sound Turkish; a German address should follow German format. But you don't want a separate "Turkish name generator" and "German name generator" — you want one name generator whose data changes by locale.

Data is separate from the generator ​

Generators never embed data. They reference an asset by a logical name (person.first_names, coin.flip). A resolver maps (asset, locale) → data. The generator is locale-agnostic; the locale picks the data.

generator  first_name → model, source "person.first_names"   ← one definition
asset      person.first_names
             ├─ tr_TR → models/tr/first_names.ngram
             ├─ en_US → models/en/first_names.ngram
             └─ de_DE → models/de/first_names.ngram

Locales are contributed, not owned ​

Crucially, the generator does not own a list of locales. Any package can contribute (asset, locale) data — so a generator that shipped with only German and English data can gain Turkish and Dutch from a completely separate, data-only package, without anyone editing the generator.

@phony/coin        provides coin.flip for de, en
@alice/coin-trnl   contributes coin.flip for tr, nl   ← separate repo, generator untouched

This is the opposite of hard-coding a { locale → data } map inside the generator (which is what the old approach did, and why it couldn't be extended).

Resolution: a chain, most-specific first ​

The active locale is really a chain walked top to bottom, with a locale-independent (*) fallback:

["@yourco/internal", "tr_TR", "base"]
   │                    │       └─ universal defaults (uuid, http, iso codes)
   │                    └─ Turkish names, cities, address formats
   └─ your company's overrides

So you inherit Turkish data, override only what you need, and fall back to base for anything universal.

A worked resolution ​

Say three packages have contributed to the shared index, and the active chain is ["@acme/internal", "tr_TR", "base"]:

asset                 @acme/internal    tr_TR                 base (*)
────────────────────  ───────────────   ───────────────────   ─────────────────
person.first_names    —                 tr/first_names.ngram  en/first_names.ngram
geo.cities            ["HQ-City"]       ["İstanbul","Ankara"] —
http.method           —                 —                     ["GET","POST",…]

Resolution walks the chain top to bottom and stops at the first hit:

  • person.first_names → not in @acme/internal, found in tr_TR ⇒ the Turkish model. Base's English model is never reached.
  • geo.cities → found in @acme/internal ⇒ your override wins over the Turkish list below it.
  • http.method → not in either upper layer, found in base ⇒ the universal list. (A truly locale-independent asset is often stored under * so it resolves the same in every chain, regardless of what sits above it.)

So one unchanged set of generators produces Turkish names and cities, your company's own city list, and universal HTTP verbs — purely by how the chain is ordered. Swap tr_TR for de_DE and the same generators speak German, with your @acme/internal overrides still on top.

Three layers ​

Locale packages conventionally stack in three layers — base (universal) → locale (language-specific) → domain (your overrides) — each adding or replacing what the one below provides. The full design, including the locale_independent flag, is in the Locale System spec.

To wire locale data in code, see The engine → phony-pgdl: Assets & locales.

Phony Cloud — Documentation & Specification