Browse docs

Core

Units

ReactVanilla

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 setUnit with a unit outside supportedValues — rejected.
  • Reading getUnits().length for a category that isn't configured — absent by design.
  • Forgetting unitDisplay for compact UI — long labels can overflow.

Next