Plugins
FieldsMeasurement

Measurement utilities

The zero-dependency conversion and formatting engine behind the field, exported for frontend use.

@10x-media/fields/measurement/utils is the conversion and formatting engine the field itself runs, exported standalone. It has no Payload imports, so it is safe in any bundle: server, client, worker.

import {
  convert,
  formatMeasurement,
  decompose,
  compose,
  resolveDisplayUnit,
  systemForLocale,
  createEngine,
  unitsOfDimension,
  UNITS,
} from '@10x-media/fields/measurement/utils'

Format

formatMeasurement(4.5359, {
  storageUnit: 'kg',
  displayUnit: 'lb',
  locale: 'en-US',
})
// '10 lb'

formatMeasurement(value, options) converts a stored value from storageUnit to displayUnit and formats it with Intl.NumberFormat, so unit names and number formatting localize for free. unitDisplay ('short' default, 'long', 'narrow') controls the unit form; precision overrides display fraction digits per unit (a plain Partial<Record<UnitId, number>>, independent from the field's own precision modes covered in Precision, which resolve a mode's knobs down to this same shape before calling in). Displaying a compound unit (ft-in, st-lb) formats both parts and joins them with Intl.ListFormat. This top-level function only knows the built-in units; call createEngine(custom).formatMeasurement for a field with custom units (see below).

Convert

convert(100, 'cm', 'm')  // 1
convert(0, 'c', 'f')     // 32

convert(value, from, to) converts between two scalar units of the same dimension via that dimension's canonical unit (kg, m, l, celsius, m/s). Converting across dimensions, convert(1, 'kg', 'm'), throws.

Compound values

decompose(180.34, 'cm', 'ft-in')  // { major: 5, minor: 11 } (5 ft 11 in)
compose({ major: 5, minor: 11 }, 'ft-in', 'cm')  // 180.34

decompose(value, from, compound, minorDigits?) splits a stored scalar into a compound unit's major/minor parts, rounding the minor part before deriving the major so a value like 5 ft 11.996 in carries to 6 ft 0 in instead of printing an impossible 5 ft 12 in. compose(parts, compound, to) is the inverse, folding major/minor parts back into a single scalar in to.

Locale resolution

systemForLocale('en-US')  // 'us'
systemForLocale('en-GB')  // 'uk'
systemForLocale('de-DE')  // 'metric'

systemForLocale(locale) maps a BCP 47 locale to a measurement system, off a five-entry table vendored from CLDR supplemental measurement data (no shipped Intl API exposes it): US system for the US and Liberia, UK system for Great Britain and Myanmar, metric everywhere else.

import { presets } from '@10x-media/fields/measurement/utils'

resolveDisplayUnit({
  dimension: 'mass',
  system: systemForLocale('en-GB'),
  localeDefaults: presets.bodyWeight.localeDefaults,
  units: presets.bodyWeight.units,
})
// 'st-lb'

resolveDisplayUnit(args) is the exact resolution chain the field itself runs: preferenceUnit, then fallbackUnit, then registryDefault, then localeDefaults[system], then DIMENSION_LOCALE_DEFAULTS[dimension][system], then units[0] as the floor. Every argument except dimension and units is optional, so a frontend recipe that only needs locale-based resolution passes system and localeDefaults (or leans on the built-in table alone). DIMENSION_LOCALE_DEFAULTS is exported directly for the five built-in dimensions (mass, length, volume, temperature, speed); a custom dimension has no entry there and falls through to units[0] once localeDefaults is exhausted.

Dimensions and units

unitsOfDimension(dimension) lists every built-in unit (scalar and compound) of a dimension:

unitsOfDimension('mass')  // ['g', 'kg', 'oz', 'lb', 'st', 'st-lb']
DimensionUnits
massg, kg, oz, lb, st, st-lb
lengthmm, cm, m, km, in, ft, yd, mi, ft-in
volumeml, l, fl-oz, gal
temperaturec, f
speedkm/h, mph, m/s

UNITS carries each built-in scalar unit's dimension, conversion factor, Intl.NumberFormat unit identifier, and short symbol:

UnitDimensionShort label
g, kgmassg, kg
oz, lb, stmassoz, lb, st
mm, cm, m, kmlengthmm, cm, m, km
in, ft, yd, milengthin, ft, yd, mi
ml, lvolumeml, l
fl-oz, galvolumefl oz, gal
c, ftemperature°C, °F
km/h, mph, m/sspeedkm/h, mph, m/s

precisionFor(unit, overrides?) returns a unit's display fraction digits, the same lookup formatMeasurement uses internally.

Custom units

Every function above only knows the built-in units. A field (or a frontend rendering one) with custom units needs an engine built over the same config:

import { createEngine } from '@10x-media/fields/measurement/utils'

const engine = createEngine({
  units: {
    nmi: { dimension: 'length', factor: 1852, intlUnit: null, shortLabel: 'nmi' },
  },
})

engine.convert(1852, 'm', 'nmi')  // 1
engine.formatMeasurement(2778, { storageUnit: 'm', displayUnit: 'nmi', locale: 'en-US' })
// '1.5 nmi', the plain-decimal + shortLabel fallback since nmi has no intlUnit

createEngine(custom?) merges the built-in tables with custom.units and custom.dimensions and returns { convert, decompose, compose, formatMeasurement, unitLabel, precisionFor, dimensionOf, isScalarUnit, isCompoundUnit, unitsOfDimension }, the same shape the top-level functions wrap. Called with no argument it's byte-identical to the built-in functions; it's how the field itself gets a zero-custom fast path.

A whole custom dimension works the same way, declaring one custom unit as the canonical (factor: 1, no offset):

const pressure = createEngine({
  units: {
    pa: { dimension: 'pressure', factor: 1, intlUnit: null, shortLabel: 'Pa' },
    bar: { dimension: 'pressure', factor: 100000, intlUnit: null, shortLabel: 'bar' },
  },
  dimensions: { pressure: { canonicalUnit: 'pa' } },
})

pressure.convert(100000, 'pa', 'bar')  // 1

Custom unit and dimension ids can't collide with a built-in one, and a custom dimension's canonicalUnit must be a custom unit declared for that dimension with factor: 1 and no offset; createEngine throws otherwise. Compound units (ft-in, st-lb) stay built-in, so decompose/compose on a custom storage unit bridge through the compound's built-in major unit.

Frontend example

Formatting an athlete's stored weight (kg, the bodyWeight preset's storage unit) per visitor locale, in a Next.js server component:

import { headers } from 'next/headers'
import {
  formatMeasurement,
  presets,
  resolveDisplayUnit,
  systemForLocale,
} from '@10x-media/fields/measurement/utils'

export const AthleteWeight = async ({ weight }: { weight: number }) => {
  const locale = (await headers()).get('accept-language')?.split(',')[0] ?? 'en-US'
  const displayUnit = resolveDisplayUnit({
    dimension: 'mass',
    localeDefaults: presets.bodyWeight.localeDefaults,
    system: systemForLocale(locale),
    units: presets.bodyWeight.units,
  })
  return <span>{formatMeasurement(weight, { storageUnit: 'kg', displayUnit, locale })}</span>
}

A US visitor reads "154 lb", a UK visitor "11 st 0 lb", everyone else "70 kg", all from the one stored 70.

On this page