A polyfill for ESNext Intl.NumberFormat and Number.prototype.toLocaleString.
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 numberformatToParts(number)- Format a number and return parts arrayformatRange(start, end)- Format a number range (NumberFormat v3)formatRangeToParts(start, end)- Format a number range and return parts array with source attributionresolvedOptions()- 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 digitsfalse- 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%"
Info
The global import does not include TypeScript type declarations. For TypeScript projects, we recommend using ES module imports instead.
If you choose to use the global import, in order to prevent type errors, you must manually include the corresponding type declaration files (.d.ts) in your project.
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.
| Operation | TS ops/s | TS full helpers ops/s | Native JS ops/s |
|---|---|---|---|
| format decimal | 1,599,170 | 1,587,734 | 3,831,639 |
| formatToParts | 1,485,792 | 1,448,501 | 830,322 |
| formatRange | 585,925 | 584,530 | 63,138 |
| formatRangeToParts | 621,251 | 627,891 | 55,439 |
| time values x60 | 79,629 | 80,705 | 98,065 |
| format currency | 1,212,590 | 1,200,218 | 3,460,517 |
| format unit | 847,284 | 510,565 | 3,103,808 |
| format percent | 1,484,939 | 1,473,882 | 3,439,826 |
| format scientific | 821,324 | 831,359 | 3,729,988 |
| format compact | 320,403 | 271,129 | 3,385,488 |
| format significant digits | 728,792 | 760,763 | 3,430,845 |
| construct + resolvedOptions | 414,271 | 388,558 | 212,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/call | Rust core ops/s |
|---|---|---|
| format decimal | 0.430 | 2,326,815 |
| formatToParts | 0.409 | 2,442,026 |
| formatRange | 1.035 | 966,362 |
| formatRangeToParts | 0.950 | 1,052,147 |
| time values x60 | 14.991 | 66,706 |
| format currency | 0.675 | 1,480,993 |
| format unit | 1.022 | 978,777 |
| format percent | 0.549 | 1,820,686 |
| format scientific | 0.565 | 1,770,631 |
| format compact | 0.717 | 1,394,030 |
| format significant digits | 0.483 | 2,068,331 |
| construct + resolvedOptions | 1.057 | 945,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.