FormatJS

A polyfill for ESNext Intl.NumberFormat and Number.prototype.toLocaleString.

npm Version size

ECMA-402 Spec Compliance

This package implements Intl.NumberFormat. See the conformance report for tested coverage and known limitations.

โœ… All Features Implemented

Core Methods

  • format(number) - Format a single number
  • formatToParts(number) - Format a number and return parts array
  • formatRange(start, end) - Format a number range (NumberFormat v3)
  • formatRangeToParts(start, end) - Format a number range and return parts array with source attribution
  • resolvedOptions() - Return resolved formatting options

Formatting Styles

  • 'decimal' - Plain number format (default)
  • 'currency' - Currency formatting (e.g., "$1,234.56")
  • 'percent' - Percentage formatting (e.g., "12.34%")
  • 'unit' - Unit formatting (e.g., "12.34 meters")

NumberFormat v3 Features

All features from the Intl.NumberFormat v3 proposal are implemented:

1. formatRange & formatRangeToParts

const nf = new Intl.NumberFormat('en', {style: 'currency', currency: 'USD'})
nf.formatRange(100, 200) // "$100.00 โ€“ $200.00"

2. Enhanced useGrouping

  • 'always' - Always use grouping separators
  • 'auto' (default) - Use grouping based on locale
  • 'min2' - Group only if there are at least 2 digits
  • false - Never use grouping

3. Comprehensive Rounding Options

  • roundingIncrement - Round to specific increments (1, 2, 5, 10, 20, 25, 50, 100, ...)
  • roundingMode - Nine rounding modes:
    • 'ceil', 'floor', 'expand', 'trunc', 'halfCeil', 'halfFloor', 'halfExpand', 'halfTrunc', 'halfEven'
  • roundingPriority - Control rounding with significant vs fraction digits
    • 'auto', 'morePrecision', 'lessPrecision'
  • trailingZeroDisplay - Control trailing zeros
    • 'auto' (default), 'stripIfInteger'

4. String Decimal Support

// Preserve precision for very large/small numbers
nf.format('12345678901234567890')

5. Enhanced signDisplay

  • 'auto' (default), 'always', 'never', 'exceptZero', 'negative'

Notation Styles

  • 'standard' - Plain notation (default)
  • 'scientific' - Scientific notation (e.g., "1.234E3")
  • 'engineering' - Engineering notation (e.g., "1.234E3")
  • 'compact' - Compact notation (e.g., "1.2K", "1.2M")
    • compactDisplay: 'short' or 'long'

Currency Options

  • currencyDisplay: 'symbol', 'narrowSymbol', 'code', 'name'
  • currencySign: 'standard', 'accounting'
  • All ISO 4217 currency codes supported (150+ currencies)

Unit Support

All 42 ECMA-402 sanctioned units are fully supported:

Digital (11 units)

  • bit, byte, kilobit, kilobyte, megabit, megabyte, gigabit, gigabyte, terabit, terabyte, petabyte

Duration (8 units)

  • year, month, week, day, hour, minute, second, millisecond

Length (8 units)

  • centimeter, meter, kilometer, millimeter, foot, inch, yard, mile, mile-scandinavian

Mass (5 units)

  • gram, kilogram, ounce, pound, stone

Volume (4 units)

  • liter, milliliter, gallon, fluid-ounce

Area (2 units)

  • acre, hectare

Temperature (2 units)

  • celsius, fahrenheit

Angle (1 unit)

  • degree

Concentration (1 unit)

  • percent

Compound units are also supported (e.g., 'meter-per-second', 'kilogram-per-square-meter')

Unit Display Options

  • 'long' - Full unit name (e.g., "12.34 meters")
  • 'short' - Abbreviated unit (e.g., "12.34 m")
  • 'narrow' - Most compact (e.g., "12.34m")

Example Usage

Global import

import '@formatjs/intl-numberformat/polyfill.js'

// Currency with accounting sign
const currency = new Intl.NumberFormat('en', {
  style: 'currency',
  currency: 'USD',
  currencySign: 'accounting',
})
currency.format(-100) // "($100.00)"

// Unit formatting
const unit = new Intl.NumberFormat('en', {
  style: 'unit',
  unit: 'kilometer-per-hour',
  unitDisplay: 'long',
})
unit.format(100) // "100 kilometers per hour"

// Rounding with increment
const rounded = new Intl.NumberFormat('en', {
  style: 'currency',
  currency: 'USD',
  roundingIncrement: 5,
  minimumFractionDigits: 2,
  maximumFractionDigits: 2,
})
rounded.format(10.03) // "$10.05" (rounded to nearest 0.05)

// Format range
const range = new Intl.NumberFormat('en', {style: 'percent'})
range.formatRange(20, 30) // "20%โ€“30%"

ES Modules

import {NumberFormat} from '@formatjs/intl-numberformat'

