Core
Translating & Formatting
Everything in this page is plain @secundus-studio/stift-core — no adapter needed. The React hooks (Translating & Values) are thin wrappers over these exact methods.
Calling t
Stift.t('checkout', 'title') // unscoped: namespace + key
Stift.scope('checkout')('title') // scoped: bare keys
Stift.scope(['common', 'checkout'])('login') // multi: first namespace with the key wins
Stift.t('checkout', 'title', { count: 3 }, 'ar') // render for a locale without switching
Keys, param shapes, and locales are literal types once the generator emits stift.gen.d.ts — typos and wrong params are compile errors. A cache miss returns the raw key, records it in store.missingKeys, reports hooks.onError({ code: 'missing-key' }), and kicks a background load so the next read resolves. ICU select/plural/offset work through the default engine; swap it via createStift({ formatter }). Authoring the messages: Messages & Keys, ICU Select & Plural.
Params: declared vs undeclared
- Declared in
params.context(stift.config.ts) → optional at call sites. Supply them once viaStift.setParam(name, value)(validated against the declaredvalues) — everyt()call auto-merges them. - Undeclared → always required at the call site. There is no strictness dial.
Per-locale defaults (defaults: { all: …, ar: … }) are recomputed whenever the active locale switches and inherit down the dialect chain.
Rich text: the headless pattern
Tag rendering is adapter territory (React's t.rich walks the AST for you), but the AST itself is public core API — vanilla consumers walk the same tree:
import { parseMessage } from '@secundus-studio/stift-core'
const { ast } = parseMessage(stift.getMessage('common', 'rich.demo'))
// walk ast: string nodes concatenate; tag nodes yield { tagName, children }
// so you can build DOM nodes, terminal spans, or an email-safe string
Use stift.getMessage(ns, key) for the untranslated source of a message; format the plain-text version with t() as usual. Markdown messages follow the same shape (t.markdown in React is just a renderer over it). Full signatures: Translating.
Formatting: @secundus-studio/stift-format
The useFormatter hooks wrap @secundus-studio/stift-format — import it directly:
import { formatNumber, formatCurrency, formatDate, formatList, formatUnit } from '@secundus-studio/stift-format'
formatNumber(1234.5, { locale: 'fr' }) // "1 234,5"
formatCurrency(9.99, { locale: 'en', currency: 'EUR' }) // "€9.99"
formatUnit(12000, { locale: 'en', unit: 'kilometer' }) // "12 km"
convertUnit(12000, 'meter', 'kilometer') // 12
All formatters take the locale explicitly — pass Stift.getLocale() to stay in sync with the active locale. Locale switching: Dialects.
Units without React
The unit state lives on the headless instance (full rules: Units):
Stift.setUnit('length', 'mile') // validated against config supportedValues
Stift.getUnits() // { length: 'mile', … } effective per-category record
Stift.resetUnit('length') // back to the per-locale default
Stift.getUnitValues('length') // allowed units for a category
Render with formatUnit + the active unit — values are always in the category base unit:
import { formatUnit } from '@secundus-studio/stift-format'
formatUnit(1000, { locale: Stift.getLocale(), unit: Stift.getUnits().length ?? 'meter' })
Next
- Unit categories, base-unit contract, pickers → Units
- Declaring params/units in config → stift.config.ts
- Persistence and offline behavior → Offline & Persistence