Commit c242f17cf4b for php

commit c242f17cf4bd84654e528353609526c6b97a2213
Author: Weilin Du <weilindu@php.net>
Date:   Thu Oct 22 09:47:01 2026 +0800

    [RFC] ext/intl: Implement IntlRelativeDateTimeFormatter (#23514)

    Add locale-aware formatting of relative offsets, enum-based style,
    capitalization and unit options, and custom NumberFormatter support.
    Observe subsequent NumberFormatter configuration changes and cover
    formatting, error handling, and object lifetime in tests.

    RFC: https://wiki.php.net/rfc/reldateformatter

    Co-authored-by: NickSdot <32384907+NickSdot@users.noreply.github.com>
    Co-authored-by: David Carlier <devnexen@gmail.com>

diff --git a/NEWS b/NEWS
index cbd8903c061..67896bb73a8 100644
--- a/NEWS
+++ b/NEWS
@@ -23,6 +23,9 @@ PHP                                                                        NEWS
     throwing a ValueError. (Marc Bennewitz)

 - Intl:
+  . Added IntlRelativeDateTimeFormatter for locale-aware relative date and time
+    formatting, with enums for styles, capitalization contexts, and units.
+    (Weilin Du)
   . Added Transliterator::toRules() and transliterator_to_rules().
     (Weilin Du, David Carlier)
   . Fixed Collator attribute and strength methods not rejecting an
diff --git a/UPGRADING b/UPGRADING
index fbc8ddce787..60b7020cb1e 100644
--- a/UPGRADING
+++ b/UPGRADING
@@ -50,6 +50,18 @@ PHP 8.7 UPGRADE NOTES
     array of CURLFile or CURLStringFile objects to CURLOPT_POSTFIELDS.

 - Intl:
+  . Added IntlRelativeDateTimeFormatter to format relative date and time offsets
+    using locale-specific text, such as "yesterday" or "in 3 days". Its format()
+    method uses a textual form when available and a numeric form otherwise,
+    while formatNumeric() always uses a numeric form. combineDateAndTime()
+    combines already formatted date and time strings according to the locale.
+    The constructor accepts an optional NumberFormatter to customize numeric
+    output; subsequent configuration changes to that object are reflected in
+    formatting results. Styles, capitalization contexts, and units are selected
+    using the IntlRelativeDateTimeFormatterStyle,
+    IntlRelativeDateTimeFormatterCapitalization, and
+    IntlRelativeDateTimeFormatterUnit enums, respectively.
+    RFC: https://wiki.php.net/rfc/reldateformatter
   . Locale::acceptFromHttp() and locale_accept_from_http() now accept an
     optional $availableLocales array argument. When given, the
     Accept-Language header is matched against these locales instead of all
@@ -95,6 +107,11 @@ PHP 8.7 UPGRADE NOTES
 ========================================

 - Intl:
+  . IntlRelativeDateTimeFormatter
+  . enum IntlRelativeDateTimeFormatterStyle
+  . enum IntlRelativeDateTimeFormatterCapitalization
+  . enum IntlRelativeDateTimeFormatterUnit
+    RFC: https://wiki.php.net/rfc/reldateformatter
   . Added IntlNumberFormatter class to format a number with a given locale
     and ICU number skeleton, which also covers measurement units, e.g.
     "measure-unit/speed-kilometer-per-hour unit-width-full-name".
diff --git a/ext/intl/CREDITS b/ext/intl/CREDITS
index 02a0f12a139..c038d413541 100644
--- a/ext/intl/CREDITS
+++ b/ext/intl/CREDITS
@@ -1,2 +1,2 @@
 Internationalization
-Ed Batutis, David Carlier, Vladimir Iordanov, Dmitry Lakhtyuk, Stanislav Malyshev, Vadim Savchuk, Kirti Velankar
+Ed Batutis, David Carlier, Weilin Du, Vladimir Iordanov, Dmitry Lakhtyuk, Stanislav Malyshev, Vadim Savchuk, Kirti Velankar
diff --git a/ext/intl/config.m4 b/ext/intl/config.m4
index 0f426e45af7..a9de76180be 100644
--- a/ext/intl/config.m4
+++ b/ext/intl/config.m4
@@ -75,6 +75,7 @@ if test "$PHP_INTL" != "no"; then
     breakiterator/codepointiterator_internal.cpp \
     breakiterator/codepointiterator_methods.cpp \
     listformatter/listformatter_class.cpp \
+    reldateformatter/reldateformatter_class.cpp \
     transliterator/transliterator_class.cpp \
     transliterator/transliterator_methods.cpp \
     idn/idn.cpp \
@@ -125,6 +126,7 @@ if test "$PHP_INTL" != "no"; then
     $ext_builddir/idn
     $ext_builddir/locale
     $ext_builddir/listformatter
+    $ext_builddir/reldateformatter
     $ext_builddir/msgformat
     $ext_builddir/normalizer
     $ext_builddir/numberformatter
diff --git a/ext/intl/config.w32 b/ext/intl/config.w32
index 8ca7096152b..93719608d46 100644
--- a/ext/intl/config.w32
+++ b/ext/intl/config.w32
@@ -42,6 +42,9 @@ if (PHP_INTL != "no") {
 		ADD_SOURCES(configure_module_dirname + "/listformatter", "\
 				listformatter_class.cpp \
 				", "intl");
+		ADD_SOURCES(configure_module_dirname + "/reldateformatter", "\
+				reldateformatter_class.cpp \
+				", "intl");
 		ADD_SOURCES(configure_module_dirname + "/locale", "\
 				locale.cpp \
 				locale_class.cpp \
diff --git a/ext/intl/formatter/formatter_attr.cpp b/ext/intl/formatter/formatter_attr.cpp
index ff215fdffbd..dee5a203b06 100644
--- a/ext/intl/formatter/formatter_attr.cpp
+++ b/ext/intl/formatter/formatter_attr.cpp
@@ -181,6 +181,7 @@ U_CFUNC PHP_FUNCTION( numfmt_set_attribute )

 	INTL_METHOD_CHECK_STATUS( nfo, "Error setting attribute value" );

+	nfo->configuration_version++;
 	RETURN_TRUE;
 }
 /* }}} */
@@ -212,6 +213,7 @@ U_CFUNC PHP_FUNCTION( numfmt_set_text_attribute )
 	unum_setTextAttribute(FORMATTER_UNUM(nfo), static_cast<UNumberFormatTextAttribute>(attribute), svalue.getBuffer(), svalue.length(), &INTL_DATA_ERROR_CODE(nfo));
 	INTL_METHOD_CHECK_STATUS( nfo, "Error setting text attribute" );

+	nfo->configuration_version++;
 	RETURN_TRUE;
 }
 /* }}} */
@@ -293,6 +295,7 @@ U_CFUNC PHP_FUNCTION( numfmt_set_symbol )
 	unum_setSymbol(FORMATTER_UNUM(nfo), symbol, svalue.getBuffer(), svalue.length(), &INTL_DATA_ERROR_CODE(nfo));
 	INTL_METHOD_CHECK_STATUS( nfo, "Error setting symbol value" );

+	nfo->configuration_version++;
 	RETURN_TRUE;
 }
 /* }}} */
@@ -363,6 +366,7 @@ U_CFUNC PHP_FUNCTION( numfmt_set_pattern )
 		RETURN_FALSE;
 	}

+	nfo->configuration_version++;
 	RETURN_TRUE;
 }
 /* }}} */
diff --git a/ext/intl/formatter/formatter_class.cpp b/ext/intl/formatter/formatter_class.cpp
index 29817132aca..bcdfbee6355 100644
--- a/ext/intl/formatter/formatter_class.cpp
+++ b/ext/intl/formatter/formatter_class.cpp
@@ -52,6 +52,7 @@ U_CFUNC zend_object *NumberFormatter_object_create(zend_class_entry *ce)

 	intern = reinterpret_cast<NumberFormatter_object *>(zend_object_alloc(sizeof(NumberFormatter_object), ce));
 	formatter_data_init( &intern->nf_data );
+	intern->configuration_version = 0;
 	zend_object_std_init( &intern->zo, ce );
 	object_properties_init(&intern->zo, ce);

@@ -68,6 +69,7 @@ U_CFUNC zend_object *NumberFormatter_object_clone(zend_object *object)

 	/* clone standard parts */
 	zend_objects_clone_members(&new_nfo->zo, &nfo->zo);
