Skip to content

Commit ed8eae3

Browse files
committed
Translation parameters
1 parent cfb1016 commit ed8eae3

14 files changed

+339
-16
lines changed

composer.json

+1
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@
133133
"doctrine/orm": "^2.7.3",
134134
"guzzlehttp/promises": "^1.4",
135135
"masterminds/html5": "^2.6",
136+
"moneyphp/money": "^3.0|^4.0",
136137
"monolog/monolog": "^1.25.1|^2",
137138
"nyholm/psr7": "^1.0",
138139
"paragonie/sodium_compat": "^1.8",

src/Symfony/Bridge/Twig/Tests/Extension/TranslationExtensionTest.php

+5
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
use PHPUnit\Framework\TestCase;
1515
use Symfony\Bridge\Twig\Extension\TranslationExtension;
1616
use Symfony\Component\Translation\Loader\ArrayLoader;
17+
use Symfony\Component\Translation\Parameter\DateTimeParameter;
18+
use Symfony\Component\Translation\Parameter\MoneyParameter;
1719
use Symfony\Component\Translation\Translator;
1820
use Symfony\Contracts\Translation\TranslatorInterface;
1921
use Twig\Environment;
@@ -134,6 +136,9 @@ public function getTransTests()
134136

135137
// trans object with count
136138
['{{ t("{0} There is no apples|{1} There is one apple|]1,Inf] There is %count% apples", {\'%count%\': count})|trans }}', 'There is 5 apples', ['count' => 5]],
139+
140+
// trans with parameter
141+
['{% trans into "fr"%}%date%{% endtrans %}', '01/02/2021', ['date' => DateTimeParameter::date(new \DateTime('2021-02-01', new \DateTimeZone('Europe/Paris')))]],
137142
];
138143
}
139144

src/Symfony/Component/Translation/CHANGELOG.md

+7-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
CHANGELOG
22
=========
33

4-
5.3
5-
---
4+
5.4.0
5+
-----
6+
7+
* Add `ParamaterInterface` to format parameters separately as recommended per the ICU with implementation for both DateTime and Money.
8+
9+
5.3.0
10+
-----
611

