mirror of
https://github.com/twigphp/Twig.git
synced 2026-10-02 18:07:35 +00:00
Fix IntlExtension inheriting values derived by ICU from a date formatter prototype
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
# 3.30.0 (2026-XX-XX)
|
||||
|
||||
* Fix `IntlExtension` letting the pattern derived from a date formatter prototype override an explicit locale
|
||||
* Fix `IntlExtension` not honoring the locale of a date formatter prototype configured with no date and time styles
|
||||
* Report a clear error when `random`, `reverse`, `shuffle`, and `split` receive a string that is not valid UTF-8
|
||||
* Fix the deprecation about omitting parentheses when calling a macro being reported twice for the same call
|
||||
* Add the macro name to the deprecation about omitting parentheses when calling a macro
|
||||
|
||||
@@ -186,6 +186,7 @@ final class IntlExtension extends AbstractExtension
|
||||
private $listFormatters = [];
|
||||
private $dateFormatterPrototype;
|
||||
private $numberFormatterPrototype;
|
||||
private array $prototypeDerivedPatterns = [];
|
||||
|
||||
public function __construct(?\IntlDateFormatter $dateFormatterPrototype = null, ?\NumberFormatter $numberFormatterPrototype = null)
|
||||
{
|
||||
@@ -473,7 +474,7 @@ final class IntlExtension extends AbstractExtension
|
||||
|
||||
if (null === $locale) {
|
||||
if ($this->dateFormatterPrototype) {
|
||||
$locale = $this->dateFormatterPrototype->getLocale();
|
||||
$locale = $this->prototypeLocale();
|
||||
}
|
||||
$locale = $locale ?: \Locale::getDefault();
|
||||
}
|
||||
@@ -500,7 +501,7 @@ final class IntlExtension extends AbstractExtension
|
||||
// fall back to the prototype's pattern only when nothing else was given, else it would override the explicit date/time formats;
|
||||
// a pattern describes a full datetime rendering, so it cannot be honored by format_date/format_time, which pass 'none' for the other part
|
||||
if ('' === $pattern && null === $dateFormat && null === $timeFormat) {
|
||||
$pattern = $this->dateFormatterPrototype->getPattern();
|
||||
$pattern = $this->prototypePattern();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -527,6 +528,50 @@ final class IntlExtension extends AbstractExtension
|
||||
return $this->dateFormatters[$hash];
|
||||
}
|
||||
|
||||
/**
|
||||
* ICU reports "root" for a prototype with no date and no time style, whatever its pattern, and rejects
|
||||
* it as an input locale, so the configured locale is recovered from the calendar ICU opened for it.
|
||||
*/
|
||||
private function prototypeLocale(): ?string
|
||||
{
|
||||
$locale = $this->dateFormatterPrototype->getLocale();
|
||||
if (\is_string($locale) && '' !== $locale && 'root' !== $locale) {
|
||||
return $locale;
|
||||
}
|
||||
|
||||
$calendar = $this->dateFormatterPrototype->getCalendarObject();
|
||||
$locale = $calendar ? $calendar->getLocale(\Locale::VALID_LOCALE) : false;
|
||||
|
||||
return \is_string($locale) && '' !== $locale && 'root' !== $locale ? $locale : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* ICU derives a pattern from the locale and the date/time types when none was configured;
|
||||
* a pattern configured to the very value ICU derives is indistinguishable from a derived one.
|
||||
*/
|
||||
private function prototypePattern(): string
|
||||
{
|
||||
$pattern = $this->dateFormatterPrototype->getPattern();
|
||||
$dateType = $this->dateFormatterPrototype->getDateType();
|
||||
$timeType = $this->dateFormatterPrototype->getTimeType();
|
||||
|
||||
if (!\is_string($pattern) || '' === $pattern || false === $dateType || false === $timeType) {
|
||||
return \is_string($pattern) ? $pattern : '';
|
||||
}
|
||||
|
||||
$locale = $this->prototypeLocale() ?: \Locale::getDefault();
|
||||
$calendar = $this->dateFormatterPrototype->getCalendarObject();
|
||||
// ICU derives the pattern from the calendar keyword it adds to the locale, which it does not do for a calendar object
|
||||
if ($calendar && \is_int($this->dateFormatterPrototype->getCalendar())) {
|
||||
$locale .= '@calendar='.$calendar->getType();
|
||||
}
|
||||
|
||||
$key = $locale.'|'.$dateType.'|'.$timeType;
|
||||
$this->prototypeDerivedPatterns[$key] ??= (new \IntlDateFormatter($locale, $dateType, $timeType))->getPattern();
|
||||
|
||||
return $pattern === $this->prototypeDerivedPatterns[$key] ? '' : $pattern;
|
||||
}
|
||||
|
||||
private function createNumberFormatter(?string $locale, string $style, array $attrs = []): \NumberFormatter
|
||||
{
|
||||
if (!isset(self::NUMBER_STYLES[$style])) {
|
||||
|
||||
@@ -194,6 +194,47 @@ class IntlExtensionTest extends TestCase
|
||||
$this->assertSame('22:22:00', $ext->formatTime($env, $date));
|
||||
}
|
||||
|
||||
public function testFormatterProtoPatternDoesNotOverrideAnExplicitLocale(): void
|
||||
{
|
||||
$env = new Environment(new ArrayLoader());
|
||||
$date = new \DateTime('2019-08-07T23:39:12+00:00');
|
||||
|
||||
// this prototype has no configured pattern, but ICU still reports one
|
||||
$derivedProto = new \IntlDateFormatter('fr_FR', \IntlDateFormatter::LONG, \IntlDateFormatter::SHORT, 'UTC', \IntlDateFormatter::GREGORIAN);
|
||||
$expected = (new \IntlDateFormatter('en_US', \IntlDateFormatter::LONG, \IntlDateFormatter::SHORT, 'UTC', \IntlDateFormatter::GREGORIAN))->format($date);
|
||||
|
||||
$this->assertSame($expected, (new IntlExtension($derivedProto))->formatDateTime($env, $date, timezone: 'UTC', locale: 'en_US'));
|
||||
|
||||
$patternProto = new \IntlDateFormatter('fr_FR', \IntlDateFormatter::LONG, \IntlDateFormatter::SHORT, 'UTC', \IntlDateFormatter::GREGORIAN, 'yyyy-MM-dd');
|
||||
|
||||
$this->assertSame('2019-08-07', (new IntlExtension($patternProto))->formatDateTime($env, $date, timezone: 'UTC', locale: 'en_US'));
|
||||
|
||||
// ICU derives the pattern from the calendar of the prototype as well
|
||||
$calendarProto = new \IntlDateFormatter('ja_JP@calendar=japanese', \IntlDateFormatter::LONG, \IntlDateFormatter::SHORT, 'UTC', \IntlDateFormatter::TRADITIONAL);
|
||||
|
||||
$this->assertSame($expected, (new IntlExtension($calendarProto))->formatDateTime($env, $date, timezone: 'UTC', locale: 'en_US'));
|
||||
}
|
||||
|
||||
public function testFormatterProtoWithoutDateAndTimeStylesKeepsItsLocale(): void
|
||||
{
|
||||
$env = new Environment(new ArrayLoader());
|
||||
$date = new \DateTime('2019-08-07T23:39:12+00:00');
|
||||
// ICU reports the "root" locale for a prototype with no date and no time style, and rejects it as an input locale
|
||||
$proto = new \IntlDateFormatter('fr_FR', \IntlDateFormatter::NONE, \IntlDateFormatter::NONE, 'UTC', \IntlDateFormatter::GREGORIAN, 'yyyy-MM-dd');
|
||||
|
||||
$this->assertSame('2019-08-07', (new IntlExtension($proto))->formatDateTime($env, $date, timezone: 'UTC'));
|
||||
|
||||
$localeDependentProto = new \IntlDateFormatter('fr_FR', \IntlDateFormatter::NONE, \IntlDateFormatter::NONE, 'UTC', \IntlDateFormatter::GREGORIAN, 'EEEE d MMMM y');
|
||||
$default = \Locale::getDefault();
|
||||
\Locale::setDefault('en_US');
|
||||
|
||||
try {
|
||||
$this->assertSame('mercredi 7 août 2019', (new IntlExtension($localeDependentProto))->formatDateTime($env, $date, timezone: 'UTC'));
|
||||
} finally {
|
||||
\Locale::setDefault($default);
|
||||
}
|
||||
}
|
||||
|
||||
public function testDateFormatterCacheIsBounded(): void
|
||||
{
|
||||
$ext = new IntlExtension();
|
||||
|
||||
Reference in New Issue
Block a user