+	new_nfo->configuration_version = nfo->configuration_version;

 	/* clone formatter object. It may fail, the destruction code must handle this case */
 	if (FORMATTER_OBJECT(nfo) != nullptr) {
diff --git a/ext/intl/formatter/formatter_class.h b/ext/intl/formatter/formatter_class.h
index 44bdfae8deb..0d4a4208a16 100644
--- a/ext/intl/formatter/formatter_class.h
+++ b/ext/intl/formatter/formatter_class.h
@@ -29,6 +29,7 @@ extern "C" {

 typedef struct {
 	formatter_data  nf_data;
+	uint64_t        configuration_version;
 	zend_object     zo;
 } NumberFormatter_object;

diff --git a/ext/intl/php_intl.c b/ext/intl/php_intl.c
index 5b51d98dba0..6464f6e2033 100644
--- a/ext/intl/php_intl.c
+++ b/ext/intl/php_intl.c
@@ -42,6 +42,7 @@
 #include "locale/locale_class.h"

 #include "listformatter/listformatter_class.h"
+#include "reldateformatter/reldateformatter_class.h"
 #include "numberformatter/numberformatter_class.h"
 #include "rangeformatter/rangeformatter_class.h"

@@ -190,6 +191,9 @@ PHP_MINIT_FUNCTION( intl )
 	/* Register 'ListFormatter' PHP class */
 	listformatter_register_class(  );

+	/* Register 'IntlRelativeDateTimeFormatter' PHP class */
+	reldateformatter_register_class(  );
+
 #if U_ICU_VERSION_MAJOR_NUM >= 63
 	/* Register 'IntlNumberFormatter' PHP class */
 	numberformatter_register_class( );
diff --git a/ext/intl/reldateformatter/reldateformatter.stub.php b/ext/intl/reldateformatter/reldateformatter.stub.php
new file mode 100644
index 00000000000..3418e7b85d3
--- /dev/null
+++ b/ext/intl/reldateformatter/reldateformatter.stub.php
@@ -0,0 +1,65 @@
+<?php
+
+/**
+ * @generate-class-entries
+ * @generate-c-enums
+ */
+
+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 {}
+}
diff --git a/ext/intl/reldateformatter/reldateformatter_arginfo.h b/ext/intl/reldateformatter/reldateformatter_arginfo.h
new file mode 100644
index 00000000000..aa8b7a81802
Binary files /dev/null and b/ext/intl/reldateformatter/reldateformatter_arginfo.h differ
diff --git a/ext/intl/reldateformatter/reldateformatter_class.cpp b/ext/intl/reldateformatter/reldateformatter_class.cpp
new file mode 100644
index 00000000000..e2b5db0b7b0
--- /dev/null
+++ b/ext/intl/reldateformatter/reldateformatter_class.cpp
@@ -0,0 +1,541 @@
+/*
+   +----------------------------------------------------------------------+
+   | Copyright © The PHP Group and Contributors.                          |
+   +----------------------------------------------------------------------+
+   | This source file is subject to the Modified BSD License that is      |
+   | bundled with this package in the file LICENSE, and is available      |
+   | through the World Wide Web at <https://www.php.net/license/>.        |
+   |                                                                      |
+   | SPDX-License-Identifier: BSD-3-Clause                                |
+   +----------------------------------------------------------------------+
+   | Authors: Weilin Du <weilindu@php.net>                                |
+   +----------------------------------------------------------------------+
+ */
+
+extern "C" {
+#include "php.h"
+#include "zend_enum.h"
+#include "zend_exceptions.h"
+}
+
+#include "../formatter/formatter_class.h"
+
+#include <unicode/brkiter.h>
+#include <unicode/locid.h>
+#include <unicode/reldatefmt.h>
+#include <unicode/uchar.h>
+#include <unicode/ucasemap.h>
+#include <unicode/udisplaycontext.h>
+#include <unicode/ureldatefmt.h>
+#include <unicode/unistr.h>
+
+#include <memory>
+#include <new>
+
+extern "C" {
+#include "../php_intl.h"
+#include "../intl_data.h"
+#include "../intl_error.h"
+}
+
+#include "../intl_convertcpp.h"
+#include "reldateformatter_class.h"
+#include "reldateformatter_arginfo.h"
+#include "reldateformatter_decl.h"
+
+using icu::Locale;
+using icu::UnicodeString;
+
+struct reldateformatter_data {
+	Locale locale;
+	std::unique_ptr<icu::RelativeDateTimeFormatter> formatter;
+	std::unique_ptr<icu::BreakIterator> sentence_iterator;
+};
+
+zend_class_entry *IntlRelativeDateTimeFormatter_ce_ptr;
+static zend_class_entry *reldateformatter_style_ce;
+static zend_class_entry *reldateformatter_capitalization_ce;
+static zend_class_entry *reldateformatter_unit_ce;
+static zend_object_handlers reldateformatter_handlers;
+
+static void reldateformatter_free_object(zend_object *object)
+{
+	IntlRelativeDateTimeFormatter_object *obj = php_intl_reldateformatter_fetch_object(object);
+
+	if (RELDATEFORMATTER_OBJECT(obj) != nullptr) {
+		delete RELDATEFORMATTER_OBJECT(obj);
+		RELDATEFORMATTER_OBJECT(obj) = nullptr;
+	}
+	if (obj->number_formatter != nullptr) {
+		OBJ_RELEASE(obj->number_formatter);
+		obj->number_formatter = nullptr;
+	}
+	if (obj->locale != nullptr) {
+		zend_string_release(obj->locale);
+		obj->locale = nullptr;
+	}
+	intl_error_reset(RELDATEFORMATTER_ERROR_P(obj));
+
+	zend_object_std_dtor(&obj->zo);
+}
+
+static zend_object *reldateformatter_create_object(zend_class_entry *class_type)
+{
+	IntlRelativeDateTimeFormatter_object *obj = reinterpret_cast<IntlRelativeDateTimeFormatter_object *>(
+		zend_object_alloc(sizeof(IntlRelativeDateTimeFormatter_object), class_type));
+
+	intl_error_init(RELDATEFORMATTER_ERROR_P(obj));
+	RELDATEFORMATTER_OBJECT(obj) = nullptr;
+	obj->number_formatter = nullptr;
+	obj->number_formatter_version = 0;
+	obj->locale = nullptr;
+	obj->style = UDAT_STYLE_LONG;
+	obj->capitalization_context = UDISPCTX_CAPITALIZATION_NONE;
+
+	zend_object_std_init(&obj->zo, class_type);
+	object_properties_init(&obj->zo, class_type);
+
+	return &obj->zo;
+}
+
+static HashTable *reldateformatter_get_gc(zend_object *object, zval **table, int *n)
+{
+	IntlRelativeDateTimeFormatter_object *obj = php_intl_reldateformatter_fetch_object(object);
+
+	if (obj->number_formatter != nullptr) {
+		zend_get_gc_buffer *gc_buffer = zend_get_gc_buffer_create();
+		zend_get_gc_buffer_add_obj(gc_buffer, obj->number_formatter);
+		zend_get_gc_buffer_use(gc_buffer, table, n);
+	} else {
+		*table = nullptr;
+		*n = 0;
+	}
+
+	return nullptr;
+}
+
+static UDateRelativeDateTimeFormatterStyle reldateformatter_style_from_enum(zend_object *style)
+{
+	switch (static_cast<zend_enum_IntlRelativeDateTimeFormatterStyle>(zend_enum_fetch_case_id(style))) {
+		case ZEND_ENUM_IntlRelativeDateTimeFormatterStyle_Long:
+			return UDAT_STYLE_LONG;
+		case ZEND_ENUM_IntlRelativeDateTimeFormatterStyle_Short:
+			return UDAT_STYLE_SHORT;
+		case ZEND_ENUM_IntlRelativeDateTimeFormatterStyle_Narrow:
+			return UDAT_STYLE_NARROW;
+	}
+	ZEND_UNREACHABLE();
+}
+
+static UDisplayContext reldateformatter_capitalization_from_enum(zend_object *context)
+{
+	switch (static_cast<zend_enum_IntlRelativeDateTimeFormatterCapitalization>(zend_enum_fetch_case_id(context))) {
+		case ZEND_ENUM_IntlRelativeDateTimeFormatterCapitalization_None:
+			return UDISPCTX_CAPITALIZATION_NONE;
+		case ZEND_ENUM_IntlRelativeDateTimeFormatterCapitalization_MiddleOfSentence:
+			return UDISPCTX_CAPITALIZATION_FOR_MIDDLE_OF_SENTENCE;
+		case ZEND_ENUM_IntlRelativeDateTimeFormatterCapitalization_BeginningOfSentence:
+			return UDISPCTX_CAPITALIZATION_FOR_BEGINNING_OF_SENTENCE;
+		case ZEND_ENUM_IntlRelativeDateTimeFormatterCapitalization_UiListAndMenu:
+			return UDISPCTX_CAPITALIZATION_FOR_UI_LIST_OR_MENU;
+		case ZEND_ENUM_IntlRelativeDateTimeFormatterCapitalization_Standalone:
+			return UDISPCTX_CAPITALIZATION_FOR_STANDALONE;
+	}
+	ZEND_UNREACHABLE();
+}
+
+static URelativeDateTimeUnit reldateformatter_unit_from_enum(zend_object *unit)
+{
+	switch (static_cast<zend_enum_IntlRelativeDateTimeFormatterUnit>(zend_enum_fetch_case_id(unit))) {
+		case ZEND_ENUM_IntlRelativeDateTimeFormatterUnit_Year:
+			return UDAT_REL_UNIT_YEAR;
+		case ZEND_ENUM_IntlRelativeDateTimeFormatterUnit_Quarter:
+			return UDAT_REL_UNIT_QUARTER;
+		case ZEND_ENUM_IntlRelativeDateTimeFormatterUnit_Month:
+			return UDAT_REL_UNIT_MONTH;
+		case ZEND_ENUM_IntlRelativeDateTimeFormatterUnit_Week:
+			return UDAT_REL_UNIT_WEEK;
+		case ZEND_ENUM_IntlRelativeDateTimeFormatterUnit_Day:
+			return UDAT_REL_UNIT_DAY;
+		case ZEND_ENUM_IntlRelativeDateTimeFormatterUnit_Hour:
+			return UDAT_REL_UNIT_HOUR;
+		case ZEND_ENUM_IntlRelativeDateTimeFormatterUnit_Minute:
+			return UDAT_REL_UNIT_MINUTE;
+		case ZEND_ENUM_IntlRelativeDateTimeFormatterUnit_Second:
+			return UDAT_REL_UNIT_SECOND;
+		case ZEND_ENUM_IntlRelativeDateTimeFormatterUnit_Sunday:
+			return UDAT_REL_UNIT_SUNDAY;
+		case ZEND_ENUM_IntlRelativeDateTimeFormatterUnit_Monday:
+			return UDAT_REL_UNIT_MONDAY;
+		case ZEND_ENUM_IntlRelativeDateTimeFormatterUnit_Tuesday:
+			return UDAT_REL_UNIT_TUESDAY;
+		case ZEND_ENUM_IntlRelativeDateTimeFormatterUnit_Wednesday:
+			return UDAT_REL_UNIT_WEDNESDAY;
+		case ZEND_ENUM_IntlRelativeDateTimeFormatterUnit_Thursday:
+			return UDAT_REL_UNIT_THURSDAY;
+		case ZEND_ENUM_IntlRelativeDateTimeFormatterUnit_Friday:
+			return UDAT_REL_UNIT_FRIDAY;
+		case ZEND_ENUM_IntlRelativeDateTimeFormatterUnit_Saturday:
+			return UDAT_REL_UNIT_SATURDAY;
+	}
+	ZEND_UNREACHABLE();
+}
+
+static void reldateformatter_throw_constructor_failure(
+	IntlRelativeDateTimeFormatter_object *obj,
+	UErrorCode status,
+	const char *message
+)
+{
+	const bool old_use_exceptions = INTL_G(use_exceptions);
+	const zend_long old_error_level = INTL_G(error_level);
+	INTL_G(use_exceptions) = true;
+	INTL_G(error_level) = 0;
+
+	intl_errors_set(RELDATEFORMATTER_ERROR_P(obj), status, message);
+
+	INTL_G(use_exceptions) = old_use_exceptions;
+	INTL_G(error_level) = old_error_level;
+}
+
+static reldateformatter_data *reldateformatter_open(
+	const char *locale,
+	UDateRelativeDateTimeFormatterStyle style,
+	UDisplayContext capitalization_context,
+	const NumberFormatter_object *number_formatter,
+	UErrorCode *status
+)
+{
+	if (U_FAILURE(*status)) {
+		return nullptr;
+	}
+
+	std::unique_ptr<reldateformatter_data> result(new (std::nothrow) reldateformatter_data());
+	if (result == nullptr) {
+		*status = U_MEMORY_ALLOCATION_ERROR;
+		return nullptr;
+	}
+
+	result->locale = Locale(locale);
+	if (capitalization_context == UDISPCTX_CAPITALIZATION_FOR_BEGINNING_OF_SENTENCE) {
+		/* ICU can fail to create this iterator before adopting the number formatter. */
+		result->sentence_iterator.reset(icu::BreakIterator::createSentenceInstance(result->locale, *status));
+		if (U_FAILURE(*status)) {
+			return nullptr;
+		}
+		if (result->sentence_iterator == nullptr) {
+			*status = U_MEMORY_ALLOCATION_ERROR;
+			return nullptr;
+		}
+		capitalization_context = UDISPCTX_CAPITALIZATION_NONE;
+	}
+
+	std::unique_ptr<icu::NumberFormat> number_formatter_clone;
+	if (number_formatter != nullptr) {
+		number_formatter_clone.reset(static_cast<icu::NumberFormat *>(FORMATTER_OBJECT(number_formatter)->clone()));
+		if (number_formatter_clone == nullptr) {
+			*status = U_MEMORY_ALLOCATION_ERROR;
+			return nullptr;
+		}
+	}
+
+	/* C++11 may evaluate constructor arguments even if allocation fails, so keep
+	 * ownership of the clone until we know that the constructor ran. */
+	result->formatter.reset(new icu::RelativeDateTimeFormatter(
+		result->locale, number_formatter_clone.get(), style, capitalization_context, *status));
+	if (result->formatter == nullptr) {
+		*status = U_MEMORY_ALLOCATION_ERROR;
+		return nullptr;
+	}
+	/* With a valid style/context and no sentence iterator to create, the constructor
+	 * adopts the clone even if initialization fails. Release it before checking status. */
+	number_formatter_clone.release();
+	if (U_FAILURE(*status)) {
+		return nullptr;
+	}
+
+	return result.release();
+}
+
+static bool reldateformatter_refresh_number_formatter(IntlRelativeDateTimeFormatter_object *obj)
+{
+	if (obj->number_formatter == nullptr) {
+		return true;
+	}
+
+	NumberFormatter_object *number_formatter = php_intl_number_format_fetch_object(obj->number_formatter);
+	if (obj->number_formatter_version == number_formatter->configuration_version) {
+		return true;
+	}
+
+	UErrorCode status = U_ZERO_ERROR;
+	reldateformatter_data *formatter = reldateformatter_open(
+		ZSTR_VAL(obj->locale),
+		obj->style,
+		obj->capitalization_context,
+		number_formatter,
+		&status);
+	if (U_FAILURE(status) || formatter == nullptr) {
+		if (U_SUCCESS(status)) {
+			status = U_MEMORY_ALLOCATION_ERROR;
+		}
+		intl_errors_set(RELDATEFORMATTER_ERROR_P(obj), status,
+			"Failed to update IntlRelativeDateTimeFormatter from NumberFormatter");
+		return false;
+	}
+
+	delete RELDATEFORMATTER_OBJECT(obj);
+	RELDATEFORMATTER_OBJECT(obj) = formatter;
+	obj->number_formatter_version = number_formatter->configuration_version;
+	return true;
+}
+
+PHP_METHOD(IntlRelativeDateTimeFormatter, __construct)
+{
+	IntlRelativeDateTimeFormatter_object *obj = Z_INTL_RELDATEFORMATTER_P(ZEND_THIS);
+	char *locale = nullptr;
+	size_t locale_len = 0;
+	zend_object *style = nullptr;
+	zend_object *capitalization_context = nullptr;
+	zval *number_formatter = nullptr;
+
+	ZEND_PARSE_PARAMETERS_START(0, 4)
+		Z_PARAM_OPTIONAL
+		Z_PARAM_STRING_OR_NULL(locale, locale_len)
+		Z_PARAM_OBJ_OF_CLASS(style, reldateformatter_style_ce)
+		Z_PARAM_OBJ_OF_CLASS(capitalization_context, reldateformatter_capitalization_ce)
+		Z_PARAM_OBJECT_OF_CLASS_OR_NULL(number_formatter, NumberFormatter_ce_ptr)
+	ZEND_PARSE_PARAMETERS_END();
+
+	intl_errors_reset(RELDATEFORMATTER_ERROR_P(obj));
+
+	if (RELDATEFORMATTER_OBJECT(obj) != nullptr) {
+		zend_throw_error(NULL, "IntlRelativeDateTimeFormatter object is already constructed");
+		RETURN_THROWS();
+	}
+
+	if (UNEXPECTED(locale_len > INTL_MAX_LOCALE_LEN)) {
+		zend_argument_value_error(1, "must be less than or equal to %d characters", INTL_MAX_LOCALE_LEN);
+		RETURN_THROWS();
+	}
+
+	if (locale == nullptr || locale_len == 0) {
+		locale = const_cast<char *>(intl_locale_get_default());
+	}
+
+	if (Locale(locale).getISO3Language()[0] == '\0') {
+		zend_argument_value_error(1, "\"%s\" is invalid", locale);
+		RETURN_THROWS();
+	}
+
+	const UDateRelativeDateTimeFormatterStyle icu_style =
+		style != nullptr ? reldateformatter_style_from_enum(style) : UDAT_STYLE_LONG;
+	const UDisplayContext icu_capitalization_context = capitalization_context != nullptr
+		? reldateformatter_capitalization_from_enum(capitalization_context) : UDISPCTX_CAPITALIZATION_NONE;
+
+	NumberFormatter_object *number_formatter_obj = nullptr;
+	if (number_formatter != nullptr) {
+		number_formatter_obj = Z_INTL_NUMBERFORMATTER_P(number_formatter);
+		if (FORMATTER_OBJECT(number_formatter_obj) == nullptr) {
+			zend_throw_error(NULL, "Found unconstructed NumberFormatter");
+			RETURN_THROWS();
+		}
+	}
+
+	UErrorCode status = U_ZERO_ERROR;
+	RELDATEFORMATTER_OBJECT(obj) = reldateformatter_open(
+		locale,
+		icu_style,
+		icu_capitalization_context,
+		number_formatter_obj,
+		&status);
+
+	if (U_FAILURE(status) || RELDATEFORMATTER_OBJECT(obj) == nullptr) {
+		if (U_SUCCESS(status)) {
+			status = U_MEMORY_ALLOCATION_ERROR;
+		}
+		reldateformatter_throw_constructor_failure(
+			obj, status, "Failed to create IntlRelativeDateTimeFormatter");
+		RETURN_THROWS();
+	}
+
+	obj->locale = zend_string_init(locale, strlen(locale), false);
+	obj->style = icu_style;
+	obj->capitalization_context = icu_capitalization_context;
+	if (number_formatter != nullptr) {
+		obj->number_formatter = Z_OBJ_P(number_formatter);
+		GC_ADDREF(obj->number_formatter);
+		obj->number_formatter_version = number_formatter_obj->configuration_version;
+	}
+}
+
+template<typename Formatter>
+static zend_string *reldateformatter_format_result(
+	IntlRelativeDateTimeFormatter_object *obj,
+	Formatter &&formatter,
+	const char *failure_message,
+	bool capitalize = false
+)
+{
+	UErrorCode status = U_ZERO_ERROR;
+	UnicodeString result;
+	formatter(result, status);
+	if (U_FAILURE(status)) {
+		intl_errors_set(RELDATEFORMATTER_ERROR_P(obj), status, failure_message);
+		return nullptr;
+	}
+
+	icu::BreakIterator *sentence_iterator = RELDATEFORMATTER_OBJECT(obj)->sentence_iterator.get();
+	if (capitalize && sentence_iterator != nullptr && !result.isEmpty() && u_islower(result.char32At(0))) {
+		result.toTitle(sentence_iterator, RELDATEFORMATTER_OBJECT(obj)->locale,
+			U_TITLECASE_NO_LOWERCASE | U_TITLECASE_NO_BREAK_ADJUSTMENT);
+	}
+	if (result.isBogus()) {
+		intl_errors_set(RELDATEFORMATTER_ERROR_P(obj), U_MEMORY_ALLOCATION_ERROR, failure_message);
+		return nullptr;
+	}
+
+	zend_string *utf8_result = intl_charFromString(result, &status);
+	if (utf8_result == nullptr) {
+		intl_errors_set(RELDATEFORMATTER_ERROR_P(obj), status, "Failed to convert result to UTF-8");
+	}
+
+	return utf8_result;
+}
+
+static void reldateformatter_format(INTERNAL_FUNCTION_PARAMETERS, bool numeric)
+{
+	zval *offset;
+	zend_object *unit_object;
+
+	ZEND_PARSE_PARAMETERS_START(2, 2)
+		Z_PARAM_NUMBER(offset)
+		Z_PARAM_OBJ_OF_CLASS(unit_object, reldateformatter_unit_ce)
+	ZEND_PARSE_PARAMETERS_END();
+
+	IntlRelativeDateTimeFormatter_object *obj = Z_INTL_RELDATEFORMATTER_P(ZEND_THIS);
+	intl_errors_reset(RELDATEFORMATTER_ERROR_P(obj));
+
+	if (RELDATEFORMATTER_OBJECT(obj) == nullptr) {
+		zend_throw_error(NULL, "Found unconstructed IntlRelativeDateTimeFormatter");
+		RETURN_THROWS();
+	}
+
+	if (!reldateformatter_refresh_number_formatter(obj)) {
+		RETURN_FALSE;
+	}
+
+	const double numeric_offset = zval_get_double(offset);
+	const URelativeDateTimeUnit unit = reldateformatter_unit_from_enum(unit_object);
+	zend_string *result;
+	if (numeric) {
+		result = reldateformatter_format_result(obj,
+			[obj, numeric_offset, unit](UnicodeString &result, UErrorCode &status) {
+				RELDATEFORMATTER_OBJECT(obj)->formatter->formatNumeric(numeric_offset, unit, result, status);
+			},
+			"Failed to format relative date/time numerically", true);
+	} else {
+		result = reldateformatter_format_result(obj,
+			[obj, numeric_offset, unit](UnicodeString &result, UErrorCode &status) {
+				RELDATEFORMATTER_OBJECT(obj)->formatter->format(numeric_offset, unit, result, status);
+			},
+			"Failed to format relative date/time", true);
+	}
+
+	if (result == nullptr) {
+		RETURN_FALSE;
+	}
+	RETURN_STR(result);
+}
+
+PHP_METHOD(IntlRelativeDateTimeFormatter, format)
+{
+	reldateformatter_format(INTERNAL_FUNCTION_PARAM_PASSTHRU, false);
+}
+
+PHP_METHOD(IntlRelativeDateTimeFormatter, formatNumeric)
+{
+	reldateformatter_format(INTERNAL_FUNCTION_PARAM_PASSTHRU, true);
+}
+
+PHP_METHOD(IntlRelativeDateTimeFormatter, combineDateAndTime)
+{
+	zend_string *relative_date;
+	zend_string *time;
+
+	ZEND_PARSE_PARAMETERS_START(2, 2)
+		Z_PARAM_STR(relative_date)
+		Z_PARAM_STR(time)
+	ZEND_PARSE_PARAMETERS_END();
+
+	IntlRelativeDateTimeFormatter_object *obj = Z_INTL_RELDATEFORMATTER_P(ZEND_THIS);
+	intl_errors_reset(RELDATEFORMATTER_ERROR_P(obj));
+
+	if (RELDATEFORMATTER_OBJECT(obj) == nullptr) {
+		zend_throw_error(NULL, "Found unconstructed IntlRelativeDateTimeFormatter");
+		RETURN_THROWS();
+	}
+
+	UErrorCode status = U_ZERO_ERROR;
+	UnicodeString relative_date_utf16;
+	UnicodeString time_utf16;
+	intl_stringFromChar(
+		relative_date_utf16, ZSTR_VAL(relative_date), ZSTR_LEN(relative_date), &status);
+	if (U_FAILURE(status)) {
+		intl_errors_set(RELDATEFORMATTER_ERROR_P(obj), status,
+			"Failed to convert relative date to UTF-16");
+		RETURN_FALSE;
+	}
+
+	intl_stringFromChar(time_utf16, ZSTR_VAL(time), ZSTR_LEN(time), &status);
+	if (U_FAILURE(status)) {
+		intl_errors_set(RELDATEFORMATTER_ERROR_P(obj), status,
+			"Failed to convert time to UTF-16");
+		RETURN_FALSE;
+	}
+
+	zend_string *result = reldateformatter_format_result(obj,
+		[obj, &relative_date_utf16, &time_utf16](UnicodeString &result, UErrorCode &status) {
+			RELDATEFORMATTER_OBJECT(obj)->formatter->combineDateAndTime(
+				relative_date_utf16, time_utf16, result, status);
+		},
+		"Failed to combine relative date and time");
+
+	if (result == nullptr) {
+		RETURN_FALSE;
+	}
+	RETURN_STR(result);
+}
+
+PHP_METHOD(IntlRelativeDateTimeFormatter, getErrorCode)
+{
+	ZEND_PARSE_PARAMETERS_NONE();
+
+	IntlRelativeDateTimeFormatter_object *obj = Z_INTL_RELDATEFORMATTER_P(ZEND_THIS);
+	RETURN_LONG(intl_error_get_code(RELDATEFORMATTER_ERROR_P(obj)));
+}
+
+PHP_METHOD(IntlRelativeDateTimeFormatter, getErrorMessage)
+{
+	ZEND_PARSE_PARAMETERS_NONE();
+
+	IntlRelativeDateTimeFormatter_object *obj = Z_INTL_RELDATEFORMATTER_P(ZEND_THIS);
+	RETURN_STR(intl_error_get_message(RELDATEFORMATTER_ERROR_P(obj)));
+}
+
+void reldateformatter_register_class(void)
+{
+	reldateformatter_style_ce = register_class_IntlRelativeDateTimeFormatterStyle();
+	reldateformatter_capitalization_ce = register_class_IntlRelativeDateTimeFormatterCapitalization();
+	reldateformatter_unit_ce = register_class_IntlRelativeDateTimeFormatterUnit();
+
+	IntlRelativeDateTimeFormatter_ce_ptr = register_class_IntlRelativeDateTimeFormatter();
+	IntlRelativeDateTimeFormatter_ce_ptr->create_object = reldateformatter_create_object;
+	IntlRelativeDateTimeFormatter_ce_ptr->default_object_handlers = &reldateformatter_handlers;
+
+	memcpy(&reldateformatter_handlers, zend_get_std_object_handlers(), sizeof(zend_object_handlers));
+	reldateformatter_handlers.offset = offsetof(IntlRelativeDateTimeFormatter_object, zo);
+	reldateformatter_handlers.free_obj = reldateformatter_free_object;
+	reldateformatter_handlers.clone_obj = nullptr;
+	reldateformatter_handlers.get_gc = reldateformatter_get_gc;
+}
diff --git a/ext/intl/reldateformatter/reldateformatter_class.h b/ext/intl/reldateformatter/reldateformatter_class.h
new file mode 100644
index 00000000000..cd2ddc6e32e
--- /dev/null
+++ b/ext/intl/reldateformatter/reldateformatter_class.h
@@ -0,0 +1,56 @@
+/*
+   +----------------------------------------------------------------------+
+   | Copyright © The PHP Group and Contributors.                          |
+   +----------------------------------------------------------------------+
+   | This source file is subject to the Modified BSD License that is      |
+   | bundled with this package in the file LICENSE, and is available      |
+   | through the World Wide Web at <https://www.php.net/license/>.        |
+   |                                                                      |
+   | SPDX-License-Identifier: BSD-3-Clause                                |
+   +----------------------------------------------------------------------+
+   | Authors: Weilin Du <weilindu@php.net>                                |
+   +----------------------------------------------------------------------+
+ */
+
+#ifndef RELDATEFORMATTER_CLASS_H
+#define RELDATEFORMATTER_CLASS_H
+
+#include <php.h>
+#include <unicode/ureldatefmt.h>
+
+#include "../intl_error.h"
+
+typedef struct reldateformatter_data reldateformatter_data;
+
+typedef struct {
+	intl_error error;
+	reldateformatter_data *formatter;
+	zend_object *number_formatter;
+	uint64_t number_formatter_version;
+	zend_string *locale;
+	UDateRelativeDateTimeFormatterStyle style;
+	UDisplayContext capitalization_context;
+	zend_object zo;
+} IntlRelativeDateTimeFormatter_object;
+
+#define php_intl_reldateformatter_fetch_object(obj) \
+	ZEND_CONTAINER_OF(obj, IntlRelativeDateTimeFormatter_object, zo)
+#define Z_INTL_RELDATEFORMATTER_P(zv) \
+	php_intl_reldateformatter_fetch_object(Z_OBJ_P(zv))
+
+#define RELDATEFORMATTER_ERROR(obj) ((obj)->error)
+#define RELDATEFORMATTER_ERROR_P(obj) (&RELDATEFORMATTER_ERROR(obj))
+#define RELDATEFORMATTER_OBJECT(obj) ((obj)->formatter)
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+void reldateformatter_register_class(void);
+extern zend_class_entry *IntlRelativeDateTimeFormatter_ce_ptr;
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* RELDATEFORMATTER_CLASS_H */
diff --git a/ext/intl/reldateformatter/reldateformatter_decl.h b/ext/intl/reldateformatter/reldateformatter_decl.h
new file mode 100644
index 00000000000..5031a0e6fdd
Binary files /dev/null and b/ext/intl/reldateformatter/reldateformatter_decl.h differ
diff --git a/ext/intl/tests/reldateformatter/formatting/format.inc b/ext/intl/tests/reldateformatter/formatting/format.inc
new file mode 100644
index 00000000000..69a531537f7
--- /dev/null
+++ b/ext/intl/tests/reldateformatter/formatting/format.inc
@@ -0,0 +1,106 @@
+<?php
+
+const RELATIVE_DATE_TIME_UNITS = [
+    'year' => IntlRelativeDateTimeFormatterUnit::Year,
+    'quarter' => IntlRelativeDateTimeFormatterUnit::Quarter,
+    'month' => IntlRelativeDateTimeFormatterUnit::Month,
+    'week' => IntlRelativeDateTimeFormatterUnit::Week,
+    'day' => IntlRelativeDateTimeFormatterUnit::Day,
+    'hour' => IntlRelativeDateTimeFormatterUnit::Hour,
+    'minute' => IntlRelativeDateTimeFormatterUnit::Minute,
+    'second' => IntlRelativeDateTimeFormatterUnit::Second,
+    'sunday' => IntlRelativeDateTimeFormatterUnit::Sunday,
+    'monday' => IntlRelativeDateTimeFormatterUnit::Monday,
+    'tuesday' => IntlRelativeDateTimeFormatterUnit::Tuesday,
+    'wednesday' => IntlRelativeDateTimeFormatterUnit::Wednesday,
+    'thursday' => IntlRelativeDateTimeFormatterUnit::Thursday,
+    'friday' => IntlRelativeDateTimeFormatterUnit::Friday,
+    'saturday' => IntlRelativeDateTimeFormatterUnit::Saturday,
+];
+
+const RELATIVE_DATE_TIME_OFFSETS = [
+    -3, -2, -1.75, -1.5, -1, -0.5,
+    0, 0.0, -0.0,
+    0.5, 1, 1.5, 1.75, 2, 3,
+];
+
+/**
+ * Offsets may be a list shared by all units, or a map from unit names to lists.
+ * A map selects only the units it contains, preserving the shared unit order.
+ */
+function printRelativeDateTimeTable(
+    IntlRelativeDateTimeFormatter $formatter,
+    array $offsets = RELATIVE_DATE_TIME_OFFSETS,
+    array $units = RELATIVE_DATE_TIME_UNITS,
+): void {
+
+    $rows = [];
+    $headers = ['offset', 'format', 'formatNumeric'];
+
+    $widths = array_map('grapheme_strlen', $headers);
+
+    $sharedOffsets = array_is_list($offsets);
+
+    foreach ($units as $name => $unit) {
+
+        if (!$sharedOffsets && !array_key_exists($name, $offsets)) {
+            continue;
+        }
+
+        foreach ($sharedOffsets ? $offsets : $offsets[$name] as $offset) {
+
+            // NaN must not trigger float to string warning.
+            $offsetLabel = is_nan($offset) ? 'NAN' : (string) $offset;
+
+            $row = [
+                $offsetLabel,
+                (string) $formatter->format($offset, $unit),
+                (string) $formatter->formatNumeric($offset, $unit),
+            ];
+
+            foreach ($row as $column => $value) {
+                $widths[$column] = max($widths[$column], grapheme_strlen($value));
+            }
+
+            $rows[$name][] = $row;
+        }
+    }
+
+    $printRow = static function (array $row) use ($widths): void {
+
+        foreach ($row as $column => $value) {
+
+            // unpadded final column to avoid trailing whitespace.
+            if ($column === array_key_last($row)) {
+                continue;
+            }
+
+            $space = str_repeat(' ', $widths[$column] - grapheme_strlen($value));
+
+            $row[$column] = $column === 0
+                ? $space . $value
+                : $value . $space;
+        }
+
+        echo implode('  ', $row), "\n";
+    };
+
+    $fullWidth = array_sum($widths) + 2 * (count($widths) - 1);
+    $separator = array_map(static fn (int $width): string => str_repeat('-', $width), $widths);
+
+    foreach ($rows as $name => $unitRows) {
+
+        if ($name !== array_key_first($rows)) {
+            echo "\n";
+        }
+
+        echo '=== ', $name, ' ', str_repeat('=', $fullWidth - grapheme_strlen($name) - 5), "\n\n";
+
+        $printRow($headers);
+        $printRow($separator);
+
+        foreach ($unitRows as $row) {
+            $printRow($row);
+        }
+    }
+}
diff --git a/ext/intl/tests/reldateformatter/formatting/offset_success_large.phpt b/ext/intl/tests/reldateformatter/formatting/offset_success_large.phpt
new file mode 100644
index 00000000000..68034c915c9
--- /dev/null
+++ b/ext/intl/tests/reldateformatter/formatting/offset_success_large.phpt
@@ -0,0 +1,25 @@
+--TEST--
+Test IntlRelativeDateTimeFormatter formatting - large offsets - grouping and values beyond integer range
+--EXTENSIONS--
+intl
+--FILE--
+<?php
+
+require_once __DIR__ . '/format.inc';
+
+printRelativeDateTimeTable(
+    new IntlRelativeDateTimeFormatter('en_US'),
+    offsets: [-1e20, -1e6, 1e6, 1e20],
+    units: ['second' => IntlRelativeDateTimeFormatterUnit::Second],
+);
+
+?>
+--EXPECT--
+=== second ===============================================================================
+
+  offset  format                                   formatNumeric
+--------  ---------------------------------------  ---------------------------------------
+-1.0E+20  100,000,000,000,000,000,000 seconds ago  100,000,000,000,000,000,000 seconds ago
+-1000000  1,000,000 seconds ago                    1,000,000 seconds ago
+ 1000000  in 1,000,000 seconds                     in 1,000,000 seconds
+ 1.0E+20  in 100,000,000,000,000,000,000 seconds   in 100,000,000,000,000,000,000 seconds
diff --git a/ext/intl/tests/reldateformatter/formatting/offset_success_near_zero.phpt b/ext/intl/tests/reldateformatter/formatting/offset_success_near_zero.phpt
new file mode 100644
index 00000000000..574ada3e0ae
--- /dev/null
+++ b/ext/intl/tests/reldateformatter/formatting/offset_success_near_zero.phpt
@@ -0,0 +1,23 @@
+--TEST--
+Test IntlRelativeDateTimeFormatter formatting - offsets near zero - relative and numeric output
+--EXTENSIONS--
+intl
+--FILE--
+<?php
+
+require_once __DIR__ . '/format.inc';
+
+printRelativeDateTimeTable(
+    new IntlRelativeDateTimeFormatter('en_US'),
+    offsets: [-0.001, 0.001],
+    units: ['second' => IntlRelativeDateTimeFormatterUnit::Second],
+);
+
+?>
+--EXPECT--
+=== second ======================
+
+offset  format  formatNumeric
+------  ------  -----------------
+-0.001  now     0.001 seconds ago
+ 0.001  now     in 0.001 seconds
diff --git a/ext/intl/tests/reldateformatter/formatting/offset_success_non_finite.phpt b/ext/intl/tests/reldateformatter/formatting/offset_success_non_finite.phpt
new file mode 100644
index 00000000000..34170b21079
--- /dev/null
+++ b/ext/intl/tests/reldateformatter/formatting/offset_success_non_finite.phpt
@@ -0,0 +1,30 @@
+--TEST--
+Test IntlRelativeDateTimeFormatter formatting - non-finite offsets - infinities and NaN
+--EXTENSIONS--
+intl
+--FILE--
+<?php
+
+require_once __DIR__ . '/format.inc';
+
+$formatter = new IntlRelativeDateTimeFormatter('en_US');
+printRelativeDateTimeTable(
+    $formatter,
+    offsets: [-INF, INF, NAN],
+    units: ['second' => IntlRelativeDateTimeFormatterUnit::Second],
+);
+
+var_dump($formatter->getErrorCode());
+var_dump($formatter->getErrorMessage());
+
+?>
+--EXPECT--
+=== second ===========================
+
+offset  format          formatNumeric
+------  --------------  --------------
+  -INF  ∞ seconds ago   ∞ seconds ago
+   INF  in ∞ seconds    in ∞ seconds
+   NAN  in NaN seconds  in NaN seconds
+int(0)
+string(12) "U_ZERO_ERROR"
diff --git a/ext/intl/tests/reldateformatter/formatting/style_success_long.phpt b/ext/intl/tests/reldateformatter/formatting/style_success_long.phpt
new file mode 100644
index 00000000000..4c13ba5d333
--- /dev/null
+++ b/ext/intl/tests/reldateformatter/formatting/style_success_long.phpt
@@ -0,0 +1,319 @@
+--TEST--
+Test IntlRelativeDateTimeFormatter formatting - long style - all units and fractional offsets
+--EXTENSIONS--
+intl
+--SKIPIF--
+<?php
+if (version_compare(INTL_ICU_VERSION, '65.0', '<')) {
+    die('skip ICU >= 65.0 required for relative hour and minute names');
+}
+?>
+--FILE--
+<?php
+
+require_once __DIR__ . '/format.inc';
+
+$formatter = new IntlRelativeDateTimeFormatter('en_US', IntlRelativeDateTimeFormatterStyle::Long);
+printRelativeDateTimeTable($formatter);
+
+?>
+--EXPECT--
+=== year =======================================
+
+offset  format               formatNumeric
+------  -------------------  -------------------
+    -3  3 years ago          3 years ago
+    -2  2 years ago          2 years ago
+ -1.75  1.75 years ago       1.75 years ago
+  -1.5  1.5 years ago        1.5 years ago
+    -1  last year            1 year ago
+  -0.5  0.5 years ago        0.5 years ago
+     0  this year            in 0 years
+     0  this year            in 0 years
+    -0  this year            0 years ago
+   0.5  in 0.5 years         in 0.5 years
+     1  next year            in 1 year
+   1.5  in 1.5 years         in 1.5 years
+  1.75  in 1.75 years        in 1.75 years
+     2  in 2 years           in 2 years
+     3  in 3 years           in 3 years
+
+=== quarter ====================================
+
+offset  format               formatNumeric
+------  -------------------  -------------------
+    -3  3 quarters ago       3 quarters ago
+    -2  2 quarters ago       2 quarters ago
+ -1.75  1.75 quarters ago    1.75 quarters ago
+  -1.5  1.5 quarters ago     1.5 quarters ago
+    -1  last quarter         1 quarter ago
+  -0.5  0.5 quarters ago     0.5 quarters ago
+     0  this quarter         in 0 quarters
+     0  this quarter         in 0 quarters
+    -0  this quarter         0 quarters ago
+   0.5  in 0.5 quarters      in 0.5 quarters
+     1  next quarter         in 1 quarter
+   1.5  in 1.5 quarters      in 1.5 quarters
+  1.75  in 1.75 quarters     in 1.75 quarters
+     2  in 2 quarters        in 2 quarters
+     3  in 3 quarters        in 3 quarters
+
+=== month ======================================
+
+offset  format               formatNumeric
+------  -------------------  -------------------
+    -3  3 months ago         3 months ago
+    -2  2 months ago         2 months ago
+ -1.75  1.75 months ago      1.75 months ago
+  -1.5  1.5 months ago       1.5 months ago
+    -1  last month           1 month ago
+  -0.5  0.5 months ago       0.5 months ago
+     0  this month           in 0 months
+     0  this month           in 0 months
+    -0  this month           0 months ago
+   0.5  in 0.5 months        in 0.5 months
+     1  next month           in 1 month
+   1.5  in 1.5 months        in 1.5 months
+  1.75  in 1.75 months       in 1.75 months
+     2  in 2 months          in 2 months
+     3  in 3 months          in 3 months
+
+=== week =======================================
+
+offset  format               formatNumeric
+------  -------------------  -------------------
+    -3  3 weeks ago          3 weeks ago
+    -2  2 weeks ago          2 weeks ago
+ -1.75  1.75 weeks ago       1.75 weeks ago
+  -1.5  1.5 weeks ago        1.5 weeks ago
+    -1  last week            1 week ago
+  -0.5  0.5 weeks ago        0.5 weeks ago
+     0  this week            in 0 weeks
+     0  this week            in 0 weeks
+    -0  this week            0 weeks ago
+   0.5  in 0.5 weeks         in 0.5 weeks
+     1  next week            in 1 week
+   1.5  in 1.5 weeks         in 1.5 weeks
+  1.75  in 1.75 weeks        in 1.75 weeks
+     2  in 2 weeks           in 2 weeks
+     3  in 3 weeks           in 3 weeks
+
+=== day ========================================
+
+offset  format               formatNumeric
+------  -------------------  -------------------
+    -3  3 days ago           3 days ago
+    -2  2 days ago           2 days ago
+ -1.75  1.75 days ago        1.75 days ago
+  -1.5  1.5 days ago         1.5 days ago
+    -1  yesterday            1 day ago
+  -0.5  0.5 days ago         0.5 days ago
+     0  today                in 0 days
+     0  today                in 0 days
+    -0  today                0 days ago
+   0.5  in 0.5 days          in 0.5 days
+     1  tomorrow             in 1 day
+   1.5  in 1.5 days          in 1.5 days
+  1.75  in 1.75 days         in 1.75 days
+     2  in 2 days            in 2 days
+     3  in 3 days            in 3 days
+
+=== hour =======================================
+
+offset  format               formatNumeric
+------  -------------------  -------------------
+    -3  3 hours ago          3 hours ago
+    -2  2 hours ago          2 hours ago
+ -1.75  1.75 hours ago       1.75 hours ago
+  -1.5  1.5 hours ago        1.5 hours ago
+    -1  1 hour ago           1 hour ago
+  -0.5  0.5 hours ago        0.5 hours ago
+     0  this hour            in 0 hours
+     0  this hour            in 0 hours
+    -0  this hour            0 hours ago
+   0.5  in 0.5 hours         in 0.5 hours
+     1  in 1 hour            in 1 hour
+   1.5  in 1.5 hours         in 1.5 hours
+  1.75  in 1.75 hours        in 1.75 hours
+     2  in 2 hours           in 2 hours
+     3  in 3 hours           in 3 hours
+
+=== minute =====================================
+
+offset  format               formatNumeric
+------  -------------------  -------------------
+    -3  3 minutes ago        3 minutes ago
+    -2  2 minutes ago        2 minutes ago
+ -1.75  1.75 minutes ago     1.75 minutes ago
+  -1.5  1.5 minutes ago      1.5 minutes ago
+    -1  1 minute ago         1 minute ago
+  -0.5  0.5 minutes ago      0.5 minutes ago
+     0  this minute          in 0 minutes
+     0  this minute          in 0 minutes
+    -0  this minute          0 minutes ago
+   0.5  in 0.5 minutes       in 0.5 minutes
+     1  in 1 minute          in 1 minute
+   1.5  in 1.5 minutes       in 1.5 minutes
+  1.75  in 1.75 minutes      in 1.75 minutes
+     2  in 2 minutes         in 2 minutes
+     3  in 3 minutes         in 3 minutes
+
+=== second =====================================
+
+offset  format               formatNumeric
+------  -------------------  -------------------
+    -3  3 seconds ago        3 seconds ago
+    -2  2 seconds ago        2 seconds ago
+ -1.75  1.75 seconds ago     1.75 seconds ago
+  -1.5  1.5 seconds ago      1.5 seconds ago
+    -1  1 second ago         1 second ago
+  -0.5  0.5 seconds ago      0.5 seconds ago
+     0  now                  in 0 seconds
+     0  now                  in 0 seconds
+    -0  now                  0 seconds ago
+   0.5  in 0.5 seconds       in 0.5 seconds
+     1  in 1 second          in 1 second
+   1.5  in 1.5 seconds       in 1.5 seconds
+  1.75  in 1.75 seconds      in 1.75 seconds
+     2  in 2 seconds         in 2 seconds
+     3  in 3 seconds         in 3 seconds
+
+=== sunday =====================================
+
+offset  format               formatNumeric
+------  -------------------  -------------------
+    -3  3 Sundays ago        3 Sundays ago
+    -2  2 Sundays ago        2 Sundays ago
+ -1.75  1.75 Sundays ago     1.75 Sundays ago
+  -1.5  1.5 Sundays ago      1.5 Sundays ago
+    -1  last Sunday          1 Sunday ago
+  -0.5  0.5 Sundays ago      0.5 Sundays ago
+     0  this Sunday          in 0 Sundays
+     0  this Sunday          in 0 Sundays
+    -0  this Sunday          0 Sundays ago
+   0.5  in 0.5 Sundays       in 0.5 Sundays
+     1  next Sunday          in 1 Sunday
+   1.5  in 1.5 Sundays       in 1.5 Sundays
+  1.75  in 1.75 Sundays      in 1.75 Sundays
+     2  in 2 Sundays         in 2 Sundays
+     3  in 3 Sundays         in 3 Sundays
+
+=== monday =====================================
+
+offset  format               formatNumeric
+------  -------------------  -------------------
+    -3  3 Mondays ago        3 Mondays ago
+    -2  2 Mondays ago        2 Mondays ago
+ -1.75  1.75 Mondays ago     1.75 Mondays ago
+  -1.5  1.5 Mondays ago      1.5 Mondays ago
+    -1  last Monday          1 Monday ago
+  -0.5  0.5 Mondays ago      0.5 Mondays ago
+     0  this Monday          in 0 Mondays
+     0  this Monday          in 0 Mondays
+    -0  this Monday          0 Mondays ago
+   0.5  in 0.5 Mondays       in 0.5 Mondays
+     1  next Monday          in 1 Monday
+   1.5  in 1.5 Mondays       in 1.5 Mondays
+  1.75  in 1.75 Mondays      in 1.75 Mondays
+     2  in 2 Mondays         in 2 Mondays
+     3  in 3 Mondays         in 3 Mondays
+
+=== tuesday ====================================
+
+offset  format               formatNumeric
+------  -------------------  -------------------
+    -3  3 Tuesdays ago       3 Tuesdays ago
+    -2  2 Tuesdays ago       2 Tuesdays ago
+ -1.75  1.75 Tuesdays ago    1.75 Tuesdays ago
+  -1.5  1.5 Tuesdays ago     1.5 Tuesdays ago
+    -1  last Tuesday         1 Tuesday ago
+  -0.5  0.5 Tuesdays ago     0.5 Tuesdays ago
+     0  this Tuesday         in 0 Tuesdays
+     0  this Tuesday         in 0 Tuesdays
+    -0  this Tuesday         0 Tuesdays ago
+   0.5  in 0.5 Tuesdays      in 0.5 Tuesdays
+     1  next Tuesday         in 1 Tuesday
+   1.5  in 1.5 Tuesdays      in 1.5 Tuesdays
+  1.75  in 1.75 Tuesdays     in 1.75 Tuesdays
+     2  in 2 Tuesdays        in 2 Tuesdays
+     3  in 3 Tuesdays        in 3 Tuesdays
+
+=== wednesday ==================================
+
+offset  format               formatNumeric
+------  -------------------  -------------------
+    -3  3 Wednesdays ago     3 Wednesdays ago
+    -2  2 Wednesdays ago     2 Wednesdays ago
+ -1.75  1.75 Wednesdays ago  1.75 Wednesdays ago
+  -1.5  1.5 Wednesdays ago   1.5 Wednesdays ago
+    -1  last Wednesday       1 Wednesday ago
+  -0.5  0.5 Wednesdays ago   0.5 Wednesdays ago
+     0  this Wednesday       in 0 Wednesdays
+     0  this Wednesday       in 0 Wednesdays
+    -0  this Wednesday       0 Wednesdays ago
+   0.5  in 0.5 Wednesdays    in 0.5 Wednesdays
+     1  next Wednesday       in 1 Wednesday
+   1.5  in 1.5 Wednesdays    in 1.5 Wednesdays
+  1.75  in 1.75 Wednesdays   in 1.75 Wednesdays
+     2  in 2 Wednesdays      in 2 Wednesdays
+     3  in 3 Wednesdays      in 3 Wednesdays
+
+=== thursday ===================================
+
+offset  format               formatNumeric
+------  -------------------  -------------------
+    -3  3 Thursdays ago      3 Thursdays ago
+    -2  2 Thursdays ago      2 Thursdays ago
+ -1.75  1.75 Thursdays ago   1.75 Thursdays ago
+  -1.5  1.5 Thursdays ago    1.5 Thursdays ago
+    -1  last Thursday        1 Thursday ago
+  -0.5  0.5 Thursdays ago    0.5 Thursdays ago
+     0  this Thursday        in 0 Thursdays
+     0  this Thursday        in 0 Thursdays
+    -0  this Thursday        0 Thursdays ago
+   0.5  in 0.5 Thursdays     in 0.5 Thursdays
+     1  next Thursday        in 1 Thursday
+   1.5  in 1.5 Thursdays     in 1.5 Thursdays
+  1.75  in 1.75 Thursdays    in 1.75 Thursdays
+     2  in 2 Thursdays       in 2 Thursdays
+     3  in 3 Thursdays       in 3 Thursdays
+
+=== friday =====================================
+
+offset  format               formatNumeric
+------  -------------------  -------------------
+    -3  3 Fridays ago        3 Fridays ago
+    -2  2 Fridays ago        2 Fridays ago
+ -1.75  1.75 Fridays ago     1.75 Fridays ago
+  -1.5  1.5 Fridays ago      1.5 Fridays ago
+    -1  last Friday          1 Friday ago
+  -0.5  0.5 Fridays ago      0.5 Fridays ago
+     0  this Friday          in 0 Fridays
+     0  this Friday          in 0 Fridays
+    -0  this Friday          0 Fridays ago
+   0.5  in 0.5 Fridays       in 0.5 Fridays
+     1  next Friday          in 1 Friday
+   1.5  in 1.5 Fridays       in 1.5 Fridays
+  1.75  in 1.75 Fridays      in 1.75 Fridays
+     2  in 2 Fridays         in 2 Fridays
+     3  in 3 Fridays         in 3 Fridays
+
+=== saturday ===================================
+
+offset  format               formatNumeric
+------  -------------------  -------------------
+    -3  3 Saturdays ago      3 Saturdays ago
+    -2  2 Saturdays ago      2 Saturdays ago
+ -1.75  1.75 Saturdays ago   1.75 Saturdays ago
+  -1.5  1.5 Saturdays ago    1.5 Saturdays ago
+    -1  last Saturday        1 Saturday ago
+  -0.5  0.5 Saturdays ago    0.5 Saturdays ago
+     0  this Saturday        in 0 Saturdays
+     0  this Saturday        in 0 Saturdays
+    -0  this Saturday        0 Saturdays ago
+   0.5  in 0.5 Saturdays     in 0.5 Saturdays
+     1  next Saturday        in 1 Saturday
+   1.5  in 1.5 Saturdays     in 1.5 Saturdays
+  1.75  in 1.75 Saturdays    in 1.75 Saturdays
+     2  in 2 Saturdays       in 2 Saturdays
+     3  in 3 Saturdays       in 3 Saturdays
diff --git a/ext/intl/tests/reldateformatter/formatting/style_success_long_decimal_comma_locale.phpt b/ext/intl/tests/reldateformatter/formatting/style_success_long_decimal_comma_locale.phpt
new file mode 100644
index 00000000000..e72819bf56e
--- /dev/null
+++ b/ext/intl/tests/reldateformatter/formatting/style_success_long_decimal_comma_locale.phpt
@@ -0,0 +1,53 @@
+--TEST--
+Test IntlRelativeDateTimeFormatter formatting - long style - locale with a decimal comma
+--EXTENSIONS--
+intl
+--FILE--
+<?php
+
+require_once __DIR__ . '/format.inc';
+
+printRelativeDateTimeTable(
+    new IntlRelativeDateTimeFormatter('de_DE', IntlRelativeDateTimeFormatterStyle::Long),
+    offsets: [-2.75, -1.75, -1.5, 1.5, 1.75, 2.75],
+    units: [
+        'day' => IntlRelativeDateTimeFormatterUnit::Day,
+        'hour' => IntlRelativeDateTimeFormatterUnit::Hour,
+        'minute' => IntlRelativeDateTimeFormatterUnit::Minute,
+    ],
+);
+
+?>
+--EXPECT--
+=== day ==================================
+
+offset  format            formatNumeric
+------  ----------------  ----------------
+ -2.75  vor 2,75 Tagen    vor 2,75 Tagen
+ -1.75  vor 1,75 Tagen    vor 1,75 Tagen
+  -1.5  vor 1,5 Tagen     vor 1,5 Tagen
+   1.5  in 1,5 Tagen      in 1,5 Tagen
+  1.75  in 1,75 Tagen     in 1,75 Tagen
+  2.75  in 2,75 Tagen     in 2,75 Tagen
+
+=== hour =================================
+
+offset  format            formatNumeric
+------  ----------------  ----------------
+ -2.75  vor 2,75 Stunden  vor 2,75 Stunden
+ -1.75  vor 1,75 Stunden  vor 1,75 Stunden
+  -1.5  vor 1,5 Stunden   vor 1,5 Stunden
+   1.5  in 1,5 Stunden    in 1,5 Stunden
+  1.75  in 1,75 Stunden   in 1,75 Stunden
+  2.75  in 2,75 Stunden   in 2,75 Stunden
+
+=== minute ===============================
+
+offset  format            formatNumeric
+------  ----------------  ----------------
+ -2.75  vor 2,75 Minuten  vor 2,75 Minuten
+ -1.75  vor 1,75 Minuten  vor 1,75 Minuten
+  -1.5  vor 1,5 Minuten   vor 1,5 Minuten
+   1.5  in 1,5 Minuten    in 1,5 Minuten
+  1.75  in 1,75 Minuten   in 1,75 Minuten
+  2.75  in 2,75 Minuten   in 2,75 Minuten
diff --git a/ext/intl/tests/reldateformatter/formatting/style_success_narrow.phpt b/ext/intl/tests/reldateformatter/formatting/style_success_narrow.phpt
new file mode 100644
index 00000000000..3d3793ccca2
--- /dev/null
+++ b/ext/intl/tests/reldateformatter/formatting/style_success_narrow.phpt
@@ -0,0 +1,319 @@
+--TEST--
+Test IntlRelativeDateTimeFormatter formatting - narrow style - all units and fractional offsets
+--EXTENSIONS--
+intl
+--SKIPIF--
+<?php
+if (version_compare(INTL_ICU_VERSION, '65.0', '<')) {
+    die('skip ICU >= 65.0 required for relative hour and minute names');
+}
+?>
+--FILE--
+<?php
+
+require_once __DIR__ . '/format.inc';
+
+$formatter = new IntlRelativeDateTimeFormatter('en_US', IntlRelativeDateTimeFormatterStyle::Narrow);
+printRelativeDateTimeTable($formatter);
+
+?>
+--EXPECT--
+=== year =========================
+
+offset  format       formatNumeric
+------  -----------  -------------
+    -3  3y ago       3y ago
+    -2  2y ago       2y ago
+ -1.75  1.75y ago    1.75y ago
+  -1.5  1.5y ago     1.5y ago
+    -1  last yr.     1y ago
+  -0.5  0.5y ago     0.5y ago
+     0  this yr.     in 0y
+     0  this yr.     in 0y
+    -0  this yr.     0y ago
+   0.5  in 0.5y      in 0.5y
+     1  next yr.     in 1y
+   1.5  in 1.5y      in 1.5y
+  1.75  in 1.75y     in 1.75y
+     2  in 2y        in 2y
+     3  in 3y        in 3y
+
+=== quarter ======================
+
+offset  format       formatNumeric
+------  -----------  -------------
+    -3  3q ago       3q ago
+    -2  2q ago       2q ago
+ -1.75  1.75q ago    1.75q ago
+  -1.5  1.5q ago     1.5q ago
+    -1  last qtr.    1q ago
+  -0.5  0.5q ago     0.5q ago
+     0  this qtr.    in 0q
+     0  this qtr.    in 0q
+    -0  this qtr.    0q ago
+   0.5  in 0.5q      in 0.5q
+     1  next qtr.    in 1q
+   1.5  in 1.5q      in 1.5q
+  1.75  in 1.75q     in 1.75q
+     2  in 2q        in 2q
+     3  in 3q        in 3q
+
+=== month ========================
+
+offset  format       formatNumeric
+------  -----------  -------------
+    -3  3mo ago      3mo ago
+    -2  2mo ago      2mo ago
+ -1.75  1.75mo ago   1.75mo ago
+  -1.5  1.5mo ago    1.5mo ago
+    -1  last mo.     1mo ago
+  -0.5  0.5mo ago    0.5mo ago
+     0  this mo.     in 0mo
+     0  this mo.     in 0mo
+    -0  this mo.     0mo ago
+   0.5  in 0.5mo     in 0.5mo
+     1  next mo.     in 1mo
+   1.5  in 1.5mo     in 1.5mo
+  1.75  in 1.75mo    in 1.75mo
+     2  in 2mo       in 2mo
+     3  in 3mo       in 3mo
+
+=== week =========================
+
+offset  format       formatNumeric
+------  -----------  -------------
+    -3  3w ago       3w ago
+    -2  2w ago       2w ago
+ -1.75  1.75w ago    1.75w ago
+  -1.5  1.5w ago     1.5w ago
+    -1  last wk.     1w ago
+  -0.5  0.5w ago     0.5w ago
+     0  this wk.     in 0w
+     0  this wk.     in 0w
+    -0  this wk.     0w ago
+   0.5  in 0.5w      in 0.5w
+     1  next wk.     in 1w
+   1.5  in 1.5w      in 1.5w
+  1.75  in 1.75w     in 1.75w
+     2  in 2w        in 2w
+     3  in 3w        in 3w
+
+=== day ==========================
+
+offset  format       formatNumeric
+------  -----------  -------------
+    -3  3d ago       3d ago
+    -2  2d ago       2d ago
+ -1.75  1.75d ago    1.75d ago
+  -1.5  1.5d ago     1.5d ago
+    -1  yesterday    1d ago
+  -0.5  0.5d ago     0.5d ago
+     0  today        in 0d
+     0  today        in 0d
+    -0  today        0d ago
+   0.5  in 0.5d      in 0.5d
+     1  tomorrow     in 1d
+   1.5  in 1.5d      in 1.5d
+  1.75  in 1.75d     in 1.75d
+     2  in 2d        in 2d
+     3  in 3d        in 3d
+
+=== hour =========================
+
+offset  format       formatNumeric
+------  -----------  -------------
+    -3  3h ago       3h ago
+    -2  2h ago       2h ago
+ -1.75  1.75h ago    1.75h ago
+  -1.5  1.5h ago     1.5h ago
+    -1  1h ago       1h ago
+  -0.5  0.5h ago     0.5h ago
+     0  this hour    in 0h
+     0  this hour    in 0h
+    -0  this hour    0h ago
+   0.5  in 0.5h      in 0.5h
+     1  in 1h        in 1h
+   1.5  in 1.5h      in 1.5h
+  1.75  in 1.75h     in 1.75h
+     2  in 2h        in 2h
+     3  in 3h        in 3h
+
+=== minute =======================
+
+offset  format       formatNumeric
+------  -----------  -------------
+    -3  3m ago       3m ago
+    -2  2m ago       2m ago
+ -1.75  1.75m ago    1.75m ago
+  -1.5  1.5m ago     1.5m ago
+    -1  1m ago       1m ago
+  -0.5  0.5m ago     0.5m ago
+     0  this minute  in 0m
+     0  this minute  in 0m
+    -0  this minute  0m ago
+   0.5  in 0.5m      in 0.5m
+     1  in 1m        in 1m
+   1.5  in 1.5m      in 1.5m
+  1.75  in 1.75m     in 1.75m
+     2  in 2m        in 2m
+     3  in 3m        in 3m
+
+=== second =======================
+
+offset  format       formatNumeric
+------  -----------  -------------
+    -3  3s ago       3s ago
+    -2  2s ago       2s ago
+ -1.75  1.75s ago    1.75s ago
+  -1.5  1.5s ago     1.5s ago
+    -1  1s ago       1s ago
+  -0.5  0.5s ago     0.5s ago
+     0  now          in 0s
+     0  now          in 0s
+    -0  now          0s ago
+   0.5  in 0.5s      in 0.5s
+     1  in 1s        in 1s
+   1.5  in 1.5s      in 1.5s
+  1.75  in 1.75s     in 1.75s
+     2  in 2s        in 2s
+     3  in 3s        in 3s
+
+=== sunday =======================
+
+offset  format       formatNumeric
+------  -----------  -------------
+    -3  3 Su ago     3 Su ago
+    -2  2 Su ago     2 Su ago
+ -1.75  1.75 Su ago  1.75 Su ago
+  -1.5  1.5 Su ago   1.5 Su ago
+    -1  last Su      1 Su ago
+  -0.5  0.5 Su ago   0.5 Su ago
+     0  this Su      in 0 Su
+     0  this Su      in 0 Su
+    -0  this Su      0 Su ago
+   0.5  in 0.5 Su    in 0.5 Su
+     1  next Su      in 1 Su
+   1.5  in 1.5 Su    in 1.5 Su
+  1.75  in 1.75 Su   in 1.75 Su
+     2  in 2 Su      in 2 Su
+     3  in 3 Su      in 3 Su
+
+=== monday =======================
+
+offset  format       formatNumeric
+------  -----------  -------------
+    -3  3 M ago      3 M ago
+    -2  2 M ago      2 M ago
+ -1.75  1.75 M ago   1.75 M ago
+  -1.5  1.5 M ago    1.5 M ago
+    -1  last M       1 M ago
+  -0.5  0.5 M ago    0.5 M ago
+     0  this M       in 0 M
+     0  this M       in 0 M
+    -0  this M       0 M ago
+   0.5  in 0.5 M     in 0.5 M
+     1  next M       in 1 M
+   1.5  in 1.5 M     in 1.5 M
+  1.75  in 1.75 M    in 1.75 M
+     2  in 2 M       in 2 M
+     3  in 3 M       in 3 M
+
+=== tuesday ======================
+
+offset  format       formatNumeric
+------  -----------  -------------
+    -3  3 Tu ago     3 Tu ago
+    -2  2 Tu ago     2 Tu ago
+ -1.75  1.75 Tu ago  1.75 Tu ago
+  -1.5  1.5 Tu ago   1.5 Tu ago
+    -1  last Tu      1 Tu ago
+  -0.5  0.5 Tu ago   0.5 Tu ago
+     0  this Tu      in 0 Tu
+     0  this Tu      in 0 Tu
+    -0  this Tu      0 Tu ago
+   0.5  in 0.5 Tu    in 0.5 Tu
+     1  next Tu      in 1 Tu
+   1.5  in 1.5 Tu    in 1.5 Tu
+  1.75  in 1.75 Tu   in 1.75 Tu
+     2  in 2 Tu      in 2 Tu
+     3  in 3 Tu      in 3 Tu
+
+=== wednesday ====================
+
+offset  format       formatNumeric
+------  -----------  -------------
+    -3  3 W ago      3 W ago
+    -2  2 W ago      2 W ago
+ -1.75  1.75 W ago   1.75 W ago
+  -1.5  1.5 W ago    1.5 W ago
+    -1  last W       1 W ago
+  -0.5  0.5 W ago    0.5 W ago
+     0  this W       in 0 W
+     0  this W       in 0 W
+    -0  this W       0 W ago
+   0.5  in 0.5 W     in 0.5 W
+     1  next W       in 1 W
+   1.5  in 1.5 W     in 1.5 W
+  1.75  in 1.75 W    in 1.75 W
+     2  in 2 W       in 2 W
+     3  in 3 W       in 3 W
+
+=== thursday =====================
+
+offset  format       formatNumeric
+------  -----------  -------------
+    -3  3 Th ago     3 Th ago
+    -2  2 Th ago     2 Th ago
+ -1.75  1.75 Th ago  1.75 Th ago
+  -1.5  1.5 Th ago   1.5 Th ago
+    -1  last Th      1 Th ago
+  -0.5  0.5 Th ago   0.5 Th ago
+     0  this Th      in 0 Th
+     0  this Th      in 0 Th
+    -0  this Th      0 Th ago
+   0.5  in 0.5 Th    in 0.5 Th
+     1  next Th      in 1 Th
+   1.5  in 1.5 Th    in 1.5 Th
+  1.75  in 1.75 Th   in 1.75 Th
+     2  in 2 Th      in 2 Th
+     3  in 3 Th      in 3 Th
+
+=== friday =======================
+
+offset  format       formatNumeric
+------  -----------  -------------
+    -3  3 F ago      3 F ago
+    -2  2 F ago      2 F ago
+ -1.75  1.75 F ago   1.75 F ago
+  -1.5  1.5 F ago    1.5 F ago
+    -1  last F       1 F ago
+  -0.5  0.5 F ago    0.5 F ago
+     0  this F       in 0 F
+     0  this F       in 0 F
+    -0  this F       0 F ago
+   0.5  in 0.5 F     in 0.5 F
+     1  next F       in 1 F
+   1.5  in 1.5 F     in 1.5 F
+  1.75  in 1.75 F    in 1.75 F
+     2  in 2 F       in 2 F
+     3  in 3 F       in 3 F
+
+=== saturday =====================
+
+offset  format       formatNumeric
+------  -----------  -------------
+    -3  3 Sa ago     3 Sa ago
+    -2  2 Sa ago     2 Sa ago
+ -1.75  1.75 Sa ago  1.75 Sa ago
+  -1.5  1.5 Sa ago   1.5 Sa ago
+    -1  last Sa      1 Sa ago
+  -0.5  0.5 Sa ago   0.5 Sa ago
+     0  this Sa      in 0 Sa
+     0  this Sa      in 0 Sa
+    -0  this Sa      0 Sa ago
+   0.5  in 0.5 Sa    in 0.5 Sa
+     1  next Sa      in 1 Sa
+   1.5  in 1.5 Sa    in 1.5 Sa
+  1.75  in 1.75 Sa   in 1.75 Sa
+     2  in 2 Sa      in 2 Sa
+     3  in 3 Sa      in 3 Sa
diff --git a/ext/intl/tests/reldateformatter/formatting/style_success_narrow_decimal_comma_locale.phpt b/ext/intl/tests/reldateformatter/formatting/style_success_narrow_decimal_comma_locale.phpt
new file mode 100644
index 00000000000..fff2bd67395
--- /dev/null
+++ b/ext/intl/tests/reldateformatter/formatting/style_success_narrow_decimal_comma_locale.phpt
@@ -0,0 +1,53 @@
+--TEST--
+Test IntlRelativeDateTimeFormatter formatting - narrow style - locale with a decimal comma
+--EXTENSIONS--
+intl
+--FILE--
+<?php
+
+require_once __DIR__ . '/format.inc';
+
+printRelativeDateTimeTable(
+    new IntlRelativeDateTimeFormatter('de_DE', IntlRelativeDateTimeFormatterStyle::Narrow),
+    offsets: [-2.75, -1.75, -1.5, 1.5, 1.75, 2.75],
+    units: [
+        'day' => IntlRelativeDateTimeFormatterUnit::Day,
+        'hour' => IntlRelativeDateTimeFormatterUnit::Hour,
+        'minute' => IntlRelativeDateTimeFormatterUnit::Minute,
+    ],
+);
+
+?>
+--EXPECT--
+=== day ==============================
+
+offset  format          formatNumeric
+------  --------------  --------------
+ -2.75  vor 2,75 Tagen  vor 2,75 Tagen
+ -1.75  vor 1,75 Tagen  vor 1,75 Tagen
+  -1.5  vor 1,5 Tagen   vor 1,5 Tagen
+   1.5  in 1,5 Tagen    in 1,5 Tagen
+  1.75  in 1,75 Tagen   in 1,75 Tagen
+  2.75  in 2,75 Tagen   in 2,75 Tagen
+
+=== hour =============================
+
+offset  format          formatNumeric
+------  --------------  --------------
+ -2.75  vor 2,75 Std.   vor 2,75 Std.
+ -1.75  vor 1,75 Std.   vor 1,75 Std.
+  -1.5  vor 1,5 Std.    vor 1,5 Std.
+   1.5  in 1,5 Std.     in 1,5 Std.
+  1.75  in 1,75 Std.    in 1,75 Std.
+  2.75  in 2,75 Std.    in 2,75 Std.
+
+=== minute ===========================
+
+offset  format          formatNumeric
+------  --------------  --------------
+ -2.75  vor 2,75 m      vor 2,75 m
+ -1.75  vor 1,75 m      vor 1,75 m
+  -1.5  vor 1,5 m       vor 1,5 m
+   1.5  in 1,5 m        in 1,5 m
+  1.75  in 1,75 m       in 1,75 m
+  2.75  in 2,75 m       in 2,75 m
diff --git a/ext/intl/tests/reldateformatter/formatting/style_success_short.phpt b/ext/intl/tests/reldateformatter/formatting/style_success_short.phpt
new file mode 100644
index 00000000000..0873fe0cb1f
--- /dev/null
+++ b/ext/intl/tests/reldateformatter/formatting/style_success_short.phpt
@@ -0,0 +1,319 @@
+--TEST--
+Test IntlRelativeDateTimeFormatter formatting - short style - all units and fractional offsets
+--EXTENSIONS--
+intl
+--SKIPIF--
+<?php
+if (version_compare(INTL_ICU_VERSION, '65.0', '<')) {
+    die('skip ICU >= 65.0 required for relative hour and minute names');
+}
+?>
+--FILE--
+<?php
+
+require_once __DIR__ . '/format.inc';
+
+$formatter = new IntlRelativeDateTimeFormatter('en_US', IntlRelativeDateTimeFormatterStyle::Short);
+printRelativeDateTimeTable($formatter);
+
+?>
+--EXPECT--
+=== year =============================
+
+offset  format          formatNumeric
+------  --------------  --------------
+    -3  3 yr. ago       3 yr. ago
+    -2  2 yr. ago       2 yr. ago
+ -1.75  1.75 yr. ago    1.75 yr. ago
+  -1.5  1.5 yr. ago     1.5 yr. ago
+    -1  last yr.        1 yr. ago
+  -0.5  0.5 yr. ago     0.5 yr. ago
+     0  this yr.        in 0 yr.
+     0  this yr.        in 0 yr.
+    -0  this yr.        0 yr. ago
+   0.5  in 0.5 yr.      in 0.5 yr.
+     1  next yr.        in 1 yr.
+   1.5  in 1.5 yr.      in 1.5 yr.
+  1.75  in 1.75 yr.     in 1.75 yr.
+     2  in 2 yr.        in 2 yr.
+     3  in 3 yr.        in 3 yr.
+
+=== quarter ==========================
+
+offset  format          formatNumeric
+------  --------------  --------------
+    -3  3 qtrs. ago     3 qtrs. ago
+    -2  2 qtrs. ago     2 qtrs. ago
+ -1.75  1.75 qtrs. ago  1.75 qtrs. ago
+  -1.5  1.5 qtrs. ago   1.5 qtrs. ago
+    -1  last qtr.       1 qtr. ago
+  -0.5  0.5 qtrs. ago   0.5 qtrs. ago
+     0  this qtr.       in 0 qtrs.
+     0  this qtr.       in 0 qtrs.
+    -0  this qtr.       0 qtrs. ago
+   0.5  in 0.5 qtrs.    in 0.5 qtrs.
+     1  next qtr.       in 1 qtr.
+   1.5  in 1.5 qtrs.    in 1.5 qtrs.
+  1.75  in 1.75 qtrs.   in 1.75 qtrs.
+     2  in 2 qtrs.      in 2 qtrs.
+     3  in 3 qtrs.      in 3 qtrs.
+
+=== month ============================
+
+offset  format          formatNumeric
+------  --------------  --------------
+    -3  3 mo. ago       3 mo. ago
+    -2  2 mo. ago       2 mo. ago
+ -1.75  1.75 mo. ago    1.75 mo. ago
+  -1.5  1.5 mo. ago     1.5 mo. ago
+    -1  last mo.        1 mo. ago
+  -0.5  0.5 mo. ago     0.5 mo. ago
+     0  this mo.        in 0 mo.
+     0  this mo.        in 0 mo.
+    -0  this mo.        0 mo. ago
+   0.5  in 0.5 mo.      in 0.5 mo.
+     1  next mo.        in 1 mo.
+   1.5  in 1.5 mo.      in 1.5 mo.
+  1.75  in 1.75 mo.     in 1.75 mo.
+     2  in 2 mo.        in 2 mo.
+     3  in 3 mo.        in 3 mo.
+
+=== week =============================
+
+offset  format          formatNumeric
+------  --------------  --------------
+    -3  3 wk. ago       3 wk. ago
+    -2  2 wk. ago       2 wk. ago
+ -1.75  1.75 wk. ago    1.75 wk. ago
+  -1.5  1.5 wk. ago     1.5 wk. ago
+    -1  last wk.        1 wk. ago
+  -0.5  0.5 wk. ago     0.5 wk. ago
+     0  this wk.        in 0 wk.
+     0  this wk.        in 0 wk.
+    -0  this wk.        0 wk. ago
+   0.5  in 0.5 wk.      in 0.5 wk.
+     1  next wk.        in 1 wk.
+   1.5  in 1.5 wk.      in 1.5 wk.
+  1.75  in 1.75 wk.     in 1.75 wk.
+     2  in 2 wk.        in 2 wk.
+     3  in 3 wk.        in 3 wk.
+
+=== day ==============================
+
+offset  format          formatNumeric
+------  --------------  --------------
+    -3  3 days ago      3 days ago
+    -2  2 days ago      2 days ago
+ -1.75  1.75 days ago   1.75 days ago
+  -1.5  1.5 days ago    1.5 days ago
+    -1  yesterday       1 day ago
+  -0.5  0.5 days ago    0.5 days ago
+     0  today           in 0 days
+     0  today           in 0 days
+    -0  today           0 days ago
+   0.5  in 0.5 days     in 0.5 days
+     1  tomorrow        in 1 day
+   1.5  in 1.5 days     in 1.5 days
+  1.75  in 1.75 days    in 1.75 days
+     2  in 2 days       in 2 days
+     3  in 3 days       in 3 days
+
+=== hour =============================
+
+offset  format          formatNumeric
+------  --------------  --------------
+    -3  3 hr. ago       3 hr. ago
+    -2  2 hr. ago       2 hr. ago
+ -1.75  1.75 hr. ago    1.75 hr. ago
+  -1.5  1.5 hr. ago     1.5 hr. ago
+    -1  1 hr. ago       1 hr. ago
+  -0.5  0.5 hr. ago     0.5 hr. ago
+     0  this hour       in 0 hr.
+     0  this hour       in 0 hr.
+    -0  this hour       0 hr. ago
+   0.5  in 0.5 hr.      in 0.5 hr.
+     1  in 1 hr.        in 1 hr.
+   1.5  in 1.5 hr.      in 1.5 hr.
+  1.75  in 1.75 hr.     in 1.75 hr.
+     2  in 2 hr.        in 2 hr.
+     3  in 3 hr.        in 3 hr.
+
+=== minute ===========================
+
+offset  format          formatNumeric
+------  --------------  --------------
+    -3  3 min. ago      3 min. ago
+    -2  2 min. ago      2 min. ago
+ -1.75  1.75 min. ago   1.75 min. ago
+  -1.5  1.5 min. ago    1.5 min. ago
+    -1  1 min. ago      1 min. ago
+  -0.5  0.5 min. ago    0.5 min. ago
+     0  this minute     in 0 min.
+     0  this minute     in 0 min.
+    -0  this minute     0 min. ago
+   0.5  in 0.5 min.     in 0.5 min.
+     1  in 1 min.       in 1 min.
+   1.5  in 1.5 min.     in 1.5 min.
+  1.75  in 1.75 min.    in 1.75 min.
+     2  in 2 min.       in 2 min.
+     3  in 3 min.       in 3 min.
+
+=== second ===========================
+
+offset  format          formatNumeric
+------  --------------  --------------
+    -3  3 sec. ago      3 sec. ago
+    -2  2 sec. ago      2 sec. ago
+ -1.75  1.75 sec. ago   1.75 sec. ago
+  -1.5  1.5 sec. ago    1.5 sec. ago
+    -1  1 sec. ago      1 sec. ago
+  -0.5  0.5 sec. ago    0.5 sec. ago
+     0  now             in 0 sec.
+     0  now             in 0 sec.
+    -0  now             0 sec. ago
+   0.5  in 0.5 sec.     in 0.5 sec.
+     1  in 1 sec.       in 1 sec.
+   1.5  in 1.5 sec.     in 1.5 sec.
+  1.75  in 1.75 sec.    in 1.75 sec.
+     2  in 2 sec.       in 2 sec.
+     3  in 3 sec.       in 3 sec.
+
+=== sunday ===========================
+
+offset  format          formatNumeric
+------  --------------  --------------
+    -3  3 Sun. ago      3 Sun. ago
+    -2  2 Sun. ago      2 Sun. ago
+ -1.75  1.75 Sun. ago   1.75 Sun. ago
+  -1.5  1.5 Sun. ago    1.5 Sun. ago
+    -1  last Sun.       1 Sun. ago
+  -0.5  0.5 Sun. ago    0.5 Sun. ago
+     0  this Sun.       in 0 Sun.
+     0  this Sun.       in 0 Sun.
+    -0  this Sun.       0 Sun. ago
+   0.5  in 0.5 Sun.     in 0.5 Sun.
+     1  next Sun.       in 1 Sun.
+   1.5  in 1.5 Sun.     in 1.5 Sun.
+  1.75  in 1.75 Sun.    in 1.75 Sun.
+     2  in 2 Sun.       in 2 Sun.
+     3  in 3 Sun.       in 3 Sun.
+
+=== monday ===========================
+
+offset  format          formatNumeric
+------  --------------  --------------
+    -3  3 Mon. ago      3 Mon. ago
+    -2  2 Mon. ago      2 Mon. ago
+ -1.75  1.75 Mon. ago   1.75 Mon. ago
+  -1.5  1.5 Mon. ago    1.5 Mon. ago
+    -1  last Mon.       1 Mon. ago
+  -0.5  0.5 Mon. ago    0.5 Mon. ago
+     0  this Mon.       in 0 Mon.
+     0  this Mon.       in 0 Mon.
+    -0  this Mon.       0 Mon. ago
+   0.5  in 0.5 Mon.     in 0.5 Mon.
+     1  next Mon.       in 1 Mon.
+   1.5  in 1.5 Mon.     in 1.5 Mon.
+  1.75  in 1.75 Mon.    in 1.75 Mon.
+     2  in 2 Mon.       in 2 Mon.
+     3  in 3 Mon.       in 3 Mon.
+
+=== tuesday ==========================
+
+offset  format          formatNumeric
+------  --------------  --------------
+    -3  3 Tue. ago      3 Tue. ago
+    -2  2 Tue. ago      2 Tue. ago
+ -1.75  1.75 Tue. ago   1.75 Tue. ago
+  -1.5  1.5 Tue. ago    1.5 Tue. ago
+    -1  last Tue.       1 Tue. ago
+  -0.5  0.5 Tue. ago    0.5 Tue. ago
+     0  this Tue.       in 0 Tue.
+     0  this Tue.       in 0 Tue.
+    -0  this Tue.       0 Tue. ago
+   0.5  in 0.5 Tue.     in 0.5 Tue.
+     1  next Tue.       in 1 Tue.
+   1.5  in 1.5 Tue.     in 1.5 Tue.
+  1.75  in 1.75 Tue.    in 1.75 Tue.
+     2  in 2 Tue.       in 2 Tue.
+     3  in 3 Tue.       in 3 Tue.
+
+=== wednesday ========================
+
+offset  format          formatNumeric
+------  --------------  --------------
+    -3  3 Wed. ago      3 Wed. ago
+    -2  2 Wed. ago      2 Wed. ago
+ -1.75  1.75 Wed. ago   1.75 Wed. ago
+  -1.5  1.5 Wed. ago    1.5 Wed. ago
+    -1  last Wed.       1 Wed. ago
+  -0.5  0.5 Wed. ago    0.5 Wed. ago
+     0  this Wed.       in 0 Wed.
+     0  this Wed.       in 0 Wed.
+    -0  this Wed.       0 Wed. ago
+   0.5  in 0.5 Wed.     in 0.5 Wed.
+     1  next Wed.       in 1 Wed.
+   1.5  in 1.5 Wed.     in 1.5 Wed.
+  1.75  in 1.75 Wed.    in 1.75 Wed.
+     2  in 2 Wed.       in 2 Wed.
+     3  in 3 Wed.       in 3 Wed.
+
+=== thursday =========================
+
+offset  format          formatNumeric
+------  --------------  --------------
+    -3  3 Thu. ago      3 Thu. ago
+    -2  2 Thu. ago      2 Thu. ago
+ -1.75  1.75 Thu. ago   1.75 Thu. ago
+  -1.5  1.5 Thu. ago    1.5 Thu. ago
+    -1  last Thu.       1 Thu. ago
+  -0.5  0.5 Thu. ago    0.5 Thu. ago
+     0  this Thu.       in 0 Thu.
+     0  this Thu.       in 0 Thu.
+    -0  this Thu.       0 Thu. ago
+   0.5  in 0.5 Thu.     in 0.5 Thu.
+     1  next Thu.       in 1 Thu.
+   1.5  in 1.5 Thu.     in 1.5 Thu.
+  1.75  in 1.75 Thu.    in 1.75 Thu.
+     2  in 2 Thu.       in 2 Thu.
+     3  in 3 Thu.       in 3 Thu.
+
+=== friday ===========================
+
+offset  format          formatNumeric
+------  --------------  --------------
+    -3  3 Fri. ago      3 Fri. ago
+    -2  2 Fri. ago      2 Fri. ago
+ -1.75  1.75 Fri. ago   1.75 Fri. ago
+  -1.5  1.5 Fri. ago    1.5 Fri. ago
+    -1  last Fri.       1 Fri. ago
+  -0.5  0.5 Fri. ago    0.5 Fri. ago
+     0  this Fri.       in 0 Fri.
+     0  this Fri.       in 0 Fri.
+    -0  this Fri.       0 Fri. ago
+   0.5  in 0.5 Fri.     in 0.5 Fri.
+     1  next Fri.       in 1 Fri.
+   1.5  in 1.5 Fri.     in 1.5 Fri.
+  1.75  in 1.75 Fri.    in 1.75 Fri.
+     2  in 2 Fri.       in 2 Fri.
+     3  in 3 Fri.       in 3 Fri.
+
+=== saturday =========================
+
+offset  format          formatNumeric
+------  --------------  --------------
+    -3  3 Sat. ago      3 Sat. ago
+    -2  2 Sat. ago      2 Sat. ago
+ -1.75  1.75 Sat. ago   1.75 Sat. ago
+  -1.5  1.5 Sat. ago    1.5 Sat. ago
+    -1  last Sat.       1 Sat. ago
+  -0.5  0.5 Sat. ago    0.5 Sat. ago
+     0  this Sat.       in 0 Sat.
+     0  this Sat.       in 0 Sat.
+    -0  this Sat.       0 Sat. ago
+   0.5  in 0.5 Sat.     in 0.5 Sat.
+     1  next Sat.       in 1 Sat.
+   1.5  in 1.5 Sat.     in 1.5 Sat.
+  1.75  in 1.75 Sat.    in 1.75 Sat.
+     2  in 2 Sat.       in 2 Sat.
+     3  in 3 Sat.       in 3 Sat.
diff --git a/ext/intl/tests/reldateformatter/formatting/style_success_short_decimal_comma_locale.phpt b/ext/intl/tests/reldateformatter/formatting/style_success_short_decimal_comma_locale.phpt
new file mode 100644
index 00000000000..a6575980a9f
--- /dev/null
+++ b/ext/intl/tests/reldateformatter/formatting/style_success_short_decimal_comma_locale.phpt
@@ -0,0 +1,53 @@
+--TEST--
+Test IntlRelativeDateTimeFormatter formatting - short style - locale with a decimal comma
+--EXTENSIONS--
+intl
+--FILE--
+<?php
+
+require_once __DIR__ . '/format.inc';
+
+printRelativeDateTimeTable(
+    new IntlRelativeDateTimeFormatter('de_DE', IntlRelativeDateTimeFormatterStyle::Short),
+    offsets: [-2.75, -1.75, -1.5, 1.5, 1.75, 2.75],
+    units: [
+        'day' => IntlRelativeDateTimeFormatterUnit::Day,
+        'hour' => IntlRelativeDateTimeFormatterUnit::Hour,
+        'minute' => IntlRelativeDateTimeFormatterUnit::Minute,
+    ],
+);
+
+?>
+--EXPECT--
+=== day ==============================
+
+offset  format          formatNumeric
+------  --------------  --------------
+ -2.75  vor 2,75 Tagen  vor 2,75 Tagen
+ -1.75  vor 1,75 Tagen  vor 1,75 Tagen
+  -1.5  vor 1,5 Tagen   vor 1,5 Tagen
+   1.5  in 1,5 Tagen    in 1,5 Tagen
+  1.75  in 1,75 Tagen   in 1,75 Tagen
+  2.75  in 2,75 Tagen   in 2,75 Tagen
+
+=== hour =============================
+
+offset  format          formatNumeric
+------  --------------  --------------
+ -2.75  vor 2,75 Std.   vor 2,75 Std.
+ -1.75  vor 1,75 Std.   vor 1,75 Std.
+  -1.5  vor 1,5 Std.    vor 1,5 Std.
+   1.5  in 1,5 Std.     in 1,5 Std.
+  1.75  in 1,75 Std.    in 1,75 Std.
+  2.75  in 2,75 Std.    in 2,75 Std.
+
+=== minute ===========================
+
+offset  format          formatNumeric
+------  --------------  --------------
+ -2.75  vor 2,75 Min.   vor 2,75 Min.
+ -1.75  vor 1,75 Min.   vor 1,75 Min.
+  -1.5  vor 1,5 Min.    vor 1,5 Min.
+   1.5  in 1,5 Min.     in 1,5 Min.
+  1.75  in 1,75 Min.    in 1,75 Min.
+  2.75  in 2,75 Min.    in 2,75 Min.
diff --git a/ext/intl/tests/reldateformatter/reldateformatter_api.phpt b/ext/intl/tests/reldateformatter/reldateformatter_api.phpt
new file mode 100644
index 00000000000..8de80e95b0e
--- /dev/null
+++ b/ext/intl/tests/reldateformatter/reldateformatter_api.phpt
@@ -0,0 +1,72 @@
+--TEST--
+IntlRelativeDateTimeFormatter API surface
+--EXTENSIONS--
+intl
+--FILE--
+<?php
+
+$reflection = new ReflectionClass(IntlRelativeDateTimeFormatter::class);
+var_dump($reflection->isFinal());
+var_dump($reflection->isInternal());
+var_dump($reflection->isInstantiable());
+var_dump($reflection->getConstants());
+
+foreach ([
+    IntlRelativeDateTimeFormatterStyle::class,
+    IntlRelativeDateTimeFormatterCapitalization::class,
+    IntlRelativeDateTimeFormatterUnit::class,
+] as $class) {
+    $enum = new ReflectionEnum($class);
+    echo $enum->getName(), "\n";
+    var_dump($enum->isInternal(), $enum->isBacked());
+    echo implode(', ', array_column($class::cases(), 'name')), "\n";
+}
+
+$constructor = $reflection->getConstructor();
+foreach ($constructor->getParameters() as $parameter) {
+    echo $parameter->getName(), ': ', $parameter->getType(), "\n";
+    $default = $parameter->getDefaultValue();
+    echo 'default: ', $default instanceof UnitEnum ? $default::class . '::' . $default->name : 'null', "\n";
+}
+
+foreach (['format', 'formatNumeric'] as $name) {
+    $method = $reflection->getMethod($name);
+    echo $name, ': ', $method->getReturnType(), "\n";
+    foreach ($method->getParameters() as $parameter) {
+        echo $parameter->getName(), ': ', $parameter->getType(), "\n";
+    }
+}
+
+?>
+--EXPECT--
+bool(true)
+bool(true)
+bool(true)
+array(0) {
+}
+IntlRelativeDateTimeFormatterStyle
+bool(true)
+bool(false)
+Long, Short, Narrow
+IntlRelativeDateTimeFormatterCapitalization
+bool(true)
+bool(false)
+None, MiddleOfSentence, BeginningOfSentence, UiListAndMenu, Standalone
+IntlRelativeDateTimeFormatterUnit
+bool(true)
+bool(false)
+Year, Quarter, Month, Week, Day, Hour, Minute, Second, Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday
+locale: ?string
+default: null
+style: IntlRelativeDateTimeFormatterStyle
+default: IntlRelativeDateTimeFormatterStyle::Long
+capitalizationContext: IntlRelativeDateTimeFormatterCapitalization
+default: IntlRelativeDateTimeFormatterCapitalization::None
+numberFormatter: ?NumberFormatter
+default: null
+format: string|false
+offset: int|float
+unit: IntlRelativeDateTimeFormatterUnit
+formatNumeric: string|false
+offset: int|float
+unit: IntlRelativeDateTimeFormatterUnit
diff --git a/ext/intl/tests/reldateformatter/reldateformatter_basic.phpt b/ext/intl/tests/reldateformatter/reldateformatter_basic.phpt
new file mode 100644
index 00000000000..77647737086
--- /dev/null
+++ b/ext/intl/tests/reldateformatter/reldateformatter_basic.phpt
@@ -0,0 +1,40 @@
+--TEST--
+IntlRelativeDateTimeFormatter basic formatting
+--EXTENSIONS--
+intl
+--FILE--
+<?php
+
+$formatter = new IntlRelativeDateTimeFormatter('en_US');
+
+echo $formatter->format(-1, IntlRelativeDateTimeFormatterUnit::Day), "\n";
+echo $formatter->format(0, IntlRelativeDateTimeFormatterUnit::Day), "\n";
+echo $formatter->format(1, IntlRelativeDateTimeFormatterUnit::Day), "\n";
+echo $formatter->format(-1, IntlRelativeDateTimeFormatterUnit::Week), "\n";
+echo $formatter->format(2, IntlRelativeDateTimeFormatterUnit::Week), "\n";
+echo $formatter->format(-1, IntlRelativeDateTimeFormatterUnit::Sunday), "\n";
+
+echo $formatter->formatNumeric(-1, IntlRelativeDateTimeFormatterUnit::Day), "\n";
+echo $formatter->formatNumeric(1, IntlRelativeDateTimeFormatterUnit::Day), "\n";
+echo $formatter->formatNumeric(1.5, IntlRelativeDateTimeFormatterUnit::Hour), "\n";
+
+$relativeDate = $formatter->format(-1, IntlRelativeDateTimeFormatterUnit::Day);
+echo $formatter->combineDateAndTime($relativeDate, '3:45 PM'), "\n";
+
+var_dump($formatter->getErrorCode());
+var_dump($formatter->getErrorMessage());
+
+?>
+--EXPECTF--
+yesterday
+today
+tomorrow
+last week
+in 2 weeks
+last Sunday
+1 day ago
+in 1 day
+in 1.5 hours
+yesterday%s3:45 PM
+int(0)
+string(12) "U_ZERO_ERROR"
diff --git a/ext/intl/tests/reldateformatter/reldateformatter_capitalization.phpt b/ext/intl/tests/reldateformatter/reldateformatter_capitalization.phpt
new file mode 100644
index 00000000000..c5187cc819b
--- /dev/null
+++ b/ext/intl/tests/reldateformatter/reldateformatter_capitalization.phpt
@@ -0,0 +1,46 @@
+--TEST--
+IntlRelativeDateTimeFormatter capitalization enums and NumberFormatter refresh
+--EXTENSIONS--
+intl
+--FILE--
+<?php
+
+$numberFormatter = new NumberFormatter('en_US', NumberFormatter::DECIMAL);
+foreach (IntlRelativeDateTimeFormatterCapitalization::cases() as $context) {
+    $numberFormatter->setAttribute(NumberFormatter::MIN_FRACTION_DIGITS, 0);
+    $formatter = new IntlRelativeDateTimeFormatter(
+        'en_US',
+        IntlRelativeDateTimeFormatterStyle::Narrow,
+        $context,
+        $numberFormatter,
+    );
+    echo $context->name, ': ', $formatter->format(-1, IntlRelativeDateTimeFormatterUnit::Day), "\n";
+    echo $formatter->formatNumeric(2, IntlRelativeDateTimeFormatterUnit::Day), "\n";
+
+    $numberFormatter->setAttribute(NumberFormatter::MIN_FRACTION_DIGITS, 1);
+    echo $formatter->format(-1, IntlRelativeDateTimeFormatterUnit::Day), "\n";
+    echo $formatter->formatNumeric(2, IntlRelativeDateTimeFormatterUnit::Day), "\n";
+}
+
+?>
+--EXPECTF--
+None: yesterday
+in 2%r(?:d| days)%r
+yesterday
+in 2.0%r(?:d| days)%r
+MiddleOfSentence: yesterday
+in 2%r(?:d| days)%r
+yesterday
+in 2.0%r(?:d| days)%r
+BeginningOfSentence: Yesterday
+In 2%r(?:d| days)%r
+Yesterday
+In 2.0%r(?:d| days)%r
+UiListAndMenu: yesterday
+in 2%r(?:d| days)%r
+yesterday
+in 2.0%r(?:d| days)%r
+Standalone: yesterday
+in 2%r(?:d| days)%r
+yesterday
+in 2.0%r(?:d| days)%r
diff --git a/ext/intl/tests/reldateformatter/reldateformatter_capitalization_locale.phpt b/ext/intl/tests/reldateformatter/reldateformatter_capitalization_locale.phpt
new file mode 100644
index 00000000000..f68424df3e0
--- /dev/null
+++ b/ext/intl/tests/reldateformatter/reldateformatter_capitalization_locale.phpt
@@ -0,0 +1,37 @@
+--TEST--
+IntlRelativeDateTimeFormatter sentence capitalization uses the locale and preserves existing case
+--EXTENSIONS--
+intl
+--FILE--
+<?php
+
+$numberFormatter = new NumberFormatter('tr', NumberFormatter::DECIMAL);
+$numberFormatter->setTextAttribute(NumberFormatter::POSITIVE_PREFIX, 'istanbul ABC ');
+$formatter = new IntlRelativeDateTimeFormatter(
+    'tr',
+    IntlRelativeDateTimeFormatterStyle::Long,
+    IntlRelativeDateTimeFormatterCapitalization::BeginningOfSentence,
+    $numberFormatter,
+);
+
+echo $formatter->format(-1, IntlRelativeDateTimeFormatterUnit::Day), "\n";
+echo $formatter->format(3, IntlRelativeDateTimeFormatterUnit::Day), "\n";
+echo $formatter->formatNumeric(2, IntlRelativeDateTimeFormatterUnit::Day), "\n";
+
+$numberFormatter->setTextAttribute(NumberFormatter::POSITIVE_PREFIX, 'izmir XYZ ');
+echo $formatter->formatNumeric(2, IntlRelativeDateTimeFormatterUnit::Day), "\n";
+
+// Combining strings must not apply sentence capitalization.
+echo $formatter->combineDateAndTime('istanbul', '12:00'), "\n";
+
+$numberFormatter->setTextAttribute(NumberFormatter::POSITIVE_PREFIX, '');
+echo $formatter->formatNumeric(2, IntlRelativeDateTimeFormatterUnit::Day), "\n";
+
+?>
+--EXPECT--
+Dün
+İstanbul ABC 3 gün sonra
+İstanbul ABC 2 gün sonra
+İzmir XYZ 2 gün sonra
+istanbul 12:00
+2 gün sonra
diff --git a/ext/intl/tests/reldateformatter/reldateformatter_error_state.phpt b/ext/intl/tests/reldateformatter/reldateformatter_error_state.phpt
new file mode 100644
index 00000000000..1dc23bd0349
--- /dev/null
+++ b/ext/intl/tests/reldateformatter/reldateformatter_error_state.phpt
@@ -0,0 +1,35 @@
+--TEST--
+IntlRelativeDateTimeFormatter synchronizes and resets ICU error state
+--EXTENSIONS--
+intl
+--FILE--
+<?php
+
+$formatter = new IntlRelativeDateTimeFormatter('en_US');
+
+var_dump($formatter->combineDateAndTime("\x80", '3:45 PM'));
+var_dump($formatter->getErrorCode() === U_INVALID_CHAR_FOUND);
+var_dump(intl_get_error_code() === $formatter->getErrorCode());
+var_dump(str_contains($formatter->getErrorMessage(), 'Failed to convert relative date to UTF-16'));
+
+var_dump($formatter->format(1, IntlRelativeDateTimeFormatterUnit::Day));
+var_dump($formatter->getErrorCode());
+var_dump(intl_get_error_code());
+
+ini_set('intl.use_exceptions', '1');
+try {
+    $formatter->combineDateAndTime('today', "\x80");
+} catch (Throwable $e) {
+    echo $e::class, ': ', $e->getMessage(), "\n";
+}
+
+?>
+--EXPECT--
+bool(false)
+bool(true)
+bool(true)
+bool(true)
+string(8) "tomorrow"
+int(0)
+int(0)
+IntlException: IntlRelativeDateTimeFormatter::combineDateAndTime(): Failed to convert time to UTF-16
diff --git a/ext/intl/tests/reldateformatter/reldateformatter_errors.phpt b/ext/intl/tests/reldateformatter/reldateformatter_errors.phpt
new file mode 100644
index 00000000000..18c5dfb7552
--- /dev/null
+++ b/ext/intl/tests/reldateformatter/reldateformatter_errors.phpt
@@ -0,0 +1,38 @@
+--TEST--
+IntlRelativeDateTimeFormatter argument validation and object state errors
+--EXTENSIONS--
+intl
+--FILE--
+<?php
+
+function showThrowable(Closure $callback): void {
+    try {
+        $callback();
+    } catch (Throwable $e) {
+        echo $e::class, ': ', $e->getMessage(), "\n";
+    }
+}
+
+showThrowable(fn() => new IntlRelativeDateTimeFormatter('f'));
+showThrowable(fn() => new IntlRelativeDateTimeFormatter(str_repeat('a', 157)));
+
+$formatter = new IntlRelativeDateTimeFormatter('en_US');
+showThrowable(fn() => $formatter->__construct());
+showThrowable(fn() => clone $formatter);
+
+$unconstructedNumberFormatter = (new ReflectionClass(NumberFormatter::class))
+    ->newInstanceWithoutConstructor();
+showThrowable(fn() => new IntlRelativeDateTimeFormatter(
+    'en_US',
+    IntlRelativeDateTimeFormatterStyle::Long,
+    IntlRelativeDateTimeFormatterCapitalization::None,
+    $unconstructedNumberFormatter,
+));
+
+?>
+--EXPECT--
+ValueError: IntlRelativeDateTimeFormatter::__construct(): Argument #1 ($locale) "f" is invalid
+ValueError: IntlRelativeDateTimeFormatter::__construct(): Argument #1 ($locale) must be less than or equal to 156 characters
+Error: IntlRelativeDateTimeFormatter object is already constructed
+Error: Trying to clone an uncloneable object of class IntlRelativeDateTimeFormatter
+Error: Found unconstructed NumberFormatter
diff --git a/ext/intl/tests/reldateformatter/reldateformatter_format_failure.phpt b/ext/intl/tests/reldateformatter/reldateformatter_format_failure.phpt
new file mode 100644
index 00000000000..e7ccedb3ef1
--- /dev/null
+++ b/ext/intl/tests/reldateformatter/reldateformatter_format_failure.phpt
@@ -0,0 +1,35 @@
+--TEST--
+IntlRelativeDateTimeFormatter returns false on number formatting failure
+--EXTENSIONS--
+intl
+--SKIPIF--
+<?php if (version_compare(INTL_ICU_VERSION, '62.1') < 0) die('skip ICU >= 62.1 required for number formatting errors'); ?>
+--FILE--
+<?php
+
+function createFailingFormatter(): IntlRelativeDateTimeFormatter
+{
+    $numberFormatter = new NumberFormatter('en_US', NumberFormatter::DECIMAL);
+    $numberFormatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 0);
+    // ICU's UNUM_ROUND_UNNECESSARY is not exposed as a NumberFormatter constant.
+    $numberFormatter->setAttribute(NumberFormatter::ROUNDING_MODE, 7);
+
+    return new IntlRelativeDateTimeFormatter('en_US', numberFormatter: $numberFormatter);
+}
+
+foreach (['format', 'formatNumeric'] as $method) {
+    $formatter = createFailingFormatter();
+
+    var_dump($formatter->$method(1.5, IntlRelativeDateTimeFormatterUnit::Second));
+    var_dump(intl_error_name($formatter->getErrorCode()) === 'U_FORMAT_INEXACT_ERROR');
+    var_dump(intl_get_error_code() === $formatter->getErrorCode());
+}
+
+?>
+--EXPECT--
+bool(false)
+bool(true)
+bool(true)
+bool(false)
+bool(true)
+bool(true)
diff --git a/ext/intl/tests/reldateformatter/reldateformatter_locales_and_options.phpt b/ext/intl/tests/reldateformatter/reldateformatter_locales_and_options.phpt
new file mode 100644
index 00000000000..89eb79f6fec
--- /dev/null
+++ b/ext/intl/tests/reldateformatter/reldateformatter_locales_and_options.phpt
@@ -0,0 +1,45 @@
+--TEST--
+IntlRelativeDateTimeFormatter locales, styles, and capitalization context
+--EXTENSIONS--
+intl
+--FILE--
+<?php
+
+$french = new IntlRelativeDateTimeFormatter('fr_FR');
+echo $french->formatNumeric(-1, IntlRelativeDateTimeFormatterUnit::Day), "\n";
+echo $french->formatNumeric(1.5, IntlRelativeDateTimeFormatterUnit::Hour), "\n";
+
+$chinese = new IntlRelativeDateTimeFormatter('zh_CN');
+echo $chinese->formatNumeric(3, IntlRelativeDateTimeFormatterUnit::Month), "\n";
+
+$beginning = new IntlRelativeDateTimeFormatter(
+    'en_US',
+    IntlRelativeDateTimeFormatterStyle::Long,
+    IntlRelativeDateTimeFormatterCapitalization::BeginningOfSentence,
+);
+echo $beginning->format(-1, IntlRelativeDateTimeFormatterUnit::Day), "\n";
+
+$short = new IntlRelativeDateTimeFormatter(
+    'en_US',
+    IntlRelativeDateTimeFormatterStyle::Short,
+);
+$narrow = new IntlRelativeDateTimeFormatter(
+    'en_US',
+    IntlRelativeDateTimeFormatterStyle::Narrow,
+);
+echo $short->formatNumeric(3, IntlRelativeDateTimeFormatterUnit::Day), "\n";
+echo $narrow->formatNumeric(3, IntlRelativeDateTimeFormatterUnit::Day), "\n";
+
+Locale::setDefault('en_US');
+$defaultLocale = new IntlRelativeDateTimeFormatter();
+echo $defaultLocale->format(1, IntlRelativeDateTimeFormatterUnit::Day), "\n";
+
+?>
+--EXPECTF--
+il y a 1 jour
+dans 1,5 heure
+3个月后
+Yesterday
+in 3 days
+in 3%r(?:d| days)%r
+tomorrow
diff --git a/ext/intl/tests/reldateformatter/reldateformatter_numberformatter.phpt b/ext/intl/tests/reldateformatter/reldateformatter_numberformatter.phpt
new file mode 100644
index 00000000000..d51fdbcda1f
--- /dev/null
+++ b/ext/intl/tests/reldateformatter/reldateformatter_numberformatter.phpt
@@ -0,0 +1,34 @@
+--TEST--
+IntlRelativeDateTimeFormatter observes changes to a custom NumberFormatter
+--EXTENSIONS--
+intl
+--FILE--
+<?php
+
+$numberFormatter = new NumberFormatter('en_US', NumberFormatter::DECIMAL);
+$numberFormatter->setAttribute(NumberFormatter::MIN_FRACTION_DIGITS, 1);
+$formatter = new IntlRelativeDateTimeFormatter(
+    'en_US',
+    IntlRelativeDateTimeFormatterStyle::Long,
+    IntlRelativeDateTimeFormatterCapitalization::None,
+    $numberFormatter,
+);
+
+echo $formatter->formatNumeric(2, IntlRelativeDateTimeFormatterUnit::Day), "\n";
+echo $formatter->format(2, IntlRelativeDateTimeFormatterUnit::Week), "\n";
+
+$numberFormatter->setAttribute(NumberFormatter::MIN_FRACTION_DIGITS, 0);
+
+echo $formatter->formatNumeric(2, IntlRelativeDateTimeFormatterUnit::Day), "\n";
+echo $formatter->format(2, IntlRelativeDateTimeFormatterUnit::Week), "\n";
+
+unset($numberFormatter);
+echo $formatter->formatNumeric(2, IntlRelativeDateTimeFormatterUnit::Day), "\n";
+
+?>
+--EXPECT--
+in 2.0 days
+in 2.0 weeks
+in 2 days
+in 2 weeks
+in 2 days
diff --git a/ext/intl/tests/reldateformatter/reldateformatter_numberformatter_cycle.phpt b/ext/intl/tests/reldateformatter/reldateformatter_numberformatter_cycle.phpt
new file mode 100644
index 00000000000..64abfeaa4f1
--- /dev/null
+++ b/ext/intl/tests/reldateformatter/reldateformatter_numberformatter_cycle.phpt
@@ -0,0 +1,29 @@
+--TEST--
+IntlRelativeDateTimeFormatter cycle management with a custom NumberFormatter
+--EXTENSIONS--
+intl
+--FILE--
+<?php
+
+class TestNumberFormatter extends NumberFormatter
+{
+    public ?IntlRelativeDateTimeFormatter $formatter = null;
+}
+
+function collectFormatterCycle(): void
+{
+    $numberFormatter = new TestNumberFormatter('en', NumberFormatter::DECIMAL);
+    $formatter = new IntlRelativeDateTimeFormatter('en', numberFormatter: $numberFormatter);
+    $numberFormatter->formatter = $formatter;
+
+    unset($numberFormatter, $formatter);
+    var_dump(gc_collect_cycles());
+}
+
+collectFormatterCycle();
+collectFormatterCycle();
+
+?>
+--EXPECT--
+int(2)
+int(2)
diff --git a/ext/standard/credits_ext.h b/ext/standard/credits_ext.h
index 45f8ce89c8d..08b0ac5b023 100644
--- a/ext/standard/credits_ext.h
+++ b/ext/standard/credits_ext.h
@@ -31,7 +31,7 @@ CREDIT_LINE("GetText", "Alex Plotnick");
 CREDIT_LINE("GNU GMP support", "Stanislav Malyshev");
 CREDIT_LINE("Iconv", "Rui Hirokawa, Stig Bakken, Moriyoshi Koizumi");
 CREDIT_LINE("Input Filter", "Rasmus Lerdorf, Derick Rethans, Pierre-Alain Joye, Ilia Alshanetsky");
-CREDIT_LINE("Internationalization", "Ed Batutis, David Carlier, Vladimir Iordanov, Dmitry Lakhtyuk, Stanislav Malyshev, Vadim Savchuk, Kirti Velankar");
+CREDIT_LINE("Internationalization", "Ed Batutis, David Carlier, Weilin Du, Vladimir Iordanov, Dmitry Lakhtyuk, Stanislav Malyshev, Vadim Savchuk, Kirti Velankar");
 CREDIT_LINE("JSON", "Jakub Zelenka, Omar Kilani, Scott MacVicar");
 CREDIT_LINE("LDAP", "Amitay Isaacs, Eric Warnke, Rasmus Lerdorf, Gerrit Thomson, Stig Venaas");
 CREDIT_LINE("LIBXML", "Christian Stocker, Rob Richards, Marcus Boerger, Wez Furlong, Shane Caraveo");