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.