feature #27399 [Translation] Added intl message formatter. (aitboudad, Nyholm)
This PR was merged into the 4.2-dev branch. Discussion ---------- [Translation] Added intl message formatter. | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | replaces #20007 | License | MIT | Doc PR | This PR will replace #20007 and continue the work from the original author. I've have tried to address all comments made in the original PR. Commits -------2a90931e52
csfb30c77659
Be more specific with what exception we catchb1aa0047fd
Only use the default translator if intl extension is loadedf88153fa22
Updates according to feedback597a15d7f7
Use FallbackFormatter instead of support for multiple formatters2aa7181e15
Fixes according to feedbacka325a443ed
Allow config for different domain specific formattersb43fe21997
Add support for multiple formattersc2b3dc0a90
[Translation] Added intl message formatter.19e8e69979
use error940d440e87
Make it a warning
This commit is contained in:
commit
baad3321f4
@ -697,7 +697,7 @@ class Configuration implements ConfigurationInterface
|
||||
->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')
|
||||
|
@ -29,9 +29,15 @@
|
||||
<tag name="monolog.logger" channel="translation" />
|
||||
</service>
|
||||
|
||||
<service id="translator.formatter.default" class="Symfony\Component\Translation\Formatter\MessageFormatter">
|
||||
<service id="translator.formatter.symfony" class="Symfony\Component\Translation\Formatter\MessageFormatter">
|
||||
<argument type="service" id="identity_translator" />
|
||||
</service>
|
||||
<service id="translator.formatter.intl" class="Symfony\Component\Translation\Formatter\IntlMessageFormatter" public="false" />
|
||||
<service id="translator.formatter.fallback" class="Symfony\Component\Translation\Formatter\FallbackFormatter" public="false">
|
||||
<argument type="service" id="translator.formatter.intl" />
|
||||
<argument type="service" id="translator.formatter.symfony" />
|
||||
</service>
|
||||
<service id="translator.formatter.default" alias="translator.formatter.fallback" />
|
||||
|
||||
<service id="translation.loader.php" class="Symfony\Component\Translation\Loader\PhpFileLoader">
|
||||
<tag name="translation.loader" alias="php" />
|
||||
|
@ -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
|
||||
-----
|
||||
|
@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* 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.'));
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* 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 <guilhermeblanco@hotmail.com>
|
||||
* @author Abdellatif Ait boudad <a.aitboudad@gmail.com>
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
@ -0,0 +1,213 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* 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
|
||||
{
|
||||
}
|
@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* 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),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user