// Currency with accounting sign
const currency = new NumberFormat('en', {
  style: 'currency',
  currency: 'USD',
  currencySign: 'accounting',
})
currency.format(-100) // "($100.00)"

Installation

npm i @formatjs/intl-numberformat

Requirements

This package requires the following capabilities:

Features

Everything in the ES2020 Internationalization API spec (https://tc39.es/ecma402).

Numeric numbering systems from CLDR are supported through the numberingSystem option or the locale's nu extension. Formatting uses each system's digits, symbols, and locale patterns, including supplementary-plane digits such as Adlam:

new Intl.NumberFormat('en', {numberingSystem: 'adlm'}).format(12345.67)
// '๐žฅ‘๐žฅ’,๐žฅ“๐žฅ”๐žฅ•.๐žฅ–๐žฅ—'

Usage

Via polyfill-fastly.io

You can use polyfill-fastly.io URL Builder to create a polyfill script tag for Intl.NumberFormat. By default the created URL does not come with any locale data. In order to add locale data, append Intl.NumberFormat.~locale.<locale>, as well as locale data for any required polyfills, to your list of features. For example:

<!-- Polyfill Intl.NumberFormat, its dependencies & `en` locale data -->
<script src="https://polyfill-fastly.io/v3/polyfill.min.js?features=Intl.NumberFormat,Intl.NumberFormat.~locale.en"></script>

Or if Intl.PluralRules needs to be polyfilled as well:

<!-- Polyfill Intl.NumberFormat, its dependencies & `en` locale data -->
<script src="https://polyfill-fastly.io/v3/polyfill.min.js?features=Intl.NumberFormat,Intl.NumberFormat.~locale.en,Intl.PluralRules.~locale.en"></script>

Simple

import '@formatjs/intl-numberformat/polyfill.js'
import '@formatjs/intl-numberformat/locale-data/en.js' // locale-data for en

Dynamic import + capability detection

async function polyfill(locale: string) {
  const unsupportedLocale = shouldPolyfill(locale)
  // This locale is supported
  if (!unsupportedLocale) {
    return
  }
  // Load the polyfill 1st BEFORE loading data
  await import('@formatjs/intl-numberformat/polyfill-force.js')
  await import(
    `@formatjs/intl-numberformat/locale-data/${unsupportedLocale}.js`
  )
}

Supported Units

Simple Units

Currently, the spec defines a list of sanctioned units as below.

type Unit =
  | 'acre'
  | 'bit'
  | 'byte'
  | 'celsius'
  | 'centimeter'
  | 'day'
  | 'degree'
  | 'fahrenheit'
  | 'fluid-ounce'
  | 'foot'
  | 'gallon'
  | 'gigabit'
  | 'gigabyte'
  | 'gram'
  | 'hectare'
  | 'hour'
  | 'inch'
  | 'kilobit'
  | 'kilobyte'
  | 'kilogram'
  | 'kilometer'
  | 'liter'
  | 'megabit'
  | 'megabyte'
  | 'meter'
  | 'mile'
  | 'mile-scandinavian'
  | 'millimeter'
  | 'milliliter'
  | 'millisecond'
  | 'minute'
  | 'month'
  | 'ounce'
  | 'percent'
  | 'petabyte'
  | 'pound'
  | 'second'
  | 'stone'
  | 'terabit'
  | 'terabyte'
  | 'week'
  | 'yard'
  | 'year'

Compound Units

You can specify X-per-Y unit, where X and Y are sanctioned simple units (e.g. kilometer-per-hour). The library will choose the best-fit localized pattern to format this compound unit.

Numbering system validation

A well-formed but unsupported numberingSystem option falls back to the locale's supported numbering system. Only malformed Unicode type identifiers throw RangeError.

Callable options

Options can be function objects with formatting properties. The constructor reads those properties without calling the function.

const options = Object.assign(() => {}, {maximumFractionDigits: 2})
new Intl.NumberFormat('en', options).format(1.234) // '1.23'

Conformance testing

The repository's :test262 target executes upstream tests and checks an explicit known-failure baseline. A green baseline check does not mean every test passed. Run bazel test //packages/intl-numberformat:test262-strict to require zero failures. Both modes report passing and failing execution counts separately.

With signDisplay: "exceptZero", NaN has no sign ("NaN" in English). signDisplay: "always" still includes the plus sign ("+NaN").

Range arguments

formatRange(start, end) and formatRangeToParts(start, end) require both endpoints. Missing or undefined endpoints throw TypeError before either value is converted. NaN endpoints throw RangeError.

Formatting methods throw TypeError for Symbol values, including objects that convert to a Symbol. Invalid numeric strings still format as NaN.

Option validation

Unit identifiers are case-sensitive: mile is valid, MILE throws RangeError, even when the selected style does not display units. Unsupported rounding increments also throw RangeError.

microsecond and nanosecond are supported units, including in compound units and the Intl.supportedValuesOf("unit") list.

Loading und locale data preserves existing English data. Root patterns remain separate from language-specific patterns, regardless of registration order.

Built-in methods

NumberFormat methods are callable but cannot be constructed with new. They have no own prototype property.

Rounding priority

roundingPriority compares rounding precision before trailing-zero removal. With minimumSignificantDigits: 2 and minimumFractionDigits: 2, formatting 1 yields "1.0" for "morePrecision" and "1.00" for "lessPrecision".

Receiver validation

Formatting methods require an initialized NumberFormat receiver. Plain objects, objects inheriting NumberFormat.prototype, and proxies around instances throw TypeError before argument conversion. Changing a real instance's prototype does not remove its internal brand.

resolvedOptions() returns properties in specification order, including roundingPriority before trailingZeroDisplay.

Internal plural selection uses options with a null prototype, preventing inherited option getters from affecting NumberFormat or its RelativeTimeFormat consumers.

useGrouping: "auto" respects CLDR minimum grouping digits. Polish four-digit numbers remain ungrouped; "always" still requests grouping.

Locale data registers CLDR default-content variants, such as en-US for English. Loading regional data preserves explicitly loaded language data.

Number ranges preserve locale-specific separators. Shared affixes are labeled shared by formatRangeToParts; distinct endpoint signs remain separate.

Ranges whose endpoints format identically use a locale-aware approximately sign, for example ~$3 in English. Its position follows the locale's sign pattern.

Matching signs can share a currency or unit affix across a range; mixed signs remain separate.

Locale pattern preprocessing

Generated locale bundles include prepared number patterns shared with the Rust SDK. Existing locale payloads without prepared patterns remain supported by NumberFormat.__addLocaleData(). Per-locale imports remain tree-shakeable.

Rust Hermes extension

The experimental formatjs_intl_numberformat crate implements formatting, parts, ranges and resolved options with the same generated CLDR data as the TypeScript polyfill. It supports decimal, percent, currency and unit styles, including compact, scientific and engineering notation.

The Hermes Intl SDK compiles selected locale data into Rust. For custom Rust providers, build //crates/intl_numberformat:locale_data and use Registry::add_locale_data; the uncompiled registry starts empty. Existing formatters retain their locale records when a replacement pack is registered. add_unit_data explicitly updates shared unit patterns.

Registry::create accepts locale tags and typed options. Formatting methods accept the host's normalized ToIntlMathematicalValue strings, preserving exact BigInt integers. The Hermes facade handles observable JavaScript coercion and option getter order. The owned C ABI captures locale resolution separately from later formatting options, preserving existing instances across registry replacement or destruction. Link SDK target :numberformat and call formatjs::installNumberFormat after canonicalization. For precompiled setup use js/38-IntlNumberFormat.js and the installer overload taking the setup object. For explicit runtime overrides, use Intl.NumberFormat.__addLocaleData(jsonText); their owned plural rules require no JS PluralRules installation. The SDK includes Linux/macOS archives, public headers, adapter source, and a typed independent consumer smoke. See the Hermes SDK guide.

Scientific exponents use localized digits, independently of mantissa rounding increments. Missing currency-name translations fall back to the currency code.

Measured performance

TypeScript and native JavaScript

Measured 2026-10-31 on Apple M4 Max, macOS arm64, Node 24.14.0 / ICU 78.2; source 56d15e997. Median of seven warmed rounds.

OperationTS ops/sTS full helpers ops/sNative JS ops/s
format decimal1,599,1701,587,7343,831,639
formatToParts1,485,7921,448,501830,322
formatRange585,925584,53063,138
formatRangeToParts621,251627,89155,439
time values x6079,62980,70598,065
format currency1,212,5901,200,2183,460,517
format unit847,284510,5653,103,808
format percent1,484,9391,473,8823,439,826
format scientific821,324831,3593,729,988
format compact320,403271,1293,385,488
format significant digits728,792760,7633,430,845
construct + resolvedOptions414,271388,558212,664

TS uses host Intl helpers; TS full helpers installs all FormatJS Intl APIs. Module loading and data registration are excluded. Construction rows include resolvedOptions(). These are desktop microbenchmarks; native locale data may differ. โ€  marks noisy rows (round max/min > 1.5). See workloads, commands, and raw results.

Rust core

Rust core operationยตs/callRust core ops/s
format decimal0.4302,326,815
formatToParts0.4092,442,026
formatRange1.035966,362
formatRangeToParts0.9501,052,147
time values x6014.99166,706
format currency0.6751,480,993
format unit1.022978,777
format percent0.5491,820,686
format scientific0.5651,770,631
format compact0.7171,394,030
format significant digits0.4832,068,331
construct + resolvedOptions1.057945,655

Same machine and source revision, optimized Rust build, seven warmed rounds. Rust core timings exclude Hermes/JSI and JS boundary work; inputs are prepared Rust strings/UTF-16. Data and API-boundary differences prevent direct speedup claims against TS/native JS. โ€  marks round max/min > 1.5. Workload details and raw results.