Common Types

Measurement

The flat record carrying a numeric value and a unit identifier. Locale-aware rendering through the $measurement formatter, with metric/imperial selection driven by the active language.

A bottle holds 330ml. A package weighs 2.5kg. A rug measures 3m². A delivery window is 1.5h. The shape connectors and editors return for all of these is the same Measurement record: a numeric value paired with a unit identifier. The storefront's $measurement formatter turns the object into a localized string, picking the right unit name ('centimeter' vs 'cm') for the active locale.

import type { Measurement, MeasurementUnit } from '@laioutr-core/core-types/common';

interface Measurement {
  unit: MeasurementUnit;
  value: number;
}

Like Money, Measurement is a flat record, not a discriminated union. There is no separate Length, Weight, or Volume variant; the unit tells you the dimension. This keeps arithmetic and comparison simple, and matches how ECMA-402's Intl.NumberFormat treats units.

Fields

value

A plain number. Unlike Money.amount, the value is the decimal quantity in the named unit, not a count of minor units. A 330ml bottle is { value: 330, unit: 'ml' }, not { value: 33000, unit: 'cl' }. Use the unit that matches how the data arrives from the source; the renderer is the place where conversion happens, not the data layer.

Negative values are valid (a temperature of −5°C, an elevation gain of −120m). Intl.NumberFormat renders them with the locale's negative-number convention.

unit

The MeasurementUnit type is the union of the Laioutr-known unit codes (short identifiers like 'cm', 'kg', 'ml') and arbitrary strings, so you can pass either. Known codes get nice formatting; custom strings fall back to a value-plus-string render.

The known codes are:

DimensionCodes
Lengthin, ft, yd, mm, cm, m, km
Mass / weightmg, g, kg, ton, oz, lb
Volumeml, cl, l, cbm, floz, pt, qt, gal
Areasqm, sqft
Countct, sheet, item

These short codes map to ECMA-402's long unit names (e.g. 'cm''centimeter') inside the formatter so Intl.NumberFormat can localize them. You can also pass an ECMA-402 sanctioned identifier directly ('centimeter', 'kilogram', 'mile-per-hour'); both work.

Two formatter extensions worth knowing about:
  • square- and cubic- prefixes turn any unit into an area or volume measurement. 'square-meter' and 'cubic-meter' render as and . The shorthand 'sqm' is just an alias for 'square-meter'.
  • -per- separator combines two units into a rate. 'mile-per-hour' renders as mph, 'kilometer-per-hour' as km/h, 'liter-per-megabyte' as L/MB. This is straight from the Intl.NumberFormat spec.

Formatting with $measurement

A Measurement value is just data. Turning it into a localized string is the job of $measurement, a global helper the UI Kit registers as a Nuxt plugin and that is auto-available in templates and setup():

components/WeightLabel.vue
<template>
  <span>{{ $measurement({ value: 2.5, unit: 'kg' }) }}</span>
  <!-- Renders '2.5 kg' in en-US, '2,5 kg' in de-DE, '2.5 kg' in fr-FR. -->
</template>

Outside templates, access it through the Nuxt app:

const { $measurement } = useNuxtApp();
const formatted = $measurement(product.weight);

The signature is:

$measurement(measurement: Measurement, options?: Intl.NumberFormatOptions): string

options are forwarded to Intl.NumberFormat on top of the formatter's own { style: 'unit', unit: <resolved> }. Common overrides:

$measurement({ value: 1234.5, unit: 'g' }, { maximumFractionDigits: 0 });   // '1,235 g' in en-US, '1.235 g' in de-DE
$measurement({ value: 100, unit: 'cm' }, { unitDisplay: 'long' });          // '100 centimeters' in en-US
$measurement({ value: 60, unit: 'mile-per-hour' }, { unitDisplay: 'short' }); // '60 mph' in en-US

Locale resolution prefers vue-i18n's $n (so the storefront's i18n setup decides the formatting locale). When $n is unavailable, $measurement falls back to a numeric format with the unit string concatenated.

Unknown unit identifiers fall through to the same numeric-plus-suffix rendering, so a Measurement whose unit is 'imaginary-unit' still produces something readable instead of throwing.

Empty / falsy input renders as the empty string instead of throwing. This exists so partial server responses do not crash the page during SSR, but it can mask connector bugs that drop a measurement entirely. A blank label where one was expected usually means the connector returned undefined.

Metric and imperial