712
* Add `translation:pull` and `translation:push` commands to manage translations with third-party providers
813
* Add `TranslatorBagInterface::getCatalogues` method
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
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\Parameter;
13+
14+
use Symfony\Contracts\Translation\ParameterInterface;
15+
16+
/**
17+
* Wrapper around PHP IntlDateFormatter for date and time
18+
* The timezone from the DateTime instance is used instead of the server's timezone.
19+
*
20+
* @author Sylvain Fabre <syl.fabre@gmail.com>
21+
*/
22+
class DateTimeParameter implements ParameterInterface
23+
{
24+
private $dateTime;
25+
private $dateType;
26+
private $timeType;
27+
28+
private $formatters = [];
29+
30+
public function __construct(
31+
\DateTimeInterface $dateTime,
32+
int $dateType = \IntlDateFormatter::SHORT,
33+
int $timeType = \IntlDateFormatter::SHORT
34+
) {
35+
$this->dateTime = $dateTime;
36+
$this->dateType = $dateType;
37+
$this->timeType = $timeType;
38+
}
39+
40+
public function __toString(): string
41+
{
42+
return $this->format('en_US');
43+
}
44+
45+
public function format(string $locale = null): string
46+
{
47+
$timezone = $this->dateTime->getTimezone();
48+
$key = implode('.', [$locale, $this->dateType, $this->timeType, $timezone->getName()]);
49+
if (!isset($this->formatters[$key])) {
50+
$this->formatters[$key] = new \IntlDateFormatter(
51+
$locale,
52+
$this->dateType,
53+
$this->timeType,
54+
$timezone
55+
);
56+
}
57+
58+
return $this->formatters[$key]->format($this->dateTime);
59+
}
60+
61+
/**
62+
* Short-hand to only format a date.
63+
*/
64+
public static function date(\DateTimeInterface $dateTime, int $type = \IntlDateFormatter::SHORT): self
65+
{
66+
return new self($dateTime, $type, \IntlDateFormatter::NONE);
67+
}
68+
69+
/**
70+
* Short-hand to only format a time.
71+
*/
72+
public static function time(\DateTimeInterface $dateTime, int $type = \IntlDateFormatter::SHORT): self
73+
{
74+
return new self($dateTime, \IntlDateFormatter::NONE, $type);
75+
}
76+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
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\Parameter;
13+
14+
use Money\Currencies\ISOCurrencies;
15+
use Money\Formatter\DecimalMoneyFormatter;
16+
use Money\Money;
17+
use Symfony\Contracts\Translation\ParameterInterface;
18+
19+
/**
20+
* Wrapper around PHP NumberFormatter for money
21+
* The provided currency is used instead of the locale's currency.
22+
*
23+
* @author Sylvain Fabre <syl.fabre@gmail.com>
24+
*/
25+
class MoneyParameter implements ParameterInterface
26+
{
27+
private $value;
28+
private $currency;
29+
30+
private $formatters = [];
31+
32+
public function __construct($value, string $currency)
33+
{
34+
$this->value = $value;
35+
$this->currency = $currency;
36+
}
37+
38+
public function __toString(): string
39+
{
40+
return $this->format('en_US');
41+
}
42+
43+
public function format(string $locale = null): string
44+
{
45+
if (!isset($this->formatters[$locale])) {
46+
$this->formatters[$locale] = new \NumberFormatter($locale, \NumberFormatter::CURRENCY);
47+
}
48+
49+
return $this->formatters[$locale]->formatCurrency($this->value, $this->currency);
50+
}
51+
52+
/**
53+
* Short-hand to instantiate from a Money instance.
54+
*/
55+
public static function fromMoney(Money $money): self
56+
{
57+
$currencies = new ISOCurrencies();
58+
$moneyFormatter = new DecimalMoneyFormatter($currencies);
59+
60+
return new self($moneyFormatter->format($money), $money->getCurrency()->getCode());
61+
}
62+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
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\Parameter;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\Translation\Parameter\DateTimeParameter;
16+
17+
class DateTimeParameterTest extends TestCase
18+
{
19+
/**
20+
* @dataProvider getValues()
21+
*/
22+
public function testFormat(string $expected, DateTimeParameter $parameter, string $locale)
23+
{
24+
$this->assertSame($expected, $parameter->format($locale));
25+
}
26+
27+
public function getValues(): iterable
28+
{
29+
$dateTime = new \DateTime('2021-01-01 23:55:00', new \DateTimeZone('UTC'));
30+
31+
$parameterDateTime = new DateTimeParameter($dateTime);
32+
yield 'DateTime in French' => ['01/01/2021 23:55', $parameterDateTime, 'fr_FR'];
33+
yield 'DateTime in GB English' => ['01/01/2021, 23:55', $parameterDateTime, 'en_GB'];
34+
yield 'DateTime in US English' => ['1/1/21, 11:55 PM', $parameterDateTime, 'en_US'];
35+
36+
$dateTimeParis = new \DateTime('2021-01-01 23:55:00', new \DateTimeZone('UTC'));
37+
$dateTimeParis->setTimezone(new \DateTimeZone('Europe/Paris'));
38+
39+
$parameterDateTimeParis = new DateTimeParameter($dateTimeParis);
40+
yield 'DateTime in Paris in French' => ['02/01/2021 00:55', $parameterDateTimeParis, 'fr_FR'];
41+
yield 'DateTime in Paris in GB English' => ['02/01/2021, 00:55', $parameterDateTimeParis, 'en_GB'];
42+
yield 'DateTime in Paris in US English' => ['1/2/21, 12:55 AM', $parameterDateTimeParis, 'en_US'];
43+
44+
$parameterDateParis = DateTimeParameter::date($dateTimeParis);
45+
yield 'Date in Paris in French' => ['02/01/2021', $parameterDateParis, 'fr_FR'];
46+
yield 'Date in Paris in GB English' => ['02/01/2021', $parameterDateParis, 'en_GB'];
47+
yield 'Date in Paris in US English' => ['1/2/21', $parameterDateParis, 'en_US'];
48+
49+
$parameterTimeParis = DateTimeParameter::time($dateTimeParis);
50+
yield 'Time in Paris in French' => ['00:55', $parameterTimeParis, 'fr_FR'];
51+
yield 'Time in Paris in GB English' => ['00:55', $parameterTimeParis, 'en_GB'];
52+
yield 'Time in Paris in US English' => ['12:55 AM', $parameterTimeParis, 'en_US'];
53+
}
54+
55+
public function testToString()
56+
{
57+
$dateTime = new \DateTime('2021-01-01 00:00:00+00');
58+
$this->assertSame('1/1/21', (string) DateTimeParameter::date($dateTime));
59+
}
60+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
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\Parameter;
13+
14+
use Money\Money;
15+
use PHPUnit\Framework\TestCase;
16+
use Symfony\Component\Translation\Parameter\MoneyParameter;
17+
18+
class MoneyParameterTest extends TestCase
19+
{
20+
/**
21+
* @dataProvider getValues()
22+
*/
23+
public function testFormat(string $expected, MoneyParameter $parameter, string $locale)
24+
{
25+
$formatted = $parameter->format($locale);
26+
$cleaned = str_replace(['\u{202f}', '\u{a0}'], ['', ''], $formatted);
27+
$this->assertSame($expected, $cleaned);
28+
}
29+
30+
public function getValues(): iterable
31+
{
32+
$parameterEuros = new MoneyParameter(1000, 'EUR');
33+
$parameterDollars = new MoneyParameter(1000, 'USD');
34+
35+
yield 'Euros in French' => ['1 000,00 €', $parameterEuros, 'fr_FR'];
36+
yield 'Euros in US English' => ['€1,000.00', $parameterEuros, 'en_US'];
37+
yield 'US Dollars in French' => ['1 000,00 $US', $parameterDollars, 'fr_FR'];
38+
yield 'US Dollars in US English' => ['$1,000.00', $parameterDollars, 'en_US'];
39+
40+
$parameterEuros = MoneyParameter::fromMoney(Money::EUR(100000));
41+
yield 'Euros in French from Money' => ['1 000,00 €', $parameterEuros, 'fr_FR'];
42+
yield 'Euros in US English from Money' => ['€1,000.00', $parameterEuros, 'en_US'];
43+
44+
$parameterDollars = MoneyParameter::fromMoney(Money::USD(100000));
45+
yield 'US Dollars in French from Money' => ['1 000,00 $US', $parameterDollars, 'fr_FR'];
46+
yield 'US Dollars in US English from Money' => ['$1,000.00', $parameterDollars, 'en_US'];
47+
}
48+
49+
public function testToString()
50+
{
51+
$this->assertSame('€100.00', (string) new MoneyParameter(100, 'EUR'));
52+
}
53+
}

src/Symfony/Component/Translation/Tests/TranslatableTest.php

+2-4
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,8 @@ public function testToString()
4949

5050
public function getTransTests()
5151
{
52-
return [
53-
['Symfony est super !', new TranslatableMessage('Symfony is great!', [], ''), 'Symfony est super !', 'fr'],
54-
['Symfony est awesome !', new TranslatableMessage('Symfony is %what%!', ['%what%' => 'awesome'], ''), 'Symfony est %what% !', 'fr'],
55-
];
52+
yield ['Symfony est super !', new TranslatableMessage('Symfony is great!', [], ''), 'Symfony est super !', 'fr'];
53+
yield ['Symfony est awesome !', new TranslatableMessage('Symfony is %what%!', ['%what%' => 'awesome'], ''), 'Symfony est %what% !', 'fr'];
5654
}
5755

5856
public function getFlattenedTransTests()

src/Symfony/Component/Translation/Tests/TranslatorTest.php

+7-6
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
use Symfony\Component\Translation\Exception\RuntimeException;
1818
use Symfony\Component\Translation\Loader\ArrayLoader;
1919
use Symfony\Component\Translation\MessageCatalogue;
20+
use Symfony\Component\Translation\Parameter\DateTimeParameter;
21+
use Symfony\Component\Translation\Parameter\MoneyParameter;
2022
use Symfony\Component\Translation\Translator;
2123

2224
class TranslatorTest extends TestCase
@@ -463,12 +465,11 @@ public function getTransFileTests()
463465

464466
public function getTransTests()
465467
{
466-
return [
467-
['Symfony est super !', 'Symfony is great!', 'Symfony est super !', [], 'fr', ''],
468-
['Symfony est awesome !', 'Symfony is %what%!', 'Symfony est %what% !', ['%what%' => 'awesome'], 'fr', ''],
469-
['Symfony est super !', new StringClass('Symfony is great!'), 'Symfony est super !', [], 'fr', ''],
470-
['', null, '', [], 'fr', ''],
471-
];
468+
yield ['Symfony est super !', 'Symfony is great!', 'Symfony est super !', [], 'fr', ''];
469+
yield ['Symfony est awesome !', 'Symfony is %what%!', 'Symfony est %what% !', ['%what%' => 'awesome'], 'fr', ''];
470+
yield ['Symfony est super !', new StringClass('Symfony is great!'), 'Symfony est super !', [], 'fr', ''];
471+
yield ['Symfony 1 a été publié le 22/10/2005', 'Symfony 1 was published on %date%!', 'Symfony 1 a été publié le %date% !', ['%date%' => DateTimeParameter::date(new \DateTime('2005-10-22', new \DateTimeZone('Europe/Paris')))], 'fr', ''];
472+
yield ['', null, '', [], 'fr', ''];
472473
}
473474

474475
public function getTransICUTests()

src/Symfony/Component/Translation/Translator.php

+13-2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
use Symfony\Component\Translation\Formatter\MessageFormatterInterface;
2323
use Symfony\Component\Translation\Loader\LoaderInterface;
2424
use Symfony\Contracts\Translation\LocaleAwareInterface;
25+
use Symfony\Contracts\Translation\ParameterInterface;
2526
use Symfony\Contracts\Translation\TranslatorInterface;
2627

2728
// Help opcache.preload discover always-needed symbols
@@ -214,15 +215,25 @@ public function trans(?string $id, array $parameters = [], string $domain = null
214215
}
215216
}
216217

218+
$message = $catalogue->get($id, $domain);
219+
220+
$parameters = array_map(function ($parameter) use ($locale) {
221+
if ($parameter instanceof ParameterInterface) {
222+
return $parameter->format($locale);
223+
}
224+
225+
return $parameter;
226+
}, $parameters);
227+
217228
$len = \strlen(MessageCatalogue::INTL_DOMAIN_SUFFIX);
218229
if ($this->hasIntlFormatter
219230
&& ($catalogue->defines($id, $domain.MessageCatalogue::INTL_DOMAIN_SUFFIX)
220231
|| (\strlen($domain) > $len && 0 === substr_compare($domain, MessageCatalogue::INTL_DOMAIN_SUFFIX, -$len, $len)))
221232
) {
222-
return $this->formatter->formatIntl($catalogue->get($id, $domain), $locale, $parameters);
233+
return $this->formatter->formatIntl($message, $locale, $parameters);
223234
}
224235

225-
return $this->formatter->format($catalogue->get($id, $domain), $locale, $parameters);
236+
return $this->formatter->format($message, $locale, $parameters);
226237
}
227238

228239
/**

src/Symfony/Component/Translation/composer.json

+4-2
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@
3232
"symfony/service-contracts": "^1.1.2|^2",
3333
"symfony/yaml": "^4.4|^5.0|^6.0",
3434
"symfony/finder": "^4.4|^5.0|^6.0",
35-
"psr/log": "~1.0"
35+
"psr/log": "~1.0",
36+
"moneyphp/money": "^3.0|^4.0"
3637
},
3738
"conflict": {
3839
"symfony/config": "<4.4",
@@ -47,7 +48,8 @@
4748
"suggest": {
4849
"symfony/config": "",
4950
"symfony/yaml": "",
50-
"psr/log-implementation": "To use logging capability in translator"
51+
"psr/log-implementation": "To use logging capability in translator",
52+
"moneyphp/money": "To use money capability in translator"
5153
},
5254
"autoload": {
5355
"files": [ "Resources/functions.php" ],

0 commit comments

Comments
 (0)