Browse docs

Configuration

stift.config.ts

ReactVanilla

Validated by @secundus-studio/stift-generator's zod schema at the project root. All fields optional except locales.

Minimal config

import { defineConfig } from '@secundus-studio/stift-generator'

export default defineConfig({
  locales: { default: 'en', supported: ['en', 'ar'] },
  namespaces: { dir: 'locales', discovery: 'convention', default: ['common'] },
  messageFormat: 'icu-mf1',
  detection: {
    order: ['explicit', 'custom', 'navigator'],
    timeout: { default: 250, custom: 3000 },
    fallback: 'en',
  },
  loadingStrategy: 'critical-then-full-background',
  memory: { maxCachedNamespaces: 20, evictionPolicy: 'lru' },
  security: { tier: 'obfuscated', encoding: 'msgpack', compression: 'brotli' },
})

Field guide

  • locales.supported — every tag with translations plus registered dialects (ar-SY, ar-SY-x-lattakia); each tag narrows RegisteredLocale in the generated typegen. default must be in supported. Tag semantics: Dialects.
  • namespaces — where locale files live and which load by default.
  • messageFormat — 'icu-mf1'. Message syntax: ICU Select & Plural.
  • detection.order — 'custom' is the reserved runtime slot (registered in createStift, not here): explicit → custom (saved preference) → navigator → last-known-good → fallback. Boot loop: Setup & Boot Modes.
  • loadingStrategy — 'lazy-per-namespace' (default) | 'critical-then-full-background' (PWA) | 'boot-all' (pin everything; shrink with bootExclude). Full comparison: Loading Strategies.
  • memory — bounded namespace cache (maxCachedNamespaces, evictionPolicy: 'lru'); preload pins LRU-exempt namespaces.
  • security.tier — 'none' | 'obfuscated' | 'encrypted-static' | 'encrypted-session'. 'encrypted-static' needs security.staticKey, which never crosses the runtime boundary (never emitted into the generated runtime config). encoding (msgpack | cbor) and compression (brotli) select the pack codec.

Context params (params.context)

Grammatical-agreement params — values that change which string is chosen (gender, politeness, verbosity):

params: {
  context: [
    {
      name: 'gender',
      values: ['male', 'female', 'other'], // canonical union → drives the types
      defaults: { all: 'other' }, // REQUIRED catch-all
    },
    {
      name: 'politeness',
      values: ['polite', 'warm'],
      defaults: { all: 'polite', ar: 'warm', 'ar-SY': 'warm' },
    },
  ],
}

Rules: declared params are optional at call sites (the app supplies them once via setParam, per-locale defaults, or <LocaleScope>); every entry must declare defaults, and defaults.all is required; per-locale defaults inherit down the dialect chain; setParam validates against values. Undeclared params are always required — there is no strictness dial. Supplying them: Translating & Values.

Units (units)

Measurement display — values that change how a number renders, never which string is selected:

units: {
  length: {
    supportedValues: ['meter', 'kilometer', 'mile'],
    defaults: { all: 'meter', 'ar-SY-x-lattakia': 'kilometer', ar: 'mile' },
  },
  mass: { supportedValues: ['kilogram', 'pound'], defaults: { all: 'kilogram' } },
  temperature: { supportedValues: ['celsius', 'fahrenheit'], defaults: { all: 'celsius' } },
}

Categories: 'length' | 'mass' | 'area' | 'temperature' | 'digital', each with a canonical base unit (meter / kilogram / hectare / celsius / byte) — message data is always stored in the base unit. defaults.all is the catch-all; per-locale defaults inherit down the dialect chain; defaults must sit inside supportedValues. Unconfigured categories are invisible — no type entry, no setUnit, hidden from devtools. Rendering: Units.

If a value would appear in a message branch ({gender, select, …}) → context param. If it would be passed to a number formatter → unit. Never mix the two: don't branch messages on a unit system, and don't declare a unit system as a context param.

Next