Use FallbackFormatter instead of support for multiple formatters

This commit is contained in:
Nyholm 2018-09-03 23:28:48 +02:00
parent 2aa7181e15
commit 597a15d7f7
12 changed files with 303 additions and 82 deletions

View File

@ -11,7 +11,6 @@ CHANGELOG
* Deprecated the `Symfony\Bundle\FrameworkBundle\Controller\Controller` class in favor of `Symfony\Bundle\FrameworkBundle\Controller\AbstractController`.
* Enabled autoconfiguration for `Psr\Log\LoggerAwareInterface`
* Added new "auto" mode for `framework.session.cookie_secure` to turn it on when HTTPS is used
* Added support for configuring the `Translator` with multiple formatters.
4.1.0
-----

View File

@ -690,7 +690,6 @@ class Configuration implements ConfigurationInterface
->{!class_exists(FullStack::class) && class_exists(Translator::class) ? 'canBeDisabled' : 'canBeEnabled'}()
->fixXmlConfig('fallback')
->fixXmlConfig('path')
->fixXmlConfig('domain_formatter')
->children()
->arrayNode('fallbacks')
->beforeNormalization()->ifString()->then(function ($v) { return array($v); })->end()
@ -698,19 +697,7 @@ class Configuration implements ConfigurationInterface
->defaultValue(array('en'))
->end()
->booleanNode('logging')->defaultValue(false)->end()
->scalarNode('formatter')
->info('The default formatter to use if none is specified.')
->defaultValue('translator.formatter.default')
->end()
->arrayNode('domain_formatters')
->info('Configure different formatters per domain.')
->useAttributeAsKey('domain')
->prototype('array')
->children()
->scalarNode('service')->cannotBeEmpty()->end()
->end()
->end()
->end()
->scalarNode('formatter')->defaultValue('translator.formatter.default')->end()
->scalarNode('default_path')
->info('The default path used to load translations')
->defaultValue('%kernel.project_dir%/translations')

View File

@ -981,9 +981,6 @@ class FrameworkExtension extends Extension
$container->setAlias('translator.formatter', new Alias($config['formatter'], false));
$translator = $container->findDefinition('translator.default');
$translator->addMethodCall('setFallbackLocales', array($config['fallbacks']));
foreach ($config['domain_formatters'] as $formatter) {
$translator->addMethodCall('addFormatter', array($formatter['domain'], new Reference($formatter['service'])));
}
$container->setParameter('translator.logging', $config['logging']);
$container->setParameter('translator.default_path', $config['default_path']);

View File

@ -185,7 +185,6 @@
<xsd:sequence>
<xsd:element name="fallback" type="xsd:string" minOccurs="0" maxOccurs="unbounded" />
<xsd:element name="path" type="xsd:string" minOccurs="0" maxOccurs="unbounded" />
<xsd:element name="domain-formatter" type="translator_formatter" minOccurs="0" maxOccurs="unbounded" />
</xsd:sequence>
<xsd:attribute name="enabled" type="xsd:boolean" />
<xsd:attribute name="fallback" type="xsd:string" />
@ -193,11 +192,6 @@
<xsd:attribute name="formatter" type="xsd:string" />
</xsd:complexType>
<xsd:complexType name="translator_formatter">
<xsd:attribute name="domain" type="xsd:string" use="required" />
<xsd:attribute name="service" type="xsd:string" />
</xsd:complexType>
<xsd:complexType name="validation">
<xsd:choice minOccurs="0" maxOccurs="unbounded">
<xsd:element name="static-method" type="xsd:string" />

View File

@ -29,13 +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.selector" class="Symfony\Component\Translation\MessageSelector" 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" />

View File

@ -7,8 +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 intl message formatter.
* Added support for one formatter per domain
* Added `IntlMessageFormatter` and`FallbackMessageFormatter`
4.1.0
-----

View File

@ -0,0 +1,78 @@
<?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.
*/
declare(strict_types=1);
namespace Symfony\Component\Translation\Formatter;
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 (\Throwable $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 (\Throwable $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('The no formatter support plural translations.'));
}
}

View File

@ -11,6 +11,8 @@
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>
@ -25,15 +27,12 @@ class IntlMessageFormatter implements MessageFormatterInterface
try {
$formatter = new \MessageFormatter($locale, $message);
} catch (\Throwable $e) {
throw new \InvalidArgumentException('Invalid message format.', $e);
}
if (null === $formatter) {
throw new \InvalidArgumentException(sprintf('Invalid message format. Reason: %s (error #%d)', intl_get_error_message(), intl_get_error_code()));
throw new InvalidArgumentException(sprintf('Invalid message format. Reason: %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. Reason: %s (error #%s)', $formatter->getErrorMessage(), $formatter->getErrorCode()));
throw new InvalidArgumentException(sprintf('Unable to format message. Reason: %s (error #%s)', $formatter->getErrorMessage(), $formatter->getErrorCode()));
}
return $message;

View File

