feature #28375 [Translator] Deprecated transChoice and moved it away from contracts (Nyholm, nicolas-grekas)

This PR was merged into the 4.2-dev branch.

Discussion
----------

[Translator] Deprecated transChoice and moved it away from contracts

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | no
| BC breaks?    | no
| Deprecations? | yes
| Tests pass?   | yes
| Fixed tickets | n/a
| License       | MIT
| Doc PR        | Issue: https://github.com/symfony/symfony-docs/issues/10264

This will address comment made here: https://github.com/symfony/symfony/pull/27399#pullrequestreview-151929117

This PR moves the `transChoice()` method away from Contracts and back to the component. I also reverted changes in the `IdentityTranslator`. I will still use the deprecated `MessageSelector` but this is not fine since `transChoice()` is also deprecated.

Commits
-------

dc5f3bfff7 Make trans + %count% parameter resolve plurals
d870a850bd [Translator] Deprecated transChoice and moved it away from contracts
This commit is contained in:
Fabien Potencier 2018-10-06 18:26:49 +02:00
commit 23b0787a88
39 changed files with 510 additions and 125 deletions

View File

@ -114,6 +114,7 @@ FrameworkBundle
set the "APP_ENV" environment variable instead.
* The `--no-debug` console option has been deprecated,
set the "APP_DEBUG" environment variable to "0" instead.
* The `Templating\Helper\TranslatorHelper::transChoice()` method has been deprecated, use the `trans()` one instead with a `%count%` parameter.
Messenger
---------
@ -219,9 +220,15 @@ Translation
-----------
* The `TranslatorInterface` has been deprecated in favor of `Symfony\Contracts\Translation\TranslatorInterface`
* The `Translator::transChoice()` method has been deprecated in favor of using `Translator::trans()` with "%count%" as the parameter driving plurals
* The `MessageSelector`, `Interval` and `PluralizationRules` classes have been deprecated, use `IdentityTranslator` instead
* The `Translator::getFallbackLocales()` and `TranslationDataCollector::getFallbackLocales()` method have been marked as internal
TwigBundle
----------
* The `transchoice` tag and filter have been deprecated, use the `trans` ones instead with a `%count%` parameter.
Validator
---------

View File

@ -120,6 +120,7 @@ FrameworkBundle
set the "APP_ENV" environment variable instead.
* The `--no-debug` console option has been removed,
set the "APP_DEBUG" environment variable to "0" instead.
* The `Templating\Helper\TranslatorHelper::transChoice()` method has been removed, use the `trans()` one instead with a `%count%` parameter.
HttpFoundation
--------------
@ -196,11 +197,13 @@ Translation
* The `TranslatorInterface` has been removed in favor of `Symfony\Contracts\Translation\TranslatorInterface`
* The `MessageSelector`, `Interval` and `PluralizationRules` classes have been removed, use `IdentityTranslator` instead
* The `Translator::getFallbackLocales()` and `TranslationDataCollector::getFallbackLocales()` method are now internal
* The `Translator::transChoice()` method has been removed in favor of using `Translator::trans()` with "%count%" as the parameter driving plurals
TwigBundle
----------
* The default value (`false`) of the `twig.strict_variables` configuration option has been changed to `%kernel.debug%`.
* The `transchoice` tag and filter have been removed, use the `trans` ones instead with a `%count%` parameter.
Validator
--------

View File

@ -4,8 +4,9 @@ CHANGELOG
4.2.0
-----
* add bundle name suggestion on wrongly overridden templates paths
* added `name` argument in `debug:twig` command and changed `filter` argument as `--filter` option
* add bundle name suggestion on wrongly overridden templates paths
* added `name` argument in `debug:twig` command and changed `filter` argument as `--filter` option
* deprecated the `transchoice` tag and filter, use the `trans` ones instead with a `%count%` parameter
4.1.0
-----

View File

@ -16,6 +16,7 @@ use Symfony\Bridge\Twig\NodeVisitor\TranslationNodeVisitor;
use Symfony\Bridge\Twig\TokenParser\TransChoiceTokenParser;
use Symfony\Bridge\Twig\TokenParser\TransDefaultDomainTokenParser;
use Symfony\Bridge\Twig\TokenParser\TransTokenParser;
use Symfony\Component\Translation\TranslatorInterface as LegacyTranslatorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use Symfony\Contracts\Translation\TranslatorTrait;
use Twig\Extension\AbstractExtension;
@ -27,6 +28,8 @@ use Twig\TwigFilter;
* Provides integration of the Translation component with Twig.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @final since Symfony 4.2
*/
class TranslationExtension extends AbstractExtension
{
@ -34,20 +37,30 @@ class TranslationExtension extends AbstractExtension
getLocale as private;
setLocale as private;
trans as private doTrans;
transChoice as private doTransChoice;
}
private $translator;
private $translationNodeVisitor;
public function __construct(TranslatorInterface $translator = null, NodeVisitorInterface $translationNodeVisitor = null)
/**
* @param TranslatorInterface|null $translator
*/
public function __construct($translator = null, NodeVisitorInterface $translationNodeVisitor = null)
{
if (null !== $translator && !$translator instanceof LegacyTranslatorInterface && !$translator instanceof TranslatorInterface) {
throw new \TypeError(sprintf('Argument 1 passed to %s() must be an instance of %s, %s given.', __METHOD__, TranslatorInterface::class, \is_object($translator) ? \get_class($translator) : \gettype($translator)));
}
$this->translator = $translator;
$this->translationNodeVisitor = $translationNodeVisitor;
}
/**
* @deprecated since Symfony 4.2
*/
public function getTranslator()
{
@trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.2.', __METHOD__), E_USER_DEPRECATED);
return $this->translator;
}
@ -58,7 +71,7 @@ class TranslationExtension extends AbstractExtension
{
return array(
new TwigFilter('trans', array($this, 'trans')),
new TwigFilter('transchoice', array($this, 'transchoice')),
new TwigFilter('transchoice', array($this, 'transchoice'), array('deprecated' => '4.2', 'alternative' => 'trans" with parameter "%count%')),
);
}
@ -96,8 +109,11 @@ class TranslationExtension extends AbstractExtension
return $this->translationNodeVisitor ?: $this->translationNodeVisitor = new TranslationNodeVisitor();
}
public function trans($message, array $arguments = array(), $domain = null, $locale = null)
public function trans($message, array $arguments = array(), $domain = null, $locale = null, $count = null)
{
if (null !== $count) {
$arguments['%count%'] = $count;
}
if (null === $this->translator) {
return $this->doTrans($message, $arguments, $domain, $locale);
}
@ -105,10 +121,16 @@ class TranslationExtension extends AbstractExtension
return $this->translator->trans($message, $arguments, $domain, $locale);
}
/**
* @deprecated since Symfony 4.2, use the trans() method instead with a %count% parameter
*/
public function transchoice($message, $count, array $arguments = array(), $domain = null, $locale = null)
{
if (null === $this->translator) {
return $this->doTransChoice($message, $count, array_merge(array('%count%' => $count), $arguments), $domain, $locale);
return $this->doTrans($message, array_merge(array('%count%' => $count), $arguments), $domain, $locale);
}
if ($this->translator instanceof TranslatorInterface) {
return $this->translator->trans($message, array_merge(array('%count%' => $count), $arguments), $domain, $locale);
}
return $this->translator->transChoice($message, $count, array_merge(array('%count%' => $count), $arguments), $domain, $locale);

View File

@ -60,19 +60,12 @@ class TransNode extends Node
$method = !$this->hasNode('count') ? 'trans' : 'transChoice';
$compiler
->write('echo $this->env->getExtension(\'Symfony\Bridge\Twig\Extension\TranslationExtension\')->getTranslator()->'.$method.'(')
->write('echo $this->env->getExtension(\'Symfony\Bridge\Twig\Extension\TranslationExtension\')->trans(')
->subcompile($msg)
;
$compiler->raw(', ');
if ($this->hasNode('count')) {
$compiler
->subcompile($this->getNode('count'))
->raw(', ')
;
}
if (null !== $vars) {
$compiler
->raw('array_merge(')
@ -98,7 +91,17 @@ class TransNode extends Node
->raw(', ')
->subcompile($this->getNode('locale'))
;
} elseif ($this->hasNode('count')) {
$compiler->raw(', null');
}
if ($this->hasNode('count')) {
$compiler
->raw(', ')
->subcompile($this->getNode('count'))
;
}
$compiler->raw(");\n");
}