Each language carries a measurementSystem field derived from its region code (US, LR, MM resolve to 'imperial'; everything else to 'metric'). Read it from the active language when you need to pick which unit a connector emits or which presentation the UI shows:

const measurementSystem = useLanguage().value.measurementSystem; // 'metric' | 'imperial'

$measurement itself does not convert between systems. It renders whatever unit the Measurement carries, in the active locale's number format. If your storefront needs to show pounds in en-US and kilograms in de-DE for the same backend weight, the conversion happens in your component (or in the connector that picks which unit to emit), not inside the formatter.

A common pattern for connectors that have both metric and imperial source data is to emit the unit that matches the active language's measurement system:

const measurementSystem = useLanguage().value.measurementSystem;
const weight: Measurement =
  measurementSystem === 'imperial' ?
    { value: variant.weightInPounds, unit: 'lb' }
  : { value: variant.weightInKilograms, unit: 'kg' };

Where it shows up

Measurement appears in two main places in the canonical type system:

Beyond the canonical types, custom entity components routinely use Measurement for product weight, package dimensions, drink volume, fabric area, and similar attributes. Anywhere a backend exposes "value plus unit," Measurement is the right shape.

Filter ranges with Measurement

Faceted filters for physical quantities use Measurement for min and max:

const weightFilter = {
  type: 'range',
  id: 'weight',
  label: 'Weight',
  min: { value: 0, unit: 'g' },
  max: { value: 5000, unit: 'g' },
};

The user's selection arrives back as plain numbers in the min/max unit, so a { min: 100, max: 1000 } selection against a gram-denominated filter means 100g to 1000g. Your handler is responsible for matching the request numbers against products in the right unit (most catalogs store weights in a single canonical unit; convert at query time if needed). See Filters for the full request/response shape.

For connector authors

When the source backend exposes a value plus unit (Shopify's product weight, Adobe Commerce's custom attributes, Storyblok dimension fields), map the two fields directly:

import type { Measurement } from '@laioutr-core/core-types/common';

const toMeasurement = (raw: ShopifyWeight): Measurement => ({
  value: raw.value,
  unit: raw.unit, // Shopify uses 'GRAMS', 'KILOGRAMS', 'OUNCES', 'POUNDS'
});

Two rules keep the data clean:

  1. Emit a unit the formatter knows about. Stick to the Laioutr-known codes ('kg', 'g', 'ml', 'cm') or full ECMA-402 names ('kilogram', 'milliliter', 'centimeter'). Custom strings work but fall back to value-plus-suffix rendering, which won't localize correctly. If the source backend uses non-standard codes (Shopify's all-caps 'GRAMS'), normalize them on the connector side before returning.
  2. Pick units the customer expects for the category. Express drink volumes in ml or l, not cbm. Express product weights in g or kg, not mg. The formatter localizes the rendering, but it does not pick a sensible unit for the dimension. That is the connector's job.

Wrong

// Custom commerce connector, product weight handler
return {
  weight: '2.5 kg',                          // ❌ formatted string
  height: 175,                               // ❌ bare number, unit unknown
  volume: { value: 0.33, unit: 'cubic-meter' }, // ❌ wrong dimension for a drink
};

This breaks because:

  1. '2.5 kg' strips the structure. The frontend cannot do arithmetic, cannot localize the unit name, and cannot apply consistent formatting alongside other measurements.
  2. 175 is a bare number. The frontend has no idea whether it is centimeters or millimeters or pixels.
  3. { value: 0.33, unit: 'cubic-meter' } is technically correct math (0.33 m³ = 330 liters) but renders as 0.33 m³ on the page. Customers expect drinks in liters.
return {
  weight: { value: 2.5, unit: 'kg' },
  height: { value: 175, unit: 'cm' },
  volume: { value: 330, unit: 'ml' },
};

The connector says "this product weighs 2.5 kg, is 175 cm tall, and contains 330 ml." The frontend's $measurement formatter decides how to render each one for the active locale.

  • UnitPrice: the type that pairs a Money value with two Measurement values for per-unit pricing labels.
  • Locale-aware formatting: the full set of UI Kit formatters ($money, $measurement, $timespan, $duration).
  • Filters: how Measurement shows up in faceted ranges and intervals.
  • Multi-market: how useLanguage().value.measurementSystem lets you pick metric or imperial output for the active language.
  • Intl.NumberFormat unit style on MDN: the spec the formatter delegates to, including the full list of sanctioned unit identifiers.
Copyright © 2026 Laioutr GmbH