diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php index a6d99ff4b5293..d3ee5bc7486a4 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -699,7 +699,7 @@ private function addTranslatorSection(ArrayNodeDefinition $rootNode) ->defaultValue(array('en')) ->end() ->booleanNode('logging')->defaultValue(false)->end() - ->scalarNode('formatter')->defaultValue(class_exists(\MessageFormatter::class) ? 'translator.formatter.default' : 'translator.formatter.symfony')->end() + ->scalarNode('formatter')->defaultValue('translator.formatter.default')->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 c37e556d7fad0..42434b62d5515 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation.xml @@ -29,15 +29,9 @@ - + - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php index f431885a4d78b..5ef3cbf13585b 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php @@ -189,7 +189,7 @@ protected static function getBundleDefaultConfig() 'enabled' => !class_exists(FullStack::class), 'fallbacks' => array('en'), 'logging' => false, - 'formatter' => \class_exists('MessageFormatter') ? 'translator.formatter.default' : 'translator.formatter.symfony', + 'formatter' => 'translator.formatter.default', 'paths' => array(), 'default_path' => '%kernel.project_dir%/translations', ), diff --git a/src/Symfony/Component/Translation/CHANGELOG.md b/src/Symfony/Component/Translation/CHANGELOG.md index e3a4725c3d5d6..87eb2fa4a3775 100644 --- a/src/Symfony/Component/Translation/CHANGELOG.md +++ b/src/Symfony/Component/Translation/CHANGELOG.md @@ -5,10 +5,11 @@ CHANGELOG ----- * Started using ICU parent locales as fallback locales. + * allow using the ICU message format using domains with the "+intl-icu" suffix * deprecated `Translator::transChoice()` in favor of using `Translator::trans()` with a `%count%` parameter * deprecated `TranslatorInterface` in favor of `Symfony\Contracts\Translation\TranslatorInterface` * deprecated `MessageSelector`, `Interval` and `PluralizationRules`; use `IdentityTranslator` instead - * Added `IntlMessageFormatter` and `FallbackMessageFormatter` + * Added `IntlFormatter` and `IntlFormatterInterface` * added support for multiple files and directories in `XliffLintCommand` * Marked `Translator::getFallbackLocales()` and `TranslationDataCollector::getFallbackLocales()` as internal diff --git a/src/Symfony/Component/Translation/Formatter/FallbackFormatter.php b/src/Symfony/Component/Translation/Formatter/FallbackFormatter.php deleted file mode 100644 index fac98a4a52deb..0000000000000 --- a/src/Symfony/Component/Translation/Formatter/FallbackFormatter.php +++ /dev/null @@ -1,77 +0,0 @@ - - * - * 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/IntlFormatter.php b/src/Symfony/Component/Translation/Formatter/IntlFormatter.php new file mode 100644 index 0000000000000..338d5151f56c8 --- /dev/null +++ b/src/Symfony/Component/Translation/Formatter/IntlFormatter.php @@ -0,0 +1,55 @@ + + * + * 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; + +/** + * @author Guilherme Blanco + * @author Abdellatif Ait boudad + */ +class IntlFormatter implements IntlFormatterInterface +{ + private $hasMessageFormatter; + private $cache = array(); + + /** + * {@inheritdoc} + */ + public function formatIntl(string $message, string $locale, array $parameters = array()): string + { + if (!$formatter = $this->cache[$locale][$message] ?? null) { + if (!($this->hasMessageFormatter ?? $this->hasMessageFormatter = class_exists(\MessageFormatter::class))) { + throw new LogicException('Cannot parse message translation: please install the "intl" PHP extension or the "symfony/polyfill-intl-messageformatter" package.'); + } + try { + $this->cache[$locale][$message] = $formatter = new \MessageFormatter($locale, $message); + } catch (\IntlException $e) { + throw new InvalidArgumentException(sprintf('Invalid message format (error #%d): %s.', intl_get_error_code(), intl_get_error_message()), 0, $e); + } + } + + foreach ($parameters as $key => $value) { + if (\in_array($key[0] ?? null, array('%', '{'), true)) { + unset($parameters[$key]); + $parameters[trim($key, '%{ }')] = $value; + } + } + + if (false === $message = $formatter->format($parameters)) { + throw new InvalidArgumentException(sprintf('Unable to format message (error #%s): %s.', $formatter->getErrorCode(), $formatter->getErrorMessage())); + } + + return $message; + } +} diff --git a/src/Symfony/Component/Translation/Formatter/IntlFormatterInterface.php b/src/Symfony/Component/Translation/Formatter/IntlFormatterInterface.php new file mode 100644 index 0000000000000..eb8590db3d01d --- /dev/null +++ b/src/Symfony/Component/Translation/Formatter/IntlFormatterInterface.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Formatter; + +/** + * Formats ICU message patterns. + * + * @author Nicolas Grekas + */ +interface IntlFormatterInterface +{ + const DOMAIN_SUFFIX = '+intl-icu'; + + /** + * Formats a localized message using rules defined by ICU MessageFormat. + * + * @see http://icu-project.org/apiref/icu4c/classMessageFormat.html#details + */ + public function formatIntl(string $message, string $locale, array $parameters = array()): string; +} diff --git a/src/Symfony/Component/Translation/Formatter/IntlMessageFormatter.php b/src/Symfony/Component/Translation/Formatter/IntlMessageFormatter.php deleted file mode 100644 index 8f1ee797a0cc7..0000000000000 --- a/src/Symfony/Component/Translation/Formatter/IntlMessageFormatter.php +++ /dev/null @@ -1,40 +0,0 @@ - - * - * 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/Formatter/MessageFormatter.php b/src/Symfony/Component/Translation/Formatter/MessageFormatter.php index 1119c356f5afe..11f766c8cd216 100644 --- a/src/Symfony/Component/Translation/Formatter/MessageFormatter.php +++ b/src/Symfony/Component/Translation/Formatter/MessageFormatter.php @@ -19,14 +19,15 @@ /** * @author Abdellatif Ait boudad */ -class MessageFormatter implements MessageFormatterInterface, ChoiceMessageFormatterInterface +class MessageFormatter implements MessageFormatterInterface, IntlFormatterInterface, ChoiceMessageFormatterInterface { private $translator; + private $intlFormatter; /** * @param TranslatorInterface|null $translator An identity translator to use as selector for pluralization */ - public function __construct($translator = null) + public function __construct($translator = null, IntlFormatterInterface $intlFormatter = null) { if ($translator instanceof MessageSelector) { $translator = new IdentityTranslator($translator); @@ -35,6 +36,7 @@ public function __construct($translator = null) } $this->translator = $translator ?? new IdentityTranslator(); + $this->intlFormatter = $intlFormatter ?? new IntlFormatter(); } /** @@ -49,6 +51,14 @@ public function format($message, $locale, array $parameters = array()) return strtr($message, $parameters); } + /** + * {@inheritdoc} + */ + public function formatIntl(string $message, string $locale, array $parameters = array()): string + { + return $this->intlFormatter->formatIntl($message, $locale, $parameters); + } + /** * {@inheritdoc} * diff --git a/src/Symfony/Component/Translation/Tests/Formatter/FallbackFormatterTest.php b/src/Symfony/Component/Translation/Tests/Formatter/FallbackFormatterTest.php deleted file mode 100644 index 7b3cba109afb1..0000000000000 --- a/src/Symfony/Component/Translation/Tests/Formatter/FallbackFormatterTest.php +++ /dev/null @@ -1,213 +0,0 @@ - - * - * 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/IntlFormatterTest.php similarity index 72% rename from src/Symfony/Component/Translation/Tests/Formatter/IntlMessageFormatterTest.php rename to src/Symfony/Component/Translation/Tests/Formatter/IntlFormatterTest.php index 7b5d89f8353e1..89eaa18f32b4e 100644 --- a/src/Symfony/Component/Translation/Tests/Formatter/IntlMessageFormatterTest.php +++ b/src/Symfony/Component/Translation/Tests/Formatter/IntlFormatterTest.php @@ -12,29 +12,26 @@ namespace Symfony\Component\Translation\Tests\Formatter; use Symfony\Component\Translation\Exception\InvalidArgumentException; -use Symfony\Component\Translation\Formatter\IntlMessageFormatter; +use Symfony\Component\Translation\Formatter\IntlFormatter; +use Symfony\Component\Translation\Formatter\IntlFormatterInterface; -class IntlMessageFormatterTest extends \PHPUnit\Framework\TestCase +/** + * @requires extension intl + */ +class IntlFormatterTest 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))); + $this->assertEquals($expected, trim((new IntlFormatter())->formatIntl($message, 'en', $arguments))); } public function testInvalidFormat() { $this->expectException(InvalidArgumentException::class); - (new IntlMessageFormatter())->format('{foo', 'en', array(2)); + (new IntlFormatter())->formatIntl('{foo', 'en', array(2)); } public function testFormatWithNamedArguments() @@ -62,7 +59,7 @@ public function testFormatWithNamedArguments() other {{host} invites {guest} as one of the # people invited to their party.}}}} _MSG_; - $message = (new IntlMessageFormatter())->format($chooseMessage, 'en', array( + $message = (new IntlFormatter())->formatIntl($chooseMessage, 'en', array( 'gender_of_host' => 'male', 'num_guests' => 10, 'host' => 'Fabien', @@ -87,4 +84,13 @@ public function provideDataForFormat() ), ); } + + public function testPercentsAndBracketsAreTrimmed() + { + $formatter = new IntlFormatter(); + $this->assertInstanceof(IntlFormatterInterface::class, $formatter); + $this->assertSame('Hello Fab', $formatter->formatIntl('Hello {name}', 'en', array('name' => 'Fab'))); + $this->assertSame('Hello Fab', $formatter->formatIntl('Hello {name}', 'en', array('%name%' => 'Fab'))); + $this->assertSame('Hello Fab', $formatter->formatIntl('Hello {name}', 'en', array('{{ name }}' => 'Fab'))); + } } diff --git a/src/Symfony/Component/Translation/Tests/TranslatorTest.php b/src/Symfony/Component/Translation/Tests/TranslatorTest.php index d630a7491d4bc..bbc8ce2d21acd 100644 --- a/src/Symfony/Component/Translation/Tests/TranslatorTest.php +++ b/src/Symfony/Component/Translation/Tests/TranslatorTest.php @@ -540,6 +540,21 @@ public function getValidLocalesTests() ); } + /** + * @requires extension intl + */ + public function testIntlFormattedDomain() + { + $translator = new Translator('en'); + $translator->addLoader('array', new ArrayLoader()); + + $translator->addResource('array', array('some_message' => 'Hello %name%'), 'en'); + $this->assertSame('Hello Bob', $translator->trans('some_message', array('%name%' => 'Bob'))); + + $translator->addResource('array', array('some_message' => 'Hi {name}'), 'en', 'messages+intl-icu'); + $this->assertSame('Hi Bob', $translator->trans('some_message', array('%name%' => 'Bob'))); + } + /** * @group legacy */ diff --git a/src/Symfony/Component/Translation/Translator.php b/src/Symfony/Component/Translation/Translator.php index d8d27301f04df..eca5674e6753e 100644 --- a/src/Symfony/Component/Translation/Translator.php +++ b/src/Symfony/Component/Translation/Translator.php @@ -19,6 +19,7 @@ use Symfony\Component\Translation\Exception\NotFoundResourceException; use Symfony\Component\Translation\Exception\RuntimeException; use Symfony\Component\Translation\Formatter\ChoiceMessageFormatterInterface; +use Symfony\Component\Translation\Formatter\IntlFormatterInterface; use Symfony\Component\Translation\Formatter\MessageFormatter; use Symfony\Component\Translation\Formatter\MessageFormatterInterface; use Symfony\Component\Translation\Loader\LoaderInterface; @@ -80,6 +81,8 @@ class Translator implements LegacyTranslatorInterface, TranslatorInterface, Tran */ private $parentLocales; + private $hasIntlFormatter; + /** * @throws InvalidArgumentException If a locale contains invalid characters */ @@ -94,6 +97,7 @@ public function __construct(?string $locale, MessageFormatterInterface $formatte $this->formatter = $formatter; $this->cacheDir = $cacheDir; $this->debug = $debug; + $this->hasIntlFormatter = $formatter instanceof IntlFormatterInterface; } public function setConfigCacheFactory(ConfigCacheFactoryInterface $configCacheFactory) @@ -196,7 +200,26 @@ public function trans($id, array $parameters = array(), $domain = null, $locale $domain = 'messages'; } - return $this->formatter->format($this->getCatalogue($locale)->get((string) $id, $domain), $locale, $parameters); + $id = (string) $id; + $catalogue = $this->getCatalogue($locale); + $locale = $catalogue->getLocale(); + $intlDomain = $this->hasIntlFormatter ? $domain.IntlFormatterInterface::DOMAIN_SUFFIX : null; + while (true) { + if (null !== $intlDomain && $catalogue->defines($id, $intlDomain)) { + return $this->formatter->formatIntl($catalogue->get($id, $intlDomain), $locale, $parameters); + } + if ($catalogue->defines($id, $domain)) { + break; + } + if ($cat = $catalogue->getFallbackCatalogue()) { + $catalogue = $cat; + $locale = $catalogue->getLocale(); + } else { + break; + } + } + + return $this->formatter->format($catalogue->get($id, $domain), $locale, $parameters); } /**