Core
Units
Units change how a number renders, never which string is selected — that is a context-param job. If a value would appear in a message branch ({gender, select, …}) it's a param; if it goes to a number formatter it's a unit. Declaring categories: stift.config.ts.
Value contract: store in the base unit
| Category | Base |
|---|---|
| length | meter |
| mass | kilogram |
| area | hectare |
| temperature | celsius |
| digital | byte |
Data and messages carry base-unit numbers; the library converts base → active unit at render. Never branch messages on a unit system and never convert in the message.
Reading and setting units (headless)
Stift.getUnits() // effective per-category record for the active locale
Stift.getUnitValues('length') // allowed units — config supportedValues
Stift.setUnit('length', 'kilometer') // global override; validated against supportedValues
Stift.resetUnit('length') // back to the per-locale default
setUnit validates — an invalid unit is rejected and reported via the runtime hooks. Only setUnit overrides are persisted (unitsPersistence in createStift); defaults come from config. Unconfigured categories are invisible: absent from getUnits(), setUnit rejected, empty getUnitValues. Per-locale defaults inherit down the dialect chain exactly like params.
Formatting
@secundus-studio/stift-format does the rendering (state → render is deliberately split so the formatter is tree-shakable):
import { formatUnit, convertUnit } from '@secundus-studio/stift-format'
formatUnit(1000, { locale: 'en', unit: 'kilometer' }) // "1 km"
formatUnit(2.5, { locale: 'en', unit: 'pound', unitDisplay: 'long' }) // "5.5 pounds"
convertUnit(1, 'mile', 'meter') // 1609.344
unitDisplay?: 'long' | 'short' | 'narrow'. Values are always in the category base unit — formatUnit receives meters regardless of the active unit. Wire the two layers together:
function formatDistance(meters: number) {
return formatUnit(meters, { locale: Stift.getLocale(), unit: Stift.getUnits().length ?? 'meter' })
}
Subtree-level overrides (React's useFormatter({ units })) don't exist headlessly — just pass the unit per call.
Common mistakes
- Passing a converted value to
formatUnit(double conversion) — always pass base-unit values. - Branching a message on a unit system (
{unitSystem, select, …}) — units never select strings. - Calling
setUnitwith a unit outsidesupportedValues— rejected. - Reading
getUnits().lengthfor a category that isn't configured — absent by design. - Forgetting
unitDisplayfor compact UI — long labels can overflow.
Next
- The React hook surface (
useUnits, pickers) → Units - Config declaration rules → stift.config.ts
- Number/date/list formatting → Translating & Formatting