Skip to main content
@craft-ts
LearnLearn with EffectAI agentsGuideReference
@craft-ts/core@craft-ts/component@craft-ts/dev-tools@craft-ts/effect@craft-ts/style@craft-ts/style-testing@craft-ts/i18n@craft-ts/i18n-effect@craft-ts/mcp
ExamplesMigrationEffect compatibility & maturityAdopting CraftTS progressivelyPress kitRoadmapBacklog
Beta
  • Learn
  • Learn with Effect
  • AI agents
  • Guide
  • Reference
  • Packages
  • Resources
  • Guide overview
  • Create a project
i.Core concepts
  • The mental model
  • Which primitive should I use?
  • Anatomy of a primitive
  • Generators and yield*
  • Insertions
  • Typed insertion pipes
  • Exceptions as values
ii.Managing state
  • Local state
  • State machines
  • query
  • Mutations
  • queryParams
  • asyncProcess
  • Selecting
  • Reacting to mutations
  • Collections
  • Collection utilities
  • Persistence
  • GlobalPersisterHandler
  • Pagination placeholders
  • Schema validation
iii.Recommended approaches
  • Inject at the point of use
iv.Structuring the app
  • craftService
  • Service scopes
  • Shaping the public API
  • Abstract services
  • App start
  • Lazy services
  • Server functions
  • craftRegisterFor
  • Target wrapper
v.Routing & type-safe DI
  • Setup
  • CLI automation
  • ESLint rules
  • Route providers
  • Route guards
  • Route exception handling
  • Non-blocking navigation
  • Global error component
  • Route load errors
  • Scaling routes
vi.Components & templates
  • Components
  • Activating the style system
  • Typed styles
  • Defining a design system
  • Tokens and variables
  • Global foundation and fonts
  • Pseudo-elements and animations
  • Axes and the matrix
  • Context obligations
  • Testing visual states
  • Text contrast
  • Visual attestation
  • Template obligations
  • Typed CSS variables
  • Fine-grained reactivity
  • Progressive rendering
  • Directives and .pipe(...)
  • settledValue & pendingNode
  • Accessibility
  • Customization
  • Content projection
  • Styling a component
  • Template migrator
vii.Forms
  • Overview
  • Validators
  • Submitting
  • Nested forms
  • Exception handling
  • Complete examples
viii.Testing
  • Testing services
  • Testing components
  • Type-level tests
  • Browser boundaries
  • Page stability
  • Architecture rules
    • Overview
    • Declarative baseline
    • Unique identities
    • HTTP endpoint ownership
    • Computed purity
    • Dependency cycles
    • Mutation reactions
    • Route DI proofs
    • Route component files
    • Path boundaries
    • Exclusive links
    • Persisted identities
    • insertSelect keys
    • craftEffect and network
    • craftEffect and imperative sync
    • Interactive element names
    • Metric thresholds
    • Server-state loaders
    • Primitive loader requirements
  • Extensible architecture graph
  • Graph insights
  • Craft graph vs Nx
ix.Reactivity utilities
  • craftComputed
  • craftEffect
  • craftMethod
  • source$
  • on$
  • fromEventToSource$
  • sourceFromEvent
  • afterRecomputation
x.Internationalisation
  • Type-safe i18n
  • The catalogue
  • Tokens
  • The runtime
  • With Effect
xi.Deployment (experimental)
  • Overview
  • Manifest reference
  • Diagnostics
  • Providers
  • Alchemy provider
xii.Going further
  • SSR and hydration
  • Effect integration
  • Program operators
  • Pattern matching
  • Observability
  • AI agents
  • Temporal runtime
  1. Internationalisation
  2. The catalogue

The catalogue#

A catalogue is a plain nested object. Nothing is parsed, nothing is loaded from JSON at build time, and every guarantee on this page comes from the type of the value itself.

ts
import {
  defineCatalog,
  defineLocale,
  defineLocaleLike,
  money,
  msg,
  number,
  plural,
} from '@craft-ts/i18n';

Tokens name the parameters#

ts
// A token names a parameter and decides how it is formatted. `amount` is a
// currency, `count` a number — and that is what types the params object.
const amount = money('amount', undefined, { currency: 'EUR' });
const count = number('count');

A token carries a name and a formatter. The name becomes the parameter key; the formatter decides how the value is rendered in the active locale. That is why msg can derive the params type of a message from the tokens it interpolates — see Tokens for the full list.

defineCatalog, msg, plural#

ts
export const enCatalog = defineCatalog({
  order: {
    total: msg`Order total ${amount}.`,
    items: plural(count, {
      one: msg`${count} item is in the order.`,
      other: msg`${count} items are in the order.`,
    }),
  },
});

export const en = defineLocale('en-US', enCatalog);

msg is a tagged template: the literal parts are text, the interpolations are tokens. msg`Order total ${amount}.` has params { amount: number }, and nothing else.

plural(count, branches) takes the counting token and one message per category. Which categories are required is decided by the locale id, not by you: defineLocale('pl-PL', …) will not accept a plural missing few or many. That check is a type error, before any Polish speaker sees the wrong branch.

Keys nest as deeply as you like; the key used at the call site is the dotted path.

Every other locale is defineLocaleLike#

ts
export const fr = defineLocaleLike(en, 'fr-FR', {
  order: {
    total: msg`Total de la commande : ${amount}.`,
    items: plural(count, {
      one: msg`${count} article est dans la commande.`,
      other: msg`${count} articles sont dans la commande.`,
    }),
  },
});

defineLocale is for the reference locale — the one that decides what the key set is. Every other locale goes through defineLocaleLike(reference, id, catalog), which checks three things against the reference at compile time:

  • the same keys, no more and no fewer;

  • the same parameters on every message;

  • the plural categories that this locale requires, which may differ from the reference's.

assertLocaleParity adds one check the types cannot express: two locales must also agree on how each token is resolved. A locale that swapped a service-resolved money token for a static one, or dropped a parameter's schema, renders through a different path — it is reported as a LOCALE_MISMATCH rather than silently formatting in the wrong currency.

A renamed key in the reference therefore breaks every translation file that still has the old name, which is the entire point. It also runs assertLocaleParity at construction, so a mismatch that slips past the types — a catalogue built dynamically, say — still fails loudly rather than rendering a key name.

Checking outside the typechecker#

bash
npm run i18n:check

Runs catalogue validation and locale parity as an ordinary command, so CI and a pre-commit hook can see what tsc sees. Under the hood it is validateCatalog / assertValidCatalog (also exported from @craft-ts/i18n/testing) and validateLocaleParity / assertLocaleParity.

When it fails, it names the key and the locale. Add the key; do not loosen the catalogue's type to make the message go away.

Delivering a catalogue as data#

serializeCatalog(catalog) produces a JSON-safe shape where each token is reduced to its stable tokenId and parameter name — the receiving application registers the executable formatters. A token that parses its input is marked, and a token whose formatter is resolved from the injector is refused: its formatter only exists at render time, so a serialised copy would silently format with the default options. Deliver such a message from the application instead.

Next#

  • Tokens — what amount and count above actually are.

  • The runtime — turning these locales into a t.

←Type-safe i18nTokens→

On this page

  • Tokens name the parameters
  • defineCatalog, msg, plural
  • Every other locale is defineLocaleLike
  • Checking outside the typechecker
  • Delivering a catalogue as data
  • Next

@craft-ts · MIT licensed

GitHubnpm

Search