| ayesh | Yes | 2026-10-07 16:33 |
| calvinb | Yes | 2026-10-07 17:13 |
| crell | Yes | 2026-10-07 16:27 |
| devnexen | Yes | 2026-10-07 16:23 |
| duncan3dc | Yes | 2026-10-08 08:35 |
| edorian | Yes | 2026-10-09 02:18 |
| galvao | Yes | 2026-10-10 19:35 |
| heiglandreas | Yes | 2026-10-08 12:34 |
| jordikroon | Yes | 2026-10-11 21:11 |
| kguest | Yes | 2026-10-09 16:05 |
| kocsismate | Yes | 2026-10-09 06:21 |
| ocramius | Yes | 2026-10-07 16:30 |
| svpernova09 | Yes | 2026-10-09 13:40 |
| timwolla | Yes | 2026-10-07 16:33 |
| weilindu | Yes | 2026-10-07 16:11 |
Copied from wiki.php.net, last synchronized Oct 13, 2026 14:46 UTC
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.
Add a new class IntlRelativeDateTimeFormatter and three enums to ext/intl. The class and enums are defined in the global namespace.
<?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 $unitparameters accept only cases of their respective enum types. ICU's integer values are mapped internally and are not part of the public API.
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 dataIntlRelativeDateTimeFormatterStyle::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().
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
$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
?>
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
$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:
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.
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
$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
$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个月后
?>
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
$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.
The class follows the existing ext/intl error handling conventions for ICU operations, with PHP's normal type checking for enum parameters:
getErrorCode() and getErrorMessage() expose the object's last error.false on ICU formatting or conversion failure.$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.
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 DateTimeInterfaceobjects. 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
$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 displayedIntlRelativeDateTimeFormatter: How should this relative offset and unit be displayedThis RFC adds a new class and three enums to ext/intl:
IntlRelativeDateTimeFormatterIntlRelativeDateTimeFormatterStyleIntlRelativeDateTimeFormatterCapitalizationIntlRelativeDateTimeFormatterUnitThese global names can conflict with userland declarations while the intl extension is loaded. Existing ext/intl classes and their constants are unchanged.
PHP 8.7
None.
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.
None.
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.
None.
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 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)
TBD.
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.
People raised whether this class should use an appropriate namespace to avoid conflicts. This RFC keeps the global IntlRelativeDateTimeFormattername 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.
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.