Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit b9f24b7

Browse files
committed
[Form] Fix ICU 72+ whitespace handling in DateTimeToLocalizedStringTransformer
ICU 72+ uses Unicode whitespace characters (such as U+202F narrow no-break space) in formatted date strings instead of regular ASCII spaces. This caused parsing failures when users typed dates with regular spaces. This fix: - Normalizes transform() output to use regular ASCII spaces for consistent display - Makes reverseTransform() accept both regular spaces and ICU 72+ whitespace by trying both formats when parsing fails Fix #63113 (review)
1 parent 92935e5 commit b9f24b7

2 files changed

Lines changed: 56 additions & 13 deletions

File tree

src/Symfony/Component/Form/Extension/Core/DataTransformer/DateTimeToLocalizedStringTransformer.php

Lines changed: 54 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,15 @@
2525
*/
2626
class DateTimeToLocalizedStringTransformer extends BaseDateTimeTransformer
2727
{
28+
/**
29+
* Unicode whitespace characters used by ICU in formatted date strings.
30+
*
31+
* @see https://unicode-org.atlassian.net/browse/CLDR-14032
32+
*/
33+
private const NO_BREAK_SPACE = "\u{00A0}";
34+
private const NARROW_NO_BREAK_SPACE = "\u{202F}"; // Used by ICU 72+ before AM/PM
35+
private const THIN_SPACE = "\u{2009}";
36+
2837
private int $dateFormat;
2938
private int $timeFormat;
3039

@@ -85,7 +94,7 @@ public function transform(mixed $dateTime): string
8594
throw new TransformationFailedException(intl_get_error_message());
8695
}
8796

88-
return $value;
97+
return self::normalizeWhitespace($value);
8998
}
9099

91100
public function reverseTransform(mixed $value): ?\DateTime
@@ -103,11 +112,7 @@ public function reverseTransform(mixed $value): ?\DateTime
103112
$dateOnly = $this->isPatternDateOnly();
104113
$dateFormatter = $this->getIntlDateFormatter($dateOnly);
105114

106-
try {
107-
$timestamp = @$dateFormatter->parse($value);
108-
} catch (\IntlException $e) {
109-
throw new TransformationFailedException($e->getMessage(), $e->getCode(), $e);
110-
}
115+
$timestamp = $this->parse($dateFormatter, $value);
111116

112117
if (0 != intl_get_error_code()) {
113118
throw new TransformationFailedException(intl_get_error_message(), intl_get_error_code());
@@ -177,4 +182,47 @@ protected function isPatternDateOnly(): bool
177182
// check for the absence of time-related placeholders
178183
return 0 === preg_match('#[ahHkKmsSAzZOvVxX]#', $pattern);
179184
}
185+
186+
/**
187+
* Normalizes various Unicode whitespace characters to regular ASCII spaces.
188+
*
189+
* ICU 72+ uses special Unicode whitespace characters (such as narrow no-break space U+202F)
190+
* in formatted date strings. This method ensures consistent handling regardless of ICU version
191+
* by normalizing these characters to regular ASCII spaces (U+0020).
192+
*/
193+
private static function normalizeWhitespace(string $string): string
194+
{
195+
return str_replace([self::NO_BREAK_SPACE, self::NARROW_NO_BREAK_SPACE, self::THIN_SPACE], ' ', $string);
196+
}
197+
198+
/**
199+
* Parses a localized date string, handling ICU version differences in whitespace.
200+
*
201+
* ICU 72+ uses special Unicode whitespace characters (such as narrow no-break space U+202F)
202+
* that users typically don't type. This method first tries parsing the input as-is, then
203+
* tries with whitespace normalization to ensure compatibility across ICU versions.
204+
*
205+
* @throws TransformationFailedException When the input cannot be parsed
206+
*/
207+
private function parse(\IntlDateFormatter $dateFormatter, string $value): int|float|false
208+
{
209+
try {
210+
$timestamp = @$dateFormatter->parse($value);
211+
} catch (\IntlException $e) {
212+
throw new TransformationFailedException($e->getMessage(), $e->getCode(), $e);
213+
}
214+
215+
// If parsing failed and the value contains regular spaces, try with ICU 72+ whitespace
216+
if ((false === $timestamp || 0 !== intl_get_error_code()) && str_contains($value, ' ')) {
217+
$icuValue = str_replace(' ', self::NARROW_NO_BREAK_SPACE, $value);
218+
219+
try {
220+
$timestamp = @$dateFormatter->parse($icuValue);
221+
} catch (\IntlException) {
222+
// Ignore, we'll use the original error below
223+
}
224+
}
225+
226+
return $timestamp;
227+
}
180228
}

src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/DateTimeToLocalizedStringTransformerTest.php

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
use Symfony\Component\Form\Extension\Core\DataTransformer\BaseDateTimeTransformer;
2020
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToLocalizedStringTransformer;
2121
use Symfony\Component\Form\Tests\Extension\Core\DataTransformer\Traits\DateTimeEqualsTrait;
22-
use Symfony\Component\Intl\Intl;
2322
use Symfony\Component\Intl\Util\IntlTestHelper;
2423

2524
class DateTimeToLocalizedStringTransformerTest extends BaseDateTimeTransformerTestCase
@@ -131,7 +130,7 @@ public function testTransformToDifferentLocale()
131130

132131
$transformer = new DateTimeToLocalizedStringTransformer('UTC', 'UTC');
133132

134-
$this->assertMatchesRegularExpression('/^Feb 3, 2010, 4:05\s+AM$/u', $transformer->transform($this->dateTime));
133+
$this->assertSame('Feb 3, 2010, 4:05 AM', $transformer->transform($this->dateTime));
135134
}
136135

137136
public function testTransformEmpty()
@@ -235,15 +234,11 @@ public function testReverseTransformFullTime()
235234

236235
public function testReverseTransformFromDifferentLocale()
237236
{
238-
if (version_compare(Intl::getIcuVersion(), '71.1', '>')) {
239-
$this->markTestSkipped('ICU version 71.1 or lower is required.');
240-
}
241-
242237
\Locale::setDefault('en_US');
243238

244239
$transformer = new DateTimeToLocalizedStringTransformer('UTC', 'UTC');
245240

246-
$this->assertDateTimeEquals($this->dateTimeWithoutSeconds, $transformer->reverseTransform('Feb 3, 2010, 04:05 AM'));
241+
$this->assertDateTimeEquals($this->dateTimeWithoutSeconds, $transformer->reverseTransform('Feb 3, 2010, 4:05 AM'));
247242
}
248243

249244
public function testReverseTransformWithDifferentTimezones()

0 commit comments

Comments
 (0)