@ -0,0 +1,196 @@
<?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 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
{
}

View File

@ -11,13 +11,14 @@
namespace Symfony\Component\Translation\Tests\Formatter;
use Symfony\Component\Translation\Exception\InvalidArgumentException;
use Symfony\Component\Translation\Formatter\IntlMessageFormatter;
class IntlMessageFormatterTest extends \PHPUnit\Framework\TestCase
{
public function setUp()
{
if (!extension_loaded('intl')) {
if (!\extension_loaded('intl')) {
$this->markTestSkipped(
'The Intl extension is not available.'
);
@ -32,6 +33,12 @@ class IntlMessageFormatterTest extends \PHPUnit\Framework\TestCase
$this->assertEquals($expected, trim($this->getMessageFormatter()->format($message, 'en', $arguments)));
}
public function testInvalidFormat()
{
$this->expectException(InvalidArgumentException::class);
$this->getMessageFormatter()->format('{foo', 'en', array(2));
}
public function testFormatWithNamedArguments()
{
if (version_compare(INTL_ICU_VERSION, '4.8', '<')) {

View File

@ -12,8 +12,6 @@
namespace Symfony\Component\Translation\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Translation\Formatter\MessageFormatterInterface;
use Symfony\Component\Translation\Translator;
use Symfony\Component\Translation\Loader\ArrayLoader;
use Symfony\Component\Translation\MessageCatalogue;
use Symfony\Component\Translation\Translator;
@ -569,30 +567,6 @@ class TranslatorTest extends TestCase
// unchanged if it can't be found
$this->assertEquals('some_message2', $translator->transChoice('some_message2', 10, array('%count%' => 10)));
}
public function testDomainSpecificFormatter()
{
$fooFormatter = $this->getMockBuilder(MessageFormatterInterface::class)
->setMethods(array('format'))
->getMock();
$fooFormatter->expects($this->exactly(2))
->method('format')
->with('foo', 'en', array());
$barFormatter = $this->getMockBuilder(MessageFormatterInterface::class)
->setMethods(array('format'))
->getMock();
$barFormatter->expects($this->exactly(1))
->method('format')
->with('bar', 'en', array());
$translator = new Translator('en', $fooFormatter);
$translator->addFormatter('bar_domain', $barFormatter);
$translator->trans('foo');
$translator->trans('foo', array(), 'foo_domain');
$translator->trans('bar', array(), 'bar_domain');
}
}
class StringClass

View File

@ -54,9 +54,9 @@ class Translator implements TranslatorInterface, TranslatorBagInterface
private $resources = array();
/**
* @var MessageFormatterInterface[]
* @var MessageFormatterInterface
*/
private $formatters;
private $formatter;
/**
* @var string
@ -89,7 +89,7 @@ class Translator implements TranslatorInterface, TranslatorBagInterface
$formatter = new MessageFormatter();
}
$this->formatters['_default'] = $formatter;
$this->formatter = $formatter;
$this->cacheDir = $cacheDir;
$this->debug = $debug;
}
@ -137,11 +137,6 @@ class Translator implements TranslatorInterface, TranslatorBagInterface
}
}
public function addFormatter(string $domain, MessageFormatterInterface $formatter): void
{
$this->formatters[$domain] = $formatter;
}
/**
* {@inheritdoc}
*/
@ -197,7 +192,7 @@ class Translator implements TranslatorInterface, TranslatorBagInterface
$domain = 'messages';
}
return $this->getFormatter($domain)->format($this->getCatalogue($locale)->get((string) $id, $domain), $locale, $parameters);
return $this->formatter->format($this->getCatalogue($locale)->get((string) $id, $domain), $locale, $parameters);
}
/**
@ -205,13 +200,12 @@ class Translator implements TranslatorInterface, TranslatorBagInterface
*/
public function transChoice($id, $number, array $parameters = array(), $domain = null, $locale = null)
{
if (null === $domain) {
$domain = 'messages';
if (!$this->formatter instanceof ChoiceMessageFormatterInterface) {
throw new LogicException(sprintf('The formatter "%s" does not support plural translations.', \get_class($this->formatter)));
}
$formatter = $this->getFormatter($domain);
if (!$formatter instanceof ChoiceMessageFormatterInterface) {
throw new LogicException(sprintf('The formatter "%s" does not support plural translations.', \get_class($formatter)));
if (null === $domain) {
$domain = 'messages';
}
$id = (string) $id;
@ -226,7 +220,7 @@ class Translator implements TranslatorInterface, TranslatorBagInterface
}
}
return $formatter->choiceFormat($catalogue->get($id, $domain), $number, $locale, $parameters);
return $this->formatter->choiceFormat($catalogue->get($id, $domain), $number, $locale, $parameters);
}
/**
@ -461,9 +455,4 @@ EOF
return $this->configCacheFactory;
}
private function getFormatter(string $domain): MessageFormatterInterface
{
return $this->formatters[$domain] ?? $this->formatters['_default'];
}
}