Commit 0c1d809a75d for php.net
commit 0c1d809a75d05963511eaee8057f6147051dde30
Author: Weilin Du <weilindu@php.net>
Date: Tue Jun 30 14:52:10 2026 +0800
Add Locale::getDisplayKeyword() and Locale::getDisplayKeywordValue() (#22264)
implement RFC:
https://wiki.php.net/rfc/getdisplaykeyword_and_getdisplaykeywordvalue
diff --git a/NEWS b/NEWS
index 308ebe4d8f2..9d326431321 100644
--- a/NEWS
+++ b/NEWS
@@ -88,6 +88,9 @@ PHP NEWS
- Intl:
. Fixed malformed ResourceBundle::get() error message when fallback is
disabled. (Weilin Du)
+ . Added Locale::getDisplayKeyword() and Locale::getDisplayKeywordValue(),
+ with the alias of locale_get_display_keyword() and
+ locale_get_display_keyword_value() respectively. (Weilin Du)
. Fix incorrect argument positions for invalid start/end arguments in
transliterator_transliterate(). (Weilin Du)
. Fixed IntlTimeZone::getDisplayName() to synchronize object error state
diff --git a/UPGRADING b/UPGRADING
index 1c75894512f..02aa821acec 100644
--- a/UPGRADING
+++ b/UPGRADING
@@ -208,6 +208,10 @@ PHP 8.6 UPGRADE NOTES
. finfo_file() now works with remote streams.
- Intl:
+ . Added Locale::getDisplayKeyword() and Locale::getDisplayKeywordValue(),
+ with the alias of locale_get_display_keyword() and
+ locale_get_display_keyword_value() respectively.
+ RFC: https://wiki.php.net/rfc/getdisplaykeyword_and_getdisplaykeywordvalue
. Added IntlNumberRangeFormatter class to format an interval of two numbers
with a given skeleton, locale, IntlNumberRangeFormatter::COLLAPSE_AUTO,
IntlNumberRangeFormatter::COLLAPSE_NONE,
diff --git a/ext/intl/locale/locale.stub.php b/ext/intl/locale/locale.stub.php
index 03813ecaf19..fc012768de9 100644
--- a/ext/intl/locale/locale.stub.php
+++ b/ext/intl/locale/locale.stub.php
@@ -92,6 +92,16 @@ public static function getDisplayLanguage(string $locale, ?string $displayLocale
*/
public static function getDisplayVariant(string $locale, ?string $displayLocale = null): string|false {}
+ /**
+ * @alias locale_get_display_keyword
+ */
+ public static function getDisplayKeyword(string $keyword, ?string $displayLocale = null): string|false {}
+
+ /**
+ * @alias locale_get_display_keyword_value
+ */
+ public static function getDisplayKeywordValue(string $locale, string $keyword, ?string $displayLocale = null): string|false {}
+
/**
* @tentative-return-type
* @alias locale_compose
diff --git a/ext/intl/locale/locale_arginfo.h b/ext/intl/locale/locale_arginfo.h
index 1d6e6683e21..a01f583b045 100644
Binary files a/ext/intl/locale/locale_arginfo.h and b/ext/intl/locale/locale_arginfo.h differ
diff --git a/ext/intl/locale/locale_methods.cpp b/ext/intl/locale/locale_methods.cpp
index 14fa2a8f6bf..0733ffc8abb 100644
--- a/ext/intl/locale/locale_methods.cpp
+++ b/ext/intl/locale/locale_methods.cpp
@@ -44,6 +44,8 @@ ZEND_EXTERN_MODULE_GLOBALS( intl )
#define EXTLANG_PREFIX "a"
#define PRIVATE_PREFIX "x"
#define DISP_NAME "name"
+#define DISP_KEYWORD "keyword"
+#define DISP_KEYWORD_VALUE "keyword_value"
#define MAX_NO_VARIANT 15
#define MAX_NO_EXTLANG 3
@@ -671,6 +673,107 @@ static void get_icu_disp_value_src_php( const char* tag_name, INTERNAL_FUNCTION_
}
/* }}} */
+/* {{{
+ * common code shared by display keyword functions to get the value from ICU
+ }}} */
+static void get_icu_disp_keyword_value_src_php(const char* tag_name, INTERNAL_FUNCTION_PARAMETERS)
+{
+ char* loc_name = NULL;
+ size_t loc_name_len = 0;
+ char* keyword_name = NULL;
+ size_t keyword_name_len = 0;
+ char* disp_loc_name = NULL;
+ size_t disp_loc_name_len = 0;
+ int free_loc_name = 0;
+
+ UChar* disp_name = NULL;
+ int32_t disp_name_len = 0;
+ int32_t buflen = 512;
+ UErrorCode status = U_ZERO_ERROR;
+
+ zend_string* u8str;
+ char* msg = NULL;
+
+ intl_error_reset( NULL );
+
+ if (strcmp(tag_name, DISP_KEYWORD) == 0) {
+ ZEND_PARSE_PARAMETERS_START(1, 2)
+ Z_PARAM_PATH(keyword_name, keyword_name_len)
+ Z_PARAM_OPTIONAL
+ Z_PARAM_PATH_OR_NULL(disp_loc_name, disp_loc_name_len)
+ ZEND_PARSE_PARAMETERS_END();
+ } else {
+ ZEND_PARSE_PARAMETERS_START(2, 3)
+ Z_PARAM_PATH(loc_name, loc_name_len)
+ Z_PARAM_PATH(keyword_name, keyword_name_len)
+ Z_PARAM_OPTIONAL
+ Z_PARAM_PATH_OR_NULL(disp_loc_name, disp_loc_name_len)
+ ZEND_PARSE_PARAMETERS_END();
+
+ if (loc_name_len > ULOC_FULLNAME_CAPACITY) {
+ intl_error_set( NULL, U_ILLEGAL_ARGUMENT_ERROR, "name too long");
+ RETURN_FALSE;
+ }
+
+ if (loc_name_len == 0) {
+ loc_name = (char *)intl_locale_get_default();
+ }
+ }
+
+ if (!disp_loc_name) {
+ disp_loc_name = estrdup(intl_locale_get_default());
+ free_loc_name = 1;
+ }
+
+ do {
+ disp_name = reinterpret_cast<UChar *>(erealloc(disp_name, buflen * sizeof(UChar)));
+ disp_name_len = buflen;
+
+ if (strcmp(tag_name, DISP_KEYWORD) == 0) {
+ buflen = uloc_getDisplayKeyword(keyword_name, disp_loc_name, disp_name, disp_name_len, &status);
+ } else {
+ buflen = uloc_getDisplayKeywordValue(loc_name, keyword_name, disp_loc_name, disp_name, disp_name_len, &status);
+ }
+
+ /* U_STRING_NOT_TERMINATED_WARNING is admissible here; don't look for it */
+ if (U_FAILURE(status)) {
+ if (status == U_BUFFER_OVERFLOW_ERROR) {
+ status = U_ZERO_ERROR;
+ continue;
+ }
+
+ spprintf(&msg, 0, "unable to get locale %s", tag_name);
+ intl_error_set( NULL, status, msg);
+ efree(msg);
+ if (disp_name) {
+ efree(disp_name);
+ }
+ if (free_loc_name) {
+ efree((void *)disp_loc_name);
+ disp_loc_name = NULL;
+ }
+ RETURN_FALSE;
+ }
+ } while (buflen > disp_name_len);
+
+ if (free_loc_name) {
+ efree((void *)disp_loc_name);
+ disp_loc_name = NULL;
+ }
+
+ u8str = intl_convert_utf16_to_utf8(disp_name, buflen, &status);
+ efree(disp_name);
+ if (!u8str) {
+ spprintf(&msg, 0, "error converting display name for %s to UTF-8", tag_name);
+ intl_error_set( NULL, status, msg);
+ efree(msg);
+ RETURN_FALSE;
+ }
+
+ RETVAL_NEW_STR(u8str);
+}
+/* }}} */
+
/* {{{ gets the name for the $locale in $in_locale or default_locale */
U_CFUNC PHP_FUNCTION(locale_get_display_name)
{
@@ -711,6 +814,20 @@ U_CFUNC PHP_FUNCTION(locale_get_display_variant)
{
get_icu_disp_value_src_php( LOC_VARIANT_TAG , INTERNAL_FUNCTION_PARAM_PASSTHRU );
}
+/* }}} */
+
+/* {{{ gets the keyword display label in $in_locale or default_locale */
+U_CFUNC PHP_FUNCTION(locale_get_display_keyword)
+{
+ get_icu_disp_keyword_value_src_php(DISP_KEYWORD, INTERNAL_FUNCTION_PARAM_PASSTHRU);
+}
+/* }}} */
+
+/* {{{ gets the keyword value display label in $in_locale or default_locale */
+U_CFUNC PHP_FUNCTION(locale_get_display_keyword_value)
+{
+ get_icu_disp_keyword_value_src_php(DISP_KEYWORD_VALUE, INTERNAL_FUNCTION_PARAM_PASSTHRU);
+}
/* }}} */
/* {{{ return an associative array containing keyword-value
diff --git a/ext/intl/php_intl.stub.php b/ext/intl/php_intl.stub.php
index 4bcb8587f78..1e6c5cacf20 100644
--- a/ext/intl/php_intl.stub.php
+++ b/ext/intl/php_intl.stub.php
@@ -487,6 +487,10 @@ function locale_get_display_language(string $locale, ?string $displayLocale = nu
function locale_get_display_variant(string $locale, ?string $displayLocale = null): string|false {}
+function locale_get_display_keyword(string $keyword, ?string $displayLocale = null): string|false {}
+
+function locale_get_display_keyword_value(string $locale, string $keyword, ?string $displayLocale = null): string|false {}
+
function locale_compose(array $subtags): string|false {}
function locale_parse(string $locale): ?array {}
diff --git a/ext/intl/php_intl_arginfo.h b/ext/intl/php_intl_arginfo.h
index 81160349980..00de5986f1e 100644
Binary files a/ext/intl/php_intl_arginfo.h and b/ext/intl/php_intl_arginfo.h differ
diff --git a/ext/intl/tests/locale/bug74993.phpt b/ext/intl/tests/locale/bug74993.phpt
index 1d0a1a97b75..7bcf931c4e2 100644
--- a/ext/intl/tests/locale/bug74993.phpt
+++ b/ext/intl/tests/locale/bug74993.phpt
@@ -5,6 +5,8 @@
--FILE--
<?php
$funcs = [
+ 'locale_get_display_keyword',
+ 'locale_get_display_keyword_value',
'locale_get_display_language',
'locale_get_display_name',
'locale_get_display_region',
@@ -19,6 +21,23 @@
}
?>
--EXPECT--
+Function [ <internal:intl> function locale_get_display_keyword ] {
+
+ - Parameters [2] {
+ Parameter #0 [ <required> string $keyword ]
+ Parameter #1 [ <optional> ?string $displayLocale = null ]
+ }
+ - Return [ string|false ]
+}
+Function [ <internal:intl> function locale_get_display_keyword_value ] {
+
+ - Parameters [3] {
+ Parameter #0 [ <required> string $locale ]
+ Parameter #1 [ <required> string $keyword ]
+ Parameter #2 [ <optional> ?string $displayLocale = null ]
+ }
+ - Return [ string|false ]
+}
Function [ <internal:intl> function locale_get_display_language ] {
- Parameters [2] {
diff --git a/ext/intl/tests/locale_get_display_keyword.phpt b/ext/intl/tests/locale_get_display_keyword.phpt
new file mode 100644
index 00000000000..9d53d7a0258
--- /dev/null
+++ b/ext/intl/tests/locale_get_display_keyword.phpt
@@ -0,0 +1,40 @@
+--TEST--
+locale_get_display_keyword() basic
+--EXTENSIONS--
+intl
+--FILE--
+<?php
+
+function ut_main()
+{
+ $default = ut_loc_get_default();
+ ut_loc_set_default('en');
+
+ $keyword = ut_loc_get_display_keyword('calendar', 'en');
+ var_dump($keyword);
+ var_dump(ut_loc_get_display_keyword('calendar', null) === $keyword);
+
+ $keywordValue = ut_loc_get_display_keyword_value('de_DE@calendar=gregorian', 'calendar', 'en');
+ var_dump($keywordValue);
+ var_dump(ut_loc_get_display_keyword_value('de_DE@calendar=gregorian', 'calendar', null) === $keywordValue);
+
+ $collationValue = ut_loc_get_display_keyword_value('de_DE@collation=phonebook', 'collation', 'en');
+ var_dump($collationValue);
+
+ ut_loc_set_default($default);
+}
+
+include_once 'ut_common.inc';
+ut_run();
+?>
+--EXPECTREGEX--
+string\([1-9][0-9]*\) "[A-Za-z ]*Calendar[A-Za-z ]*"
+bool\(true\)
+string\([1-9][0-9]*\) "[A-Za-z ]*Gregorian[A-Za-z ]*"
+bool\(true\)
+string\([1-9][0-9]*\) "[A-Za-z ]*Phonebook[A-Za-z ]*"
+string\([1-9][0-9]*\) "[A-Za-z ]*Calendar[A-Za-z ]*"
+bool\(true\)
+string\([1-9][0-9]*\) "[A-Za-z ]*Gregorian[A-Za-z ]*"
+bool\(true\)
+string\([1-9][0-9]*\) "[A-Za-z ]*Phonebook[A-Za-z ]*"
diff --git a/ext/intl/tests/locale_get_display_keyword_error.phpt b/ext/intl/tests/locale_get_display_keyword_error.phpt
new file mode 100644
index 00000000000..6f489ccfe8f
--- /dev/null
+++ b/ext/intl/tests/locale_get_display_keyword_error.phpt
@@ -0,0 +1,20 @@
+--TEST--
+locale_get_display_keyword_value() error path
+--EXTENSIONS--
+intl
+--FILE--
+<?php
+
+$locale = str_repeat('*', 256);
+
+var_dump(Locale::getDisplayKeywordValue($locale, 'calendar', 'en'));
+var_dump(intl_get_error_message());
+var_dump(locale_get_display_keyword_value($locale, 'calendar', 'en'));
+var_dump(intl_get_error_message());
+
+?>
+--EXPECT--
+bool(false)
+string(73) "Locale::getDisplayKeywordValue(): name too long: U_ILLEGAL_ARGUMENT_ERROR"
+bool(false)
+string(75) "locale_get_display_keyword_value(): name too long: U_ILLEGAL_ARGUMENT_ERROR"
diff --git a/ext/intl/tests/locale_get_display_keyword_null_bytes.phpt b/ext/intl/tests/locale_get_display_keyword_null_bytes.phpt
new file mode 100644
index 00000000000..fe83ac1b731
--- /dev/null
+++ b/ext/intl/tests/locale_get_display_keyword_null_bytes.phpt
@@ -0,0 +1,40 @@
+--TEST--
+locale_get_display_keyword() throwing null bytes exceptions.
+--EXTENSIONS--
+intl
+--FILE--
+<?php
+
+function ut_main()
+{
+ $calls = [
+ fn() => ut_loc_get_display_keyword("cur\0rency", "fr"),
+ fn() => ut_loc_get_display_keyword("currency", "f\0r"),
+ fn() => ut_loc_get_display_keyword_value("de_DE@calendar=gregorian\0", "calendar", "en"),
+ fn() => ut_loc_get_display_keyword_value("de_DE@calendar=gregorian", "cal\0endar", "en"),
+ fn() => ut_loc_get_display_keyword_value("de_DE@calendar=gregorian", "calendar", "e\0n"),
+ ];
+
+ foreach ($calls as $call) {
+ try {
+ $call();
+ } catch (\ValueError $e) {
+ echo $e->getMessage(), PHP_EOL;
+ }
+ }
+}
+
+include_once 'ut_common.inc';
+ut_run();
+?>
+--EXPECT--
+Locale::getDisplayKeyword(): Argument #1 ($keyword) must not contain any null bytes
+Locale::getDisplayKeyword(): Argument #2 ($displayLocale) must not contain any null bytes
+Locale::getDisplayKeywordValue(): Argument #1 ($locale) must not contain any null bytes
+Locale::getDisplayKeywordValue(): Argument #2 ($keyword) must not contain any null bytes
+Locale::getDisplayKeywordValue(): Argument #3 ($displayLocale) must not contain any null bytes
+locale_get_display_keyword(): Argument #1 ($keyword) must not contain any null bytes
+locale_get_display_keyword(): Argument #2 ($displayLocale) must not contain any null bytes
+locale_get_display_keyword_value(): Argument #1 ($locale) must not contain any null bytes
+locale_get_display_keyword_value(): Argument #2 ($keyword) must not contain any null bytes
+locale_get_display_keyword_value(): Argument #3 ($displayLocale) must not contain any null bytes
diff --git a/ext/intl/tests/ut_common.inc b/ext/intl/tests/ut_common.inc
index fdc013dea41..26f33f75f7f 100644
--- a/ext/intl/tests/ut_common.inc
+++ b/ext/intl/tests/ut_common.inc
@@ -272,6 +272,14 @@ function ut_loc_get_display_variant( $locale , $dispLocale )
{
return $GLOBALS['oo-mode'] ? Locale::getDisplayVariant( $locale , $dispLocale ) : locale_get_display_variant( $locale, $dispLocale );
}
+function ut_loc_get_display_keyword( $keyword , $dispLocale )
+{
+ return $GLOBALS['oo-mode'] ? Locale::getDisplayKeyword( $keyword , $dispLocale ) : locale_get_display_keyword( $keyword, $dispLocale );
+}
+function ut_loc_get_display_keyword_value( $locale , $keyword , $dispLocale )
+{
+ return $GLOBALS['oo-mode'] ? Locale::getDisplayKeywordValue( $locale , $keyword , $dispLocale ) : locale_get_display_keyword_value( $locale, $keyword, $dispLocale );
+}
function ut_loc_locale_compose( $loc_parts_arr )
{
return $GLOBALS['oo-mode'] ? Locale::composeLocale( $loc_parts_arr ) : locale_compose( $loc_parts_arr );