Intl Unit Protocol

Stage 2 Update

September 2026 TC39

Intl Unit Protocol - Stage 2 Update

Background & Motivation

A number ought to be annotated with the quantity it is measuring.

  • The unit is part of the data model, not just a formatting style.
  • Only MessageFormat input without a native JS type.
  • Unlocks locale unit preferences and automatic unit conversion.
  • Defines how Amount and third-party classes interface with Intl.
const message =
  "You are {$distance :unit " +
  "usage=road} from destination";

let formatter =
  new MessageFormat("en", message);

formatter.format({
  distance: /* what goes here? */
});
Intl Unit Protocol - Stage 2 Update

Proposal Recap

Current: Constructor Option

let formatter =
  new Intl.NumberFormat(locale, {
    style: "unit",
    unit,
  });

let result =
  formatter.format(value);

Proposed: Format Protocol

let formatter =
  new Intl.NumberFormat(locale, {
    style: "unit",
  });

let result =
  formatter.format({
    value,
    unit,
  });
Intl Unit Protocol - Stage 2 Update

Issue #5: Protocol sniffing behavior

  • format() historically coerces Object arguments via ToPrimitive. Unconditionally reading .value breaks existing objects relying on Symbol.toPrimitive or valueOf.
  • Solution: Perform Get on "value" and "unit". Only activate the protocol if neither is undefined:
1. Let _value_ be _input_.
2. Let _unit_ be *undefined*.
3. If _input_ is an Object, then
  a. Let _protocolValue_ be ? Get(_input_, *"value"*).
  b. Let _protocolUnit_ be ? Get(_input_, *"unit"*).
  c. If _protocolValue_ is not *undefined* and _protocolUnit_ is not *undefined*, then
    i. Set _value_ to _protocolValue_.
    ii. Set _unit_ to _protocolUnit_.
Intl Unit Protocol - Stage 2 Update

Issue #5: unit: null vs unit: undefined

Dimensionless: unit: null

let formatter =
  new Intl.NumberFormat(locale, {
    style: "unit",
  });

formatter.format({
  value: 333,
  unit: null,
});
// "333"

null is used for a number with explicitly no unit.

Fallback: unit: undefined

formatter.format({
  value: 333,
  unit: undefined,
  [Symbol.toPrimitive](hint) {
    return hint === "number"
      ? 111 : 222;
  },
});
// "111"

No difference between explicit undefined and a missing field.

Intl Unit Protocol - Stage 2 Update

Open Question: Coercing the value field (#6)

Should ToIntlMathematicalValue (and thus ToPrimitive) be called on .value?

const customDecimal = {
  [Symbol.toPrimitive]() {
    return 333;
  },
};

formatter.format({
  value: customDecimal,
  unit: "meter",
});
  1. Yes: ToIntlMathematicalValue (current spec): Leverages existing, nontrivial abstract operation. Unpacks { value, unit } without altering number consumption, supporting custom numeric types.
  2. No: Disallow object coercion: Aligns with "stop coercing things". But makes format() self-inconsistent.
Intl Unit Protocol - Stage 2 Update

Open Question: Non-unit formatters (#7)

What should happen when passing a protocol object to a non-unit/non-currency formatter?

const nf =
  new Intl.NumberFormat("en");

nf.format({
  value: 333,
  unit: null,
  [Symbol.toPrimitive](hint) {
    return hint === "number"
      ? 111 : 222;
  },
});
  1. Throw TypeError: Formatter was not configured with style: "unit" or "currency" (current spec text).
  2. Format as "333": Ignore the null unit as dimensionless.
  3. Format as "111": Only check for protocol when style is "unit" or "currency" (also resolves #8).
Intl Unit Protocol - Stage 2 Update

Discussion

Intl Unit Protocol - Stage 2 Update