Skip to content

Commit b642d18

Browse files
committed
[Translation] TranslatableInterface implementations
1 parent e68d1cf commit b642d18

File tree

8 files changed

+371
-1
lines changed

8 files changed

+371
-1
lines changed

src/Symfony/Component/Translation/CHANGELOG.md

+6
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
CHANGELOG
22
=========
33

4+
6.1
5+
---
6+
7+
* Add new implementations of `TranslatableInterface` to format parameters
8+
separately as recommended per the ICU for DateTime, money, and decimal values.
9+
410
5.4
511
---
612

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Translation\Tests\Translatable;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\Translation\Translatable\DateTimeTranslatable;
16+
use Symfony\Contracts\Translation\TranslatorInterface;
17+
18+
class DateTimeTranslatableTest extends TestCase
19+
{
20+
protected function setUp(): void
21+
{
22+
if (!\extension_loaded('intl')) {
23+
$this->markTestSkipped('Extension intl is required.');
24+
}
25+
}
26+
27+
/**
28+
* @dataProvider getValues()
29+
*/
30+
public function testFormat(string $expected, DateTimeTranslatable $parameter, string $locale)
31+
{
32+
$translator = $this->createMock(TranslatorInterface::class);
33+
$this->assertSame($expected, $parameter->trans($translator, $locale));
34+
}
35+
36+
public function getValues(): iterable
37+
{
38+
$dateTime = new \DateTime('2021-01-01 23:55:00', new \DateTimeZone('UTC'));
39+
40+
$parameterDateTime = new DateTimeTranslatable($dateTime);
41+
yield 'DateTime in French' => ['01/01/2021 23:55', $parameterDateTime, 'fr_FR'];
42+
yield 'DateTime in GB English' => ['01/01/2021, 23:55', $parameterDateTime, 'en_GB'];
43+
yield 'DateTime in US English' => ['1/1/21, 11:55 PM', $parameterDateTime, 'en_US'];
44+
45+
$dateTimeParis = new \DateTime('2021-01-01 23:55:00', new \DateTimeZone('UTC'));
46+
$dateTimeParis->setTimezone(new \DateTimeZone('Europe/Paris'));
47+
48+
$parameterDateTimeParis = new DateTimeTranslatable($dateTimeParis);
49+
yield 'DateTime in Paris in French' => ['02/01/2021 00:55', $parameterDateTimeParis, 'fr_FR'];
50+
yield 'DateTime in Paris in GB English' => ['02/01/2021, 00:55', $parameterDateTimeParis, 'en_GB'];
51+
yield 'DateTime in Paris in US English' => ['1/2/21, 12:55 AM', $parameterDateTimeParis, 'en_US'];
52+
53+
$parameterDateParis = DateTimeTranslatable::date($dateTimeParis);
54+
yield 'Date in Paris in French' => ['02/01/2021', $parameterDateParis, 'fr_FR'];
55+
yield 'Date in Paris in GB English' => ['02/01/2021', $parameterDateParis, 'en_GB'];
56+
yield 'Date in Paris in US English' => ['1/2/21', $parameterDateParis, 'en_US'];
57+
58+
$parameterTimeParis = DateTimeTranslatable::time($dateTimeParis);
59+
yield 'Time in Paris in French' => ['00:55', $parameterTimeParis, 'fr_FR'];
60+
yield 'Time in Paris in GB English' => ['00:55', $parameterTimeParis, 'en_GB'];
61+
yield 'Time in Paris in US English' => ['12:55 AM', $parameterTimeParis, 'en_US'];
62+
}
63+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Translation\Tests\Translatable;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\Translation\Translatable\DecimalTranslatable;
16+
use Symfony\Contracts\Translation\TranslatorInterface;
17+
18+
class DecimalTranslatableTest extends TestCase
19+
{
20+
protected function setUp(): void
21+
{
22+
if (!\extension_loaded('intl')) {
23+
$this->markTestSkipped('Extension intl is required.');
24+
}
25+
}
26+
27+
/**
28+
* @dataProvider getValues()
29+
*/
30+
public function testFormat(string $expected, DecimalTranslatable $parameter, string $locale)
31+
{
32+
$translator = $this->createMock(TranslatorInterface::class);
33+
// Non-breakable spaces are added differently depending the PHP version
34+
$cleaned = str_replace(["\u{202f}", "\u{a0}"], ['', ''], $parameter->trans($translator, $locale));
35+
$this->assertSame($expected, $cleaned);
36+
}
37+
38+
public function getValues(): iterable
39+
{
40+
$parameter = new DecimalTranslatable(1000);
41+
42+
yield 'French' => ['1000', $parameter, 'fr_FR'];
43+
yield 'US English' => ['1,000', $parameter, 'en_US'];
44+
45+
$parameter = new DecimalTranslatable(1000.01);
46+
47+
yield 'Float in French' => ['1000,01', $parameter, 'fr_FR'];
48+
yield 'Float in US English' => ['1,000.01', $parameter, 'en_US'];
49+
50+
$parameter = new DecimalTranslatable(1, \NumberFormatter::PERCENT);
51+
52+
yield 'Styled in French' => ['100%', $parameter, 'fr_FR'];
53+
yield 'Styled in US English' => ['100%', $parameter, 'en_US'];
54+
}
55+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Translation\Tests\Translatable;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\Intl\Intl;
16+
use Symfony\Component\Translation\Translatable\MoneyTranslatable;
17+
use Symfony\Contracts\Translation\TranslatorInterface;
18+
19+
class MoneyTranslatableTest extends TestCase
20+
{
21+
protected function setUp(): void
22+
{
23+
if (!\extension_loaded('intl')) {
24+
$this->markTestSkipped('Extension intl is required.');
25+
}
26+
}
27+
28+
/**
29+
* @dataProvider getValues()
30+
*/
31+
public function testTrans(string $expected, MoneyTranslatable $parameter, string $locale)
32+
{
33+
$translator = $this->createMock(TranslatorInterface::class);
34+
// DecimalMoneyFormatter output may contain non-breakable spaces:
35+
// - this is done for good reasons
36+
// - output "style" changes depending on the PHP version
37+
// This normalization in only done here in the test so a new PHP version won't break the test
38+
$normalized = str_replace(["\u{202f}", "\u{a0}"], ['', ''], $parameter->trans($translator, $locale));
39+
$this->assertSame($expected, $normalized);
40+
}
41+
42+
public function getValues(): iterable
43+
{
44+
$parameterEuros = new MoneyTranslatable(1000, 'EUR');
45+
$parameterDollars = new MoneyTranslatable(1000, 'USD');
46+
47+
yield 'Euros in French' => ['1000,00€', $parameterEuros, 'fr_FR'];
48+
yield 'Euros in US English' => ['€1,000.00', $parameterEuros, 'en_US'];
49+
yield 'US Dollars in French' => ['1000,00$US', $parameterDollars, 'fr_FR'];
50+
yield 'US Dollars in US English' => ['$1,000.00', $parameterDollars, 'en_US'];
51+
52+
if (defined('\NumberFormatter::CURRENCY_ACCOUNTING')) {
53+
$parameterEuros = new MoneyTranslatable(-1000, 'EUR', \NumberFormatter::CURRENCY_ACCOUNTING);
54+
yield 'Accounting style in French' => ['(1000,00€)', $parameterEuros, 'fr_FR'];
55+
yield 'Accounting style in US English' => ['(€1,000.00)', $parameterEuros, 'en_US'];
56+
}
57+
}
58+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Translation\Translatable;
13+
14+
use Symfony\Contracts\Translation\TranslatableInterface;
15+
use Symfony\Contracts\Translation\TranslatorInterface;
16+
17+
/**
18+
* Wrapper around PHP IntlDateFormatter for date and time
19+
* The timezone from the DateTime instance is used instead of the server's timezone.
20+
*
21+
* Implementation of the ICU recommendation to first format advanced parameters before translation.
22+
*
23+
* @see https://unicode-org.github.io/icu/userguide/format_parse/messages/#format-the-parameters-separately-recommended
24+
*
25+
* @author Sylvain Fabre <syl.fabre@gmail.com>
26+
*/
27+
class DateTimeTranslatable implements TranslatableInterface
28+
{
29+
private \DateTimeInterface $dateTime;
30+
private int $dateType;
31+
private int $timeType;
32+
33+
private static array $formatters = [];
34+
35+
public function __construct(
36+
\DateTimeInterface $dateTime,
37+
int $dateType = \IntlDateFormatter::SHORT,
38+
int $timeType = \IntlDateFormatter::SHORT
39+
) {
40+
$this->dateTime = $dateTime;
41+
$this->dateType = $dateType;
42+
$this->timeType = $timeType;
43+
}
44+
45+
public function trans(TranslatorInterface $translator, string $locale = null): string
46+
{
47+
if (!$locale) {
48+
$locale = $translator->getLocale();
49+
}
50+
51+
$timezone = $this->dateTime->getTimezone();
52+
$key = implode('.', [$locale, $this->dateType, $this->timeType, $timezone->getName()]);
53+
if (!isset(self::$formatters[$key])) {
54+
self::$formatters[$key] = new \IntlDateFormatter(
55+
$locale ?? $translator->getLocale(),
56+
$this->dateType,
57+
$this->timeType,
58+
$timezone
59+
);
60+
}
61+
62+
return self::$formatters[$key]->format($this->dateTime);
63+
}
64+
65+
/**
66+
* Short-hand to only format a date.
67+
*/
68+
public static function date(\DateTimeInterface $dateTime, int $type = \IntlDateFormatter::SHORT): self
69+
{
70+
return new self($dateTime, $type, \IntlDateFormatter::NONE);
71+
}
72+
73+
/**
74+
* Short-hand to only format a time.
75+
*/
76+
public static function time(\DateTimeInterface $dateTime, int $type = \IntlDateFormatter::SHORT): self
77+
{
78+
return new self($dateTime, \IntlDateFormatter::NONE, $type);
79+
}
80+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Translation\Translatable;
13+
14+
use Symfony\Contracts\Translation\TranslatableInterface;
15+
use Symfony\Contracts\Translation\TranslatorInterface;
16+
17+
/**
18+
* Wrapper around PHP NumberFormatter for decimal values.
19+
*
20+
* Implementation of the ICU recommendation to first format advanced parameters before translation.
21+
*
22+
* @see https://unicode-org.github.io/icu/userguide/format_parse/messages/#format-the-parameters-separately-recommended
23+
*
24+
* @author Sylvain Fabre <syl.fabre@gmail.com>
25+
*/
26+
class DecimalTranslatable implements TranslatableInterface
27+
{
28+
private float|int $value;
29+
private int $style;
30+
31+
private static array $formatters = [];
32+
33+
public function __construct(float|int $value, int $style = \NumberFormatter::DECIMAL)
34+
{
35+
$this->value = $value;
36+
$this->style = $style;
37+
}
38+
39+
public function trans(TranslatorInterface $translator, string $locale = null): string
40+
{
41+
if (!$locale) {
42+
$locale = $translator->getLocale();
43+
}
44+
45+
$key = implode('.', [$locale, $this->style]);
46+
if (!isset(self::$formatters[$key])) {
47+
self::$formatters[$key] = new \NumberFormatter($locale, $this->style);
48+
}
49+
50+
return self::$formatters[$key]->format($this->value);
51+
}
52+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Translation\Translatable;
13+
14+
use Symfony\Contracts\Translation\TranslatableInterface;
15+
use Symfony\Contracts\Translation\TranslatorInterface;
16+
17+
/**
18+
* Wrapper around PHP NumberFormatter for money
19+
* The provided currency is used instead of the locale's currency.
20+
*
21+
* Implementation of the ICU recommendation to first format advanced parameters before translation.
22+
*
23+
* @see https://unicode-org.github.io/icu/userguide/format_parse/messages/#format-the-parameters-separately-recommended
24+
*
25+
* @author Sylvain Fabre <syl.fabre@gmail.com>
26+
*/
27+
class MoneyTranslatable implements TranslatableInterface
28+
{
29+
private float|int $value;
30+
private string $currency;
31+
private int $style;
32+
33+
private static array $formatters = [];
34+
35+
public function __construct(float|int $value, string $currency, int $style = \NumberFormatter::CURRENCY)
36+
{
37+
$this->value = $value;
38+
$this->currency = $currency;
39+
$this->style = $style;
40+
}
41+
42+
public function trans(TranslatorInterface $translator, string $locale = null): string
43+
{
44+
if (!$locale) {
45+
$locale = $translator->getLocale();
46+
}
47+
48+
$key = implode('.', [$locale, $this->style]);
49+
if (!isset(self::$formatters[$key])) {
50+
self::$formatters[$key] = new \NumberFormatter($locale, $this->style);
51+
}
52+
53+
return self::$formatters[$key]->formatCurrency($this->value, $this->currency);
54+
}
55+
}

src/Symfony/Component/Translation/composer.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@
4747
"suggest": {
4848
"symfony/config": "",
4949
"symfony/yaml": "",
50-
"psr/log-implementation": "To use logging capability in translator"
50+
"psr/log-implementation": "To use logging capability in translator",
51+
"moneyphp/money": "To use money capability in translator"
5152
},
5253
"autoload": {
5354
"files": [ "Resources/functions.php" ],

0 commit comments

Comments
 (0)