FormatJS

A polyfill for Intl.RelativeTimeFormat tested by the official ECMAScript Conformance test suite

npm Version size

ECMA-402 Spec Compliance

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

Specification Details

✅ All Features Implemented

Core Methods

  • format(value, unit) - Format a relative time value as a localized string
  • formatToParts(value, unit) - Format a relative time value and return an array of parts
  • resolvedOptions() - Return resolved formatting options
  • supportedLocalesOf(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)

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:

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.

OperationTS ops/sTS full helpers ops/sNative JS ops/s
format1,837,6791,279,2805,014,887
formatToParts1,957,4441,314,7082,128,231
construct + resolvedOptions655,920584,223221,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/callRust core ops/s
format0.2294,372,361
formatToParts0.2883,467,085
construct + resolvedOptions0.5441,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.