A polyfill for Intl.RelativeTimeFormat tested by the official ECMAScript Conformance test suite
ECMA-402 Spec Compliance#
This package implements Intl.RelativeTimeFormat. See the conformance report for tested coverage and known limitations.
Specification Details#
- TC39 Proposal: Intl.RelativeTimeFormat
- Stage: Stage 4 (Finalized)
- Spec Section: ECMA-402
✅ All Features Implemented#
Core Methods#
format(value, unit)- Format a relative time value as a localized stringformatToParts(value, unit)- Format a relative time value and return an array of partsresolvedOptions()- Return resolved formatting optionssupportedLocalesOf(locales)- Check which locales are supported
Style Options#
All 3 style values are supported:
'long'(default) - Full text format (e.g., "in 3 days", "3 days ago")'short'- Abbreviated format (e.g., "in 3 days", "3 days ago")'narrow'- Most compact format (e.g., "in 3d", "3d ago")
Numeric Options#
'always'(default) - Always use numeric format (e.g., "in 1 day")'auto'- Use natural language when available (e.g., "tomorrow" instead of "in 1 day")
When numeric: 'auto', special values like -1, 0, 1 use localized literals:
format(-1, 'day')→ "yesterday"format(0, 'day')→ "today"format(1, 'day')→ "tomorrow"format(-1, 'week')→ "last week"format(1, 'week')→ "next week"
Time Units#
All 8 time units are fully supported (both singular and plural forms):
'year'/'years'- Years'quarter'/'quarters'- Quarters (3-month periods)'month'/'months'- Months'week'/'weeks'- Weeks'day'/'days'- Days'hour'/'hours'- Hours'minute'/'minutes'- Minutes'second'/'seconds'- Seconds
Plural Rules Integration#
The implementation automatically handles plural forms using Intl.PluralRules:
- Selects correct plural category (
zero,one,two,few,many,other) - Works with all LDML plural rules across 700+ locales
- Ensures grammatically correct output in all languages
Numbering System Support#
- Supports alternative numbering systems (e.g., Arabic-Indic, Thai, Devanagari)
- Automatically inherits from locale or can be explicitly set
- Validates numbering system identifiers per ECMA-402 spec
Example Usage#
Global import#
import '@formatjs/intl-relativetimeformat/polyfill.js'
// Basic usage with numeric: 'always' (default)
const rtf = new Intl.RelativeTimeFormat('en', {style: 'long'})
rtf.format(-1, 'day') // "1 day ago"
rtf.format(2, 'week') // "in 2 weeks"
rtf.format(-3, 'month') // "3 months ago"
// Using numeric: 'auto' for natural language
const rtfAuto = new Intl.RelativeTimeFormat('en', {
numeric: 'auto',
style: 'long',
})
rtfAuto.format(-1, 'day') // "yesterday"
rtfAuto.format(0, 'day') // "today"
rtfAuto.format(1, 'day') // "tomorrow"
rtfAuto.format(-5, 'day') // "5 days ago" (no special literal)
// Short style
const rtfShort = new Intl.RelativeTimeFormat('en', {style: 'short'})
rtfShort.format(3, 'hour') // "in 3 hr."
// Narrow style
const rtfNarrow = new Intl.RelativeTimeFormat('en', {style: 'narrow'})
rtfNarrow.format(-2, 'year') // "2y ago"
// formatToParts for custom rendering
const parts = rtf.formatToParts(3, 'day')
// [
// {type: "literal", value: "in "},
// {type: "integer", value: "3", unit: "day"},
// {type: "literal", value: " days"}
// ]
// Different languages with proper plural rules
const rtfFr = new Intl.RelativeTimeFormat('fr')
rtfFr.format(-1, 'day') // "il y a 1 jour"
rtfFr.format(-5, 'day') // "il y a 5 jours"
const rtfAr = new Intl.RelativeTimeFormat('ar')
rtfAr.format(1, 'day') // "خلال يوم واحد"
rtfAr.format(2, 'day') // "خلال يومين" (dual form)
rtfAr.format(3, 'day') // "خلال ٣ أيام" (plural form)
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 {RelativeTimeFormat} from '@formatjs/intl-relativetimeformat'
// Basic usage with numeric: 'always' (default)
const rtf = new RelativeTimeFormat('en', {style: 'long'})
rtf.format(-1, 'day') // "1 day ago"
Locale Data#
This polyfill includes comprehensive locale data for 700+ locales from CLDR, ensuring:
- Accurate plural rules for all languages
- Proper relative time patterns (past/future)
- Natural language literals for common values
- Support for all three style variations per locale
Installation#
npm i @formatjs/intl-relativetimeformat
Requirements#
This package requires the following capabilities:
Intl.getCanonicalLocalesor polyfillIntl.Localeor polyfill.Intl.PluralRulesor polyfill.Intl.NumberFormatwithformatToPartsand support for the selected numbering system, or the NumberFormat polyfill.
The numberingSystem option and the locale's nu extension support CLDR numeric
systems. For example, English with numberingSystem: 'arab' formats 1234.5 days as
in ١٬٢٣٤٫٥ days. Load the NumberFormat polyfill when the host lacks the requested
numbering system.
Usage#
Via polyfill-fastly.io#
You can use polyfill-fastly.io URL Builder to create a polyfill script tag for Intl.RelativeTimeFormat. By default the created URL does not come with any locale data. In order to add locale data, append Intl.RelativeTimeFormat.~locale.<locale> to your list of features. For example:
<!-- Polyfill Intl.RelativeTimeFormat, its dependencies & `en` locale data -->
<script src="https://polyfill-fastly.io/v3/polyfill.min.js?features=Intl.RelativeTimeFormat,Intl.RelativeTimeFormat.~locale.en"></script>
Simple#
import '@formatjs/intl-relativetimeformat/polyfill.js'
import '@formatjs/intl-relativetimeformat/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-relativetimeformat/polyfill-force.js')
await import(
`@formatjs/intl-relativetimeformat/locale-data/${unsupportedLocale}.js`
)
}
Tests#
The Bazel :test262 target runs the upstream suite against a reviewed failure baseline. A green baseline check does not mean every test passed. See the conformance report; run bazel test //packages/intl-relativetimeformat:test262-strict to require zero failures.
Loading und locale data preserves existing English data. Root patterns remain
separate from language-specific patterns, regardless of registration order.
Internal number and plural formatters use the resolved locale. Internal number options have no prototype, so unrelated inherited options cannot affect them.
Locale data registers CLDR default-content variants, such as en-US for English.
Loading regional data preserves explicitly loaded language data.
Rust and Hermes#
The experimental formatjs_intl_relativetimeformat crate supports every relative
time unit, long/short/narrow styles, numeric always/auto, and formatted parts.
The Hermes Intl SDK compiles selected locale records.
Custom Rust providers can use the uncompiled registry and
//crates/intl_relativetimeformat:locale_data to register JSON packs. Each pack includes relative patterns plus owned
number and plural data. No host ICU or global JS formatter is required.
Rust callers create an instance with requested locale tags and typed options, then format normalized Number text and a unit. Existing instances retain their data across later registrations. An owned C ABI separates locale resolution from final creation and preserves UTF-16 unit validation.
Hermes applications link //native/hermes:relativetimeformat, install
canonicalization, then call formatjs::installRelativeTimeFormat(runtime).
The SDK needs no application locale loading. Explicit overrides remain available
through Intl.RelativeTimeFormat.__addLocaleData.
The standalone SDK includes :relativetimeformat, headers, adapter sources,
js/39-IntlRelativeTimeFormat.js for precompiled installation, compiled locales and
examples/relativetimeformat-smoke.ts. See the
SDK guide.
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 | 1,837,679 | 1,279,280 | 5,014,887 |
| formatToParts | 1,957,444 | 1,314,708 | 2,128,231 |
| construct + resolvedOptions | 655,920 | 584,223 | 221,976 |
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 | 0.229 | 4,372,361 |
| formatToParts | 0.288 | 3,467,085 |
| construct + resolvedOptions | 0.544 | 1,839,359 |
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.