View File

@ -45,6 +45,15 @@ class TranslationExtensionTest extends TestCase
$this->assertEquals($expected, $this->getTemplate($template)->render($variables));
}
/**
* @group legacy
* @dataProvider getTransChoiceTests
*/
public function testTransChoice($template, $expected, array $variables = array())
{
$this->testTrans($template, $expected, $variables);
}
/**
* @expectedException \Twig\Error\SyntaxError
* @expectedExceptionMessage Unexpected token. Twig was looking for the "with", "from", or "into" keyword in "index" at line 3.
@ -64,6 +73,7 @@ class TranslationExtensionTest extends TestCase
}
/**
* @group legacy
* @expectedException \Twig\Error\SyntaxError
* @expectedExceptionMessage A message inside a transchoice tag must be a simple text in "index" at line 2.
*/
@ -87,6 +97,69 @@ class TranslationExtensionTest extends TestCase
array('{% trans into "fr"%}Hello{% endtrans %}', 'Hello'),
// trans with count
array(
'{% trans from "messages" %}{0} There is no apples|{1} There is one apple|]1,Inf] There is %count% apples{% endtrans %}',
'There is no apples',
array('count' => 0),
),
array(
'{% trans %}{0} There is no apples|{1} There is one apple|]1,Inf] There is %count% apples{% endtrans %}',
'There is 5 apples',
array('count' => 5),
),
array(
'{% trans %}{0} There is no apples|{1} There is one apple|]1,Inf] There is %count% apples (%name%){% endtrans %}',
'There is 5 apples (Symfony)',
array('count' => 5, 'name' => 'Symfony'),
),
array(
'{% trans with { \'%name%\': \'Symfony\' } %}{0} There is no apples|{1} There is one apple|]1,Inf] There is %count% apples (%name%){% endtrans %}',
'There is 5 apples (Symfony)',
array('count' => 5),
),
array(
'{% trans into "fr"%}{0} There is no apples|{1} There is one apple|]1,Inf] There is %count% apples{% endtrans %}',
'There is no apples',
array('count' => 0),
),
array(
'{% trans count 5 into "fr"%}{0} There is no apples|{1} There is one apple|]1,Inf] There is %count% apples{% endtrans %}',
'There is 5 apples',
),
// trans filter
array('{{ "Hello"|trans }}', 'Hello'),
array('{{ name|trans }}', 'Symfony', array('name' => 'Symfony')),
array('{{ hello|trans({ \'%name%\': \'Symfony\' }) }}', 'Hello Symfony', array('hello' => 'Hello %name%')),
array('{% set vars = { \'%name%\': \'Symfony\' } %}{{ hello|trans(vars) }}', 'Hello Symfony', array('hello' => 'Hello %name%')),
array('{{ "Hello"|trans({}, "messages", "fr") }}', 'Hello'),
// trans filter with count
array('{{ "{0} There is no apples|{1} There is one apple|]1,Inf] There is %count% apples"|trans(count=count) }}', 'There is 5 apples', array('count' => 5)),
array('{{ text|trans(count=5, arguments={\'%name%\': \'Symfony\'}) }}', 'There is 5 apples (Symfony)', array('text' => '{0} There is no apples|{1} There is one apple|]1,Inf] There is %count% apples (%name%)')),
array('{{ "{0} There is no apples|{1} There is one apple|]1,Inf] There is %count% apples"|trans({}, "messages", "fr", count) }}', 'There is 5 apples', array('count' => 5)),
);
}
/**
* @group legacy
*/
public function getTransChoiceTests()
{
return array(
// trans tag
array('{% trans %}Hello{% endtrans %}', 'Hello'),
array('{% trans %}%name%{% endtrans %}', 'Symfony', array('name' => 'Symfony')),
array('{% trans from elsewhere %}Hello{% endtrans %}', 'Hello'),
array('{% trans %}Hello %name%{% endtrans %}', 'Hello Symfony', array('name' => 'Symfony')),
array('{% trans with { \'%name%\': \'Symfony\' } %}Hello %name%{% endtrans %}', 'Hello Symfony'),
array('{% set vars = { \'%name%\': \'Symfony\' } %}{% trans with vars %}Hello %name%{% endtrans %}', 'Hello Symfony'),
array('{% trans into "fr"%}Hello{% endtrans %}', 'Hello'),
// transchoice
array(
'{% transchoice count from "messages" %}{0} There is no apples|{1} There is one apple|]1,Inf] There is %count% apples{% endtranschoice %}',
@ -145,8 +218,8 @@ class TranslationExtensionTest extends TestCase
{%- trans from "custom" %}foo{% endtrans %}
{{- "foo"|trans }}
{{- "foo"|trans({}, "custom") }}
{{- "foo"|transchoice(1) }}
{{- "foo"|transchoice(1, {}, "custom") }}
{{- "foo"|trans(count=1) }}
{{- "foo"|trans({"%count%":1}, "custom") }}
{% endblock %}
',
@ -174,12 +247,12 @@ class TranslationExtensionTest extends TestCase
{%- block content %}
{{- "foo"|trans(arguments = {}, domain = "custom") }}
{{- "foo"|transchoice(count = 1) }}
{{- "foo"|transchoice(count = 1, arguments = {}, domain = "custom") }}
{{- "foo"|trans(count = 1) }}
{{- "foo"|trans(count = 1, arguments = {}, domain = "custom") }}
{{- "foo"|trans({}, domain = "custom") }}
{{- "foo"|trans({}, "custom", locale = "fr") }}
{{- "foo"|transchoice(1, arguments = {}, domain = "custom") }}
{{- "foo"|transchoice(1, {}, "custom", locale = "fr") }}
{{- "foo"|trans(arguments = {"%count%":1}, domain = "custom") }}
{{- "foo"|trans({"%count%":1}, "custom", locale = "fr") }}
{% endblock %}
',

View File

@ -34,7 +34,7 @@ class TransNodeTest extends TestCase
$this->assertEquals(
sprintf(
'echo $this->env->getExtension(\'Symfony\Bridge\Twig\Extension\TranslationExtension\')->getTranslator()->trans("trans %%var%%", array_merge(array("%%var%%" => %s), %s), "messages");',
'echo $this->env->getExtension(\'Symfony\Bridge\Twig\Extension\TranslationExtension\')->trans("trans %%var%%", array_merge(array("%%var%%" => %s), %s), "messages");',
$this->getVariableGetterWithoutStrictCheck('var'),
$this->getVariableGetterWithStrictCheck('foo')
),

View File

@ -50,15 +50,21 @@ class TwigExtractorTest extends TestCase
}
}
/**
* @group legacy
* @dataProvider getLegacyExtractData
*/
public function testLegacyExtract($template, $messages)
{
$this->testExtract($template, $messages);
}
public function getExtractData()
{
return array(
array('{{ "new key" | trans() }}', array('new key' => 'messages')),
array('{{ "new key" | trans() | upper }}', array('new key' => 'messages')),
array('{{ "new key" | trans({}, "domain") }}', array('new key' => 'domain')),
array('{{ "new key" | transchoice(1) }}', array('new key' => 'messages')),
array('{{ "new key" | transchoice(1) | upper }}', array('new key' => 'messages')),
array('{{ "new key" | transchoice(1, {}, "domain") }}', array('new key' => 'domain')),
array('{% trans %}new key{% endtrans %}', array('new key' => 'messages')),
array('{% trans %} new key {% endtrans %}', array('new key' => 'messages')),
array('{% trans from "domain" %}new key{% endtrans %}', array('new key' => 'domain')),
@ -67,11 +73,27 @@ class TwigExtractorTest extends TestCase
// make sure 'trans_default_domain' tag is supported
array('{% trans_default_domain "domain" %}{{ "new key"|trans }}', array('new key' => 'domain')),
array('{% trans_default_domain "domain" %}{{ "new key"|transchoice }}', array('new key' => 'domain')),
array('{% trans_default_domain "domain" %}{% trans %}new key{% endtrans %}', array('new key' => 'domain')),
// make sure this works with twig's named arguments
array('{{ "new key" | trans(domain="domain") }}', array('new key' => 'domain')),
);
}
/**
* @group legacy
*/
public function getLegacyExtractData()
{
return array(
array('{{ "new key" | transchoice(1) }}', array('new key' => 'messages')),
array('{{ "new key" | transchoice(1) | upper }}', array('new key' => 'messages')),
array('{{ "new key" | transchoice(1, {}, "domain") }}', array('new key' => 'domain')),
// make sure 'trans_default_domain' tag is supported
array('{% trans_default_domain "domain" %}{{ "new key"|transchoice }}', array('new key' => 'domain')),
// make sure this works with twig's named arguments
array('{{ "new key" | transchoice(domain="domain", count=1) }}', array('new key' => 'domain')),
);
}

View File

@ -23,6 +23,8 @@ use Twig\Token;
* Token Parser for the 'transchoice' tag.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @deprecated since Symfony 4.2, use the "trans" tag with a "%count%" parameter instead
*/
class TransChoiceTokenParser extends TransTokenParser
{
@ -38,6 +40,8 @@ class TransChoiceTokenParser extends TransTokenParser
$lineno = $token->getLine();
$stream = $this->parser->getStream();
@trigger_error(sprintf('The "transchoice" tag is deprecated since Symfony 4.2, use the "trans" one instead with a "%count%" parameter in %s line %d.', $stream->getSourceContext()->getName(), $lineno), E_USER_DEPRECATED);
$vars = new ArrayExpression(array(), $lineno);
$count = $this->parser->getExpressionParser()->parseExpression();

View File

@ -39,10 +39,17 @@ class TransTokenParser extends AbstractTokenParser
$lineno = $token->getLine();
$stream = $this->parser->getStream();
$count = null;
$vars = new ArrayExpression(array(), $lineno);
$domain = null;
$locale = null;
if (!$stream->test(Token::BLOCK_END_TYPE)) {
if ($stream->test('count')) {
// {% trans count 5 %}
$stream->next();
$count = $this->parser->getExpressionParser()->parseExpression();
}
if ($stream->test('with')) {
// {% trans with vars %}
$stream->next();
@ -74,7 +81,7 @@ class TransTokenParser extends AbstractTokenParser
$stream->expect(Token::BLOCK_END_TYPE);
return new TransNode($body, $domain, null, $vars, $locale, $lineno, $this->getTag());
return new TransNode($body, $domain, $count, $vars, $locale, $lineno, $this->getTag());
}
public function decideTransFork($token)

View File

@ -18,6 +18,7 @@ CHANGELOG
the "APP_ENV" environment variable instead.
* Deprecated the `--no-debug` console option, set the "APP_DEBUG"
environment variable to "0" instead.
* Deprecated the `Templating\Helper\TranslatorHelper::transChoice()` method, use the `trans()` one instead with a `%count%` parameter
4.1.0
-----

View File

@ -26,6 +26,7 @@ use Symfony\Component\Translation\LoggingTranslator;
use Symfony\Component\Translation\MessageCatalogue;
use Symfony\Component\Translation\Reader\TranslationReaderInterface;
use Symfony\Component\Translation\Translator;
use Symfony\Component\Translation\TranslatorInterface as LegacyTranslatorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
@ -50,8 +51,14 @@ class TranslationDebugCommand extends Command
private $defaultTransPath;
private $defaultViewsPath;
public function __construct(TranslatorInterface $translator, TranslationReaderInterface $reader, ExtractorInterface $extractor, string $defaultTransPath = null, string $defaultViewsPath = null)
/**
* @param TranslatorInterface $translator
*/
public function __construct($translator, TranslationReaderInterface $reader, ExtractorInterface $extractor, string $defaultTransPath = null, string $defaultViewsPath = null)
{
if (!$translator instanceof LegacyTranslatorInterface && !$translator instanceof TranslatorInterface) {
throw new \TypeError(sprintf('Argument 1 passed to %s() must be an instance of %s, %s given.', __METHOD__, TranslatorInterface::class, \is_object($translator) ? \get_class($translator) : \gettype($translator)));
}
parent::__construct();
$this->translator = $translator;

View File

@ -12,6 +12,7 @@
namespace Symfony\Bundle\FrameworkBundle\Templating\Helper;
use Symfony\Component\Templating\Helper\Helper;
use Symfony\Component\Translation\TranslatorInterface as LegacyTranslatorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use Symfony\Contracts\Translation\TranslatorTrait;
@ -24,13 +25,18 @@ class TranslatorHelper extends Helper
getLocale as private;
setLocale as private;
trans as private doTrans;
transChoice as private doTransChoice;
}
protected $translator;
public function __construct(TranslatorInterface $translator = null)
/**
* @param TranslatorInterface|null $translator
*/
public function __construct($translator = null)
{
if (null !== $translator && !$translator instanceof LegacyTranslatorInterface && !$translator instanceof TranslatorInterface) {
throw new \TypeError(sprintf('Argument 1 passed to %s() must be an instance of %s, %s given.', __METHOD__, TranslatorInterface::class, \is_object($translator) ? \get_class($translator) : \gettype($translator)));
}
$this->translator = $translator;
}
@ -48,11 +54,17 @@ class TranslatorHelper extends Helper
/**
* @see TranslatorInterface::transChoice()
* @deprecated since Symfony 4.2, use the trans() method instead with a %count% parameter
*/
public function transChoice($id, $number, array $parameters = array(), $domain = 'messages', $locale = null)
{
@trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.2, use the trans() one instead with a "%count%" parameter.', __METHOD__), E_USER_DEPRECATED);
if (null === $this->translator) {
return $this->doTransChoice($id, $number, $parameters, $domain, $locale);
return $this->doTrans($id, array('%count%' => $number) + $parameters, $domain, $locale);
}
if ($this->translator instanceof TranslatorInterface) {
return $this->translator->trans($id, array('%count%' => $number) + $parameters, $domain, $locale);
}
return $this->translator->transChoice($id, $number, $parameters, $domain, $locale);

View File

@ -52,14 +52,27 @@ class TranslatorTest extends TestCase
$this->assertEquals('foo (FR)', $translator->trans('foo'));
$this->assertEquals('bar (EN)', $translator->trans('bar'));
$this->assertEquals('foobar (ES)', $translator->trans('foobar'));
$this->assertEquals('choice 0 (EN)', $translator->transChoice('choice', 0));
$this->assertEquals('choice 0 (EN)', $translator->trans('choice', array('%count%' => 0)));
$this->assertEquals('no translation', $translator->trans('no translation'));
$this->assertEquals('foobarfoo (PT-PT)', $translator->trans('foobarfoo'));
$this->assertEquals('other choice 1 (PT-BR)', $translator->transChoice('other choice', 1));
$this->assertEquals('other choice 1 (PT-BR)', $translator->trans('other choice', array('%count%' => 1)));
$this->assertEquals('foobarbaz (fr.UTF-8)', $translator->trans('foobarbaz'));
$this->assertEquals('foobarbax (sr@latin)', $translator->trans('foobarbax'));
}
/**
* @group legacy
*/
public function testTransChoiceWithoutCaching()
{
$translator = $this->getTranslator($this->getLoader());
$translator->setLocale('fr');
$translator->setFallbackLocales(array('en', 'es', 'pt-PT', 'pt_BR', 'fr.UTF-8', 'sr@latin'));
$this->assertEquals('choice 0 (EN)', $translator->transChoice('choice', 0));
$this->assertEquals('other choice 1 (PT-BR)', $translator->transChoice('other choice', 1));
}
public function testTransWithCaching()
{
// prime the cache
@ -70,10 +83,10 @@ class TranslatorTest extends TestCase
$this->assertEquals('foo (FR)', $translator->trans('foo'));
$this->assertEquals('bar (EN)', $translator->trans('bar'));
$this->assertEquals('foobar (ES)', $translator->trans('foobar'));
$this->assertEquals('choice 0 (EN)', $translator->transChoice('choice', 0));
$this->assertEquals('choice 0 (EN)', $translator->trans('choice', array('%count%' => 0)));
$this->assertEquals('no translation', $translator->trans('no translation'));
$this->assertEquals('foobarfoo (PT-PT)', $translator->trans('foobarfoo'));
$this->assertEquals('other choice 1 (PT-BR)', $translator->transChoice('other choice', 1));
$this->assertEquals('other choice 1 (PT-BR)', $translator->trans('other choice', array('%count%' => 1)));
$this->assertEquals('foobarbaz (fr.UTF-8)', $translator->trans('foobarbaz'));
$this->assertEquals('foobarbax (sr@latin)', $translator->trans('foobarbax'));
@ -88,14 +101,37 @@ class TranslatorTest extends TestCase
$this->assertEquals('foo (FR)', $translator->trans('foo'));
$this->assertEquals('bar (EN)', $translator->trans('bar'));
$this->assertEquals('foobar (ES)', $translator->trans('foobar'));
$this->assertEquals('choice 0 (EN)', $translator->transChoice('choice', 0));
$this->assertEquals('no translation', $translator->trans('no translation'));
$this->assertEquals('foobarfoo (PT-PT)', $translator->trans('foobarfoo'));
$this->assertEquals('other choice 1 (PT-BR)', $translator->transChoice('other choice', 1));
$this->assertEquals('foobarbaz (fr.UTF-8)', $translator->trans('foobarbaz'));
$this->assertEquals('foobarbax (sr@latin)', $translator->trans('foobarbax'));
}
/**
* @group legacy
*/
public function testTransChoiceWithCaching()
{
// prime the cache
$translator = $this->getTranslator($this->getLoader(), array('cache_dir' => $this->tmpDir));
$translator->setLocale('fr');
$translator->setFallbackLocales(array('en', 'es', 'pt-PT', 'pt_BR', 'fr.UTF-8', 'sr@latin'));
$this->assertEquals('choice 0 (EN)', $translator->transChoice('choice', 0));
$this->assertEquals('other choice 1 (PT-BR)', $translator->transChoice('other choice', 1));
// do it another time as the cache is primed now
$loader = $this->getMockBuilder('Symfony\Component\Translation\Loader\LoaderInterface')->getMock();
$loader->expects($this->never())->method('load');
$translator = $this->getTranslator($loader, array('cache_dir' => $this->tmpDir));
$translator->setLocale('fr');
$translator->setFallbackLocales(array('en', 'es', 'pt-PT', 'pt_BR', 'fr.UTF-8', 'sr@latin'));
$this->assertEquals('choice 0 (EN)', $translator->transChoice('choice', 0));
$this->assertEquals('other choice 1 (PT-BR)', $translator->transChoice('other choice', 1));
}
/**
* @expectedException \InvalidArgumentException
* @expectedExceptionMessage Invalid "invalid locale" locale.

View File

@ -13,6 +13,7 @@ namespace Symfony\Component\Form\Extension\Csrf;
use Symfony\Component\Form\AbstractExtension;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Component\Translation\TranslatorInterface as LegacyTranslatorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
@ -28,11 +29,14 @@ class CsrfExtension extends AbstractExtension
/**
* @param CsrfTokenManagerInterface $tokenManager The CSRF token manager
* @param TranslatorInterface $translator The translator for translating error messages
* @param TranslatorInterface|null $translator The translator for translating error messages
* @param string|null $translationDomain The translation domain for translating
*/
public function __construct(CsrfTokenManagerInterface $tokenManager, TranslatorInterface $translator = null, string $translationDomain = null)
public function __construct(CsrfTokenManagerInterface $tokenManager, $translator = null, string $translationDomain = null)
{
if (null !== $translator && !$translator instanceof LegacyTranslatorInterface && !$translator instanceof TranslatorInterface) {
throw new \TypeError(sprintf('Argument 2 passed to %s() must be an instance of %s, %s given.', __METHOD__, TranslatorInterface::class, \is_object($translator) ? \get_class($translator) : \gettype($translator)));
}
$this->tokenManager = $tokenManager;
$this->translator = $translator;
$this->translationDomain = $translationDomain;

View File

@ -18,6 +18,7 @@ use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\Util\ServerParams;
use Symfony\Component\Security\Csrf\CsrfToken;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Component\Translation\TranslatorInterface as LegacyTranslatorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
@ -40,8 +41,14 @@ class CsrfValidationListener implements EventSubscriberInterface
);
}
public function __construct(string $fieldName, CsrfTokenManagerInterface $tokenManager, string $tokenId, string $errorMessage, TranslatorInterface $translator = null, string $translationDomain = null, ServerParams $serverParams = null)
/**
* @param TranslatorInterface|null $translator
*/
public function __construct(string $fieldName, CsrfTokenManagerInterface $tokenManager, string $tokenId, string $errorMessage, $translator = null, string $translationDomain = null, ServerParams $serverParams = null)
{
if (null !== $translator && !$translator instanceof LegacyTranslatorInterface && !$translator instanceof TranslatorInterface) {
throw new \TypeError(sprintf('Argument 5 passed to %s() must be an instance of %s, %s given.', __METHOD__, TranslatorInterface::class, \is_object($translator) ? \get_class($translator) : \gettype($translator)));
}
$this->fieldName = $fieldName;
$this->tokenManager = $tokenManager;
$this->tokenId = $tokenId;

View File

@ -19,6 +19,7 @@ use Symfony\Component\Form\FormView;
use Symfony\Component\Form\Util\ServerParams;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Component\Translation\TranslatorInterface as LegacyTranslatorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
@ -33,8 +34,14 @@ class FormTypeCsrfExtension extends AbstractTypeExtension
private $translationDomain;
private $serverParams;
public function __construct(CsrfTokenManagerInterface $defaultTokenManager, bool $defaultEnabled = true, string $defaultFieldName = '_token', TranslatorInterface $translator = null, string $translationDomain = null, ServerParams $serverParams = null)
/**
* @param TranslatorInterface|null $translator
*/
public function __construct(CsrfTokenManagerInterface $defaultTokenManager, bool $defaultEnabled = true, string $defaultFieldName = '_token', $translator = null, string $translationDomain = null, ServerParams $serverParams = null)
{
if (null !== $translator && !$translator instanceof LegacyTranslatorInterface && !$translator instanceof TranslatorInterface) {
throw new \TypeError(sprintf('Argument 4 passed to %s() must be an instance of %s, %s given.', __METHOD__, TranslatorInterface::class, \is_object($translator) ? \get_class($translator) : \gettype($translator)));
}
$this->defaultTokenManager = $defaultTokenManager;
$this->defaultEnabled = $defaultEnabled;
$this->defaultFieldName = $defaultFieldName;

View File

@ -14,6 +14,7 @@ namespace Symfony\Component\Form\Extension\Validator\Type;
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Translation\TranslatorInterface as LegacyTranslatorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
@ -25,8 +26,14 @@ class UploadValidatorExtension extends AbstractTypeExtension
private $translator;
private $translationDomain;
public function __construct(TranslatorInterface $translator, string $translationDomain = null)
/**
* @param TranslatorInterface $translator
*/
public function __construct($translator, string $translationDomain = null)
{
if (!$translator instanceof LegacyTranslatorInterface && !$translator instanceof TranslatorInterface) {
throw new \TypeError(sprintf('Argument 1 passed to %s() must be an instance of %s, %s given.', __METHOD__, TranslatorInterface::class, \is_object($translator) ? \get_class($translator) : \gettype($translator)));
}
$this->translator = $translator;
$this->translationDomain = $translationDomain;
}

View File

@ -17,6 +17,7 @@ use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel\Event\FinishRequestEvent;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Translation\TranslatorInterface as LegacyTranslatorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
@ -29,8 +30,14 @@ class TranslatorListener implements EventSubscriberInterface
private $translator;
private $requestStack;
public function __construct(TranslatorInterface $translator, RequestStack $requestStack)
/**
* @param TranslatorInterface $translator
*/
public function __construct($translator, RequestStack $requestStack)
{
if (!$translator instanceof LegacyTranslatorInterface && !$translator instanceof TranslatorInterface) {
throw new \TypeError(sprintf('Argument 1 passed to %s() must be an instance of %s, %s given.', __METHOD__, TranslatorInterface::class, \is_object($translator) ? \get_class($translator) : \gettype($translator)));
}
$this->translator = $translator;
$this->requestStack = $requestStack;
}

View File

@ -5,6 +5,7 @@ CHANGELOG
-----
* Started using ICU parent locales as fallback locales.
* 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`

View File

@ -18,7 +18,7 @@ use Symfony\Contracts\Translation\TranslatorInterface;
/**
* @author Abdellatif Ait boudad <a.aitboudad@gmail.com>
*/
class DataCollectorTranslator implements LegacyTranslatorInterface, TranslatorBagInterface
class DataCollectorTranslator implements LegacyTranslatorInterface, TranslatorInterface, TranslatorBagInterface
{
const MESSAGE_DEFINED = 0;
const MESSAGE_MISSING = 1;
@ -34,8 +34,11 @@ class DataCollectorTranslator implements LegacyTranslatorInterface, TranslatorBa
/**
* @param TranslatorInterface $translator The translator must implement TranslatorBagInterface
*/
public function __construct(TranslatorInterface $translator)
public function __construct($translator)
{
if (!$translator instanceof LegacyTranslatorInterface && !$translator instanceof TranslatorInterface) {
throw new \TypeError(sprintf('Argument 1 passed to %s() must be an instance of %s, %s given.', __METHOD__, TranslatorInterface::class, \is_object($translator) ? \get_class($translator) : \gettype($translator)));
}
if (!$translator instanceof TranslatorBagInterface) {
throw new InvalidArgumentException(sprintf('The Translator "%s" must implement TranslatorInterface and TranslatorBagInterface.', \get_class($translator)));
}
@ -56,11 +59,18 @@ class DataCollectorTranslator implements LegacyTranslatorInterface, TranslatorBa
/**
* {@inheritdoc}
*
* @deprecated since Symfony 4.2, use the trans() method instead with a %count% parameter
*/
public function transChoice($id, $number, array $parameters = array(), $domain = null, $locale = null)
{
if ($this->translator instanceof TranslatorInterface) {
$trans = $this->translator->trans($id, array('%count%' => $number) + $parameters, $domain, $locale);
}
$trans = $this->translator->transChoice($id, $number, $parameters, $domain, $locale);
$this->collectMessage($locale, $domain, $id, $trans, $parameters, $number);
$this->collectMessage($locale, $domain, $id, $trans, array('%count%' => $number) + $parameters);
return $trans;
}
@ -125,9 +135,8 @@ class DataCollectorTranslator implements LegacyTranslatorInterface, TranslatorBa
* @param string $id
* @param string $translation
* @param array|null $parameters
* @param int|null $number
*/
private function collectMessage($locale, $domain, $id, $translation, $parameters = array(), $number = null)
private function collectMessage($locale, $domain, $id, $translation, $parameters = array())
{
if (null === $domain) {
$domain = 'messages';
@ -160,8 +169,8 @@ class DataCollectorTranslator implements LegacyTranslatorInterface, TranslatorBa
'id' => $id,
'translation' => $translation,
'parameters' => $parameters,
'transChoiceNumber' => $number,
'state' => $state,
'transChoiceNumber' => isset($parameters['%count%']) && is_numeric($parameters['%count%']) ? $parameters['%count%'] : null,
);
}
}

View File

@ -13,6 +13,8 @@ namespace Symfony\Component\Translation\Formatter;
/**
* @author Abdellatif Ait boudad <a.aitboudad@gmail.com>
*
* @deprecated since Symfony 4.2, use MessageFormatterInterface::format() with a %count% parameter instead
*/
interface ChoiceMessageFormatterInterface
{

View File

@ -13,6 +13,7 @@ namespace Symfony\Component\Translation\Formatter;
use Symfony\Component\Translation\IdentityTranslator;
use Symfony\Component\Translation\MessageSelector;
use Symfony\Component\Translation\TranslatorInterface as LegacyTranslatorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
@ -29,7 +30,7 @@ class MessageFormatter implements MessageFormatterInterface, ChoiceMessageFormat
{
if ($translator instanceof MessageSelector) {
$translator = new IdentityTranslator($translator);
} elseif (null !== $translator && !$translator instanceof TranslatorInterface) {
} elseif (null !== $translator && !$translator instanceof TranslatorInterface && !$translator instanceof LegacyTranslatorInterface) {
throw new \TypeError(sprintf('Argument 1 passed to %s() must be an instance of %s, %s given.', __METHOD__, TranslatorInterface::class, \is_object($translator) ? \get_class($translator) : \gettype($translator)));
}
@ -41,15 +42,27 @@ class MessageFormatter implements MessageFormatterInterface, ChoiceMessageFormat
*/
public function format($message, $locale, array $parameters = array())
{
if ($this->translator instanceof TranslatorInterface) {
return $this->translator->trans($message, $parameters, null, $locale);
}
return strtr($message, $parameters);
}
/**
* {@inheritdoc}
*
* @deprecated since Symfony 4.2, use format() with a %count% parameter instead
*/
public function choiceFormat($message, $number, $locale, array $parameters = array())
{
$parameters = array_merge(array('%count%' => $number), $parameters);
@trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.2, use the format() one instead with a %count% parameter.', __METHOD__), E_USER_DEPRECATED);
$parameters = array('%count%' => $number) + $parameters;
if ($this->translator instanceof TranslatorInterface) {
return $this->format($message, $locale, $parameters);
}
return $this->format($this->translator->transChoice($message, $number, array(), null, $locale), $locale, $parameters);
}

View File

@ -11,6 +11,8 @@
namespace Symfony\Component\Translation;
use Symfony\Component\Translation\TranslatorInterface as LegacyTranslatorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use Symfony\Contracts\Translation\TranslatorTrait;
/**
@ -18,11 +20,9 @@ use Symfony\Contracts\Translation\TranslatorTrait;
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class IdentityTranslator implements TranslatorInterface
class IdentityTranslator implements LegacyTranslatorInterface, TranslatorInterface
{
use TranslatorTrait {
transChoice as private doTransChoice;
}
use TranslatorTrait;
private $selector;
@ -40,14 +40,18 @@ class IdentityTranslator implements TranslatorInterface
/**
* {@inheritdoc}
*
* @deprecated since Symfony 4.2, use the trans() method instead with a %count% parameter
*/
public function transChoice($id, $number, array $parameters = array(), $domain = null, $locale = null)
{
@trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.2, use the trans() one instead with a "%count%" parameter.', __METHOD__), E_USER_DEPRECATED);
if ($this->selector) {
return strtr($this->selector->choose((string) $id, (int) $number, $locale ?: $this->getLocale()), $parameters);
return strtr($this->selector->choose((string) $id, $number, $locale ?: $this->getLocale()), $parameters);
}
return $this->doTransChoice($id, $number, $parameters, $domain, $locale);
return $this->trans($id, array('%count%' => $number) + $parameters, $domain, $locale);
}
private function getPluralizationRule(int $number, string $locale): int

View File

@ -32,8 +32,11 @@ class LoggingTranslator implements LegacyTranslatorInterface, TranslatorBagInter
* @param TranslatorInterface $translator The translator must implement TranslatorBagInterface
* @param LoggerInterface $logger
*/
public function __construct(TranslatorInterface $translator, LoggerInterface $logger)
public function __construct($translator, LoggerInterface $logger)
{
if (!$translator instanceof LegacyTranslatorInterface && !$translator instanceof TranslatorInterface) {
throw new \TypeError(sprintf('Argument 1 passed to %s() must be an instance of %s, %s given.', __METHOD__, TranslatorInterface::class, \is_object($translator) ? \get_class($translator) : \gettype($translator)));
}
if (!$translator instanceof TranslatorBagInterface) {
throw new InvalidArgumentException(sprintf('The Translator "%s" must implement TranslatorInterface and TranslatorBagInterface.', \get_class($translator)));
}
@ -55,10 +58,19 @@ class LoggingTranslator implements LegacyTranslatorInterface, TranslatorBagInter
/**
* {@inheritdoc}
*
* @deprecated since Symfony 4.2, use the trans() method instead with a %count% parameter
*/
public function transChoice($id, $number, array $parameters = array(), $domain = null, $locale = null)
{
$trans = $this->translator->transChoice($id, $number, $parameters, $domain, $locale);
@trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.2, use the trans() one instead with a "%count%" parameter.', __METHOD__), E_USER_DEPRECATED);
if ($this->translator instanceof TranslatorInterface) {
$trans = $this->translator->trans($id, array('%count%' => $number) + $parameters, $domain, $locale);
} else {
$trans = $this->translator->transChoice($id, $number, $parameters, $domain, $locale);
}
$this->log($id, $domain, $locale);
return $trans;

View File

@ -43,9 +43,9 @@ class MessageSelector
* The two methods can also be mixed:
* {0} There are no apples|one: There is one apple|more: There are %count% apples
*
* @param string $message The message being translated
* @param int $number The number of items represented for the message
* @param string $locale The locale to use for choosing
* @param string $message The message being translated
* @param int|float $number The number of items represented for the message
* @param string $locale The locale to use for choosing
*
* @return string
*

View File

@ -25,7 +25,7 @@ class DataCollectorTranslatorTest extends TestCase
$collector->trans('foo');
$collector->trans('bar');
$collector->transChoice('choice', 0);
$collector->trans('choice', array('%count%' => 0));
$collector->trans('bar_ru');
$collector->trans('bar_ru', array('foo' => 'bar'));
@ -54,7 +54,7 @@ class DataCollectorTranslatorTest extends TestCase
'locale' => 'en',
'domain' => 'messages',
'state' => DataCollectorTranslator::MESSAGE_MISSING,
'parameters' => array(),
'parameters' => array('%count%' => 0),
'transChoiceNumber' => 0,
);
$expectedMessages[] = array(
@ -79,6 +79,30 @@ class DataCollectorTranslatorTest extends TestCase
$this->assertEquals($expectedMessages, $collector->getCollectedMessages());
}
/**
* @group legacy
*/
public function testCollectMessagesTransChoice()
{
$collector = $this->createCollector();
$collector->setFallbackLocales(array('fr', 'ru'));
$collector->transChoice('choice', 0);
$expectedMessages = array();
$expectedMessages[] = array(
'id' => 'choice',
'translation' => 'choice',
'locale' => 'en',
'domain' => 'messages',
'state' => DataCollectorTranslator::MESSAGE_MISSING,
'parameters' => array('%count%' => 0),
'transChoiceNumber' => 0,
);
$this->assertEquals($expectedMessages, $collector->getCollectedMessages());
}
private function createCollector()
{
$translator = new Translator('en');

View File

@ -26,6 +26,7 @@ class MessageFormatterTest extends TestCase
/**
* @dataProvider getTransChoiceMessages
* @group legacy
*/
public function testFormatPlural($expected, $message, $number, $parameters)
{

View File

@ -21,17 +21,19 @@ class LoggingTranslatorTest extends TestCase
public function testTransWithNoTranslationIsLogged()
{
$logger = $this->getMockBuilder('Psr\Log\LoggerInterface')->getMock();
$logger->expects($this->exactly(2))
$logger->expects($this->exactly(1))
->method('warning')
->with('Translation not found.')
;
$translator = new Translator('ar');
$loggableTranslator = new LoggingTranslator($translator, $logger);
$loggableTranslator->transChoice('some_message2', 10, array('%count%' => 10));
$loggableTranslator->trans('bar');
}
/**
* @group legacy
*/
public function testTransChoiceFallbackIsLogged()
{
$logger = $this->getMockBuilder('Psr\Log\LoggerInterface')->getMock();
@ -47,4 +49,20 @@ class LoggingTranslatorTest extends TestCase
$loggableTranslator = new LoggingTranslator($translator, $logger);
$loggableTranslator->transChoice('some_message2', 10, array('%count%' => 10));
}
/**
* @group legacy
*/
public function testTransChoiceWithNoTranslationIsLogged()
{
$logger = $this->getMockBuilder('Psr\Log\LoggerInterface')->getMock();
$logger->expects($this->exactly(1))
->method('warning')
->with('Translation not found.')
;
$translator = new Translator('ar');
$loggableTranslator = new LoggingTranslator($translator, $logger);
$loggableTranslator->transChoice('some_message2', 10, array('%count%' => 10));
}
}

View File

@ -393,6 +393,7 @@ class TranslatorTest extends TestCase
/**
* @dataProvider getTransChoiceTests
* @group legacy
*/
public function testTransChoice($expected, $id, $translation, $number, $parameters, $locale, $domain)
{
@ -406,6 +407,7 @@ class TranslatorTest extends TestCase
/**
* @dataProvider getInvalidLocalesTests
* @expectedException \Symfony\Component\Translation\Exception\InvalidArgumentException
* @group legacy
*/
public function testTransChoiceInvalidLocale($locale)
{
@ -418,6 +420,7 @@ class TranslatorTest extends TestCase
/**
* @dataProvider getValidLocalesTests
* @group legacy
*/
public function testTransChoiceValidLocale($locale)
{
@ -499,7 +502,7 @@ class TranslatorTest extends TestCase
array('Il y a 0 pomme', new StringClass('{0} There are no appless|{1} There is one apple|]1,Inf] There is %count% apples'), '[0,1] Il y a %count% pomme|]1,Inf] Il y a %count% pommes', 0, array(), 'fr', ''),
// Override %count% with a custom value
array('Il y a quelques pommes', 'one: There is one apple|more: There are %count% apples', 'one: Il y a %count% pomme|more: Il y a %count% pommes', 2, array('%count%' => 'quelques'), 'fr', ''),
array('Il y a quelques pommes', 'one: There is one apple|more: There are %count% apples', 'one: Il y a %count% pomme|more: Il y a quelques pommes', 2, array('%count%' => 'quelques'), 'fr', ''),
);
}
@ -537,6 +540,9 @@ class TranslatorTest extends TestCase
);
}
/**
* @group legacy
*/
public function testTransChoiceFallback()
{
$translator = new Translator('ru');
@ -547,6 +553,9 @@ class TranslatorTest extends TestCase
$this->assertEquals('10 things', $translator->transChoice('some_message2', 10, array('%count%' => 10)));
}
/**
* @group legacy
*/
public function testTransChoiceFallbackBis()
{
$translator = new Translator('ru');
@ -557,6 +566,9 @@ class TranslatorTest extends TestCase
$this->assertEquals('10 things', $translator->transChoice('some_message2', 10, array('%count%' => 10)));
}
/**
* @group legacy
*/
public function testTransChoiceFallbackWithNoTranslation()
{
$translator = new Translator('ru');

View File

@ -22,11 +22,13 @@ use Symfony\Component\Translation\Formatter\ChoiceMessageFormatterInterface;
use Symfony\Component\Translation\Formatter\MessageFormatter;
use Symfony\Component\Translation\Formatter\MessageFormatterInterface;
use Symfony\Component\Translation\Loader\LoaderInterface;
use Symfony\Component\Translation\TranslatorInterface as LegacyTranslatorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
class Translator implements TranslatorInterface, TranslatorBagInterface
class Translator implements LegacyTranslatorInterface, TranslatorInterface, TranslatorBagInterface
{
/**
* @var MessageCatalogueInterface[]
@ -199,9 +201,13 @@ class Translator implements TranslatorInterface, TranslatorBagInterface
/**
* {@inheritdoc}
*
* @deprecated since Symfony 4.2, use the trans() method instead with a %count% parameter
*/
public function transChoice($id, $number, array $parameters = array(), $domain = null, $locale = null)
{
@trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.2, use the trans() one instead with a "%count%" parameter.', __METHOD__), E_USER_DEPRECATED);
if (!$this->formatter instanceof ChoiceMessageFormatterInterface) {
throw new LogicException(sprintf('The formatter "%s" does not support plural translations.', \get_class($this->formatter)));
}

View File

@ -11,13 +11,59 @@
namespace Symfony\Component\Translation;
use Symfony\Contracts\Translation\TranslatorInterface as BaseTranslatorInterface;
use Symfony\Component\Translation\Exception\InvalidArgumentException;
/**
* TranslatorInterface.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @deprecated since Symfony 4.2, use the same interface from the Symfony\Contracts\Translation namespace
* @deprecated since Symfony 4.2, use Symfony\Contracts\Translation\TranslatorInterface instead
*/
interface TranslatorInterface extends BaseTranslatorInterface
interface TranslatorInterface
{
/**
* Translates the given message.
*
* @param string $id The message id (may also be an object that can be cast to string)
* @param array $parameters An array of parameters for the message
* @param string|null $domain The domain for the message or null to use the default
* @param string|null $locale The locale or null to use the default
*
* @return string The translated string
*
* @throws InvalidArgumentException If the locale contains invalid characters
*/
public function trans($id, array $parameters = array(), $domain = null, $locale = null);
/**
* Translates the given choice message by choosing a translation according to a number.
*
* @param string $id The message id (may also be an object that can be cast to string)
* @param int $number The number to use to find the indice of the message
* @param array $parameters An array of parameters for the message
* @param string|null $domain The domain for the message or null to use the default
* @param string|null $locale The locale or null to use the default
*
* @return string The translated string
*
* @throws InvalidArgumentException If the locale contains invalid characters
*/
public function transChoice($id, $number, array $parameters = array(), $domain = null, $locale = null);
/**
* Sets the current locale.
*
* @param string $locale The locale
*
* @throws InvalidArgumentException If the locale contains invalid characters
*/
public function setLocale($locale);
/**
* Returns the current locale.
*
* @return string The locale
*/
public function getLocale();
}

View File

@ -11,6 +11,7 @@
namespace Symfony\Component\Validator\Context;
use Symfony\Component\Translation\TranslatorInterface as LegacyTranslatorInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintViolation;
use Symfony\Component\Validator\ConstraintViolationList;
@ -140,8 +141,11 @@ class ExecutionContext implements ExecutionContextInterface
* @internal Called by {@link ExecutionContextFactory}. Should not be used
* in user code.
*/
public function __construct(ValidatorInterface $validator, $root, TranslatorInterface $translator, string $translationDomain = null)
public function __construct(ValidatorInterface $validator, $root, $translator, string $translationDomain = null)
{
if (!$translator instanceof LegacyTranslatorInterface && !$translator instanceof TranslatorInterface) {
throw new \TypeError(sprintf('Argument 3 passed to %s() must be an instance of %s, %s given.', __METHOD__, TranslatorInterface::class, \is_object($translator) ? \get_class($translator) : \gettype($translator)));
}
$this->validator = $validator;
$this->root = $root;
$this->translator = $translator;

View File

@ -11,6 +11,7 @@
namespace Symfony\Component\Validator\Context;
use Symfony\Component\Translation\TranslatorInterface as LegacyTranslatorInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
@ -34,8 +35,12 @@ class ExecutionContextFactory implements ExecutionContextFactoryInterface
* use for translating
* violation messages
*/
public function __construct(TranslatorInterface $translator, string $translationDomain = null)
public function __construct($translator, string $translationDomain = null)
{
if (!$translator instanceof LegacyTranslatorInterface && !$translator instanceof TranslatorInterface) {
throw new \TypeError(sprintf('Argument 1 passed to %s() must be an instance of %s, %s given.', __METHOD__, TranslatorInterface::class, \is_object($translator) ? \get_class($translator) : \gettype($translator)));
}
$this->translator = $translator;
$this->translationDomain = $translationDomain;
}

View File

@ -17,7 +17,7 @@ use Symfony\Contracts\Translation\TranslatorInterface;
/**
* @internal to be removed in Symfony 5.0.
*/
class LegacyTranslatorProxy implements LegacyTranslatorInterface
class LegacyTranslatorProxy implements LegacyTranslatorInterface, TranslatorInterface
{
private $translator;
@ -60,6 +60,6 @@ class LegacyTranslatorProxy implements LegacyTranslatorInterface
*/
public function transChoice($id, $number, array $parameters = array(), $domain = null, $locale = null)
{
return $this->translator->transChoice($id, $number, $parameters, $domain, $locale);
return $this->translator->trans($id, array('%count%' => $number) + $parameters, $domain, $locale);
}
}

View File

@ -11,6 +11,7 @@
namespace Symfony\Component\Validator\Violation;
use Symfony\Component\Translation\TranslatorInterface as LegacyTranslatorInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintViolation;
use Symfony\Component\Validator\ConstraintViolationList;
@ -43,8 +44,14 @@ class ConstraintViolationBuilder implements ConstraintViolationBuilderInterface
*/
private $cause;
public function __construct(ConstraintViolationList $violations, Constraint $constraint, $message, array $parameters, $root, $propertyPath, $invalidValue, TranslatorInterface $translator, $translationDomain = null)
/**
* @param TranslatorInterface $translator
*/
public function __construct(ConstraintViolationList $violations, Constraint $constraint, $message, array $parameters, $root, $propertyPath, $invalidValue, $translator, $translationDomain = null)
{
if (!$translator instanceof LegacyTranslatorInterface && !$translator instanceof TranslatorInterface) {
throw new \TypeError(sprintf('Argument 8 passed to %s() must be an instance of %s, %s given.', __METHOD__, TranslatorInterface::class, \is_object($translator) ? \get_class($translator) : \gettype($translator)));
}
$this->violations = $violations;
$this->message = $message;
$this->parameters = $parameters;
@ -147,6 +154,12 @@ class ConstraintViolationBuilder implements ConstraintViolationBuilderInterface
$this->parameters,
$this->translationDomain
);
} elseif ($this->translator instanceof TranslatorInterface) {
$translatedMessage = $this->translator->trans(
$this->message,
array('%count%' => $this->plural) + $this->parameters,
$this->translationDomain
);
} else {
try {
$translatedMessage = $this->translator->transChoice(

View File

@ -50,24 +50,24 @@ class TranslatorTest extends TestCase
/**
* @dataProvider getTransChoiceTests
*/
public function testTransChoiceWithExplicitLocale($expected, $id, $number, $parameters)
public function testTransChoiceWithExplicitLocale($expected, $id, $number)
{
$translator = $this->getTranslator();
$translator->setLocale('en');
$this->assertEquals($expected, $translator->transChoice($id, $number, $parameters));
$this->assertEquals($expected, $translator->trans($id, array('%count%' => $number)));
}
/**
* @dataProvider getTransChoiceTests
*/
public function testTransChoiceWithDefaultLocale($expected, $id, $number, $parameters)
public function testTransChoiceWithDefaultLocale($expected, $id, $number)
{
\Locale::setDefault('en');
$translator = $this->getTranslator();
$this->assertEquals($expected, $translator->transChoice($id, $number, $parameters));
$this->assertEquals($expected, $translator->trans($id, array('%count%' => $number)));
}
public function testGetSetLocale()
@ -103,14 +103,14 @@ class TranslatorTest extends TestCase
public function getTransChoiceTests()
{
return array(
array('There are no apples', '{0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples', 0, array('%count%' => 0)),
array('There is one apple', '{0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples', 1, array('%count%' => 1)),
array('There are 10 apples', '{0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples', 10, array('%count%' => 10)),
array('There are 0 apples', 'There is 1 apple|There are %count% apples', 0, array('%count%' => 0)),
array('There is 1 apple', 'There is 1 apple|There are %count% apples', 1, array('%count%' => 1)),
array('There are 10 apples', 'There is 1 apple|There are %count% apples', 10, array('%count%' => 10)),
array('There are no apples', '{0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples', 0),
array('There is one apple', '{0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples', 1),
array('There are 10 apples', '{0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples', 10),
array('There are 0 apples', 'There is 1 apple|There are %count% apples', 0),
array('There is 1 apple', 'There is 1 apple|There are %count% apples', 1),
array('There are 10 apples', 'There is 1 apple|There are %count% apples', 10),
// custom validation messages may be coded with a fixed value
array('There are 2 apples', 'There are 2 apples', 2, array('%count%' => 2)),
array('There are 2 apples', 'There are 2 apples', 2),
);
}
@ -121,7 +121,7 @@ class TranslatorTest extends TestCase
{
$translator = $this->getTranslator();
$this->assertEquals($expected, $translator->transChoice($interval.' foo|[1,Inf[ bar', $number));
$this->assertEquals($expected, $translator->trans($interval.' foo|[1,Inf[ bar', array('%count%' => $number)));
}
public function getInternal()
@ -146,14 +146,14 @@ class TranslatorTest extends TestCase
{
$translator = $this->getTranslator();
$this->assertEquals($expected, $translator->transChoice($id, $number));
$this->assertEquals($expected, $translator->trans($id, array('%count%' => $number)));
}
public function testReturnMessageIfExactlyOneStandardRuleIsGiven()
{
$translator = $this->getTranslator();
$this->assertEquals('There are two apples', $translator->transChoice('There are two apples', 2));
$this->assertEquals('There are two apples', $translator->trans('There are two apples', array('%count%' => 2)));
}
/**
@ -164,7 +164,7 @@ class TranslatorTest extends TestCase
{
$translator = $this->getTranslator();
$translator->transChoice($id, $number);
$translator->trans($id, array('%count%' => $number));
}
public function getNonMatchingMessages()
@ -186,29 +186,29 @@ class TranslatorTest extends TestCase
array('There is one apple', '{0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples', 1),
array('There are %count% apples', '{0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples', 10),
array('There are %count% apples', '{0} There are no apples|{1} There is one apple|]1,Inf]There are %count% apples', 10),
array('There are %count% apples', '{0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples', 10),
array('There are 10 apples', '{0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples', 10),
array('There are 10 apples', '{0} There are no apples|{1} There is one apple|]1,Inf]There are %count% apples', 10),
array('There are 10 apples', '{0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples', 10),
array('There are %count% apples', 'There is one apple|There are %count% apples', 0),
array('There are 0 apples', 'There is one apple|There are %count% apples', 0),
array('There is one apple', 'There is one apple|There are %count% apples', 1),
array('There are %count% apples', 'There is one apple|There are %count% apples', 10),
array('There are 10 apples', 'There is one apple|There are %count% apples', 10),
array('There are %count% apples', 'one: There is one apple|more: There are %count% apples', 0),
array('There are 0 apples', 'one: There is one apple|more: There are %count% apples', 0),
array('There is one apple', 'one: There is one apple|more: There are %count% apples', 1),
array('There are %count% apples', 'one: There is one apple|more: There are %count% apples', 10),
array('There are 10 apples', 'one: There is one apple|more: There are %count% apples', 10),
array('There are no apples', '{0} There are no apples|one: There is one apple|more: There are %count% apples', 0),
array('There is one apple', '{0} There are no apples|one: There is one apple|more: There are %count% apples', 1),
array('There are %count% apples', '{0} There are no apples|one: There is one apple|more: There are %count% apples', 10),
array('There are 10 apples', '{0} There are no apples|one: There is one apple|more: There are %count% apples', 10),
array('', '{0}|{1} There is one apple|]1,Inf] There are %count% apples', 0),
array('', '{0} There are no apples|{1}|]1,Inf] There are %count% apples', 1),
// Indexed only tests which are Gettext PoFile* compatible strings.
array('There are %count% apples', 'There is one apple|There are %count% apples', 0),
array('There are 0 apples', 'There is one apple|There are %count% apples', 0),
array('There is one apple', 'There is one apple|There are %count% apples', 1),
array('There are %count% apples', 'There is one apple|There are %count% apples', 2),
array('There are 2 apples', 'There is one apple|There are %count% apples', 2),
// Tests for float numbers
array('There is almost one apple', '{0} There are no apples|]0,1[ There is almost one apple|{1} There is one apple|[1,Inf] There is more than one apple', 0.7),

View File

@ -19,19 +19,8 @@ interface TranslatorInterface
/**
* Translates the given message.
*
* @param string $id The message id (may also be an object that can be cast to string)
* @param array $parameters An array of parameters for the message
* @param string|null $domain The domain for the message or null to use the default
* @param string|null $locale The locale or null to use the default
*
* @return string The translated string
*
* @throws \InvalidArgumentException If the locale contains invalid characters
*/
public function trans($id, array $parameters = array(), $domain = null, $locale = null);
/**
* Translates the given choice message by choosing a translation according to a number.
* When a number is provided as a parameter named "%count%", the message is parsed for plural
* forms and a translation is chosen according to this number using the following rules:
*
* Given a message with different plural translations separated by a
* pipe (|), this method returns the correct portion of the message based
@ -64,7 +53,6 @@ interface TranslatorInterface
* @see https://en.wikipedia.org/wiki/ISO_31-11
*
* @param string $id The message id (may also be an object that can be cast to string)
* @param int $number The number to use to find the indice of the message
* @param array $parameters An array of parameters for the message
* @param string|null $domain The domain for the message or null to use the default
* @param string|null $locale The locale or null to use the default
@ -73,7 +61,7 @@ interface TranslatorInterface
*
* @throws \InvalidArgumentException If the locale contains invalid characters
*/
public function transChoice($id, $number, array $parameters = array(), $domain = null, $locale = null);
public function trans($id, array $parameters = array(), $domain = null, $locale = null);
/**
* Sets the current locale.

View File

@ -42,17 +42,14 @@ trait TranslatorTrait
* {@inheritdoc}
*/
public function trans($id, array $parameters = array(), $domain = null, $locale = null)
{
return strtr((string) $id, $parameters);
}
/**
* {@inheritdoc}
*/
public function transChoice($id, $number, array $parameters = array(), $domain = null, $locale = null)
{
$id = (string) $id;
$number = (float) $number;
if (!isset($parameters['%count%']) || !is_numeric($parameters['%count%'])) {
return strtr($id, $parameters);
}
$number = (float) $parameters['%count%'];
$locale = (string) $locale ?: $this->getLocale();
$parts = array();