Configuration
stift.config.ts
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 narrowsRegisteredLocalein the generated typegen.defaultmust be insupported. 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 increateStift, 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 withbootExclude). Full comparison: Loading Strategies.memory— bounded namespace cache (maxCachedNamespaces,evictionPolicy: 'lru');preloadpins LRU-exempt namespaces.security.tier—'none' | 'obfuscated' | 'encrypted-static' | 'encrypted-session'.'encrypted-static'needssecurity.staticKey, which never crosses the runtime boundary (never emitted into the generated runtime config).encoding(msgpack|cbor) andcompression(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
- When each strategy loads what → Loading Strategies
- The build that consumes this config → Build: Vite Plugin & CLI
- Offline behavior per strategy → Offline & Persistence