diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php
index 6625570009b4..9ba2d0ad45d0 100644
--- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php
+++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php
@@ -697,7 +697,7 @@ private function addTranslatorSection(ArrayNodeDefinition $rootNode)
->defaultValue(array('en'))
->end()
->booleanNode('logging')->defaultValue(false)->end()
- ->scalarNode('formatter')->defaultValue('translator.formatter.default')->end()
+ ->scalarNode('formatter')->defaultValue(class_exists(\MessageFormatter::class) ? 'translator.formatter.default' : 'translator.formatter.symfony')->end()
->scalarNode('default_path')
->info('The default path used to load translations')
->defaultValue('%kernel.project_dir%/translations')
diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation.xml
index 42434b62d551..c37e556d7fad 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation.xml
+++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation.xml
@@ -29,9 +29,15 @@
-
+
+
+
+
+
+
+
diff --git a/src/Symfony/Component/Translation/CHANGELOG.md b/src/Symfony/Component/Translation/CHANGELOG.md
index 6397c3c23219..9f9fa5abf374 100644
--- a/src/Symfony/Component/Translation/CHANGELOG.md
+++ b/src/Symfony/Component/Translation/CHANGELOG.md
@@ -7,6 +7,7 @@ CHANGELOG
* Started using ICU parent locales as fallback locales.
* deprecated `TranslatorInterface` in favor of `Symfony\Contracts\Translation\TranslatorInterface`
* deprecated `MessageSelector`, `Interval` and `PluralizationRules`; use `IdentityTranslator` instead
+ * Added `IntlMessageFormatter` and `FallbackMessageFormatter`
4.1.0
-----
diff --git a/src/Symfony/Component/Translation/Formatter/FallbackFormatter.php b/src/Symfony/Component/Translation/Formatter/FallbackFormatter.php
new file mode 100644
index 000000000000..fac98a4a52de
--- /dev/null
+++ b/src/Symfony/Component/Translation/Formatter/FallbackFormatter.php
@@ -0,0 +1,77 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Translation\Formatter;
+
+use Symfony\Component\Translation\Exception\InvalidArgumentException;
+use Symfony\Component\Translation\Exception\LogicException;
+
+class FallbackFormatter implements MessageFormatterInterface, ChoiceMessageFormatterInterface
+{
+ /**
+ * @var MessageFormatterInterface|ChoiceMessageFormatterInterface
+ */
+ private $firstFormatter;
+
+ /**
+ * @var MessageFormatterInterface|ChoiceMessageFormatterInterface
+ */
+ private $secondFormatter;
+
+ public function __construct(MessageFormatterInterface $firstFormatter, MessageFormatterInterface $secondFormatter)
+ {
+ $this->firstFormatter = $firstFormatter;
+ $this->secondFormatter = $secondFormatter;
+ }
+
+ public function format($message, $locale, array $parameters = array())
+ {
+ try {
+ $result = $this->firstFormatter->format($message, $locale, $parameters);
+ } catch (InvalidArgumentException $e) {
+ return $this->secondFormatter->format($message, $locale, $parameters);
+ }
+
+ if ($result === $message) {
+ $result = $this->secondFormatter->format($message, $locale, $parameters);
+ }
+
+ return $result;
+ }
+
+ public function choiceFormat($message, $number, $locale, array $parameters = array())
+ {
+ // If both support ChoiceMessageFormatterInterface
+ if ($this->firstFormatter instanceof ChoiceMessageFormatterInterface && $this->secondFormatter instanceof ChoiceMessageFormatterInterface) {
+ try {
+ $result = $this->firstFormatter->choiceFormat($message, $number, $locale, $parameters);
+ } catch (InvalidArgumentException $e) {
+ return $this->secondFormatter->choiceFormat($message, $number, $locale, $parameters);
+ }
+
+ if ($result === $message) {
+ $result = $this->secondFormatter->choiceFormat($message, $number, $locale, $parameters);
+ }
+
+ return $result;
+ }
+
+ if ($this->firstFormatter instanceof ChoiceMessageFormatterInterface) {
+ return $this->firstFormatter->choiceFormat($message, $number, $locale, $parameters);
+ }
+
+ if ($this->secondFormatter instanceof ChoiceMessageFormatterInterface) {
+ return $this->secondFormatter->choiceFormat($message, $number, $locale, $parameters);
+ }
+
+ throw new LogicException(sprintf('No formatters support plural translations.'));
+ }
+}
diff --git a/src/Symfony/Component/Translation/Formatter/IntlMessageFormatter.php b/src/Symfony/Component/Translation/Formatter/IntlMessageFormatter.php
new file mode 100644
index 000000000000..8f1ee797a0cc
--- /dev/null
+++ b/src/Symfony/Component/Translation/Formatter/IntlMessageFormatter.php
@@ -0,0 +1,40 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Translation\Formatter;
+
+use Symfony\Component\Translation\Exception\InvalidArgumentException;
+
+/**
+ * @author Guilherme Blanco
+ * @author Abdellatif Ait boudad
+ */
+class IntlMessageFormatter implements MessageFormatterInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function format($message, $locale, array $parameters = array())
+ {
+ try {
+ $formatter = new \MessageFormatter($locale, $message);
+ } catch (\Throwable $e) {
+ throw new InvalidArgumentException(sprintf('Invalid message format (%s, error #%d).', intl_get_error_message(), intl_get_error_code()), 0, $e);
+ }
+
+ $message = $formatter->format($parameters);
+ if (U_ZERO_ERROR !== $formatter->getErrorCode()) {
+ throw new InvalidArgumentException(sprintf('Unable to format message ( %s, error #%s).', $formatter->getErrorMessage(), $formatter->getErrorCode()));
+ }
+
+ return $message;
+ }
+}
diff --git a/src/Symfony/Component/Translation/Tests/Formatter/FallbackFormatterTest.php b/src/Symfony/Component/Translation/Tests/Formatter/FallbackFormatterTest.php
new file mode 100644
index 000000000000..7b3cba109afb
--- /dev/null
+++ b/src/Symfony/Component/Translation/Tests/Formatter/FallbackFormatterTest.php
@@ -0,0 +1,213 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Translation\Tests\Formatter;
+
+use Symfony\Component\Translation\Exception\InvalidArgumentException;
+use Symfony\Component\Translation\Exception\LogicException;
+use Symfony\Component\Translation\Formatter\ChoiceMessageFormatterInterface;
+use Symfony\Component\Translation\Formatter\FallbackFormatter;
+use Symfony\Component\Translation\Formatter\MessageFormatterInterface;
+
+class FallbackFormatterTest extends \PHPUnit\Framework\TestCase
+{
+ public function testFormatSame()
+ {
+ $first = $this->getMockBuilder(MessageFormatterInterface::class)->setMethods(array('format'))->getMock();
+ $first
+ ->expects($this->once())
+ ->method('format')
+ ->with('foo', 'en', array(2))
+ ->willReturn('foo');
+
+ $second = $this->getMockBuilder(MessageFormatterInterface::class)->setMethods(array('format'))->getMock();
+ $second
+ ->expects($this->once())
+ ->method('format')
+ ->with('foo', 'en', array(2))
+ ->willReturn('bar');
+
+ $this->assertEquals('bar', (new FallbackFormatter($first, $second))->format('foo', 'en', array(2)));
+ }
+
+ public function testFormatDifferent()
+ {
+ $first = $this->getMockBuilder(MessageFormatterInterface::class)->setMethods(array('format'))->getMock();
+ $first
+ ->expects($this->once())
+ ->method('format')
+ ->with('foo', 'en', array(2))
+ ->willReturn('new value');
+
+ $second = $this->getMockBuilder(MessageFormatterInterface::class)->setMethods(array('format'))->getMock();
+ $second
+ ->expects($this->exactly(0))
+ ->method('format')
+ ->withAnyParameters();
+
+ $this->assertEquals('new value', (new FallbackFormatter($first, $second))->format('foo', 'en', array(2)));
+ }
+
+ public function testFormatException()
+ {
+ $first = $this->getMockBuilder(MessageFormatterInterface::class)->setMethods(array('format'))->getMock();
+ $first
+ ->expects($this->once())
+ ->method('format')
+ ->willThrowException(new InvalidArgumentException());
+
+ $second = $this->getMockBuilder(MessageFormatterInterface::class)->setMethods(array('format'))->getMock();
+ $second
+ ->expects($this->once())
+ ->method('format')
+ ->with('foo', 'en', array(2))
+ ->willReturn('bar');
+
+ $this->assertEquals('bar', (new FallbackFormatter($first, $second))->format('foo', 'en', array(2)));
+ }
+
+ public function testFormatExceptionUnknown()
+ {
+ $first = $this->getMockBuilder(MessageFormatterInterface::class)->setMethods(array('format'))->getMock();
+ $first
+ ->expects($this->once())
+ ->method('format')
+ ->willThrowException(new \RuntimeException());
+
+ $second = $this->getMockBuilder(MessageFormatterInterface::class)->setMethods(array('format'))->getMock();
+ $second
+ ->expects($this->exactly(0))
+ ->method('format');
+
+ $this->expectException(\RuntimeException::class);
+ $this->assertEquals('bar', (new FallbackFormatter($first, $second))->format('foo', 'en', array(2)));
+ }
+
+ public function testChoiceFormatSame()
+ {
+ $first = $this->getMockBuilder(SuperFormatterInterface::class)->setMethods(array('format', 'choiceFormat'))->getMock();
+ $first
+ ->expects($this->once())
+ ->method('choiceFormat')
+ ->with('foo', 1, 'en', array(2))
+ ->willReturn('foo');
+
+ $second = $this->getMockBuilder(SuperFormatterInterface::class)->setMethods(array('format', 'choiceFormat'))->getMock();
+ $second
+ ->expects($this->once())
+ ->method('choiceFormat')
+ ->with('foo', 1, 'en', array(2))
+ ->willReturn('bar');
+
+ $this->assertEquals('bar', (new FallbackFormatter($first, $second))->choiceFormat('foo', 1, 'en', array(2)));
+ }
+
+ public function testChoiceFormatDifferent()
+ {
+ $first = $this->getMockBuilder(SuperFormatterInterface::class)->setMethods(array('format', 'choiceFormat'))->getMock();
+ $first
+ ->expects($this->once())
+ ->method('choiceFormat')
+ ->with('foo', 1, 'en', array(2))
+ ->willReturn('new value');
+
+ $second = $this->getMockBuilder(SuperFormatterInterface::class)->setMethods(array('format', 'choiceFormat'))->getMock();
+ $second
+ ->expects($this->exactly(0))
+ ->method('choiceFormat')
+ ->withAnyParameters()
+ ->willReturn('bar');
+
+ $this->assertEquals('new value', (new FallbackFormatter($first, $second))->choiceFormat('foo', 1, 'en', array(2)));
+ }
+
+ public function testChoiceFormatException()
+ {
+ $first = $this->getMockBuilder(SuperFormatterInterface::class)->setMethods(array('format', 'choiceFormat'))->getMock();
+ $first
+ ->expects($this->once())
+ ->method('choiceFormat')
+ ->willThrowException(new InvalidArgumentException());
+
+ $second = $this->getMockBuilder(SuperFormatterInterface::class)->setMethods(array('format', 'choiceFormat'))->getMock();
+ $second
+ ->expects($this->once())
+ ->method('choiceFormat')
+ ->with('foo', 1, 'en', array(2))
+ ->willReturn('bar');
+
+ $this->assertEquals('bar', (new FallbackFormatter($first, $second))->choiceFormat('foo', 1, 'en', array(2)));
+ }
+
+ public function testChoiceFormatOnlyFirst()
+ {
+ // Implements both interfaces
+ $first = $this->getMockBuilder(SuperFormatterInterface::class)->setMethods(array('format', 'choiceFormat'))->getMock();
+ $first
+ ->expects($this->once())
+ ->method('choiceFormat')
+ ->with('foo', 1, 'en', array(2))
+ ->willReturn('bar');
+
+ // Implements only one interface
+ $second = $this->getMockBuilder(MessageFormatterInterface::class)->setMethods(array('format'))->getMock();
+ $second
+ ->expects($this->exactly(0))
+ ->method('format')
+ ->withAnyParameters()
+ ->willReturn('error');
+
+ $this->assertEquals('bar', (new FallbackFormatter($first, $second))->choiceFormat('foo', 1, 'en', array(2)));
+ }
+
+ public function testChoiceFormatOnlySecond()
+ {
+ // Implements only one interface
+ $first = $this->getMockBuilder(MessageFormatterInterface::class)->setMethods(array('format'))->getMock();
+ $first
+ ->expects($this->exactly(0))
+ ->method('format')
+ ->withAnyParameters()
+ ->willReturn('error');
+
+ // Implements both interfaces
+ $second = $this->getMockBuilder(SuperFormatterInterface::class)->setMethods(array('format', 'choiceFormat'))->getMock();
+ $second
+ ->expects($this->once())
+ ->method('choiceFormat')
+ ->with('foo', 1, 'en', array(2))
+ ->willReturn('bar');
+
+ $this->assertEquals('bar', (new FallbackFormatter($first, $second))->choiceFormat('foo', 1, 'en', array(2)));
+ }
+
+ public function testChoiceFormatNoChoiceFormat()
+ {
+ // Implements only one interface
+ $first = $this->getMockBuilder(MessageFormatterInterface::class)->setMethods(array('format'))->getMock();
+ $first
+ ->expects($this->exactly(0))
+ ->method('format');
+
+ // Implements both interfaces
+ $second = $this->getMockBuilder(MessageFormatterInterface::class)->setMethods(array('format'))->getMock();
+ $second
+ ->expects($this->exactly(0))
+ ->method('format');
+
+ $this->expectException(LogicException::class);
+ $this->assertEquals('bar', (new FallbackFormatter($first, $second))->choiceFormat('foo', 1, 'en', array(2)));
+ }
+}
+
+interface SuperFormatterInterface extends MessageFormatterInterface, ChoiceMessageFormatterInterface
+{
+}
diff --git a/src/Symfony/Component/Translation/Tests/Formatter/IntlMessageFormatterTest.php b/src/Symfony/Component/Translation/Tests/Formatter/IntlMessageFormatterTest.php
new file mode 100644
index 000000000000..7b5d89f8353e
--- /dev/null
+++ b/src/Symfony/Component/Translation/Tests/Formatter/IntlMessageFormatterTest.php
@@ -0,0 +1,90 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Translation\Tests\Formatter;
+
+use Symfony\Component\Translation\Exception\InvalidArgumentException;
+use Symfony\Component\Translation\Formatter\IntlMessageFormatter;
+
+class IntlMessageFormatterTest extends \PHPUnit\Framework\TestCase
+{
+ protected function setUp()
+ {
+ if (!\extension_loaded('intl')) {
+ $this->markTestSkipped('The Intl extension is not available.');
+ }
+ }
+
+ /**
+ * @dataProvider provideDataForFormat
+ */
+ public function testFormat($expected, $message, $arguments)
+ {
+ $this->assertEquals($expected, trim((new IntlMessageFormatter())->format($message, 'en', $arguments)));
+ }
+
+ public function testInvalidFormat()
+ {
+ $this->expectException(InvalidArgumentException::class);
+ (new IntlMessageFormatter())->format('{foo', 'en', array(2));
+ }
+
+ public function testFormatWithNamedArguments()
+ {
+ if (version_compare(INTL_ICU_VERSION, '4.8', '<')) {
+ $this->markTestSkipped('Format with named arguments can only be run with ICU 4.8 or higher and PHP >= 5.5');
+ }
+
+ $chooseMessage = <<<'_MSG_'
+{gender_of_host, select,
+ female {{num_guests, plural, offset:1
+ =0 {{host} does not give a party.}
+ =1 {{host} invites {guest} to her party.}
+ =2 {{host} invites {guest} and one other person to her party.}
+ other {{host} invites {guest} as one of the # people invited to her party.}}}
+ male {{num_guests, plural, offset:1
+ =0 {{host} does not give a party.}
+ =1 {{host} invites {guest} to his party.}
+ =2 {{host} invites {guest} and one other person to his party.}
+ other {{host} invites {guest} as one of the # people invited to his party.}}}
+ other {{num_guests, plural, offset:1
+ =0 {{host} does not give a party.}
+ =1 {{host} invites {guest} to their party.}
+ =2 {{host} invites {guest} and one other person to their party.}
+ other {{host} invites {guest} as one of the # people invited to their party.}}}}
+_MSG_;
+
+ $message = (new IntlMessageFormatter())->format($chooseMessage, 'en', array(
+ 'gender_of_host' => 'male',
+ 'num_guests' => 10,
+ 'host' => 'Fabien',
+ 'guest' => 'Guilherme',
+ ));
+
+ $this->assertEquals('Fabien invites Guilherme as one of the 9 people invited to his party.', $message);
+ }
+
+ public function provideDataForFormat()
+ {
+ return array(
+ array(
+ 'There is one apple',
+ 'There is one apple',
+ array(),
+ ),
+ array(
+ '4,560 monkeys on 123 trees make 37.073 monkeys per tree',
+ '{0,number,integer} monkeys on {1,number,integer} trees make {2,number} monkeys per tree',
+ array(4560, 123, 4560 / 123),
+ ),
+ );
+ }
+}