====== PHP RFC: IntlRelativeDateTimeFormatter ====== * Version: 0.1 * Date: 2026-08-07 * Author: Weilin Du * Status: Under Discussion * Implementation: TBD * Discussion thread: TBD * Voting thread: TBD ===== 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 to ext/intl. No procedural aliases are proposed of course. ==== 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: * IntlRelativeDateTimeFormatter::STYLE_LONG, for example "in 3 days" * IntlRelativeDateTimeFormatter::STYLE_SHORT, for example "in 3 days" or "in 3 d" depending on locale data * IntlRelativeDateTimeFormatter::STYLE_NARROW, the shortest locale-specific form $capitalizationContext controls ICU's capitalization context. This is useful for locales where relative date strings differ depending on whether they appear in the middle of a sentence, or at the beginning of a sentence, or stand alone, or even on menus, it goes on. ==== format() ==== public function format(int|float $offset, int $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. Examples for en_US: format(-1, IntlRelativeDateTimeFormatter::UNIT_DAY), "\n"; // yesterday echo $formatter->format(0, IntlRelativeDateTimeFormatter::UNIT_DAY), "\n"; // today echo $formatter->format(1, IntlRelativeDateTimeFormatter::UNIT_DAY), "\n"; // tomorrow echo $formatter->format(-1, IntlRelativeDateTimeFormatter::UNIT_WEEK), "\n"; // last week echo $formatter->format(2, IntlRelativeDateTimeFormatter::UNIT_WEEK), "\n"; // in 2 weeks echo $formatter->format(-1, IntlRelativeDateTimeFormatter::UNIT_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 constants, such as IntlRelativeDateTimeFormatter::UNIT_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() ==== public function formatNumeric(int|float $offset, int $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". formatNumeric(-1, IntlRelativeDateTimeFormatter::UNIT_DAY), "\n"; // 1 day ago echo $formatter->formatNumeric(1, IntlRelativeDateTimeFormatter::UNIT_DAY), "\n"; // in 1 day echo $formatter->formatNumeric(1.5, IntlRelativeDateTimeFormatter::UNIT_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: formatNumeric(-1, IntlRelativeDateTimeFormatter::UNIT_DAY), "\n"; // il y a 1 jour echo $french->formatNumeric(1.5, IntlRelativeDateTimeFormatter::UNIT_HOUR), "\n"; // dans 1,5 heure $chinese = new IntlRelativeDateTimeFormatter('zh_CN'); echo $chinese->formatNumeric(3, IntlRelativeDateTimeFormatter::UNIT_MONTH), "\n"; // 3个月后 ?> ==== combineDateAndTime() ==== 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: format(-1, IntlRelativeDateTimeFormatter::UNIT_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 ==== TL;DR: Nothing special. This is exact what other classes do in the rest of the extension. The class follows the existing ext/intl error handling conventions: * Instance methods reset the global intl error and the object's error before operation. * Instance method 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. * Invalid argument values, such as unsupported style or unit constants, throw ValueError. If intl.use_exceptions is enabled, ICU failures follow the existing intl exception behavior. ===== 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: format(new DateTimeImmutable('tomorrow')), "\n"; // tomorrow $relativeFormatter = new IntlRelativeDateTimeFormatter('en_US'); echo $relativeFormatter->format(1, IntlRelativeDateTimeFormatter::UNIT_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 to ext/intl. Adding a new global class name can conflict with userland code declaring IntlRelativeDateTimeFormatter while the intl extension is loaded. This is the normal compatibility consideration for adding classes to extensions. ===== 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 ==== The RFC adds the class constants listed in the proposed stub. No deprecated ICU sentinel constants such as UDAT_STYLE_COUNT or UDAT_REL_UNIT_COUNT are exposed. ===== Open Issues ===== None. ===== Future Scope ===== None. Perhaps there will be some according to further discussion :) ===== Proposed Voting Choices ===== Primary vote requiring a 2/3 majority to accept the RFC: * Yes * No * Abstain ===== 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 ===== 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 ===== Rejected Features ===== None. ===== Changelog ===== * 2026-08-07: Initial version.