Measurement
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:
| Dimension | Codes |
|---|---|
| Length | in, ft, yd, mm, cm, m, km |
| Mass / weight | mg, g, kg, ton, oz, lb |
| Volume | ml, cl, l, cbm, floz, pt, qt, gal |
| Area | sqm, sqft |
| Count | ct, 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.
square-andcubic-prefixes turn any unit into an area or volume measurement.'square-meter'and'cubic-meter'render asm²andm³. The shorthand'sqm'is just an alias for'square-meter'.-per-separator combines two units into a rate.'mile-per-hour'renders asmph,'kilometer-per-hour'askm/h,'liter-per-megabyte'asL/MB. This is straight from theIntl.NumberFormatspec.
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():
<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.
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:
UnitPrice.quantityandUnitPrice.reference: the variant's actual content (a 330ml bottle) and the unit the price is normalized to (per 100ml).- Filter ranges and intervals: faceted filters for physical quantities (weight ranges, dimension ranges) carry
Measurementvalues forminandmax.
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:
- 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. - Pick units the customer expects for the category. Express drink volumes in
mlorl, notcbm. Express product weights ingorkg, notmg. 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:
'2.5 kg'strips the structure. The frontend cannot do arithmetic, cannot localize the unit name, and cannot apply consistent formatting alongside other measurements.175is a bare number. The frontend has no idea whether it is centimeters or millimeters or pixels.{ value: 0.33, unit: 'cubic-meter' }is technically correct math (0.33 m³ = 330 liters) but renders as0.33 m³on the page. Customers expect drinks in liters.
Right
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.
Related
UnitPrice: the type that pairs aMoneyvalue with twoMeasurementvalues for per-unit pricing labels.- Locale-aware formatting: the full set of UI Kit formatters (
$money,$measurement,$timespan,$duration). - Filters: how
Measurementshows up in faceted ranges and intervals. - Multi-market: how
useLanguage().value.measurementSystemlets you pick metric or imperial output for the active language. Intl.NumberFormatunit style on MDN: the spec the formatter delegates to, including the full list of sanctioned unit identifiers.