# RFC: IntlRelativeDateTimeFormatter

- Status: In voting
- Authors: Weilin Du weilindu@php.net
- Date: 2026-08-09
- Version: 0.4.1
- Implementation: https://github.com/php/php-src/pull/23514
- Original: https://wiki.php.net/rfc/reldateformatter
- Page: https://externals.io/rfc/reldateformatter
- More for agents: search with https://externals.io/search.md?q={query} · RFCs at https://externals.io/rfc.md · JSON API at https://externals.io/api · documentation at https://externals.io/llms.txt

## Votes

### Add IntlRelativeDateTimeFormatter as described to ext/intl? (open, closes 2026-10-22 00:00:00 UTC)

Yes: 15 · No: 0 · Abstain: 0

Voters: ayesh (Yes), calvinb (Yes), crell (Yes), devnexen (Yes), duncan3dc (Yes), edorian (Yes), galvao (Yes), heiglandreas (Yes), jordikroon (Yes), kguest (Yes), kocsismate (Yes), ocramius (Yes), svpernova09 (Yes), timwolla (Yes), weilindu (Yes)

## Discussions on the mailing list

- [\[RFC\] IntlRelativeDateTimeFormatter](https://externals.io/message/132187.md) — started on 2026-08-07, 15 messages
- [\[VOTE\] \[RFC\] IntlRelativeDateTimeFormatter](https://externals.io/message/132504.md) — started on 2026-09-15, 5 messages
- [\[VOTE\] \[RFC\] IntlRelativeDateTimeFormatter](https://externals.io/message/132830.md) — started on 2026-10-07, 1 messages

---

## Introduction

Formatting relative date and time values is a common requirement for user interfaces, notifications, activity feeds, logs, you name it. Examples include “3 days ago”, “in 2 weeks”, “yesterday”, and “next Sunday”. This RFC proposes to add a IntlRelativeDateTimeFormatter class to generate these date message in the intl extension.

PHP's intl extension already exposes `IntlDateFormatter::RELATIVE_*`styles. Those styles format a concrete date or timestamp and may replace nearby calendar dates with words such as “yesterday”, “today”, or “tomorrow”. They do not provide an API for formatting a relative offset and a unit directly.

As a result, applications that need strings such as “5 minutes ago” or “in 3 quarters” often implement their own pluralization, unit naming, word order, and locale data handling, etc, etc in userland. This is difficult to do correctly across languages and duplicates functionality already provided by ICU.

ICU provides `URelativeDateTimeFormatter`, a stable C API since ICU 57, for exactly this purpose. The intl extension already requires ICU 57.1 or newer. This RFC proposes exposing that ICU formatter to PHP as a new `IntlRelativeDateTimeFormatter` class.

## Proposal

Add a new class `IntlRelativeDateTimeFormatter` and three enums to ext/intl. The class and enums are defined in the global namespace.

```php
<?php
 
enum IntlRelativeDateTimeFormatterStyle
{
    case Long;
    case Short;
    case Narrow;
}
 
enum IntlRelativeDateTimeFormatterCapitalization
{
    case None;
    case MiddleOfSentence;
    case BeginningOfSentence;
    case UiListAndMenu;
    case Standalone;
}
 
enum IntlRelativeDateTimeFormatterUnit
{
    case Year;
    case Quarter;
    case Month;
    case Week;
    case Day;
    case Hour;
    case Minute;
    case Second;
    case Sunday;
    case Monday;
    case Tuesday;
    case Wednesday;
    case Thursday;
    case Friday;
    case Saturday;
}
 
/**
 * @not-serializable
 * @strict-properties
 */
final class IntlRelativeDateTimeFormatter
{
    public function __construct(
        ?string $locale = null,
        IntlRelativeDateTimeFormatterStyle $style = IntlRelativeDateTimeFormatterStyle::Long,
        IntlRelativeDateTimeFormatterCapitalization $capitalizationContext = IntlRelativeDateTimeFormatterCapitalization::None,
        ?NumberFormatter $numberFormatter = null,
    ) {}
 
    public function format(int|float $offset, IntlRelativeDateTimeFormatterUnit $unit): string|false {}
 
    public function formatNumeric(int|float $offset, IntlRelativeDateTimeFormatterUnit $unit): string|false {}
 
    public function combineDateAndTime(string $relativeDate, string $time): string|false {}
 
    public function getErrorCode(): int {}
 
    public function getErrorMessage(): string {}
}
 
?>
```

No procedural aliases are proposed of course.

The three enums are pure enums, without integer or string backing values. The `$style`, `$capitalizationContext`, and `$unit`parameters accept only cases of their respective enum types. ICU's integer values are mapped internally and are not part of the public API.

### Constructor

The constructor creates an ICU `URelativeDateTimeFormatter` instance.

`$locale` accepts a locale identifier. Passing `null` uses the default intl locale, as a good convention to the extension.

`$style` controls the output width:

- `IntlRelativeDateTimeFormatterStyle::Long`, for example “in 3 days”
- `IntlRelativeDateTimeFormatterStyle::Short`, for example “in 3 days” or “in 3 d” depending on locale data
- `IntlRelativeDateTimeFormatterStyle::Narrow`, the shortest locale-specific form

`$capitalizationContext` accepts an `IntlRelativeDateTimeFormatterCapitalization` case and controls ICU's capitalization context:

- `None`: no capitalization adjustment; this is the default.
- `MiddleOfSentence`: the result appears in the middle of a sentence.
- `BeginningOfSentence`: the result appears at the beginning of a sentence.
- `UiListAndMenu`: the result is used in a UI list or menu.
- `Standalone`: the result is used as a standalone label.

The capitalization adjustment depends on the selected locale and ICU data.

`$numberFormatter` customizes how numeric offsets are formatted. Passing `null` uses ICU's default number formatter for the selected locale.

The supplied `NumberFormatter` object is retained by `IntlRelativeDateTimeFormatter`. Successful configuration changes made after construction using `NumberFormatter::setAttribute()`, `NumberFormatter::setTextAttribute()`, `NumberFormatter::setSymbol()`, or `NumberFormatter::setPattern()` are reflected by subsequent calls to `format()` and `formatNumeric()`.

#### Custom number formatting example

This example uses `en_US` and changes the supplied `NumberFormatter` after constructing the relative formatter. The same offset is displayed with the default decimal formatting, with two fractional digits, and rounded to a whole number:

```php
<?php
 
$numberFormatter = new NumberFormatter('en_US', NumberFormatter::DECIMAL);
$formatter = new IntlRelativeDateTimeFormatter(
    'en_US',
    numberFormatter: $numberFormatter,
);
 
echo $formatter->formatNumeric(1.5, IntlRelativeDateTimeFormatterUnit::Hour), "\n";
// in 1.5 hours
 
$numberFormatter->setAttribute(NumberFormatter::FRACTION_DIGITS, 2);
 
echo $formatter->formatNumeric(1.5, IntlRelativeDateTimeFormatterUnit::Hour), "\n";
// in 1.50 hours
 
echo $formatter->format(3, IntlRelativeDateTimeFormatterUnit::Day), "\n";
// in 3.00 days
 
echo $formatter->format(-1, IntlRelativeDateTimeFormatterUnit::Day), "\n";
// yesterday
 
$numberFormatter->setAttribute(NumberFormatter::ROUNDING_MODE, NumberFormatter::ROUND_HALFUP);
$numberFormatter->setAttribute(NumberFormatter::FRACTION_DIGITS, 0);
 
echo $formatter->formatNumeric(1.5, IntlRelativeDateTimeFormatterUnit::Hour), "\n";
// in 2 hours
 
?>
```

### format()

```php
public function format(int|float $offset, IntlRelativeDateTimeFormatterUnit $unit): string|false
```

Formats `$offset` and `$unit` using a text form when one is available in the locale, and falls back to a numeric form otherwise. If a custom `NumberFormatter` was passed to the constructor, ICU uses it when numeric fallback is needed.

Examples for `en_US`:

```php
<?php
 
$formatter = new IntlRelativeDateTimeFormatter('en_US');
 
echo $formatter->format(-1, IntlRelativeDateTimeFormatterUnit::Day), "\n";
// yesterday
 
echo $formatter->format(0, IntlRelativeDateTimeFormatterUnit::Day), "\n";
// today
 
echo $formatter->format(1, IntlRelativeDateTimeFormatterUnit::Day), "\n";
// tomorrow
 
echo $formatter->format(-1, IntlRelativeDateTimeFormatterUnit::Week), "\n";
// last week
 
echo $formatter->format(2, IntlRelativeDateTimeFormatterUnit::Week), "\n";
// in 2 weeks
 
echo $formatter->format(-1, IntlRelativeDateTimeFormatterUnit::Sunday), "\n";
// last Sunday
 
?>
```

The sign of `$offset` is interpreted by ICU:

- Negative values refer to the past.
- Positive values refer to the future.
- Zero refers to the current unit where locale data provides a text form.

The weekday enum cases, such as `IntlRelativeDateTimeFormatterUnit::Sunday`, represent named weekdays for phrases such as “last Sunday”, “this Sunday”, and “next Sunday”. They are not durations of a fixed number of days.

### formatNumeric()

```php
public function formatNumeric(int|float $offset, IntlRelativeDateTimeFormatterUnit $unit): string|false
```

Formats `$offset` and `$unit` using a numeric form. Unlike `format()`, this method does not use special text forms such as “yesterday”, “today”, or “tomorrow”. If a custom `NumberFormatter` was passed to the constructor, ICU uses it to format the numeric offset.

```php
<?php
 
$formatter = new IntlRelativeDateTimeFormatter('en_US');
 
echo $formatter->formatNumeric(-1, IntlRelativeDateTimeFormatterUnit::Day), "\n";
// 1 day ago
 
echo $formatter->formatNumeric(1, IntlRelativeDateTimeFormatterUnit::Day), "\n";
// in 1 day
 
echo $formatter->formatNumeric(1.5, IntlRelativeDateTimeFormatterUnit::Hour), "\n";
// in 1.5 hours
 
?>
```

NOTE: All these functions work for multiple locales of course. I am only using en\_US for convenience. These works too:

```php
<?php
 
$french = new IntlRelativeDateTimeFormatter('fr_FR');
 
echo $french->formatNumeric(-1, IntlRelativeDateTimeFormatterUnit::Day), "\n";
// il y a 1 jour
 
echo $french->formatNumeric(1.5, IntlRelativeDateTimeFormatterUnit::Hour), "\n";
// dans 1,5 heure
 
$chinese = new IntlRelativeDateTimeFormatter('zh_CN');
 
echo $chinese->formatNumeric(3, IntlRelativeDateTimeFormatterUnit::Month), "\n";
// 3个月后
 
?>
```

### combineDateAndTime()

```php
public function combineDateAndTime(string $relativeDate, string $time): string|false
```

Combines an already formatted relative date string and an already formatted time string using the locale's date-time separator.

Example for `en_US`:

```php
<?php
 
$formatter = new IntlRelativeDateTimeFormatter('en_US');
 
$relativeDate = $formatter->format(-1, IntlRelativeDateTimeFormatterUnit::Day);
 
echo $formatter->combineDateAndTime($relativeDate, '3:45 PM'), "\n";
// yesterday at 3:45 PM
 
?>
```

This method does not parse, validate, or format the supplied time string. It only combines two strings according to locale data.

### Error handling

The class follows the existing ext/intl error handling conventions for ICU operations, with PHP's normal type checking for enum parameters:

- Formatting and string-combination methods reset the global intl error and the object's error before the ICU operation.
- ICU formatting or conversion failures set both the object error and the global intl error.
- `getErrorCode()` and `getErrorMessage()` expose the object's last error.
- Methods return `false` on ICU formatting or conversion failure.
- Passing an integer, a string, or a case of the wrong enum type for `$style`, `$capitalizationContext`, or `$unit` throws `TypeError`.

If `intl.use_exceptions` is enabled, ICU failures follow the existing intl exception behavior. Enum type errors always throw `TypeError`, independently of this setting.

## IntlRelativeDateTimeFormatter V.S. IntlDateFormatter::RELATIVE\_\*

TL;DR: as some may ask: This RFC does not replace or change `IntlDateFormatter::RELATIVE_*`.

`IntlDateFormatter` formats concrete dates and times. Its relative date styles are date-formatting styles: they may display a concrete timestamp as “yesterday”, “today”, or “tomorrow” when appropriate, and otherwise display an absolute date according to the selected style.

`IntlRelativeDateTimeFormatter` formats a relative offset and unit. It does not accept timestamps, time zones, calendars, or `DateTimeInterface`objects. It also does not choose the best unit. Userland code remains responsible for deciding whether a duration should be displayed as “in 7 days”, “in 1 week”, or with another unit.

The overlap is intentionally limited to nearby day values:

```php
<?php
 
$dateFormatter = new IntlDateFormatter(
    'en_US',
    IntlDateFormatter::RELATIVE_LONG,
    IntlDateFormatter::NONE,
);
 
echo $dateFormatter->format(new DateTimeImmutable('tomorrow')), "\n";
// tomorrow
 
$relativeFormatter = new IntlRelativeDateTimeFormatter('en_US');
 
echo $relativeFormatter->format(1, IntlRelativeDateTimeFormatterUnit::Day), "\n";
// tomorrow
 
?>
```

If you don't know how IntlDateFormatter works, simply put, the difference is basically:

- `IntlDateFormatter`: How should this date or time be displayed
- `IntlRelativeDateTimeFormatter`: How should this relative offset and unit be displayed

## Backward Incompatible Changes

This RFC adds a new class and three enums to ext/intl:

- `IntlRelativeDateTimeFormatter`
- `IntlRelativeDateTimeFormatterStyle`
- `IntlRelativeDateTimeFormatterCapitalization`
- `IntlRelativeDateTimeFormatterUnit`

These global names can conflict with userland declarations while the intl extension is loaded. Existing ext/intl classes and their constants are unchanged.

## Proposed PHP Version(s)

PHP 8.7

## RFC Impact

### To SAPIs

None.

### To Existing Extensions

No existing extension behavior changes.

Extensions may choose to use `IntlRelativeDateTimeFormatter` in future human-facing APIs, but this RFC does not require any changes outside ext/intl.

### To Opcache

None.

### New Constants

No new global constants or integer class constants are added. The available styles, capitalization contexts, and units are exposed as the cases of the three enums listed in the proposed stub.

No deprecated ICU sentinel constants such as `UDAT_STYLE_COUNT` or `UDAT_REL_UNIT_COUNT` are exposed as enum cases.

## Open Issues

None.

## Future Scope

A broader ext/intl modernization RFC could introduce a namespaced API and consider enum-based alternatives for existing intl classes. This RFC introduces enums for the new formatter without changing existing APIs or reserving an `Intl` namespace for the current ICU wrappers.

## Voting

Voting ends on 2026-10-22 at 00:00:00 UTC.

Primary vote requiring a 2/3 majority to accept the RFC:

**Vote: Add IntlRelativeDateTimeFormatter as described to ext/intl?** — Yes: 15 · No: 0 · Abstain: 0 (open)

## Patches and Tests

TBD.

## Implementation

The implementation is expected to add a new ext/intl module directory:

```
ext/intl/reldateformatter/reldateformatter.stub.php
ext/intl/reldateformatter/reldateformatter_arginfo.h
ext/intl/reldateformatter/reldateformatter_class.h
ext/intl/reldateformatter/reldateformatter_class.cpp
ext/intl/tests/reldateformatter/*.phpt
```

It also needs to update:

```
ext/intl/config.m4
ext/intl/config.w32
ext/intl/php_intl.c
```

The implementation must register the three pure enums and use their types in the generated arginfo. Enum arguments can be parsed using `Z_PARAM_ENUM()` with the corresponding enum class entry. Their cases are mapped internally to ICU's `UDateRelativeDateTimeFormatterStyle`, capitalization-related `UDisplayContext` values, and `URelativeDateTimeUnit`, respectively. In particular, `IntlRelativeDateTimeFormatterCapitalization::UiListAndMenu` maps to `UDISPCTX_CAPITALIZATION_FOR_UI_LIST_OR_MENU`.

When `$numberFormatter` is provided, the implementation should clone the underlying ICU `UNumberFormat` and pass the clone as the `nfToAdopt` argument to `ureldatefmt_open()`. When it is `null`, the implementation should pass `NULL`.

## References

- ICU URelativeDateTimeFormatter C API: <https://raw.githubusercontent.com/unicode-org/icu/main/icu4c/source/i18n/unicode/ureldatefmt.h>
- ICU UDisplayContext C API: <https://raw.githubusercontent.com/unicode-org/icu/main/icu4c/source/common/unicode/udisplaycontext.h>
- Tim Düsterhus's proposal for global enums: <https://externals.io/message/132187#132513>

## Rejected Features

### Class name and namespace

People raised whether this class should use an appropriate namespace to avoid conflicts. This RFC keeps the global `IntlRelativeDateTimeFormatter`name because ext/intl currently exposes similar formatter classes as global classes, for example `IntlDateFormatter`, `IntlListFormatter`, `IntlNumberRangeFormatter`, and `IntlDatePatternGenerator`.

The three enums also use global, formatter-specific names. A broader redesign of ext/intl can introduce a namespaced API separately.

### Integer constants for the new API

Earlier drafts used integer class constants to match existing ext/intl APIs. Following Tim Düsterhus's feedback, this RFC instead uses three global enums. They make the accepted values explicit in the signatures and prevent mixing styles, capitalization contexts, and units.

This also improves IDE completion and allows documentation for each enum and its cases. Enums are already used by PHP's standard library, for example `RoundingMode`. These benefits justify the difference from older ext/intl APIs without requiring a namespace change.

## Changelog

- 2026-09-22: Add an example of custom NumberFormatter precision, rounding, and configuration changes after construction.
- 2026-09-19: Adopt Tim Düsterhus's proposal for three global pure enums; update parameter types, examples, error handling, and implementation notes.
- 2026-09-08: Address Tim Düsterhus's feedback about observing subsequent NumberFormatter configuration changes.
- 2026-08-09: Address David's feedback about namespace, enums, and the number formatter argument of ureldatefmt\_open().
- 2026-08-07: Initial version.
