---
id: configuration/config-file
title: stift.config.ts
---

Validated by `@secundus-studio/stift-generator`'s zod schema at the project root. All fields optional except `locales`.

## Minimal config

```ts
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](../authoring/dialects.md).
- **`namespaces`** — where locale files live and which load by default.
- **`messageFormat`** — `'icu-mf1'`. Message syntax: [ICU Select & Plural](../authoring/icu-select-plural.md).
- **`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](../react/setup.md).
- **`loadingStrategy`** — `'lazy-per-namespace'` (default) | `'critical-then-full-background'` (PWA) | `'boot-all'` (pin everything; shrink with `bootExclude`). Full comparison: [Loading Strategies](./loading-strategies.md).
- **`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`):

```ts
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](../react/translating.md).

## Units (`units`)

Measurement display — values that change **how a number renders**, never which string is selected:

```ts
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](../react/units.md).

> 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

- When each strategy loads what → [Loading Strategies](./loading-strategies.md)
- The build that consumes this config → [Build: Vite Plugin & CLI](../tooling/build.md)
- Offline behavior per strategy → [Offline & Persistence](../runtime/offline-persistence.md)
