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 18b88f2fa2318..95f2b980c8f47 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 607302d9917c6..090aed55484a1 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\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 @@ public function trans($message, array $arguments = array(), $domain = null, $loc 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 b3dd76d42adc2..2052fef695701 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 @@ public function trans($id, array $parameters = array(), $domain = 'messages', $l */ 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 eeb4e0daa48de..93e070040b921 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Translation/TranslatorTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Translation/TranslatorTest.php @@ -52,14 +52,25 @@ public function testTransWithoutCaching() $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 @@ public function testTransWithCaching() $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 @@ public function testTransWithCaching() $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 6bbecdd099a30..87b7e5a9f1e64 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 10ac93516b315..fd8c135e26744 100644 --- a/src/Symfony/Component/Translation/DataCollectorTranslator.php +++ b/src/Symfony/Component/Translation/DataCollectorTranslator.php @@ -20,6 +20,10 @@ */ 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 @@ public function trans($id, array $parameters = array(), $domain = null, $locale */ 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 b69312ec79de3..c1385630493ba 100644 --- a/src/Symfony/Component/Translation/IdentityTranslator.php +++ b/src/Symfony/Component/Translation/IdentityTranslator.php @@ -20,7 +20,8 @@ */ class IdentityTranslator implements TranslatorInterface { - use TranslatorTrait { + use TranslatorTrait; + use LegacyTranslatorTrait { transChoice as private doTransChoice; } @@ -43,15 +44,11 @@ public function __construct(MessageSelector $selector = null) */ 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 0000000000000..1b243722061c6 --- /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 cfb60ab0b0fcd..7a10952a2acd1 100644 --- a/src/Symfony/Component/Translation/LoggingTranslator.php +++ b/src/Symfony/Component/Translation/LoggingTranslator.php @@ -21,6 +21,10 @@ */ class LoggingTranslator implements LegacyTranslatorInterface, TranslatorBagInterface { + use LegacyTranslatorTrait { + transChoice as private doTransChoice; + } + /** * @var TranslatorInterface|TranslatorBagInterface */ @@ -58,7 +62,12 @@ public function trans($id, array $parameters = array(), $domain = null, $locale */ 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 867a345cedeb9..06639417b623a 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 1cdd33b395f0d..ae15339bce41b 100644 --- a/src/Symfony/Component/Translation/Tests/DataCollectorTranslatorTest.php +++ b/src/Symfony/Component/Translation/Tests/DataCollectorTranslatorTest.php @@ -25,7 +25,6 @@ public function testCollectMessages() $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 @@ public function testCollectMessages() '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 @@ public function testCollectMessages() $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 1fa736e7e3df4..f4c96aa12f4fc 100644 --- a/src/Symfony/Component/Translation/Tests/Formatter/MessageFormatterTest.php +++ b/src/Symfony/Component/Translation/Tests/Formatter/MessageFormatterTest.php @@ -26,6 +26,7 @@ public function testFormat($expected, $message, $parameters = array()) /** * @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 be0a548aa1f06..443c63718e46b 100644 --- a/src/Symfony/Component/Translation/Tests/IdentityTranslatorTest.php +++ b/src/Symfony/Component/Translation/Tests/IdentityTranslatorTest.php @@ -20,4 +20,193 @@ public function getTranslator() { 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 022379e934834..0e43cbecf4197 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 @@ public function testTransChoiceFallbackIsLogged() $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 b0efefb7d089e..c632d69041e29 100644 --- a/src/Symfony/Component/Translation/Tests/TranslatorTest.php +++ b/src/Symfony/Component/Translation/Tests/TranslatorTest.php @@ -393,6 +393,7 @@ public function testFlattenedTrans($expected, $messages, $id) /** * @dataProvider getTransChoiceTests + * @group legacy */ public function testTransChoice($expected, $id, $translation, $number, $parameters, $locale, $domain) { @@ -406,6 +407,7 @@ public function testTransChoice($expected, $id, $translation, $number, $paramete /** * @dataProvider getInvalidLocalesTests * @expectedException \Symfony\Component\Translation\Exception\InvalidArgumentException + * @group legacy */ public function testTransChoiceInvalidLocale($locale) { @@ -418,6 +420,7 @@ public function testTransChoiceInvalidLocale($locale) /** * @dataProvider getValidLocalesTests + * @group legacy */ public function testTransChoiceValidLocale($locale) { @@ -537,6 +540,9 @@ public function getValidLocalesTests() ); } + /** + * @group legacy + */ public function testTransChoiceFallback() { $translator = new Translator('ru'); @@ -547,6 +553,9 @@ public function testTransChoiceFallback() $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 @@ public function testTransChoiceFallbackBis() $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 b9a876497dbe8..5670e613e347c 100644 --- a/src/Symfony/Component/Translation/Translator.php +++ b/src/Symfony/Component/Translation/Translator.php @@ -202,6 +202,7 @@ public function trans($id, array $parameters = array(), $domain = null, $locale */ 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 829c6f3d144fa..2eea19866bc99 100644 --- a/src/Symfony/Component/Translation/TranslatorInterface.php +++ b/src/Symfony/Component/Translation/TranslatorInterface.php @@ -16,8 +16,54 @@ /** * @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 077d37931f98e..972c9bd931ced 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 @@ */ class LegacyTranslatorProxy implements LegacyTranslatorInterface { + use LegacyTranslatorTrait { + transChoice as private doTransChoice; + } private $translator; public function __construct(TranslatorInterface $translator) @@ -60,6 +64,10 @@ public function trans($id, array $parameters = array(), $domain = null, $locale */ 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 621c38d9afc68..34b49f962a9e9 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 @@ public function setCause($cause) */ 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 e7af8e4da3820..a7642bfa11406 100644 --- a/src/Symfony/Contracts/Tests/Translation/TranslatorTest.php +++ b/src/Symfony/Contracts/Tests/Translation/TranslatorTest.php @@ -47,29 +47,6 @@ public function testTrans($expected, $id, $parameters) $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 @@ public function getTransTests() 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 a54718733b06e..7620fd3787336 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 7c392b2bd418a..b92105f190284 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 @@ public function trans($id, array $parameters = array(), $domain = null, $locale { 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 | 68 ++--- .../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, 888 insertions(+), 431 deletions(-) delete mode 100644 src/Symfony/Component/Translation/LegacyTranslatorTrait.php diff --git a/UPGRADE-4.2.md b/UPGRADE-4.2.md index 95f2b980c8f47..6b3c4423d19fd 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 e7470a2467db7..a876f151b9b6a 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 a2e55f6db2364..341bcfa538fdb 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 090aed55484a1..ec5d452cef460 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\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 @@ * 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 @@ public function getFilters() { 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 @@ public function getTranslationNodeVisitor() 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 @@ public function trans($message, array $arguments = array(), $domain = null, $loc 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 7eb8d743e9b3e..578e37dd2c3ad 100644 --- a/src/Symfony/Bridge/Twig/Node/TransNode.php +++ b/src/Symfony/Bridge/Twig/Node/TransNode.php @@ -60,19 +60,12 @@ public function compile(Compiler $compiler) $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 @@ public function compile(Compiler $compiler) ->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 feba2b2b6b0d0..bb36cda8eaf56 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/TranslationExtensionTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/TranslationExtensionTest.php @@ -45,6 +45,15 @@ public function testTrans($template, $expected, array $variables = array()) $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 @@ public function testTransComplexBody() } /** + * @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 @@ public function getTransTests() 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 @@ public function testDefaultTranslationDomain() {%- 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 @@ public function testDefaultTranslationDomainWithNamedArguments() {%- 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 d45f60c09d1c6..e4a20d72fc0a7 100644 --- a/src/Symfony/Bridge/Twig/Tests/Node/TransNodeTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Node/TransNodeTest.php @@ -34,7 +34,7 @@ public function testCompileStrict() $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 e46ba205e5e17..fca5f4d4a9d3a 100644 --- a/src/Symfony/Bridge/Twig/Tests/Translation/TwigExtractorTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Translation/TwigExtractorTest.php @@ -50,15 +50,21 @@ public function testExtract($template, $messages) } } + /** + * @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 @@ public function getExtractData() // 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 5261d7ad52cee..490fb14ff45e5 100644 --- a/src/Symfony/Bridge/Twig/TokenParser/TransChoiceTokenParser.php +++ b/src/Symfony/Bridge/Twig/TokenParser/TransChoiceTokenParser.php @@ -23,6 +23,8 @@ * 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 @@ public function parse(Token $token) $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 76c8dc06100c1..f5bf12b57ee4b 100644 --- a/src/Symfony/Bridge/Twig/TokenParser/TransTokenParser.php +++ b/src/Symfony/Bridge/Twig/TokenParser/TransTokenParser.php @@ -39,10 +39,17 @@ public function parse(Token $token) $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 @@ public function parse(Token $token) $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 e2e56664891c5..ed75ed1f90dcb 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 dc14dea0ef644..6e2c1324ba884 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\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 2052fef695701..8120a0cd8efb3 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 @@ public function trans($id, array $parameters = array(), $domain = 'messages', $l /** * @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 93e070040b921..f5a4818553b2b 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Translation/TranslatorTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Translation/TranslatorTest.php @@ -52,8 +52,10 @@ public function testTransWithoutCaching() $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 @@ public function testTransWithCaching() $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 8c519a81ec03e..d84d09b38600c 100644 --- a/src/Symfony/Component/Form/Extension/Csrf/CsrfExtension.php +++ b/src/Symfony/Component/Form/Extension/Csrf/CsrfExtension.php @@ -13,6 +13,7 @@ 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 f8987378438f3..4a49a85fcdd31 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\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 @@ public static function getSubscribedEvents() ); } - 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 35621c585bf32..7e16f6dc375f2 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\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 fa09cbc21f55b..9e5eec80014f2 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 @@ 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 792e347816af0..7cf059886df3b 100644 --- a/src/Symfony/Component/HttpKernel/EventListener/TranslatorListener.php +++ b/src/Symfony/Component/HttpKernel/EventListener/TranslatorListener.php @@ -17,6 +17,7 @@ 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 87b7e5a9f1e64..e3a4725c3d5d6 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 fd8c135e26744..3e2087220d657 100644 --- a/src/Symfony/Component/Translation/DataCollectorTranslator.php +++ b/src/Symfony/Component/Translation/DataCollectorTranslator.php @@ -18,12 +18,8 @@ /** * @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 @@ public function trans($id, array $parameters = array(), $domain = null, $locale /** * {@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 @@ public function getCollectedMessages() * @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 @@ private function collectMessage($locale, $domain, $id, $translation, $parameters '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 92acbcafe2032..6bc68384af920 100644 --- a/src/Symfony/Component/Translation/Formatter/ChoiceMessageFormatterInterface.php +++ b/src/Symfony/Component/Translation/Formatter/ChoiceMessageFormatterInterface.php @@ -13,6 +13,8 @@ /** * @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 862e03aab17d7..1119c356f5afe 100644 --- a/src/Symfony/Component/Translation/Formatter/MessageFormatter.php +++ b/src/Symfony/Component/Translation/Formatter/MessageFormatter.php @@ -13,6 +13,7 @@ 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 @@ public function __construct($translator = null) { 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 @@ public function __construct($translator = null) */ 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 c1385630493ba..31abdaab12513 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 @@ * * @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 @@ public function __construct(MessageSelector $selector = null) /** * {@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 1b243722061c6..0000000000000 --- 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 7a10952a2acd1..ee711568ab15d 100644 --- a/src/Symfony/Component/Translation/LoggingTranslator.php +++ b/src/Symfony/Component/Translation/LoggingTranslator.php @@ -21,10 +21,6 @@ */ 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 @@ public function trans($id, array $parameters = array(), $domain = null, $locale /** * {@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 ae15339bce41b..19e056c12e9d4 100644 --- a/src/Symfony/Component/Translation/Tests/DataCollectorTranslatorTest.php +++ b/src/Symfony/Component/Translation/Tests/DataCollectorTranslatorTest.php @@ -25,6 +25,7 @@ public function testCollectMessages() $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 @@ public function testCollectMessages() '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 @@ public function testCollectMessagesTransChoice() '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 443c63718e46b..be0a548aa1f06 100644 --- a/src/Symfony/Component/Translation/Tests/IdentityTranslatorTest.php +++ b/src/Symfony/Component/Translation/Tests/IdentityTranslatorTest.php @@ -20,193 +20,4 @@ public function getTranslator() { 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 c632d69041e29..d630a7491d4bc 100644 --- a/src/Symfony/Component/Translation/Tests/TranslatorTest.php +++ b/src/Symfony/Component/Translation/Tests/TranslatorTest.php @@ -502,7 +502,7 @@ public function getTransChoiceTests() 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 5670e613e347c..d8d27301f04df 100644 --- a/src/Symfony/Component/Translation/Translator.php +++ b/src/Symfony/Component/Translation/Translator.php @@ -22,11 +22,13 @@ 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 @@ public function trans($id, array $parameters = array(), $domain = null, $locale /** * {@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 2eea19866bc99..02cb5027b862b 100644 --- a/src/Symfony/Component/Translation/TranslatorInterface.php +++ b/src/Symfony/Component/Translation/TranslatorInterface.php @@ -11,47 +11,33 @@ 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 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 + * Translates the given message. * - * 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[ + * @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 * - * 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. + * @return string The translated string * - * @see https://en.wikipedia.org/wiki/ISO_31-11 + * @throws InvalidArgumentException If the locale contains invalid characters + */ + public function trans($id, array $parameters = array(), $domain = null, $locale = null); + + /** + * Translates the given choice message by choosing a translation according to a number. * * @param string $id The message id (may also be an object that can be cast to string) * @param int $number The number to use to find the indice of the message @@ -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 03f56e9175088..fd9d9605e01f7 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 0bb1be767e6ee..b8d56eee9a968 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 972c9bd931ced..12f487bf2f896 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 @@ public function trans($id, array $parameters = array(), $domain = null, $locale */ 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 34b49f962a9e9..abca36a536622 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 @@ public function setCause($cause) */ 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 a7642bfa11406..a3b67dfe5eea9 100644 --- a/src/Symfony/Contracts/Tests/Translation/TranslatorTest.php +++ b/src/Symfony/Contracts/Tests/Translation/TranslatorTest.php @@ -47,6 +47,29 @@ public function testTrans($expected, $id, $parameters) $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 @@ public function getTransTests() 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 7620fd3787336..2130c1b2cf0a6 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 b92105f190284..e19e37cfe5208 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 @@ public function getLocale() */ 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; + } } } pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy