From d870a850bd71fd1fc0b340cc291f62868f52d317 Mon Sep 17 00:00:00 2001 From: Nyholm Date: Wed, 5 Sep 2018 21:29:06 +0200 Subject: [PATCH 1/2] [Translator] Deprecated transChoice and moved it away from contracts --- UPGRADE-4.2.md | 1 + .../Twig/Extension/TranslationExtension.php | 7 +- .../Templating/Helper/TranslatorHelper.php | 7 +- .../Tests/Translation/TranslatorTest.php | 44 ++- .../Component/Translation/CHANGELOG.md | 1 + .../Translation/DataCollectorTranslator.php | 11 +- .../Translation/IdentityTranslator.php | 11 +- .../Translation/LegacyTranslatorTrait.php | 110 +++++++ .../Translation/LoggingTranslator.php | 11 +- .../Component/Translation/MessageSelector.php | 6 +- .../Tests/DataCollectorTranslatorTest.php | 34 ++- .../Tests/Formatter/MessageFormatterTest.php | 1 + .../Tests/IdentityTranslatorTest.php | 189 ++++++++++++ .../Tests/LoggingTranslatorTest.php | 22 +- .../Translation/Tests/TranslatorTest.php | 12 + .../Component/Translation/Translator.php | 1 + .../Translation/TranslatorInterface.php | 48 ++- .../Validator/Util/LegacyTranslatorProxy.php | 10 +- .../Violation/ConstraintViolationBuilder.php | 3 +- .../Tests/Translation/TranslatorTest.php | 274 ------------------ .../Translation/TranslatorInterface.php | 45 --- .../Contracts/Translation/TranslatorTrait.php | 212 -------------- 22 files changed, 494 insertions(+), 566 deletions(-) create mode 100644 src/Symfony/Component/Translation/LegacyTranslatorTrait.php diff --git a/UPGRADE-4.2.md b/UPGRADE-4.2.md index 18b88f2fa2..95f2b980c8 100644 --- a/UPGRADE-4.2.md +++ b/UPGRADE-4.2.md @@ -219,6 +219,7 @@ Translation ----------- * The `TranslatorInterface` has been deprecated in favor of `Symfony\Contracts\Translation\TranslatorInterface` + * The `Translator::transChoice()` has been deprecated in favor of using `Translator::trans()` with intl message format * The `MessageSelector`, `Interval` and `PluralizationRules` classes have been deprecated, use `IdentityTranslator` instead * The `Translator::getFallbackLocales()` and `TranslationDataCollector::getFallbackLocales()` method have been marked as internal diff --git a/src/Symfony/Bridge/Twig/Extension/TranslationExtension.php b/src/Symfony/Bridge/Twig/Extension/TranslationExtension.php index 607302d991..090aed5548 100644 --- a/src/Symfony/Bridge/Twig/Extension/TranslationExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/TranslationExtension.php @@ -16,6 +16,8 @@ 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\LegacyTranslatorTrait; +use Symfony\Component\Translation\TranslatorInterface as LegacyTranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorTrait; use Twig\Extension\AbstractExtension; @@ -34,6 +36,9 @@ class TranslationExtension extends AbstractExtension getLocale as private; setLocale as private; trans as private doTrans; + } + + use LegacyTranslatorTrait { transChoice as private doTransChoice; } @@ -107,7 +112,7 @@ class TranslationExtension extends AbstractExtension public function transchoice($message, $count, array $arguments = array(), $domain = null, $locale = null) { - if (null === $this->translator) { + if (null === $this->translator || !$this->translator instanceof LegacyTranslatorInterface) { return $this->doTransChoice($message, $count, array_merge(array('%count%' => $count), $arguments), $domain, $locale); } diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/TranslatorHelper.php b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/TranslatorHelper.php index b3dd76d42a..2052fef695 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/TranslatorHelper.php +++ b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/TranslatorHelper.php @@ -12,6 +12,8 @@ namespace Symfony\Bundle\FrameworkBundle\Templating\Helper; use Symfony\Component\Templating\Helper\Helper; +use Symfony\Component\Translation\LegacyTranslatorTrait; +use Symfony\Component\Translation\TranslatorInterface as LegacyTranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorTrait; @@ -24,6 +26,9 @@ class TranslatorHelper extends Helper getLocale as private; setLocale as private; trans as private doTrans; + } + + use LegacyTranslatorTrait { transChoice as private doTransChoice; } @@ -51,7 +56,7 @@ class TranslatorHelper extends Helper */ public function transChoice($id, $number, array $parameters = array(), $domain = 'messages', $locale = null) { - if (null === $this->translator) { + if (null === $this->translator || !$this->translator instanceof LegacyTranslatorInterface) { return $this->doTransChoice($id, $number, $parameters, $domain, $locale); } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Translation/TranslatorTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Translation/TranslatorTest.php index eeb4e0daa4..93e070040b 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Translation/TranslatorTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Translation/TranslatorTest.php @@ -52,14 +52,25 @@ 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 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 +81,8 @@ 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')); @@ -88,14 +97,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. diff --git a/src/Symfony/Component/Translation/CHANGELOG.md b/src/Symfony/Component/Translation/CHANGELOG.md index 6bbecdd099..87b7e5a9f1 100644 --- a/src/Symfony/Component/Translation/CHANGELOG.md +++ b/src/Symfony/Component/Translation/CHANGELOG.md @@ -5,6 +5,7 @@ CHANGELOG ----- * Started using ICU parent locales as fallback locales. + * deprecated `Translator::transChoice()` in favor of using `Translator::trans()` with intl message format * deprecated `TranslatorInterface` in favor of `Symfony\Contracts\Translation\TranslatorInterface` * deprecated `MessageSelector`, `Interval` and `PluralizationRules`; use `IdentityTranslator` instead * Added `IntlMessageFormatter` and `FallbackMessageFormatter` diff --git a/src/Symfony/Component/Translation/DataCollectorTranslator.php b/src/Symfony/Component/Translation/DataCollectorTranslator.php index 10ac93516b..fd8c135e26 100644 --- a/src/Symfony/Component/Translation/DataCollectorTranslator.php +++ b/src/Symfony/Component/Translation/DataCollectorTranslator.php @@ -20,6 +20,10 @@ use Symfony\Contracts\Translation\TranslatorInterface; */ class DataCollectorTranslator implements LegacyTranslatorInterface, TranslatorBagInterface { + use LegacyTranslatorTrait { + transChoice as private doTransChoice; + } + const MESSAGE_DEFINED = 0; const MESSAGE_MISSING = 1; const MESSAGE_EQUALS_FALLBACK = 2; @@ -59,7 +63,12 @@ class DataCollectorTranslator implements LegacyTranslatorInterface, TranslatorBa */ public function transChoice($id, $number, array $parameters = array(), $domain = null, $locale = null) { - $trans = $this->translator->transChoice($id, $number, $parameters, $domain, $locale); + if ($this->translator instanceof LegacyTranslatorInterface) { + $trans = $this->translator->transChoice($id, $number, $parameters, $domain, $locale); + } else { + $trans = $this->doTransChoice($id, $number, $parameters, $domain, $locale); + } + $this->collectMessage($locale, $domain, $id, $trans, $parameters, $number); return $trans; diff --git a/src/Symfony/Component/Translation/IdentityTranslator.php b/src/Symfony/Component/Translation/IdentityTranslator.php index b69312ec79..c138563049 100644 --- a/src/Symfony/Component/Translation/IdentityTranslator.php +++ b/src/Symfony/Component/Translation/IdentityTranslator.php @@ -20,7 +20,8 @@ use Symfony\Contracts\Translation\TranslatorTrait; */ class IdentityTranslator implements TranslatorInterface { - use TranslatorTrait { + use TranslatorTrait; + use LegacyTranslatorTrait { transChoice as private doTransChoice; } @@ -43,15 +44,11 @@ class IdentityTranslator implements TranslatorInterface */ 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()" method with intl formatted messages instead.', __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); } - - private function getPluralizationRule(int $number, string $locale): int - { - return PluralizationRules::get($number, $locale, false); - } } diff --git a/src/Symfony/Component/Translation/LegacyTranslatorTrait.php b/src/Symfony/Component/Translation/LegacyTranslatorTrait.php new file mode 100644 index 0000000000..1b24372206 --- /dev/null +++ b/src/Symfony/Component/Translation/LegacyTranslatorTrait.php @@ -0,0 +1,110 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation; + +use Symfony\Component\Translation\Exception\InvalidArgumentException; + +/** + * @author Tobias Nyholm + * @author Fabien Potencier + * + * @deprecated since Symfony 4.2, use IdentityTranslator instead + */ +trait LegacyTranslatorTrait +{ + /** + * Implementation of Symfony\Component\Translation\TranslationInterface::transChoice + * {@inheritdoc} + */ + 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()" method with intl formatted messages instead.', __METHOD__), E_USER_DEPRECATED); + + $id = (string) $id; + $number = (float) $number; + $locale = (string) $locale ?: $this->getLocale(); + + $parts = array(); + if (preg_match('/^\|++$/', $id)) { + $parts = explode('|', $id); + } elseif (preg_match_all('/(?:\|\||[^\|])++/', $id, $matches)) { + $parts = $matches[0]; + } + + $intervalRegexp = <<<'EOF' +/^(?P + ({\s* + (\-?\d+(\.\d+)?[\s*,\s*\-?\d+(\.\d+)?]*) + \s*}) + + | + + (?P[\[\]]) + \s* + (?P-Inf|\-?\d+(\.\d+)?) + \s*,\s* + (?P\+?Inf|\-?\d+(\.\d+)?) + \s* + (?P[\[\]]) +)\s*(?P.*?)$/xs +EOF; + + $standardRules = array(); + foreach ($parts as $part) { + $part = trim(str_replace('||', '|', $part)); + + // try to match an explicit rule, then fallback to the standard ones + if (preg_match($intervalRegexp, $part, $matches)) { + if ($matches[2]) { + foreach (explode(',', $matches[3]) as $n) { + if ($number == $n) { + return strtr($matches['message'], $parameters); + } + } + } else { + $leftNumber = '-Inf' === $matches['left'] ? -INF : (float) $matches['left']; + $rightNumber = \is_numeric($matches['right']) ? (float) $matches['right'] : INF; + + if (('[' === $matches['left_delimiter'] ? $number >= $leftNumber : $number > $leftNumber) + && (']' === $matches['right_delimiter'] ? $number <= $rightNumber : $number < $rightNumber) + ) { + return strtr($matches['message'], $parameters); + } + } + } elseif (preg_match('/^\w+\:\s*(.*?)$/', $part, $matches)) { + $standardRules[] = $matches[1]; + } else { + $standardRules[] = $part; + } + } + + $position = PluralizationRules::get($number, $locale, false); + + if (!isset($standardRules[$position])) { + // when there's exactly one rule given, and that rule is a standard + // rule, use this rule + if (1 === \count($parts) && isset($standardRules[0])) { + return strtr($standardRules[0], $parameters); + } + + $message = sprintf('Unable to choose a translation for "%s" with locale "%s" for value "%d". Double check that this translation has the correct plural options (e.g. "There is one apple|There are %%count%% apples").', $id, $locale, $number); + + if (\class_exists(InvalidArgumentException::class)) { + throw new InvalidArgumentException($message); + } + + throw new \InvalidArgumentException($message); + } + + return strtr($standardRules[$position], $parameters); + } +} diff --git a/src/Symfony/Component/Translation/LoggingTranslator.php b/src/Symfony/Component/Translation/LoggingTranslator.php index cfb60ab0b0..7a10952a2a 100644 --- a/src/Symfony/Component/Translation/LoggingTranslator.php +++ b/src/Symfony/Component/Translation/LoggingTranslator.php @@ -21,6 +21,10 @@ use Symfony\Contracts\Translation\TranslatorInterface; */ class LoggingTranslator implements LegacyTranslatorInterface, TranslatorBagInterface { + use LegacyTranslatorTrait { + transChoice as private doTransChoice; + } + /** * @var TranslatorInterface|TranslatorBagInterface */ @@ -58,7 +62,12 @@ class LoggingTranslator implements LegacyTranslatorInterface, TranslatorBagInter */ public function transChoice($id, $number, array $parameters = array(), $domain = null, $locale = null) { - $trans = $this->translator->transChoice($id, $number, $parameters, $domain, $locale); + if ($this->translator instanceof LegacyTranslatorInterface) { + $trans = $this->translator->transChoice($id, $number, $parameters, $domain, $locale); + } else { + $trans = $this->doTransChoice($id, $number, $parameters, $domain, $locale); + } + $this->log($id, $domain, $locale); return $trans; diff --git a/src/Symfony/Component/Translation/MessageSelector.php b/src/Symfony/Component/Translation/MessageSelector.php index 867a345ced..06639417b6 100644 --- a/src/Symfony/Component/Translation/MessageSelector.php +++ b/src/Symfony/Component/Translation/MessageSelector.php @@ -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 * diff --git a/src/Symfony/Component/Translation/Tests/DataCollectorTranslatorTest.php b/src/Symfony/Component/Translation/Tests/DataCollectorTranslatorTest.php index 1cdd33b395..ae15339bce 100644 --- a/src/Symfony/Component/Translation/Tests/DataCollectorTranslatorTest.php +++ b/src/Symfony/Component/Translation/Tests/DataCollectorTranslatorTest.php @@ -25,7 +25,6 @@ class DataCollectorTranslatorTest extends TestCase $collector->trans('foo'); $collector->trans('bar'); - $collector->transChoice('choice', 0); $collector->trans('bar_ru'); $collector->trans('bar_ru', array('foo' => 'bar')); @@ -48,15 +47,6 @@ class DataCollectorTranslatorTest extends TestCase 'parameters' => array(), 'transChoiceNumber' => null, ); - $expectedMessages[] = array( - 'id' => 'choice', - 'translation' => 'choice', - 'locale' => 'en', - 'domain' => 'messages', - 'state' => DataCollectorTranslator::MESSAGE_MISSING, - 'parameters' => array(), - 'transChoiceNumber' => 0, - ); $expectedMessages[] = array( 'id' => 'bar_ru', 'translation' => 'bar (ru)', @@ -79,6 +69,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(), + 'transChoiceNumber' => 0, + ); + + $this->assertEquals($expectedMessages, $collector->getCollectedMessages()); + } + private function createCollector() { $translator = new Translator('en'); diff --git a/src/Symfony/Component/Translation/Tests/Formatter/MessageFormatterTest.php b/src/Symfony/Component/Translation/Tests/Formatter/MessageFormatterTest.php index 1fa736e7e3..f4c96aa12f 100644 --- a/src/Symfony/Component/Translation/Tests/Formatter/MessageFormatterTest.php +++ b/src/Symfony/Component/Translation/Tests/Formatter/MessageFormatterTest.php @@ -26,6 +26,7 @@ class MessageFormatterTest extends TestCase /** * @dataProvider getTransChoiceMessages + * @group legacy */ public function testFormatPlural($expected, $message, $number, $parameters) { diff --git a/src/Symfony/Component/Translation/Tests/IdentityTranslatorTest.php b/src/Symfony/Component/Translation/Tests/IdentityTranslatorTest.php index be0a548aa1..443c63718e 100644 --- a/src/Symfony/Component/Translation/Tests/IdentityTranslatorTest.php +++ b/src/Symfony/Component/Translation/Tests/IdentityTranslatorTest.php @@ -20,4 +20,193 @@ class IdentityTranslatorTest extends TranslatorTest { return new IdentityTranslator(); } + + /** + * @dataProvider getTransChoiceTests + * @group legacy + */ + public function testTransChoiceWithExplicitLocale($expected, $id, $number, $parameters) + { + $translator = $this->getTranslator(); + $translator->setLocale('en'); + + $this->assertEquals($expected, $translator->transChoice($id, $number, $parameters)); + } + + /** + * @dataProvider getTransChoiceTests + * @group legacy + */ + public function testTransChoiceWithDefaultLocale($expected, $id, $number, $parameters) + { + \Locale::setDefault('en'); + + $translator = $this->getTranslator(); + + $this->assertEquals($expected, $translator->transChoice($id, $number, $parameters)); + } + + /** + * @dataProvider getInternal + * @group legacy + */ + public function testInterval($expected, $number, $interval) + { + $translator = $this->getTranslator(); + + $this->assertEquals($expected, $translator->transChoice($interval.' foo|[1,Inf[ bar', $number)); + } + + /** + * @dataProvider getChooseTests + * @group legacy + */ + public function testChoose($expected, $id, $number) + { + $translator = $this->getTranslator(); + + $this->assertEquals($expected, $translator->transChoice($id, $number)); + } + + /** + * @group legacy + */ + public function testReturnMessageIfExactlyOneStandardRuleIsGiven() + { + $translator = $this->getTranslator(); + + $this->assertEquals('There are two apples', $translator->transChoice('There are two apples', 2)); + } + + /** + * @dataProvider getNonMatchingMessages + * @expectedException \InvalidArgumentException + * @group legacy + */ + public function testThrowExceptionIfMatchingMessageCannotBeFound($id, $number) + { + $translator = $this->getTranslator(); + + $translator->transChoice($id, $number); + } + + 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)), + // custom validation messages may be coded with a fixed value + array('There are 2 apples', 'There are 2 apples', 2, array('%count%' => 2)), + ); + } + + public function getInternal() + { + return array( + array('foo', 3, '{1,2, 3 ,4}'), + array('bar', 10, '{1,2, 3 ,4}'), + array('bar', 3, '[1,2]'), + array('foo', 1, '[1,2]'), + array('foo', 2, '[1,2]'), + array('bar', 1, ']1,2['), + array('bar', 2, ']1,2['), + array('foo', log(0), '[-Inf,2['), + array('foo', -log(0), '[-2,+Inf]'), + ); + } + + public function getNonMatchingMessages() + { + return array( + array('{0} There are no apples|{1} There is one apple', 2), + array('{1} There is one apple|]1,Inf] There are %count% apples', 0), + array('{1} There is one apple|]2,Inf] There are %count% apples', 2), + array('{0} There are no apples|There is one apple', 2), + ); + } + + public function getChooseTests() + { + 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('There are no apples', '{0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples', 0), + 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 %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 %count% 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 %count% 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 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('', '{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 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), + + // 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), + array('There is one apple', '{0} There are no apples|]0,1[There are %count% apples|{1} There is one apple|[1,Inf] There is more than one apple', 1), + array('There is more than one apple', '{0} There are no apples|]0,1[There are %count% apples|{1} There is one apple|[1,Inf] There is more than one apple', 1.7), + array('There are no apples', '{0} There are no apples|]0,1[There are %count% apples|{1} There is one apple|[1,Inf] There is more than one apple', 0), + array('There are no apples', '{0} There are no apples|]0,1[There are %count% apples|{1} There is one apple|[1,Inf] There is more than one apple', 0.0), + array('There are no apples', '{0.0} There are no apples|]0,1[There are %count% apples|{1} There is one apple|[1,Inf] There is more than one apple', 0), + + // Test texts with new-lines + // with double-quotes and \n in id & double-quotes and actual newlines in text + array("This is a text with a\n new-line in it. Selector = 0.", '{0}This is a text with a + new-line in it. Selector = 0.|{1}This is a text with a + new-line in it. Selector = 1.|[1,Inf]This is a text with a + new-line in it. Selector > 1.', 0), + // with double-quotes and \n in id and single-quotes and actual newlines in text + array("This is a text with a\n new-line in it. Selector = 1.", '{0}This is a text with a + new-line in it. Selector = 0.|{1}This is a text with a + new-line in it. Selector = 1.|[1,Inf]This is a text with a + new-line in it. Selector > 1.', 1), + array("This is a text with a\n new-line in it. Selector > 1.", '{0}This is a text with a + new-line in it. Selector = 0.|{1}This is a text with a + new-line in it. Selector = 1.|[1,Inf]This is a text with a + new-line in it. Selector > 1.', 5), + // with double-quotes and id split accros lines + array('This is a text with a + new-line in it. Selector = 1.', '{0}This is a text with a + new-line in it. Selector = 0.|{1}This is a text with a + new-line in it. Selector = 1.|[1,Inf]This is a text with a + new-line in it. Selector > 1.', 1), + // with single-quotes and id split accros lines + array('This is a text with a + new-line in it. Selector > 1.', '{0}This is a text with a + new-line in it. Selector = 0.|{1}This is a text with a + new-line in it. Selector = 1.|[1,Inf]This is a text with a + new-line in it. Selector > 1.', 5), + // with single-quotes and \n in text + array('This is a text with a\nnew-line in it. Selector = 0.', '{0}This is a text with a\nnew-line in it. Selector = 0.|{1}This is a text with a\nnew-line in it. Selector = 1.|[1,Inf]This is a text with a\nnew-line in it. Selector > 1.', 0), + // with double-quotes and id split accros lines + array("This is a text with a\nnew-line in it. Selector = 1.", "{0}This is a text with a\nnew-line in it. Selector = 0.|{1}This is a text with a\nnew-line in it. Selector = 1.|[1,Inf]This is a text with a\nnew-line in it. Selector > 1.", 1), + // esacape pipe + array('This is a text with | in it. Selector = 0.', '{0}This is a text with || in it. Selector = 0.|{1}This is a text with || in it. Selector = 1.', 0), + // Empty plural set (2 plural forms) from a .PO file + array('', '|', 1), + // Empty plural set (3 plural forms) from a .PO file + array('', '||', 1), + ); + } } diff --git a/src/Symfony/Component/Translation/Tests/LoggingTranslatorTest.php b/src/Symfony/Component/Translation/Tests/LoggingTranslatorTest.php index 022379e934..0e43cbecf4 100644 --- a/src/Symfony/Component/Translation/Tests/LoggingTranslatorTest.php +++ b/src/Symfony/Component/Translation/Tests/LoggingTranslatorTest.php @@ -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)); + } } diff --git a/src/Symfony/Component/Translation/Tests/TranslatorTest.php b/src/Symfony/Component/Translation/Tests/TranslatorTest.php index b0efefb7d0..c632d69041 100644 --- a/src/Symfony/Component/Translation/Tests/TranslatorTest.php +++ b/src/Symfony/Component/Translation/Tests/TranslatorTest.php @@ -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) { @@ -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'); diff --git a/src/Symfony/Component/Translation/Translator.php b/src/Symfony/Component/Translation/Translator.php index b9a876497d..5670e613e3 100644 --- a/src/Symfony/Component/Translation/Translator.php +++ b/src/Symfony/Component/Translation/Translator.php @@ -202,6 +202,7 @@ class Translator implements TranslatorInterface, TranslatorBagInterface */ 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()" method with intl formatted messages instead.', __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))); } diff --git a/src/Symfony/Component/Translation/TranslatorInterface.php b/src/Symfony/Component/Translation/TranslatorInterface.php index 829c6f3d14..2eea19866b 100644 --- a/src/Symfony/Component/Translation/TranslatorInterface.php +++ b/src/Symfony/Component/Translation/TranslatorInterface.php @@ -16,8 +16,54 @@ use Symfony\Contracts\Translation\TranslatorInterface as BaseTranslatorInterface /** * @author Fabien Potencier * - * @deprecated since Symfony 4.2, use the same interface from the Symfony\Contracts\Translation namespace + * @deprecated since Symfony 4.2, use the interface from the Symfony\Contracts\Translation namespace */ interface TranslatorInterface extends BaseTranslatorInterface { + /** + * Translates the given choice message by choosing a translation according to a number. + * + * Given a message with different plural translations separated by a + * pipe (|), this method returns the correct portion of the message based + * on the given number, locale and the pluralization rules in the message + * itself. + * + * The message supports two different types of pluralization rules: + * + * interval: {0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples + * indexed: There is one apple|There are %count% apples + * + * The indexed solution can also contain labels (e.g. one: There is one apple). + * This is purely for making the translations more clear - it does not + * affect the functionality. + * + * The two methods can also be mixed: + * {0} There are no apples|one: There is one apple|more: There are %count% apples + * + * An interval can represent a finite set of numbers: + * {1,2,3,4} + * + * An interval can represent numbers between two numbers: + * [1, +Inf] + * ]-1,2[ + * + * The left delimiter can be [ (inclusive) or ] (exclusive). + * The right delimiter can be [ (exclusive) or ] (inclusive). + * Beside numbers, you can use -Inf and +Inf for the infinite. + * + * @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 + * + * @return string The translated string + * + * @throws \InvalidArgumentException If the locale contains invalid characters + * + * @deprecated since Symfony 4.2 + */ + public function transChoice($id, $number, array $parameters = array(), $domain = null, $locale = null); } diff --git a/src/Symfony/Component/Validator/Util/LegacyTranslatorProxy.php b/src/Symfony/Component/Validator/Util/LegacyTranslatorProxy.php index 077d37931f..972c9bd931 100644 --- a/src/Symfony/Component/Validator/Util/LegacyTranslatorProxy.php +++ b/src/Symfony/Component/Validator/Util/LegacyTranslatorProxy.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Validator\Util; +use Symfony\Component\Translation\LegacyTranslatorTrait; use Symfony\Component\Translation\TranslatorInterface as LegacyTranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface; @@ -19,6 +20,9 @@ use Symfony\Contracts\Translation\TranslatorInterface; */ class LegacyTranslatorProxy implements LegacyTranslatorInterface { + use LegacyTranslatorTrait { + transChoice as private doTransChoice; + } private $translator; public function __construct(TranslatorInterface $translator) @@ -60,6 +64,10 @@ 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); + if ($this->translator instanceof LegacyTranslatorInterface) { + return $this->translator->transChoice($id, $number, $parameters, $domain, $locale); + } + + $this->doTransChoice($id, $number, $parameters, $domain, $locale); } } diff --git a/src/Symfony/Component/Validator/Violation/ConstraintViolationBuilder.php b/src/Symfony/Component/Validator/Violation/ConstraintViolationBuilder.php index 621c38d9af..34b49f962a 100644 --- a/src/Symfony/Component/Validator/Violation/ConstraintViolationBuilder.php +++ b/src/Symfony/Component/Validator/Violation/ConstraintViolationBuilder.php @@ -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; @@ -141,7 +142,7 @@ class ConstraintViolationBuilder implements ConstraintViolationBuilderInterface */ public function addViolation() { - if (null === $this->plural) { + if (null === $this->plural || !$this->translator instanceof LegacyTranslatorInterface) { $translatedMessage = $this->translator->trans( $this->message, $this->parameters, diff --git a/src/Symfony/Contracts/Tests/Translation/TranslatorTest.php b/src/Symfony/Contracts/Tests/Translation/TranslatorTest.php index e7af8e4da3..a7642bfa11 100644 --- a/src/Symfony/Contracts/Tests/Translation/TranslatorTest.php +++ b/src/Symfony/Contracts/Tests/Translation/TranslatorTest.php @@ -47,29 +47,6 @@ class TranslatorTest extends TestCase $this->assertEquals($expected, $translator->trans($id, $parameters)); } - /** - * @dataProvider getTransChoiceTests - */ - public function testTransChoiceWithExplicitLocale($expected, $id, $number, $parameters) - { - $translator = $this->getTranslator(); - $translator->setLocale('en'); - - $this->assertEquals($expected, $translator->transChoice($id, $number, $parameters)); - } - - /** - * @dataProvider getTransChoiceTests - */ - public function testTransChoiceWithDefaultLocale($expected, $id, $number, $parameters) - { - \Locale::setDefault('en'); - - $translator = $this->getTranslator(); - - $this->assertEquals($expected, $translator->transChoice($id, $number, $parameters)); - } - public function testGetSetLocale() { $translator = $this->getTranslator(); @@ -99,255 +76,4 @@ class TranslatorTest extends TestCase array('Symfony is awesome!', 'Symfony is %what%!', array('%what%' => 'awesome')), ); } - - 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)), - // custom validation messages may be coded with a fixed value - array('There are 2 apples', 'There are 2 apples', 2, array('%count%' => 2)), - ); - } - - /** - * @dataProvider getInternal - */ - public function testInterval($expected, $number, $interval) - { - $translator = $this->getTranslator(); - - $this->assertEquals($expected, $translator->transChoice($interval.' foo|[1,Inf[ bar', $number)); - } - - public function getInternal() - { - return array( - array('foo', 3, '{1,2, 3 ,4}'), - array('bar', 10, '{1,2, 3 ,4}'), - array('bar', 3, '[1,2]'), - array('foo', 1, '[1,2]'), - array('foo', 2, '[1,2]'), - array('bar', 1, ']1,2['), - array('bar', 2, ']1,2['), - array('foo', log(0), '[-Inf,2['), - array('foo', -log(0), '[-2,+Inf]'), - ); - } - - /** - * @dataProvider getChooseTests - */ - public function testChoose($expected, $id, $number) - { - $translator = $this->getTranslator(); - - $this->assertEquals($expected, $translator->transChoice($id, $number)); - } - - public function testReturnMessageIfExactlyOneStandardRuleIsGiven() - { - $translator = $this->getTranslator(); - - $this->assertEquals('There are two apples', $translator->transChoice('There are two apples', 2)); - } - - /** - * @dataProvider getNonMatchingMessages - * @expectedException \InvalidArgumentException - */ - public function testThrowExceptionIfMatchingMessageCannotBeFound($id, $number) - { - $translator = $this->getTranslator(); - - $translator->transChoice($id, $number); - } - - public function getNonMatchingMessages() - { - return array( - array('{0} There are no apples|{1} There is one apple', 2), - array('{1} There is one apple|]1,Inf] There are %count% apples', 0), - array('{1} There is one apple|]2,Inf] There are %count% apples', 2), - array('{0} There are no apples|There is one apple', 2), - ); - } - - public function getChooseTests() - { - 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('There are no apples', '{0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples', 0), - 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 %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 %count% 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 %count% 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 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('', '{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 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), - - // 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), - array('There is one apple', '{0} There are no apples|]0,1[There are %count% apples|{1} There is one apple|[1,Inf] There is more than one apple', 1), - array('There is more than one apple', '{0} There are no apples|]0,1[There are %count% apples|{1} There is one apple|[1,Inf] There is more than one apple', 1.7), - array('There are no apples', '{0} There are no apples|]0,1[There are %count% apples|{1} There is one apple|[1,Inf] There is more than one apple', 0), - array('There are no apples', '{0} There are no apples|]0,1[There are %count% apples|{1} There is one apple|[1,Inf] There is more than one apple', 0.0), - array('There are no apples', '{0.0} There are no apples|]0,1[There are %count% apples|{1} There is one apple|[1,Inf] There is more than one apple', 0), - - // Test texts with new-lines - // with double-quotes and \n in id & double-quotes and actual newlines in text - array("This is a text with a\n new-line in it. Selector = 0.", '{0}This is a text with a - new-line in it. Selector = 0.|{1}This is a text with a - new-line in it. Selector = 1.|[1,Inf]This is a text with a - new-line in it. Selector > 1.', 0), - // with double-quotes and \n in id and single-quotes and actual newlines in text - array("This is a text with a\n new-line in it. Selector = 1.", '{0}This is a text with a - new-line in it. Selector = 0.|{1}This is a text with a - new-line in it. Selector = 1.|[1,Inf]This is a text with a - new-line in it. Selector > 1.', 1), - array("This is a text with a\n new-line in it. Selector > 1.", '{0}This is a text with a - new-line in it. Selector = 0.|{1}This is a text with a - new-line in it. Selector = 1.|[1,Inf]This is a text with a - new-line in it. Selector > 1.', 5), - // with double-quotes and id split accros lines - array('This is a text with a - new-line in it. Selector = 1.', '{0}This is a text with a - new-line in it. Selector = 0.|{1}This is a text with a - new-line in it. Selector = 1.|[1,Inf]This is a text with a - new-line in it. Selector > 1.', 1), - // with single-quotes and id split accros lines - array('This is a text with a - new-line in it. Selector > 1.', '{0}This is a text with a - new-line in it. Selector = 0.|{1}This is a text with a - new-line in it. Selector = 1.|[1,Inf]This is a text with a - new-line in it. Selector > 1.', 5), - // with single-quotes and \n in text - array('This is a text with a\nnew-line in it. Selector = 0.', '{0}This is a text with a\nnew-line in it. Selector = 0.|{1}This is a text with a\nnew-line in it. Selector = 1.|[1,Inf]This is a text with a\nnew-line in it. Selector > 1.', 0), - // with double-quotes and id split accros lines - array("This is a text with a\nnew-line in it. Selector = 1.", "{0}This is a text with a\nnew-line in it. Selector = 0.|{1}This is a text with a\nnew-line in it. Selector = 1.|[1,Inf]This is a text with a\nnew-line in it. Selector > 1.", 1), - // esacape pipe - array('This is a text with | in it. Selector = 0.', '{0}This is a text with || in it. Selector = 0.|{1}This is a text with || in it. Selector = 1.', 0), - // Empty plural set (2 plural forms) from a .PO file - array('', '|', 1), - // Empty plural set (3 plural forms) from a .PO file - array('', '||', 1), - ); - } - - /** - * @dataProvider failingLangcodes - */ - public function testFailedLangcodes($nplural, $langCodes) - { - $matrix = $this->generateTestData($langCodes); - $this->validateMatrix($nplural, $matrix, false); - } - - /** - * @dataProvider successLangcodes - */ - public function testLangcodes($nplural, $langCodes) - { - $matrix = $this->generateTestData($langCodes); - $this->validateMatrix($nplural, $matrix); - } - - /** - * This array should contain all currently known langcodes. - * - * As it is impossible to have this ever complete we should try as hard as possible to have it almost complete. - * - * @return array - */ - public function successLangcodes() - { - return array( - array('1', array('ay', 'bo', 'cgg', 'dz', 'id', 'ja', 'jbo', 'ka', 'kk', 'km', 'ko', 'ky')), - array('2', array('nl', 'fr', 'en', 'de', 'de_GE', 'hy', 'hy_AM')), - array('3', array('be', 'bs', 'cs', 'hr')), - array('4', array('cy', 'mt', 'sl')), - array('6', array('ar')), - ); - } - - /** - * This array should be at least empty within the near future. - * - * This both depends on a complete list trying to add above as understanding - * the plural rules of the current failing languages. - * - * @return array with nplural together with langcodes - */ - public function failingLangcodes() - { - return array( - array('1', array('fa')), - array('2', array('jbo')), - array('3', array('cbs')), - array('4', array('gd', 'kw')), - array('5', array('ga')), - ); - } - - /** - * We validate only on the plural coverage. Thus the real rules is not tested. - * - * @param string $nplural Plural expected - * @param array $matrix Containing langcodes and their plural index values - * @param bool $expectSuccess - */ - protected function validateMatrix($nplural, $matrix, $expectSuccess = true) - { - foreach ($matrix as $langCode => $data) { - $indexes = array_flip($data); - if ($expectSuccess) { - $this->assertEquals($nplural, \count($indexes), "Langcode '$langCode' has '$nplural' plural forms."); - } else { - $this->assertNotEquals((int) $nplural, \count($indexes), "Langcode '$langCode' has '$nplural' plural forms."); - } - } - } - - protected function generateTestData($langCodes) - { - $translator = new class() { - use TranslatorTrait { - getPluralizationRule as public; - } - }; - - $matrix = array(); - foreach ($langCodes as $langCode) { - for ($count = 0; $count < 200; ++$count) { - $plural = $translator->getPluralizationRule($count, $langCode); - $matrix[$langCode][$count] = $plural; - } - } - - return $matrix; - } } diff --git a/src/Symfony/Contracts/Translation/TranslatorInterface.php b/src/Symfony/Contracts/Translation/TranslatorInterface.php index a54718733b..7620fd3787 100644 --- a/src/Symfony/Contracts/Translation/TranslatorInterface.php +++ b/src/Symfony/Contracts/Translation/TranslatorInterface.php @@ -30,51 +30,6 @@ interface TranslatorInterface */ public function trans($id, array $parameters = array(), $domain = null, $locale = null); - /** - * Translates the given choice message by choosing a translation according to a number. - * - * Given a message with different plural translations separated by a - * pipe (|), this method returns the correct portion of the message based - * on the given number, locale and the pluralization rules in the message - * itself. - * - * The message supports two different types of pluralization rules: - * - * interval: {0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples - * indexed: There is one apple|There are %count% apples - * - * The indexed solution can also contain labels (e.g. one: There is one apple). - * This is purely for making the translations more clear - it does not - * affect the functionality. - * - * The two methods can also be mixed: - * {0} There are no apples|one: There is one apple|more: There are %count% apples - * - * An interval can represent a finite set of numbers: - * {1,2,3,4} - * - * An interval can represent numbers between two numbers: - * [1, +Inf] - * ]-1,2[ - * - * The left delimiter can be [ (inclusive) or ] (exclusive). - * The right delimiter can be [ (exclusive) or ] (inclusive). - * Beside numbers, you can use -Inf and +Inf for the infinite. - * - * @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 - * - * @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. * diff --git a/src/Symfony/Contracts/Translation/TranslatorTrait.php b/src/Symfony/Contracts/Translation/TranslatorTrait.php index 7c392b2bd4..b92105f190 100644 --- a/src/Symfony/Contracts/Translation/TranslatorTrait.php +++ b/src/Symfony/Contracts/Translation/TranslatorTrait.php @@ -11,8 +11,6 @@ namespace Symfony\Contracts\Translation; -use Symfony\Component\Translation\Exception\InvalidArgumentException; - /** * A trait to help implement TranslatorInterface. * @@ -45,214 +43,4 @@ trait TranslatorTrait { return strtr((string) $id, $parameters); } - - /** - * {@inheritdoc} - */ - public function transChoice($id, $number, array $parameters = array(), $domain = null, $locale = null) - { - $id = (string) $id; - $number = (float) $number; - $locale = (string) $locale ?: $this->getLocale(); - - $parts = array(); - if (preg_match('/^\|++$/', $id)) { - $parts = explode('|', $id); - } elseif (preg_match_all('/(?:\|\||[^\|])++/', $id, $matches)) { - $parts = $matches[0]; - } - - $intervalRegexp = <<<'EOF' -/^(?P - ({\s* - (\-?\d+(\.\d+)?[\s*,\s*\-?\d+(\.\d+)?]*) - \s*}) - - | - - (?P[\[\]]) - \s* - (?P-Inf|\-?\d+(\.\d+)?) - \s*,\s* - (?P\+?Inf|\-?\d+(\.\d+)?) - \s* - (?P[\[\]]) -)\s*(?P.*?)$/xs -EOF; - - $standardRules = array(); - foreach ($parts as $part) { - $part = trim(str_replace('||', '|', $part)); - - // try to match an explicit rule, then fallback to the standard ones - if (preg_match($intervalRegexp, $part, $matches)) { - if ($matches[2]) { - foreach (explode(',', $matches[3]) as $n) { - if ($number == $n) { - return strtr($matches['message'], $parameters); - } - } - } else { - $leftNumber = '-Inf' === $matches['left'] ? -INF : (float) $matches['left']; - $rightNumber = \is_numeric($matches['right']) ? (float) $matches['right'] : INF; - - if (('[' === $matches['left_delimiter'] ? $number >= $leftNumber : $number > $leftNumber) - && (']' === $matches['right_delimiter'] ? $number <= $rightNumber : $number < $rightNumber) - ) { - return strtr($matches['message'], $parameters); - } - } - } elseif (preg_match('/^\w+\:\s*(.*?)$/', $part, $matches)) { - $standardRules[] = $matches[1]; - } else { - $standardRules[] = $part; - } - } - - $position = $this->getPluralizationRule($number, $locale); - - if (!isset($standardRules[$position])) { - // when there's exactly one rule given, and that rule is a standard - // rule, use this rule - if (1 === \count($parts) && isset($standardRules[0])) { - return strtr($standardRules[0], $parameters); - } - - $message = sprintf('Unable to choose a translation for "%s" with locale "%s" for value "%d". Double check that this translation has the correct plural options (e.g. "There is one apple|There are %%count%% apples").', $id, $locale, $number); - - if (\class_exists(InvalidArgumentException::class)) { - throw new InvalidArgumentException($message); - } - - throw new \InvalidArgumentException($message); - } - - return strtr($standardRules[$position], $parameters); - } - - /** - * Returns the plural position to use for the given locale and number. - * - * The plural rules are derived from code of the Zend Framework (2010-09-25), - * which is subject to the new BSD license (http://framework.zend.com/license/new-bsd). - * Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com) - */ - private function getPluralizationRule(int $number, string $locale): int - { - switch ('pt_BR' !== $locale && \strlen($locale) > 3 ? substr($locale, 0, strrpos($locale, '_')) : $locale) { - case 'af': - case 'bn': - case 'bg': - case 'ca': - case 'da': - case 'de': - case 'el': - case 'en': - case 'eo': - case 'es': - case 'et': - case 'eu': - case 'fa': - case 'fi': - case 'fo': - case 'fur': - case 'fy': - case 'gl': - case 'gu': - case 'ha': - case 'he': - case 'hu': - case 'is': - case 'it': - case 'ku': - case 'lb': - case 'ml': - case 'mn': - case 'mr': - case 'nah': - case 'nb': - case 'ne': - case 'nl': - case 'nn': - case 'no': - case 'oc': - case 'om': - case 'or': - case 'pa': - case 'pap': - case 'ps': - case 'pt': - case 'so': - case 'sq': - case 'sv': - case 'sw': - case 'ta': - case 'te': - case 'tk': - case 'ur': - case 'zu': - return (1 == $number) ? 0 : 1; - - case 'am': - case 'bh': - case 'fil': - case 'fr': - case 'gun': - case 'hi': - case 'hy': - case 'ln': - case 'mg': - case 'nso': - case 'pt_BR': - case 'ti': - case 'wa': - return ((0 == $number) || (1 == $number)) ? 0 : 1; - - case 'be': - case 'bs': - case 'hr': - case 'ru': - case 'sh': - case 'sr': - case 'uk': - return ((1 == $number % 10) && (11 != $number % 100)) ? 0 : ((($number % 10 >= 2) && ($number % 10 <= 4) && (($number % 100 < 10) || ($number % 100 >= 20))) ? 1 : 2); - - case 'cs': - case 'sk': - return (1 == $number) ? 0 : ((($number >= 2) && ($number <= 4)) ? 1 : 2); - - case 'ga': - return (1 == $number) ? 0 : ((2 == $number) ? 1 : 2); - - case 'lt': - return ((1 == $number % 10) && (11 != $number % 100)) ? 0 : ((($number % 10 >= 2) && (($number % 100 < 10) || ($number % 100 >= 20))) ? 1 : 2); - - case 'sl': - return (1 == $number % 100) ? 0 : ((2 == $number % 100) ? 1 : (((3 == $number % 100) || (4 == $number % 100)) ? 2 : 3)); - - case 'mk': - return (1 == $number % 10) ? 0 : 1; - - case 'mt': - return (1 == $number) ? 0 : (((0 == $number) || (($number % 100 > 1) && ($number % 100 < 11))) ? 1 : ((($number % 100 > 10) && ($number % 100 < 20)) ? 2 : 3)); - - case 'lv': - return (0 == $number) ? 0 : (((1 == $number % 10) && (11 != $number % 100)) ? 1 : 2); - - case 'pl': - return (1 == $number) ? 0 : ((($number % 10 >= 2) && ($number % 10 <= 4) && (($number % 100 < 12) || ($number % 100 > 14))) ? 1 : 2); - - case 'cy': - return (1 == $number) ? 0 : ((2 == $number) ? 1 : (((8 == $number) || (11 == $number)) ? 2 : 3)); - - case 'ro': - return (1 == $number) ? 0 : (((0 == $number) || (($number % 100 > 0) && ($number % 100 < 20))) ? 1 : 2); - - case 'ar': - return (0 == $number) ? 0 : ((1 == $number) ? 1 : ((2 == $number) ? 2 : ((($number % 100 >= 3) && ($number % 100 <= 10)) ? 3 : ((($number % 100 >= 11) && ($number % 100 <= 99)) ? 4 : 5)))); - - default: - return 0; - } - } } From dc5f3bfff7b83eb95fcbd150b99e87c010014f5b Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 1 Oct 2018 13:27:53 +0200 Subject: [PATCH 2/2] Make trans + %count% parameter resolve plurals --- UPGRADE-4.2.md | 8 +- UPGRADE-5.0.md | 3 + src/Symfony/Bridge/Twig/CHANGELOG.md | 5 +- .../Twig/Extension/TranslationExtension.php | 37 ++- src/Symfony/Bridge/Twig/Node/TransNode.php | 19 +- .../Extension/TranslationExtensionTest.php | 85 +++++- .../Bridge/Twig/Tests/Node/TransNodeTest.php | 2 +- .../Tests/Translation/TwigExtractorTest.php | 30 +- .../TokenParser/TransChoiceTokenParser.php | 4 + .../Twig/TokenParser/TransTokenParser.php | 9 +- .../Bundle/FrameworkBundle/CHANGELOG.md | 1 + .../Command/TranslationDebugCommand.php | 9 +- .../Templating/Helper/TranslatorHelper.php | 23 +- .../Tests/Translation/TranslatorTest.php | 4 + .../Form/Extension/Csrf/CsrfExtension.php | 8 +- .../EventListener/CsrfValidationListener.php | 9 +- .../Csrf/Type/FormTypeCsrfExtension.php | 9 +- .../Type/UploadValidatorExtension.php | 9 +- .../EventListener/TranslatorListener.php | 9 +- .../Component/Translation/CHANGELOG.md | 2 +- .../Translation/DataCollectorTranslator.php | 28 +- .../ChoiceMessageFormatterInterface.php | 2 + .../Formatter/MessageFormatter.php | 17 +- .../Translation/IdentityTranslator.php | 19 +- .../Translation/LegacyTranslatorTrait.php | 110 ------- .../Translation/LoggingTranslator.php | 19 +- .../Tests/DataCollectorTranslatorTest.php | 12 +- .../Tests/IdentityTranslatorTest.php | 189 ------------ .../Translation/Tests/TranslatorTest.php | 2 +- .../Component/Translation/Translator.php | 9 +- .../Translation/TranslatorInterface.php | 72 ++--- .../Validator/Context/ExecutionContext.php | 6 +- .../Context/ExecutionContextFactory.php | 7 +- .../Validator/Util/LegacyTranslatorProxy.php | 12 +- .../Violation/ConstraintViolationBuilder.php | 16 +- .../Tests/Translation/TranslatorTest.php | 274 ++++++++++++++++++ .../Translation/TranslatorInterface.php | 33 +++ .../Contracts/Translation/TranslatorTrait.php | 211 +++++++++++++- 38 files changed, 890 insertions(+), 433 deletions(-) delete mode 100644 src/Symfony/Component/Translation/LegacyTranslatorTrait.php diff --git a/UPGRADE-4.2.md b/UPGRADE-4.2.md index 95f2b980c8..6b3c4423d1 100644 --- a/UPGRADE-4.2.md +++ b/UPGRADE-4.2.md @@ -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,10 +220,15 @@ Translation ----------- * The `TranslatorInterface` has been deprecated in favor of `Symfony\Contracts\Translation\TranslatorInterface` - * The `Translator::transChoice()` has been deprecated in favor of using `Translator::trans()` with intl message format + * 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 --------- diff --git a/UPGRADE-5.0.md b/UPGRADE-5.0.md index e7470a2467..a876f151b9 100644 --- a/UPGRADE-5.0.md +++ b/UPGRADE-5.0.md @@ -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 -------- diff --git a/src/Symfony/Bridge/Twig/CHANGELOG.md b/src/Symfony/Bridge/Twig/CHANGELOG.md index a2e55f6db2..341bcfa538 100644 --- a/src/Symfony/Bridge/Twig/CHANGELOG.md +++ b/src/Symfony/Bridge/Twig/CHANGELOG.md @@ -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 ----- diff --git a/src/Symfony/Bridge/Twig/Extension/TranslationExtension.php b/src/Symfony/Bridge/Twig/Extension/TranslationExtension.php index 090aed5548..ec5d452cef 100644 --- a/src/Symfony/Bridge/Twig/Extension/TranslationExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/TranslationExtension.php @@ -16,7 +16,6 @@ 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\LegacyTranslatorTrait; use Symfony\Component\Translation\TranslatorInterface as LegacyTranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorTrait; @@ -29,6 +28,8 @@ use Twig\TwigFilter; * Provides integration of the Translation component with Twig. * * @author Fabien Potencier + * + * @final since Symfony 4.2 */ class TranslationExtension extends AbstractExtension { @@ -38,21 +39,28 @@ class TranslationExtension extends AbstractExtension trans as private doTrans; } - use LegacyTranslatorTrait { - 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; } @@ -63,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%')), ); } @@ -101,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); } @@ -110,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 || !$this->translator instanceof LegacyTranslatorInterface) { - return $this->doTransChoice($message, $count, array_merge(array('%count%' => $count), $arguments), $domain, $locale); + if (null === $this->translator) { + 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); diff --git a/src/Symfony/Bridge/Twig/Node/TransNode.php b/src/Symfony/Bridge/Twig/Node/TransNode.php index 7eb8d743e9..578e37dd2c 100644 --- a/src/Symfony/Bridge/Twig/Node/TransNode.php +++ b/src/Symfony/Bridge/Twig/Node/TransNode.php @@ -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"); } diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/TranslationExtensionTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/TranslationExtensionTest.php index feba2b2b6b..bb36cda8ea 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/TranslationExtensionTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/TranslationExtensionTest.php @@ -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 %} ', diff --git a/src/Symfony/Bridge/Twig/Tests/Node/TransNodeTest.php b/src/Symfony/Bridge/Twig/Tests/Node/TransNodeTest.php index d45f60c09d..e4a20d72fc 100644 --- a/src/Symfony/Bridge/Twig/Tests/Node/TransNodeTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Node/TransNodeTest.php @@ -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') ), diff --git a/src/Symfony/Bridge/Twig/Tests/Translation/TwigExtractorTest.php b/src/Symfony/Bridge/Twig/Tests/Translation/TwigExtractorTest.php index e46ba205e5..fca5f4d4a9 100644 --- a/src/Symfony/Bridge/Twig/Tests/Translation/TwigExtractorTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Translation/TwigExtractorTest.php @@ -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')), ); } diff --git a/src/Symfony/Bridge/Twig/TokenParser/TransChoiceTokenParser.php b/src/Symfony/Bridge/Twig/TokenParser/TransChoiceTokenParser.php index 5261d7ad52..490fb14ff4 100644 --- a/src/Symfony/Bridge/Twig/TokenParser/TransChoiceTokenParser.php +++ b/src/Symfony/Bridge/Twig/TokenParser/TransChoiceTokenParser.php @@ -23,6 +23,8 @@ use Twig\Token; * Token Parser for the 'transchoice' tag. * * @author Fabien Potencier + * + * @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(); diff --git a/src/Symfony/Bridge/Twig/TokenParser/TransTokenParser.php b/src/Symfony/Bridge/Twig/TokenParser/TransTokenParser.php index 76c8dc0610..f5bf12b57e 100644 --- a/src/Symfony/Bridge/Twig/TokenParser/TransTokenParser.php +++ b/src/Symfony/Bridge/Twig/TokenParser/TransTokenParser.php @@ -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) diff --git a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md index e2e5666489..ed75ed1f90 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md @@ -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 ----- diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/TranslationDebugCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/TranslationDebugCommand.php index dc14dea0ef..6e2c1324ba 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/TranslationDebugCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/TranslationDebugCommand.php @@ -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; diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/TranslatorHelper.php b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/TranslatorHelper.php index 2052fef695..8120a0cd8e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/TranslatorHelper.php +++ b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/TranslatorHelper.php @@ -12,7 +12,6 @@ namespace Symfony\Bundle\FrameworkBundle\Templating\Helper; use Symfony\Component\Templating\Helper\Helper; -use Symfony\Component\Translation\LegacyTranslatorTrait; use Symfony\Component\Translation\TranslatorInterface as LegacyTranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorTrait; @@ -28,14 +27,16 @@ class TranslatorHelper extends Helper trans as private doTrans; } - use LegacyTranslatorTrait { - 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; } @@ -53,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) { - if (null === $this->translator || !$this->translator instanceof LegacyTranslatorInterface) { - return $this->doTransChoice($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 (null === $this->translator) { + 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); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Translation/TranslatorTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Translation/TranslatorTest.php index 93e070040b..f5a4818553 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Translation/TranslatorTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Translation/TranslatorTest.php @@ -52,8 +52,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->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->trans('other choice', array('%count%' => 1))); $this->assertEquals('foobarbaz (fr.UTF-8)', $translator->trans('foobarbaz')); $this->assertEquals('foobarbax (sr@latin)', $translator->trans('foobarbax')); } @@ -81,8 +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->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->trans('other choice', array('%count%' => 1))); $this->assertEquals('foobarbaz (fr.UTF-8)', $translator->trans('foobarbaz')); $this->assertEquals('foobarbax (sr@latin)', $translator->trans('foobarbax')); diff --git a/src/Symfony/Component/Form/Extension/Csrf/CsrfExtension.php b/src/Symfony/Component/Form/Extension/Csrf/CsrfExtension.php index 8c519a81ec..d84d09b386 100644 --- a/src/Symfony/Component/Form/Extension/Csrf/CsrfExtension.php +++ b/src/Symfony/Component/Form/Extension/Csrf/CsrfExtension.php @@ -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; diff --git a/src/Symfony/Component/Form/Extension/Csrf/EventListener/CsrfValidationListener.php b/src/Symfony/Component/Form/Extension/Csrf/EventListener/CsrfValidationListener.php index f898737843..4a49a85fcd 100644 --- a/src/Symfony/Component/Form/Extension/Csrf/EventListener/CsrfValidationListener.php +++ b/src/Symfony/Component/Form/Extension/Csrf/EventListener/CsrfValidationListener.php @@ -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; diff --git a/src/Symfony/Component/Form/Extension/Csrf/Type/FormTypeCsrfExtension.php b/src/Symfony/Component/Form/Extension/Csrf/Type/FormTypeCsrfExtension.php index 35621c585b..7e16f6dc37 100644 --- a/src/Symfony/Component/Form/Extension/Csrf/Type/FormTypeCsrfExtension.php +++ b/src/Symfony/Component/Form/Extension/Csrf/Type/FormTypeCsrfExtension.php @@ -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; diff --git a/src/Symfony/Component/Form/Extension/Validator/Type/UploadValidatorExtension.php b/src/Symfony/Component/Form/Extension/Validator/Type/UploadValidatorExtension.php index fa09cbc21f..9e5eec8001 100644 --- a/src/Symfony/Component/Form/Extension/Validator/Type/UploadValidatorExtension.php +++ b/src/Symfony/Component/Form/Extension/Validator/Type/UploadValidatorExtension.php @@ -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; } diff --git a/src/Symfony/Component/HttpKernel/EventListener/TranslatorListener.php b/src/Symfony/Component/HttpKernel/EventListener/TranslatorListener.php index 792e347816..7cf059886d 100644 --- a/src/Symfony/Component/HttpKernel/EventListener/TranslatorListener.php +++ b/src/Symfony/Component/HttpKernel/EventListener/TranslatorListener.php @@ -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; } diff --git a/src/Symfony/Component/Translation/CHANGELOG.md b/src/Symfony/Component/Translation/CHANGELOG.md index 87b7e5a9f1..e3a4725c3d 100644 --- a/src/Symfony/Component/Translation/CHANGELOG.md +++ b/src/Symfony/Component/Translation/CHANGELOG.md @@ -5,7 +5,7 @@ CHANGELOG ----- * Started using ICU parent locales as fallback locales. - * deprecated `Translator::transChoice()` in favor of using `Translator::trans()` with intl message format + * 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` diff --git a/src/Symfony/Component/Translation/DataCollectorTranslator.php b/src/Symfony/Component/Translation/DataCollectorTranslator.php index fd8c135e26..3e2087220d 100644 --- a/src/Symfony/Component/Translation/DataCollectorTranslator.php +++ b/src/Symfony/Component/Translation/DataCollectorTranslator.php @@ -18,12 +18,8 @@ use Symfony\Contracts\Translation\TranslatorInterface; /** * @author Abdellatif Ait boudad */ -class DataCollectorTranslator implements LegacyTranslatorInterface, TranslatorBagInterface +class DataCollectorTranslator implements LegacyTranslatorInterface, TranslatorInterface, TranslatorBagInterface { - use LegacyTranslatorTrait { - transChoice as private doTransChoice; - } - const MESSAGE_DEFINED = 0; const MESSAGE_MISSING = 1; const MESSAGE_EQUALS_FALLBACK = 2; @@ -38,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))); } @@ -60,16 +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 LegacyTranslatorInterface) { - $trans = $this->translator->transChoice($id, $number, $parameters, $domain, $locale); - } else { - $trans = $this->doTransChoice($id, $number, $parameters, $domain, $locale); + if ($this->translator instanceof TranslatorInterface) { + $trans = $this->translator->trans($id, array('%count%' => $number) + $parameters, $domain, $locale); } - $this->collectMessage($locale, $domain, $id, $trans, $parameters, $number); + $trans = $this->translator->transChoice($id, $number, $parameters, $domain, $locale); + + $this->collectMessage($locale, $domain, $id, $trans, array('%count%' => $number) + $parameters); return $trans; } @@ -134,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'; @@ -169,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, ); } } diff --git a/src/Symfony/Component/Translation/Formatter/ChoiceMessageFormatterInterface.php b/src/Symfony/Component/Translation/Formatter/ChoiceMessageFormatterInterface.php index 92acbcafe2..6bc68384af 100644 --- a/src/Symfony/Component/Translation/Formatter/ChoiceMessageFormatterInterface.php +++ b/src/Symfony/Component/Translation/Formatter/ChoiceMessageFormatterInterface.php @@ -13,6 +13,8 @@ namespace Symfony\Component\Translation\Formatter; /** * @author Abdellatif Ait boudad + * + * @deprecated since Symfony 4.2, use MessageFormatterInterface::format() with a %count% parameter instead */ interface ChoiceMessageFormatterInterface { diff --git a/src/Symfony/Component/Translation/Formatter/MessageFormatter.php b/src/Symfony/Component/Translation/Formatter/MessageFormatter.php index 862e03aab1..1119c356f5 100644 --- a/src/Symfony/Component/Translation/Formatter/MessageFormatter.php +++ b/src/Symfony/Component/Translation/Formatter/MessageFormatter.php @@ -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); } diff --git a/src/Symfony/Component/Translation/IdentityTranslator.php b/src/Symfony/Component/Translation/IdentityTranslator.php index c138563049..31abdaab12 100644 --- a/src/Symfony/Component/Translation/IdentityTranslator.php +++ b/src/Symfony/Component/Translation/IdentityTranslator.php @@ -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,12 +20,9 @@ use Symfony\Contracts\Translation\TranslatorTrait; * * @author Fabien Potencier */ -class IdentityTranslator implements TranslatorInterface +class IdentityTranslator implements LegacyTranslatorInterface, TranslatorInterface { use TranslatorTrait; - use LegacyTranslatorTrait { - transChoice as private doTransChoice; - } private $selector; @@ -41,14 +40,22 @@ 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()" method with intl formatted messages instead.', __METHOD__), E_USER_DEPRECATED); + @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, $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 + { + return PluralizationRules::get($number, $locale, false); } } diff --git a/src/Symfony/Component/Translation/LegacyTranslatorTrait.php b/src/Symfony/Component/Translation/LegacyTranslatorTrait.php deleted file mode 100644 index 1b24372206..0000000000 --- a/src/Symfony/Component/Translation/LegacyTranslatorTrait.php +++ /dev/null @@ -1,110 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Translation; - -use Symfony\Component\Translation\Exception\InvalidArgumentException; - -/** - * @author Tobias Nyholm - * @author Fabien Potencier - * - * @deprecated since Symfony 4.2, use IdentityTranslator instead - */ -trait LegacyTranslatorTrait -{ - /** - * Implementation of Symfony\Component\Translation\TranslationInterface::transChoice - * {@inheritdoc} - */ - 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()" method with intl formatted messages instead.', __METHOD__), E_USER_DEPRECATED); - - $id = (string) $id; - $number = (float) $number; - $locale = (string) $locale ?: $this->getLocale(); - - $parts = array(); - if (preg_match('/^\|++$/', $id)) { - $parts = explode('|', $id); - } elseif (preg_match_all('/(?:\|\||[^\|])++/', $id, $matches)) { - $parts = $matches[0]; - } - - $intervalRegexp = <<<'EOF' -/^(?P - ({\s* - (\-?\d+(\.\d+)?[\s*,\s*\-?\d+(\.\d+)?]*) - \s*}) - - | - - (?P[\[\]]) - \s* - (?P-Inf|\-?\d+(\.\d+)?) - \s*,\s* - (?P\+?Inf|\-?\d+(\.\d+)?) - \s* - (?P[\[\]]) -)\s*(?P.*?)$/xs -EOF; - - $standardRules = array(); - foreach ($parts as $part) { - $part = trim(str_replace('||', '|', $part)); - - // try to match an explicit rule, then fallback to the standard ones - if (preg_match($intervalRegexp, $part, $matches)) { - if ($matches[2]) { - foreach (explode(',', $matches[3]) as $n) { - if ($number == $n) { - return strtr($matches['message'], $parameters); - } - } - } else { - $leftNumber = '-Inf' === $matches['left'] ? -INF : (float) $matches['left']; - $rightNumber = \is_numeric($matches['right']) ? (float) $matches['right'] : INF; - - if (('[' === $matches['left_delimiter'] ? $number >= $leftNumber : $number > $leftNumber) - && (']' === $matches['right_delimiter'] ? $number <= $rightNumber : $number < $rightNumber) - ) { - return strtr($matches['message'], $parameters); - } - } - } elseif (preg_match('/^\w+\:\s*(.*?)$/', $part, $matches)) { - $standardRules[] = $matches[1]; - } else { - $standardRules[] = $part; - } - } - - $position = PluralizationRules::get($number, $locale, false); - - if (!isset($standardRules[$position])) { - // when there's exactly one rule given, and that rule is a standard - // rule, use this rule - if (1 === \count($parts) && isset($standardRules[0])) { - return strtr($standardRules[0], $parameters); - } - - $message = sprintf('Unable to choose a translation for "%s" with locale "%s" for value "%d". Double check that this translation has the correct plural options (e.g. "There is one apple|There are %%count%% apples").', $id, $locale, $number); - - if (\class_exists(InvalidArgumentException::class)) { - throw new InvalidArgumentException($message); - } - - throw new \InvalidArgumentException($message); - } - - return strtr($standardRules[$position], $parameters); - } -} diff --git a/src/Symfony/Component/Translation/LoggingTranslator.php b/src/Symfony/Component/Translation/LoggingTranslator.php index 7a10952a2a..ee711568ab 100644 --- a/src/Symfony/Component/Translation/LoggingTranslator.php +++ b/src/Symfony/Component/Translation/LoggingTranslator.php @@ -21,10 +21,6 @@ use Symfony\Contracts\Translation\TranslatorInterface; */ class LoggingTranslator implements LegacyTranslatorInterface, TranslatorBagInterface { - use LegacyTranslatorTrait { - transChoice as private doTransChoice; - } - /** * @var TranslatorInterface|TranslatorBagInterface */ @@ -36,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))); } @@ -59,13 +58,17 @@ 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) { - if ($this->translator instanceof LegacyTranslatorInterface) { - $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->doTransChoice($id, $number, $parameters, $domain, $locale); + $trans = $this->translator->transChoice($id, $number, $parameters, $domain, $locale); } $this->log($id, $domain, $locale); diff --git a/src/Symfony/Component/Translation/Tests/DataCollectorTranslatorTest.php b/src/Symfony/Component/Translation/Tests/DataCollectorTranslatorTest.php index ae15339bce..19e056c12e 100644 --- a/src/Symfony/Component/Translation/Tests/DataCollectorTranslatorTest.php +++ b/src/Symfony/Component/Translation/Tests/DataCollectorTranslatorTest.php @@ -25,6 +25,7 @@ class DataCollectorTranslatorTest extends TestCase $collector->trans('foo'); $collector->trans('bar'); + $collector->trans('choice', array('%count%' => 0)); $collector->trans('bar_ru'); $collector->trans('bar_ru', array('foo' => 'bar')); @@ -47,6 +48,15 @@ class DataCollectorTranslatorTest extends TestCase 'parameters' => array(), 'transChoiceNumber' => null, ); + $expectedMessages[] = array( + 'id' => 'choice', + 'translation' => 'choice', + 'locale' => 'en', + 'domain' => 'messages', + 'state' => DataCollectorTranslator::MESSAGE_MISSING, + 'parameters' => array('%count%' => 0), + 'transChoiceNumber' => 0, + ); $expectedMessages[] = array( 'id' => 'bar_ru', 'translation' => 'bar (ru)', @@ -86,7 +96,7 @@ class DataCollectorTranslatorTest extends TestCase 'locale' => 'en', 'domain' => 'messages', 'state' => DataCollectorTranslator::MESSAGE_MISSING, - 'parameters' => array(), + 'parameters' => array('%count%' => 0), 'transChoiceNumber' => 0, ); diff --git a/src/Symfony/Component/Translation/Tests/IdentityTranslatorTest.php b/src/Symfony/Component/Translation/Tests/IdentityTranslatorTest.php index 443c63718e..be0a548aa1 100644 --- a/src/Symfony/Component/Translation/Tests/IdentityTranslatorTest.php +++ b/src/Symfony/Component/Translation/Tests/IdentityTranslatorTest.php @@ -20,193 +20,4 @@ class IdentityTranslatorTest extends TranslatorTest { return new IdentityTranslator(); } - - /** - * @dataProvider getTransChoiceTests - * @group legacy - */ - public function testTransChoiceWithExplicitLocale($expected, $id, $number, $parameters) - { - $translator = $this->getTranslator(); - $translator->setLocale('en'); - - $this->assertEquals($expected, $translator->transChoice($id, $number, $parameters)); - } - - /** - * @dataProvider getTransChoiceTests - * @group legacy - */ - public function testTransChoiceWithDefaultLocale($expected, $id, $number, $parameters) - { - \Locale::setDefault('en'); - - $translator = $this->getTranslator(); - - $this->assertEquals($expected, $translator->transChoice($id, $number, $parameters)); - } - - /** - * @dataProvider getInternal - * @group legacy - */ - public function testInterval($expected, $number, $interval) - { - $translator = $this->getTranslator(); - - $this->assertEquals($expected, $translator->transChoice($interval.' foo|[1,Inf[ bar', $number)); - } - - /** - * @dataProvider getChooseTests - * @group legacy - */ - public function testChoose($expected, $id, $number) - { - $translator = $this->getTranslator(); - - $this->assertEquals($expected, $translator->transChoice($id, $number)); - } - - /** - * @group legacy - */ - public function testReturnMessageIfExactlyOneStandardRuleIsGiven() - { - $translator = $this->getTranslator(); - - $this->assertEquals('There are two apples', $translator->transChoice('There are two apples', 2)); - } - - /** - * @dataProvider getNonMatchingMessages - * @expectedException \InvalidArgumentException - * @group legacy - */ - public function testThrowExceptionIfMatchingMessageCannotBeFound($id, $number) - { - $translator = $this->getTranslator(); - - $translator->transChoice($id, $number); - } - - 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)), - // custom validation messages may be coded with a fixed value - array('There are 2 apples', 'There are 2 apples', 2, array('%count%' => 2)), - ); - } - - public function getInternal() - { - return array( - array('foo', 3, '{1,2, 3 ,4}'), - array('bar', 10, '{1,2, 3 ,4}'), - array('bar', 3, '[1,2]'), - array('foo', 1, '[1,2]'), - array('foo', 2, '[1,2]'), - array('bar', 1, ']1,2['), - array('bar', 2, ']1,2['), - array('foo', log(0), '[-Inf,2['), - array('foo', -log(0), '[-2,+Inf]'), - ); - } - - public function getNonMatchingMessages() - { - return array( - array('{0} There are no apples|{1} There is one apple', 2), - array('{1} There is one apple|]1,Inf] There are %count% apples', 0), - array('{1} There is one apple|]2,Inf] There are %count% apples', 2), - array('{0} There are no apples|There is one apple', 2), - ); - } - - public function getChooseTests() - { - 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('There are no apples', '{0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples', 0), - 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 %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 %count% 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 %count% 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 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('', '{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 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), - - // 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), - array('There is one apple', '{0} There are no apples|]0,1[There are %count% apples|{1} There is one apple|[1,Inf] There is more than one apple', 1), - array('There is more than one apple', '{0} There are no apples|]0,1[There are %count% apples|{1} There is one apple|[1,Inf] There is more than one apple', 1.7), - array('There are no apples', '{0} There are no apples|]0,1[There are %count% apples|{1} There is one apple|[1,Inf] There is more than one apple', 0), - array('There are no apples', '{0} There are no apples|]0,1[There are %count% apples|{1} There is one apple|[1,Inf] There is more than one apple', 0.0), - array('There are no apples', '{0.0} There are no apples|]0,1[There are %count% apples|{1} There is one apple|[1,Inf] There is more than one apple', 0), - - // Test texts with new-lines - // with double-quotes and \n in id & double-quotes and actual newlines in text - array("This is a text with a\n new-line in it. Selector = 0.", '{0}This is a text with a - new-line in it. Selector = 0.|{1}This is a text with a - new-line in it. Selector = 1.|[1,Inf]This is a text with a - new-line in it. Selector > 1.', 0), - // with double-quotes and \n in id and single-quotes and actual newlines in text - array("This is a text with a\n new-line in it. Selector = 1.", '{0}This is a text with a - new-line in it. Selector = 0.|{1}This is a text with a - new-line in it. Selector = 1.|[1,Inf]This is a text with a - new-line in it. Selector > 1.', 1), - array("This is a text with a\n new-line in it. Selector > 1.", '{0}This is a text with a - new-line in it. Selector = 0.|{1}This is a text with a - new-line in it. Selector = 1.|[1,Inf]This is a text with a - new-line in it. Selector > 1.', 5), - // with double-quotes and id split accros lines - array('This is a text with a - new-line in it. Selector = 1.', '{0}This is a text with a - new-line in it. Selector = 0.|{1}This is a text with a - new-line in it. Selector = 1.|[1,Inf]This is a text with a - new-line in it. Selector > 1.', 1), - // with single-quotes and id split accros lines - array('This is a text with a - new-line in it. Selector > 1.', '{0}This is a text with a - new-line in it. Selector = 0.|{1}This is a text with a - new-line in it. Selector = 1.|[1,Inf]This is a text with a - new-line in it. Selector > 1.', 5), - // with single-quotes and \n in text - array('This is a text with a\nnew-line in it. Selector = 0.', '{0}This is a text with a\nnew-line in it. Selector = 0.|{1}This is a text with a\nnew-line in it. Selector = 1.|[1,Inf]This is a text with a\nnew-line in it. Selector > 1.', 0), - // with double-quotes and id split accros lines - array("This is a text with a\nnew-line in it. Selector = 1.", "{0}This is a text with a\nnew-line in it. Selector = 0.|{1}This is a text with a\nnew-line in it. Selector = 1.|[1,Inf]This is a text with a\nnew-line in it. Selector > 1.", 1), - // esacape pipe - array('This is a text with | in it. Selector = 0.', '{0}This is a text with || in it. Selector = 0.|{1}This is a text with || in it. Selector = 1.', 0), - // Empty plural set (2 plural forms) from a .PO file - array('', '|', 1), - // Empty plural set (3 plural forms) from a .PO file - array('', '||', 1), - ); - } } diff --git a/src/Symfony/Component/Translation/Tests/TranslatorTest.php b/src/Symfony/Component/Translation/Tests/TranslatorTest.php index c632d69041..d630a7491d 100644 --- a/src/Symfony/Component/Translation/Tests/TranslatorTest.php +++ b/src/Symfony/Component/Translation/Tests/TranslatorTest.php @@ -502,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', ''), ); } diff --git a/src/Symfony/Component/Translation/Translator.php b/src/Symfony/Component/Translation/Translator.php index 5670e613e3..d8d27301f0 100644 --- a/src/Symfony/Component/Translation/Translator.php +++ b/src/Symfony/Component/Translation/Translator.php @@ -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 */ -class Translator implements TranslatorInterface, TranslatorBagInterface +class Translator implements LegacyTranslatorInterface, TranslatorInterface, TranslatorBagInterface { /** * @var MessageCatalogueInterface[] @@ -199,10 +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()" method with intl formatted messages instead.', __METHOD__), E_USER_DEPRECATED); + @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))); } diff --git a/src/Symfony/Component/Translation/TranslatorInterface.php b/src/Symfony/Component/Translation/TranslatorInterface.php index 2eea19866b..02cb5027b8 100644 --- a/src/Symfony/Component/Translation/TranslatorInterface.php +++ b/src/Symfony/Component/Translation/TranslatorInterface.php @@ -11,48 +11,34 @@ namespace Symfony\Component\Translation; -use Symfony\Contracts\Translation\TranslatorInterface as BaseTranslatorInterface; +use Symfony\Component\Translation\Exception\InvalidArgumentException; /** + * TranslatorInterface. + * * @author Fabien Potencier * - * @deprecated since Symfony 4.2, use the 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. * - * Given a message with different plural translations separated by a - * pipe (|), this method returns the correct portion of the message based - * on the given number, locale and the pluralization rules in the message - * itself. - * - * The message supports two different types of pluralization rules: - * - * interval: {0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples - * indexed: There is one apple|There are %count% apples - * - * The indexed solution can also contain labels (e.g. one: There is one apple). - * This is purely for making the translations more clear - it does not - * affect the functionality. - * - * The two methods can also be mixed: - * {0} There are no apples|one: There is one apple|more: There are %count% apples - * - * An interval can represent a finite set of numbers: - * {1,2,3,4} - * - * An interval can represent numbers between two numbers: - * [1, +Inf] - * ]-1,2[ - * - * The left delimiter can be [ (inclusive) or ] (exclusive). - * The right delimiter can be [ (exclusive) or ] (inclusive). - * Beside numbers, you can use -Inf and +Inf for the infinite. - * - * @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 @@ -61,9 +47,23 @@ interface TranslatorInterface extends BaseTranslatorInterface * * @return string The translated string * - * @throws \InvalidArgumentException If the locale contains invalid characters - * - * @deprecated since Symfony 4.2 + * @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(); } diff --git a/src/Symfony/Component/Validator/Context/ExecutionContext.php b/src/Symfony/Component/Validator/Context/ExecutionContext.php index 03f56e9175..fd9d9605e0 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContext.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContext.php @@ -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; diff --git a/src/Symfony/Component/Validator/Context/ExecutionContextFactory.php b/src/Symfony/Component/Validator/Context/ExecutionContextFactory.php index 0bb1be767e..b8d56eee9a 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContextFactory.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContextFactory.php @@ -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; } diff --git a/src/Symfony/Component/Validator/Util/LegacyTranslatorProxy.php b/src/Symfony/Component/Validator/Util/LegacyTranslatorProxy.php index 972c9bd931..12f487bf2f 100644 --- a/src/Symfony/Component/Validator/Util/LegacyTranslatorProxy.php +++ b/src/Symfony/Component/Validator/Util/LegacyTranslatorProxy.php @@ -11,18 +11,14 @@ namespace Symfony\Component\Validator\Util; -use Symfony\Component\Translation\LegacyTranslatorTrait; use Symfony\Component\Translation\TranslatorInterface as LegacyTranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface; /** * @internal to be removed in Symfony 5.0. */ -class LegacyTranslatorProxy implements LegacyTranslatorInterface +class LegacyTranslatorProxy implements LegacyTranslatorInterface, TranslatorInterface { - use LegacyTranslatorTrait { - transChoice as private doTransChoice; - } private $translator; public function __construct(TranslatorInterface $translator) @@ -64,10 +60,6 @@ class LegacyTranslatorProxy implements LegacyTranslatorInterface */ public function transChoice($id, $number, array $parameters = array(), $domain = null, $locale = null) { - if ($this->translator instanceof LegacyTranslatorInterface) { - return $this->translator->transChoice($id, $number, $parameters, $domain, $locale); - } - - $this->doTransChoice($id, $number, $parameters, $domain, $locale); + return $this->translator->trans($id, array('%count%' => $number) + $parameters, $domain, $locale); } } diff --git a/src/Symfony/Component/Validator/Violation/ConstraintViolationBuilder.php b/src/Symfony/Component/Validator/Violation/ConstraintViolationBuilder.php index 34b49f962a..abca36a536 100644 --- a/src/Symfony/Component/Validator/Violation/ConstraintViolationBuilder.php +++ b/src/Symfony/Component/Validator/Violation/ConstraintViolationBuilder.php @@ -44,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; @@ -142,12 +148,18 @@ class ConstraintViolationBuilder implements ConstraintViolationBuilderInterface */ public function addViolation() { - if (null === $this->plural || !$this->translator instanceof LegacyTranslatorInterface) { + if (null === $this->plural) { $translatedMessage = $this->translator->trans( $this->message, $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( diff --git a/src/Symfony/Contracts/Tests/Translation/TranslatorTest.php b/src/Symfony/Contracts/Tests/Translation/TranslatorTest.php index a7642bfa11..a3b67dfe5e 100644 --- a/src/Symfony/Contracts/Tests/Translation/TranslatorTest.php +++ b/src/Symfony/Contracts/Tests/Translation/TranslatorTest.php @@ -47,6 +47,29 @@ class TranslatorTest extends TestCase $this->assertEquals($expected, $translator->trans($id, $parameters)); } + /** + * @dataProvider getTransChoiceTests + */ + public function testTransChoiceWithExplicitLocale($expected, $id, $number) + { + $translator = $this->getTranslator(); + $translator->setLocale('en'); + + $this->assertEquals($expected, $translator->trans($id, array('%count%' => $number))); + } + + /** + * @dataProvider getTransChoiceTests + */ + public function testTransChoiceWithDefaultLocale($expected, $id, $number) + { + \Locale::setDefault('en'); + + $translator = $this->getTranslator(); + + $this->assertEquals($expected, $translator->trans($id, array('%count%' => $number))); + } + public function testGetSetLocale() { $translator = $this->getTranslator(); @@ -76,4 +99,255 @@ class TranslatorTest extends TestCase array('Symfony is awesome!', 'Symfony is %what%!', array('%what%' => 'awesome')), ); } + + 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('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), + ); + } + + /** + * @dataProvider getInternal + */ + public function testInterval($expected, $number, $interval) + { + $translator = $this->getTranslator(); + + $this->assertEquals($expected, $translator->trans($interval.' foo|[1,Inf[ bar', array('%count%' => $number))); + } + + public function getInternal() + { + return array( + array('foo', 3, '{1,2, 3 ,4}'), + array('bar', 10, '{1,2, 3 ,4}'), + array('bar', 3, '[1,2]'), + array('foo', 1, '[1,2]'), + array('foo', 2, '[1,2]'), + array('bar', 1, ']1,2['), + array('bar', 2, ']1,2['), + array('foo', log(0), '[-Inf,2['), + array('foo', -log(0), '[-2,+Inf]'), + ); + } + + /** + * @dataProvider getChooseTests + */ + public function testChoose($expected, $id, $number) + { + $translator = $this->getTranslator(); + + $this->assertEquals($expected, $translator->trans($id, array('%count%' => $number))); + } + + public function testReturnMessageIfExactlyOneStandardRuleIsGiven() + { + $translator = $this->getTranslator(); + + $this->assertEquals('There are two apples', $translator->trans('There are two apples', array('%count%' => 2))); + } + + /** + * @dataProvider getNonMatchingMessages + * @expectedException \InvalidArgumentException + */ + public function testThrowExceptionIfMatchingMessageCannotBeFound($id, $number) + { + $translator = $this->getTranslator(); + + $translator->trans($id, array('%count%' => $number)); + } + + public function getNonMatchingMessages() + { + return array( + array('{0} There are no apples|{1} There is one apple', 2), + array('{1} There is one apple|]1,Inf] There are %count% apples', 0), + array('{1} There is one apple|]2,Inf] There are %count% apples', 2), + array('{0} There are no apples|There is one apple', 2), + ); + } + + public function getChooseTests() + { + 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('There are no apples', '{0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples', 0), + 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 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 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 10 apples', 'There is one apple|There are %count% apples', 10), + + 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 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 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 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 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), + array('There is one apple', '{0} There are no apples|]0,1[There are %count% apples|{1} There is one apple|[1,Inf] There is more than one apple', 1), + array('There is more than one apple', '{0} There are no apples|]0,1[There are %count% apples|{1} There is one apple|[1,Inf] There is more than one apple', 1.7), + array('There are no apples', '{0} There are no apples|]0,1[There are %count% apples|{1} There is one apple|[1,Inf] There is more than one apple', 0), + array('There are no apples', '{0} There are no apples|]0,1[There are %count% apples|{1} There is one apple|[1,Inf] There is more than one apple', 0.0), + array('There are no apples', '{0.0} There are no apples|]0,1[There are %count% apples|{1} There is one apple|[1,Inf] There is more than one apple', 0), + + // Test texts with new-lines + // with double-quotes and \n in id & double-quotes and actual newlines in text + array("This is a text with a\n new-line in it. Selector = 0.", '{0}This is a text with a + new-line in it. Selector = 0.|{1}This is a text with a + new-line in it. Selector = 1.|[1,Inf]This is a text with a + new-line in it. Selector > 1.', 0), + // with double-quotes and \n in id and single-quotes and actual newlines in text + array("This is a text with a\n new-line in it. Selector = 1.", '{0}This is a text with a + new-line in it. Selector = 0.|{1}This is a text with a + new-line in it. Selector = 1.|[1,Inf]This is a text with a + new-line in it. Selector > 1.', 1), + array("This is a text with a\n new-line in it. Selector > 1.", '{0}This is a text with a + new-line in it. Selector = 0.|{1}This is a text with a + new-line in it. Selector = 1.|[1,Inf]This is a text with a + new-line in it. Selector > 1.', 5), + // with double-quotes and id split accros lines + array('This is a text with a + new-line in it. Selector = 1.', '{0}This is a text with a + new-line in it. Selector = 0.|{1}This is a text with a + new-line in it. Selector = 1.|[1,Inf]This is a text with a + new-line in it. Selector > 1.', 1), + // with single-quotes and id split accros lines + array('This is a text with a + new-line in it. Selector > 1.', '{0}This is a text with a + new-line in it. Selector = 0.|{1}This is a text with a + new-line in it. Selector = 1.|[1,Inf]This is a text with a + new-line in it. Selector > 1.', 5), + // with single-quotes and \n in text + array('This is a text with a\nnew-line in it. Selector = 0.', '{0}This is a text with a\nnew-line in it. Selector = 0.|{1}This is a text with a\nnew-line in it. Selector = 1.|[1,Inf]This is a text with a\nnew-line in it. Selector > 1.', 0), + // with double-quotes and id split accros lines + array("This is a text with a\nnew-line in it. Selector = 1.", "{0}This is a text with a\nnew-line in it. Selector = 0.|{1}This is a text with a\nnew-line in it. Selector = 1.|[1,Inf]This is a text with a\nnew-line in it. Selector > 1.", 1), + // esacape pipe + array('This is a text with | in it. Selector = 0.', '{0}This is a text with || in it. Selector = 0.|{1}This is a text with || in it. Selector = 1.', 0), + // Empty plural set (2 plural forms) from a .PO file + array('', '|', 1), + // Empty plural set (3 plural forms) from a .PO file + array('', '||', 1), + ); + } + + /** + * @dataProvider failingLangcodes + */ + public function testFailedLangcodes($nplural, $langCodes) + { + $matrix = $this->generateTestData($langCodes); + $this->validateMatrix($nplural, $matrix, false); + } + + /** + * @dataProvider successLangcodes + */ + public function testLangcodes($nplural, $langCodes) + { + $matrix = $this->generateTestData($langCodes); + $this->validateMatrix($nplural, $matrix); + } + + /** + * This array should contain all currently known langcodes. + * + * As it is impossible to have this ever complete we should try as hard as possible to have it almost complete. + * + * @return array + */ + public function successLangcodes() + { + return array( + array('1', array('ay', 'bo', 'cgg', 'dz', 'id', 'ja', 'jbo', 'ka', 'kk', 'km', 'ko', 'ky')), + array('2', array('nl', 'fr', 'en', 'de', 'de_GE', 'hy', 'hy_AM')), + array('3', array('be', 'bs', 'cs', 'hr')), + array('4', array('cy', 'mt', 'sl')), + array('6', array('ar')), + ); + } + + /** + * This array should be at least empty within the near future. + * + * This both depends on a complete list trying to add above as understanding + * the plural rules of the current failing languages. + * + * @return array with nplural together with langcodes + */ + public function failingLangcodes() + { + return array( + array('1', array('fa')), + array('2', array('jbo')), + array('3', array('cbs')), + array('4', array('gd', 'kw')), + array('5', array('ga')), + ); + } + + /** + * We validate only on the plural coverage. Thus the real rules is not tested. + * + * @param string $nplural Plural expected + * @param array $matrix Containing langcodes and their plural index values + * @param bool $expectSuccess + */ + protected function validateMatrix($nplural, $matrix, $expectSuccess = true) + { + foreach ($matrix as $langCode => $data) { + $indexes = array_flip($data); + if ($expectSuccess) { + $this->assertEquals($nplural, \count($indexes), "Langcode '$langCode' has '$nplural' plural forms."); + } else { + $this->assertNotEquals((int) $nplural, \count($indexes), "Langcode '$langCode' has '$nplural' plural forms."); + } + } + } + + protected function generateTestData($langCodes) + { + $translator = new class() { + use TranslatorTrait { + getPluralizationRule as public; + } + }; + + $matrix = array(); + foreach ($langCodes as $langCode) { + for ($count = 0; $count < 200; ++$count) { + $plural = $translator->getPluralizationRule($count, $langCode); + $matrix[$langCode][$count] = $plural; + } + } + + return $matrix; + } } diff --git a/src/Symfony/Contracts/Translation/TranslatorInterface.php b/src/Symfony/Contracts/Translation/TranslatorInterface.php index 7620fd3787..2130c1b2cf 100644 --- a/src/Symfony/Contracts/Translation/TranslatorInterface.php +++ b/src/Symfony/Contracts/Translation/TranslatorInterface.php @@ -19,6 +19,39 @@ interface TranslatorInterface /** * Translates the given message. * + * 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 + * on the given number, locale and the pluralization rules in the message + * itself. + * + * The message supports two different types of pluralization rules: + * + * interval: {0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples + * indexed: There is one apple|There are %count% apples + * + * The indexed solution can also contain labels (e.g. one: There is one apple). + * This is purely for making the translations more clear - it does not + * affect the functionality. + * + * The two methods can also be mixed: + * {0} There are no apples|one: There is one apple|more: There are %count% apples + * + * An interval can represent a finite set of numbers: + * {1,2,3,4} + * + * An interval can represent numbers between two numbers: + * [1, +Inf] + * ]-1,2[ + * + * The left delimiter can be [ (inclusive) or ] (exclusive). + * The right delimiter can be [ (exclusive) or ] (inclusive). + * Beside numbers, you can use -Inf and +Inf for the infinite. + * + * @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 array $parameters An array of parameters for the message * @param string|null $domain The domain for the message or null to use the default diff --git a/src/Symfony/Contracts/Translation/TranslatorTrait.php b/src/Symfony/Contracts/Translation/TranslatorTrait.php index b92105f190..e19e37cfe5 100644 --- a/src/Symfony/Contracts/Translation/TranslatorTrait.php +++ b/src/Symfony/Contracts/Translation/TranslatorTrait.php @@ -11,6 +11,8 @@ namespace Symfony\Contracts\Translation; +use Symfony\Component\Translation\Exception\InvalidArgumentException; + /** * A trait to help implement TranslatorInterface. * @@ -41,6 +43,213 @@ trait TranslatorTrait */ public function trans($id, array $parameters = array(), $domain = null, $locale = null) { - return strtr((string) $id, $parameters); + $id = (string) $id; + + if (!isset($parameters['%count%']) || !is_numeric($parameters['%count%'])) { + return strtr($id, $parameters); + } + + $number = (float) $parameters['%count%']; + $locale = (string) $locale ?: $this->getLocale(); + + $parts = array(); + if (preg_match('/^\|++$/', $id)) { + $parts = explode('|', $id); + } elseif (preg_match_all('/(?:\|\||[^\|])++/', $id, $matches)) { + $parts = $matches[0]; + } + + $intervalRegexp = <<<'EOF' +/^(?P + ({\s* + (\-?\d+(\.\d+)?[\s*,\s*\-?\d+(\.\d+)?]*) + \s*}) + + | + + (?P[\[\]]) + \s* + (?P-Inf|\-?\d+(\.\d+)?) + \s*,\s* + (?P\+?Inf|\-?\d+(\.\d+)?) + \s* + (?P[\[\]]) +)\s*(?P.*?)$/xs +EOF; + + $standardRules = array(); + foreach ($parts as $part) { + $part = trim(str_replace('||', '|', $part)); + + // try to match an explicit rule, then fallback to the standard ones + if (preg_match($intervalRegexp, $part, $matches)) { + if ($matches[2]) { + foreach (explode(',', $matches[3]) as $n) { + if ($number == $n) { + return strtr($matches['message'], $parameters); + } + } + } else { + $leftNumber = '-Inf' === $matches['left'] ? -INF : (float) $matches['left']; + $rightNumber = \is_numeric($matches['right']) ? (float) $matches['right'] : INF; + + if (('[' === $matches['left_delimiter'] ? $number >= $leftNumber : $number > $leftNumber) + && (']' === $matches['right_delimiter'] ? $number <= $rightNumber : $number < $rightNumber) + ) { + return strtr($matches['message'], $parameters); + } + } + } elseif (preg_match('/^\w+\:\s*(.*?)$/', $part, $matches)) { + $standardRules[] = $matches[1]; + } else { + $standardRules[] = $part; + } + } + + $position = $this->getPluralizationRule($number, $locale); + + if (!isset($standardRules[$position])) { + // when there's exactly one rule given, and that rule is a standard + // rule, use this rule + if (1 === \count($parts) && isset($standardRules[0])) { + return strtr($standardRules[0], $parameters); + } + + $message = sprintf('Unable to choose a translation for "%s" with locale "%s" for value "%d". Double check that this translation has the correct plural options (e.g. "There is one apple|There are %%count%% apples").', $id, $locale, $number); + + if (\class_exists(InvalidArgumentException::class)) { + throw new InvalidArgumentException($message); + } + + throw new \InvalidArgumentException($message); + } + + return strtr($standardRules[$position], $parameters); + } + + /** + * Returns the plural position to use for the given locale and number. + * + * The plural rules are derived from code of the Zend Framework (2010-09-25), + * which is subject to the new BSD license (http://framework.zend.com/license/new-bsd). + * Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com) + */ + private function getPluralizationRule(int $number, string $locale): int + { + switch ('pt_BR' !== $locale && \strlen($locale) > 3 ? substr($locale, 0, strrpos($locale, '_')) : $locale) { + case 'af': + case 'bn': + case 'bg': + case 'ca': + case 'da': + case 'de': + case 'el': + case 'en': + case 'eo': + case 'es': + case 'et': + case 'eu': + case 'fa': + case 'fi': + case 'fo': + case 'fur': + case 'fy': + case 'gl': + case 'gu': + case 'ha': + case 'he': + case 'hu': + case 'is': + case 'it': + case 'ku': + case 'lb': + case 'ml': + case 'mn': + case 'mr': + case 'nah': + case 'nb': + case 'ne': + case 'nl': + case 'nn': + case 'no': + case 'oc': + case 'om': + case 'or': + case 'pa': + case 'pap': + case 'ps': + case 'pt': + case 'so': + case 'sq': + case 'sv': + case 'sw': + case 'ta': + case 'te': + case 'tk': + case 'ur': + case 'zu': + return (1 == $number) ? 0 : 1; + + case 'am': + case 'bh': + case 'fil': + case 'fr': + case 'gun': + case 'hi': + case 'hy': + case 'ln': + case 'mg': + case 'nso': + case 'pt_BR': + case 'ti': + case 'wa': + return ((0 == $number) || (1 == $number)) ? 0 : 1; + + case 'be': + case 'bs': + case 'hr': + case 'ru': + case 'sh': + case 'sr': + case 'uk': + return ((1 == $number % 10) && (11 != $number % 100)) ? 0 : ((($number % 10 >= 2) && ($number % 10 <= 4) && (($number % 100 < 10) || ($number % 100 >= 20))) ? 1 : 2); + + case 'cs': + case 'sk': + return (1 == $number) ? 0 : ((($number >= 2) && ($number <= 4)) ? 1 : 2); + + case 'ga': + return (1 == $number) ? 0 : ((2 == $number) ? 1 : 2); + + case 'lt': + return ((1 == $number % 10) && (11 != $number % 100)) ? 0 : ((($number % 10 >= 2) && (($number % 100 < 10) || ($number % 100 >= 20))) ? 1 : 2); + + case 'sl': + return (1 == $number % 100) ? 0 : ((2 == $number % 100) ? 1 : (((3 == $number % 100) || (4 == $number % 100)) ? 2 : 3)); + + case 'mk': + return (1 == $number % 10) ? 0 : 1; + + case 'mt': + return (1 == $number) ? 0 : (((0 == $number) || (($number % 100 > 1) && ($number % 100 < 11))) ? 1 : ((($number % 100 > 10) && ($number % 100 < 20)) ? 2 : 3)); + + case 'lv': + return (0 == $number) ? 0 : (((1 == $number % 10) && (11 != $number % 100)) ? 1 : 2); + + case 'pl': + return (1 == $number) ? 0 : ((($number % 10 >= 2) && ($number % 10 <= 4) && (($number % 100 < 12) || ($number % 100 > 14))) ? 1 : 2); + + case 'cy': + return (1 == $number) ? 0 : ((2 == $number) ? 1 : (((8 == $number) || (11 == $number)) ? 2 : 3)); + + case 'ro': + return (1 == $number) ? 0 : (((0 == $number) || (($number % 100 > 0) && ($number % 100 < 20))) ? 1 : 2); + + case 'ar': + return (0 == $number) ? 0 : ((1 == $number) ? 1 : ((2 == $number) ? 2 : ((($number % 100 >= 3) && ($number % 100 <= 10)) ? 3 : ((($number % 100 >= 11) && ($number % 100 <= 99)) ? 4 : 5)))); + + default: + return 0; + } } }