diff --git a/UPGRADE-4.2.md b/UPGRADE-4.2.md index 01197dd714..41ff5eef40 100644 --- a/UPGRADE-4.2.md +++ b/UPGRADE-4.2.md @@ -149,3 +149,15 @@ Serializer * Relying on the default value (false) of the "as_collection" option is deprecated since 4.2. You should set it to false explicitly instead as true will be the default value in 5.0. + +Translation +----------- + + * The `TranslatorInterface` has been deprecated in favor of `Symfony\Contracts\Translation\TranslatorInterface` + * The `MessageSelector`, `Interval` and `PluralizationRules` classes have been deprecated, use `IdentityTranslator` instead + +Validator +--------- + + * The component is now decoupled from `symfony/translation` and uses `Symfony\Contracts\Translation\TranslatorInterface` instead + * The `ValidatorBuilderInterface` has been deprecated and `ValidatorBuilder` made final diff --git a/UPGRADE-5.0.md b/UPGRADE-5.0.md index 58a08d5d69..860b3ea1be 100644 --- a/UPGRADE-5.0.md +++ b/UPGRADE-5.0.md @@ -146,6 +146,8 @@ Translation * The `FileDumper::setBackup()` method has been removed. * The `TranslationWriter::disableBackup()` method has been removed. + * The `TranslatorInterface` has been removed in favor of `Symfony\Contracts\Translation\TranslatorInterface` + * The `MessageSelector`, `Interval` and `PluralizationRules` classes have been removed, use `IdentityTranslator` instead TwigBundle ---------- @@ -158,6 +160,8 @@ Validator * The `Email::__construct()` 'strict' property has been removed. Use 'mode'=>"strict" instead. * Calling `EmailValidator::__construct()` method with a boolean parameter has been removed, use `EmailValidator("strict")` instead. * Removed the `checkDNS` and `dnsMessage` options from the `Url` constraint. + * The component is now decoupled from `symfony/translation` and uses `Symfony\Contracts\Translation\TranslatorInterface` instead + * The `ValidatorBuilderInterface` has been removed and `ValidatorBuilder` is now final Workflow -------- diff --git a/src/Symfony/Bridge/Twig/Extension/TranslationExtension.php b/src/Symfony/Bridge/Twig/Extension/TranslationExtension.php index fe1adc4a7d..fffc593f0e 100644 --- a/src/Symfony/Bridge/Twig/Extension/TranslationExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/TranslationExtension.php @@ -16,7 +16,7 @@ use Symfony\Bridge\Twig\NodeVisitor\TranslationNodeVisitor; use Symfony\Bridge\Twig\TokenParser\TransChoiceTokenParser; use Symfony\Bridge\Twig\TokenParser\TransDefaultDomainTokenParser; use Symfony\Bridge\Twig\TokenParser\TransTokenParser; -use Symfony\Component\Translation\TranslatorInterface; +use Symfony\Contracts\Translation\TranslatorInterface; use Twig\Extension\AbstractExtension; use Twig\NodeVisitor\NodeVisitorInterface; use Twig\TokenParser\AbstractTokenParser; diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/Fixtures/StubTranslator.php b/src/Symfony/Bridge/Twig/Tests/Extension/Fixtures/StubTranslator.php index b7d011b599..083124d6af 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/Fixtures/StubTranslator.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/Fixtures/StubTranslator.php @@ -11,7 +11,7 @@ namespace Symfony\Bridge\Twig\Tests\Extension\Fixtures; -use Symfony\Component\Translation\TranslatorInterface; +use Symfony\Contracts\Translation\TranslatorInterface; class StubTranslator implements TranslatorInterface { diff --git a/src/Symfony/Bridge/Twig/Tests/Translation/TwigExtractorTest.php b/src/Symfony/Bridge/Twig/Tests/Translation/TwigExtractorTest.php index 8e5c84bbc9..e46ba205e5 100644 --- a/src/Symfony/Bridge/Twig/Tests/Translation/TwigExtractorTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Translation/TwigExtractorTest.php @@ -15,6 +15,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Bridge\Twig\Extension\TranslationExtension; use Symfony\Bridge\Twig\Translation\TwigExtractor; use Symfony\Component\Translation\MessageCatalogue; +use Symfony\Contracts\Translation\TranslatorInterface; use Twig\Environment; use Twig\Error\Error; use Twig\Loader\ArrayLoader; @@ -33,7 +34,7 @@ class TwigExtractorTest extends TestCase 'cache' => false, 'autoescape' => false, )); - $twig->addExtension(new TranslationExtension($this->getMockBuilder('Symfony\Component\Translation\TranslatorInterface')->getMock())); + $twig->addExtension(new TranslationExtension($this->getMockBuilder(TranslatorInterface::class)->getMock())); $extractor = new TwigExtractor($twig); $extractor->setPrefix('prefix'); @@ -82,7 +83,7 @@ class TwigExtractorTest extends TestCase public function testExtractSyntaxError($resources) { $twig = new Environment($this->getMockBuilder('Twig\Loader\LoaderInterface')->getMock()); - $twig->addExtension(new TranslationExtension($this->getMockBuilder('Symfony\Component\Translation\TranslatorInterface')->getMock())); + $twig->addExtension(new TranslationExtension($this->getMockBuilder(TranslatorInterface::class)->getMock())); $extractor = new TwigExtractor($twig); @@ -124,7 +125,7 @@ class TwigExtractorTest extends TestCase 'cache' => false, 'autoescape' => false, )); - $twig->addExtension(new TranslationExtension($this->getMockBuilder('Symfony\Component\Translation\TranslatorInterface')->getMock())); + $twig->addExtension(new TranslationExtension($this->getMockBuilder(TranslatorInterface::class)->getMock())); $extractor = new TwigExtractor($twig); $catalogue = new MessageCatalogue('en'); diff --git a/src/Symfony/Bridge/Twig/composer.json b/src/Symfony/Bridge/Twig/composer.json index aaf5d77527..5a342531bb 100644 --- a/src/Symfony/Bridge/Twig/composer.json +++ b/src/Symfony/Bridge/Twig/composer.json @@ -29,7 +29,7 @@ "symfony/polyfill-intl-icu": "~1.0", "symfony/routing": "~3.4|~4.0", "symfony/templating": "~3.4|~4.0", - "symfony/translation": "~3.4|~4.0", + "symfony/translation": "~4.2", "symfony/yaml": "~3.4|~4.0", "symfony/security": "~3.4|~4.0", "symfony/security-acl": "~2.8|~3.0", @@ -41,8 +41,9 @@ "symfony/workflow": "~3.4|~4.0" }, "conflict": { + "symfony/console": "<3.4", "symfony/form": "<4.1.2", - "symfony/console": "<3.4" + "symfony/translation": "<4.2" }, "suggest": { "symfony/finder": "", diff --git a/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/TranslationsCacheWarmer.php b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/TranslationsCacheWarmer.php index eab97ce9db..98f2ffdee3 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/TranslationsCacheWarmer.php +++ b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/TranslationsCacheWarmer.php @@ -15,7 +15,7 @@ use Psr\Container\ContainerInterface; use Symfony\Component\DependencyInjection\ServiceSubscriberInterface; use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface; use Symfony\Component\HttpKernel\CacheWarmer\WarmableInterface; -use Symfony\Component\Translation\TranslatorInterface; +use Symfony\Contracts\Translation\TranslatorInterface; /** * Generates the catalogues for translations. diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/TranslationDebugCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/TranslationDebugCommand.php index 6172c5e0df..dc14dea0ef 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/TranslationDebugCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/TranslationDebugCommand.php @@ -26,7 +26,7 @@ use Symfony\Component\Translation\LoggingTranslator; use Symfony\Component\Translation\MessageCatalogue; use Symfony\Component\Translation\Reader\TranslationReaderInterface; use Symfony\Component\Translation\Translator; -use Symfony\Component\Translation\TranslatorInterface; +use Symfony\Contracts\Translation\TranslatorInterface; /** * Helps finding unused or missing translation messages in a given locale diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/LoggingTranslatorPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/LoggingTranslatorPass.php index 4945e1c12e..787060b6a4 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/LoggingTranslatorPass.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/LoggingTranslatorPass.php @@ -15,7 +15,7 @@ use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\Translation\TranslatorBagInterface; -use Symfony\Component\Translation\TranslatorInterface; +use Symfony\Contracts\Translation\TranslatorInterface; /** * @author Abdellatif Ait boudad diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/identity_translator.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/identity_translator.xml index def3c1d2c6..bc98b9f294 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/identity_translator.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/identity_translator.xml @@ -6,11 +6,13 @@ - - - + + - + + + The "%service_id%" service is deprecated since Symfony 4.2, use "identity_translator" instead. + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation.xml index d756b9ae0a..42434b62d5 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation.xml @@ -21,6 +21,7 @@ + @@ -29,7 +30,7 @@ - + diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/TranslatorHelper.php b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/TranslatorHelper.php index 5c4a9ae0fa..2ba81acef0 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/TranslatorHelper.php +++ b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/TranslatorHelper.php @@ -12,7 +12,7 @@ namespace Symfony\Bundle\FrameworkBundle\Templating\Helper; use Symfony\Component\Templating\Helper\Helper; -use Symfony\Component\Translation\TranslatorInterface; +use Symfony\Contracts\Translation\TranslatorInterface; /** * @author Fabien Potencier diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/DataCollectorTranslatorPassTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/DataCollectorTranslatorPassTest.php index 2d6e8ca3be..f3e4ce9603 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/DataCollectorTranslatorPassTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/DataCollectorTranslatorPassTest.php @@ -15,7 +15,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\DataCollectorTranslatorPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; -use Symfony\Component\Translation\TranslatorInterface; +use Symfony\Contracts\Translation\TranslatorInterface; class DataCollectorTranslatorPassTest extends TestCase { diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/Fixtures/StubTranslator.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/Fixtures/StubTranslator.php index 17d1fd4fc6..08c0a476b4 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/Fixtures/StubTranslator.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/Fixtures/StubTranslator.php @@ -11,7 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Templating\Helper\Fixtures; -use Symfony\Component\Translation\TranslatorInterface; +use Symfony\Contracts\Translation\TranslatorInterface; class StubTranslator implements TranslatorInterface { diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json index 8badd284a4..4b665380c2 100644 --- a/src/Symfony/Bundle/FrameworkBundle/composer.json +++ b/src/Symfony/Bundle/FrameworkBundle/composer.json @@ -47,7 +47,7 @@ "symfony/security-csrf": "~3.4|~4.0", "symfony/serializer": "^4.1", "symfony/stopwatch": "~3.4|~4.0", - "symfony/translation": "~3.4|~4.0", + "symfony/translation": "~4.2", "symfony/templating": "~3.4|~4.0", "symfony/validator": "^4.1", "symfony/var-dumper": "~3.4|~4.0", @@ -71,7 +71,7 @@ "symfony/property-info": "<3.4", "symfony/serializer": "<4.1", "symfony/stopwatch": "<3.4", - "symfony/translation": "<3.4", + "symfony/translation": "<4.2", "symfony/twig-bridge": "<4.1.1", "symfony/validator": "<4.1", "symfony/workflow": "<4.1" diff --git a/src/Symfony/Bundle/TwigBundle/DependencyInjection/TwigExtension.php b/src/Symfony/Bundle/TwigBundle/DependencyInjection/TwigExtension.php index 074be35a46..9b2149d15d 100644 --- a/src/Symfony/Bundle/TwigBundle/DependencyInjection/TwigExtension.php +++ b/src/Symfony/Bundle/TwigBundle/DependencyInjection/TwigExtension.php @@ -19,6 +19,7 @@ use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\HttpKernel\DependencyInjection\Extension; +use Symfony\Component\Translation\Translator; use Twig\Extension\ExtensionInterface; use Twig\Extension\RuntimeExtensionInterface; use Twig\Loader\LoaderInterface; @@ -48,7 +49,7 @@ class TwigExtension extends Extension $loader->load('console.xml'); } - if (!interface_exists('Symfony\Component\Translation\TranslatorInterface')) { + if (!class_exists(Translator::class)) { $container->removeDefinition('twig.translation.extractor'); } diff --git a/src/Symfony/Bundle/TwigBundle/composer.json b/src/Symfony/Bundle/TwigBundle/composer.json index 9b31e3e70c..2ff3a8ea9e 100644 --- a/src/Symfony/Bundle/TwigBundle/composer.json +++ b/src/Symfony/Bundle/TwigBundle/composer.json @@ -41,7 +41,8 @@ }, "conflict": { "symfony/dependency-injection": "<4.1", - "symfony/framework-bundle": "<4.1" + "symfony/framework-bundle": "<4.1", + "symfony/translation": "<4.2" }, "autoload": { "psr-4": { "Symfony\\Bundle\\TwigBundle\\": "" }, diff --git a/src/Symfony/Component/Form/Extension/Csrf/CsrfExtension.php b/src/Symfony/Component/Form/Extension/Csrf/CsrfExtension.php index 865f756e55..5d95d46f5f 100644 --- a/src/Symfony/Component/Form/Extension/Csrf/CsrfExtension.php +++ b/src/Symfony/Component/Form/Extension/Csrf/CsrfExtension.php @@ -13,7 +13,7 @@ namespace Symfony\Component\Form\Extension\Csrf; use Symfony\Component\Form\AbstractExtension; use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; -use Symfony\Component\Translation\TranslatorInterface; +use Symfony\Contracts\Translation\TranslatorInterface; /** * This extension protects forms by using a CSRF token. diff --git a/src/Symfony/Component/Form/Extension/Csrf/EventListener/CsrfValidationListener.php b/src/Symfony/Component/Form/Extension/Csrf/EventListener/CsrfValidationListener.php index 6827686afd..f45de6738d 100644 --- a/src/Symfony/Component/Form/Extension/Csrf/EventListener/CsrfValidationListener.php +++ b/src/Symfony/Component/Form/Extension/Csrf/EventListener/CsrfValidationListener.php @@ -18,7 +18,7 @@ use Symfony\Component\Form\FormEvents; use Symfony\Component\Form\Util\ServerParams; use Symfony\Component\Security\Csrf\CsrfToken; use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; -use Symfony\Component\Translation\TranslatorInterface; +use Symfony\Contracts\Translation\TranslatorInterface; /** * @author Bernhard Schussek diff --git a/src/Symfony/Component/Form/Extension/Csrf/Type/FormTypeCsrfExtension.php b/src/Symfony/Component/Form/Extension/Csrf/Type/FormTypeCsrfExtension.php index 70a880d3c8..35621c585b 100644 --- a/src/Symfony/Component/Form/Extension/Csrf/Type/FormTypeCsrfExtension.php +++ b/src/Symfony/Component/Form/Extension/Csrf/Type/FormTypeCsrfExtension.php @@ -19,7 +19,7 @@ use Symfony\Component\Form\FormView; use Symfony\Component\Form\Util\ServerParams; use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; -use Symfony\Component\Translation\TranslatorInterface; +use Symfony\Contracts\Translation\TranslatorInterface; /** * @author Bernhard Schussek diff --git a/src/Symfony/Component/Form/Extension/Validator/Type/UploadValidatorExtension.php b/src/Symfony/Component/Form/Extension/Validator/Type/UploadValidatorExtension.php index fe97a93ed1..fa09cbc21f 100644 --- a/src/Symfony/Component/Form/Extension/Validator/Type/UploadValidatorExtension.php +++ b/src/Symfony/Component/Form/Extension/Validator/Type/UploadValidatorExtension.php @@ -14,7 +14,7 @@ namespace Symfony\Component\Form\Extension\Validator\Type; use Symfony\Component\Form\AbstractTypeExtension; use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; -use Symfony\Component\Translation\TranslatorInterface; +use Symfony\Contracts\Translation\TranslatorInterface; /** * @author Abdellatif Ait boudad diff --git a/src/Symfony/Component/Form/Tests/Extension/Csrf/Type/FormTypeCsrfExtensionTest.php b/src/Symfony/Component/Form/Tests/Extension/Csrf/Type/FormTypeCsrfExtensionTest.php index 599a14a3e6..51c1e55e71 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Csrf/Type/FormTypeCsrfExtensionTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Csrf/Type/FormTypeCsrfExtensionTest.php @@ -17,6 +17,8 @@ use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormError; use Symfony\Component\Form\Test\TypeTestCase; use Symfony\Component\Security\Csrf\CsrfToken; +use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; +use Symfony\Contracts\Translation\TranslatorInterface; class FormTypeCsrfExtensionTest_ChildType extends AbstractType { @@ -42,8 +44,8 @@ class FormTypeCsrfExtensionTest extends TypeTestCase protected function setUp() { - $this->tokenManager = $this->getMockBuilder('Symfony\Component\Security\Csrf\CsrfTokenManagerInterface')->getMock(); - $this->translator = $this->getMockBuilder('Symfony\Component\Translation\TranslatorInterface')->getMock(); + $this->tokenManager = $this->getMockBuilder(CsrfTokenManagerInterface::class)->getMock(); + $this->translator = $this->getMockBuilder(TranslatorInterface::class)->getMock(); parent::setUp(); } diff --git a/src/Symfony/Component/Form/Tests/Extension/Validator/Type/UploadValidatorExtensionTest.php b/src/Symfony/Component/Form/Tests/Extension/Validator/Type/UploadValidatorExtensionTest.php index 7d4178bc83..296b8baeff 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Validator/Type/UploadValidatorExtensionTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Validator/Type/UploadValidatorExtensionTest.php @@ -15,12 +15,13 @@ use Symfony\Component\Form\Extension\Validator\Type\UploadValidatorExtension; use Symfony\Component\Form\Test\TypeTestCase; use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Contracts\Translation\TranslatorInterface; class UploadValidatorExtensionTest extends TypeTestCase { public function testPostMaxSizeTranslation() { - $translator = $this->getMockBuilder('Symfony\Component\Translation\TranslatorInterface')->getMock(); + $translator = $this->getMockBuilder(TranslatorInterface::class)->getMock(); $translator->expects($this->any()) ->method('trans') diff --git a/src/Symfony/Component/Form/composer.json b/src/Symfony/Component/Form/composer.json index 4e4f65ebf3..7adcb0616c 100644 --- a/src/Symfony/Component/Form/composer.json +++ b/src/Symfony/Component/Form/composer.json @@ -33,7 +33,7 @@ "symfony/http-foundation": "~3.4|~4.0", "symfony/http-kernel": "~3.4|~4.0", "symfony/security-csrf": "~3.4|~4.0", - "symfony/translation": "~3.4|~4.0", + "symfony/translation": "~4.2", "symfony/var-dumper": "~3.4|~4.0" }, "conflict": { @@ -42,6 +42,7 @@ "symfony/doctrine-bridge": "<3.4", "symfony/framework-bundle": "<3.4", "symfony/http-kernel": "<3.4", + "symfony/translation": "<4.2", "symfony/twig-bridge": "<3.4.5|<4.0.5,>=4.0" }, "suggest": { diff --git a/src/Symfony/Component/HttpKernel/EventListener/TranslatorListener.php b/src/Symfony/Component/HttpKernel/EventListener/TranslatorListener.php index 2a5fc71280..792e347816 100644 --- a/src/Symfony/Component/HttpKernel/EventListener/TranslatorListener.php +++ b/src/Symfony/Component/HttpKernel/EventListener/TranslatorListener.php @@ -17,7 +17,7 @@ use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpKernel\Event\FinishRequestEvent; use Symfony\Component\HttpKernel\Event\GetResponseEvent; use Symfony\Component\HttpKernel\KernelEvents; -use Symfony\Component\Translation\TranslatorInterface; +use Symfony\Contracts\Translation\TranslatorInterface; /** * Synchronizes the locale between the request and the translator. diff --git a/src/Symfony/Component/HttpKernel/Tests/EventListener/TranslatorListenerTest.php b/src/Symfony/Component/HttpKernel/Tests/EventListener/TranslatorListenerTest.php index 23b833177a..c4cb9d053b 100644 --- a/src/Symfony/Component/HttpKernel/Tests/EventListener/TranslatorListenerTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/EventListener/TranslatorListenerTest.php @@ -17,6 +17,7 @@ use Symfony\Component\HttpKernel\Event\FinishRequestEvent; use Symfony\Component\HttpKernel\Event\GetResponseEvent; use Symfony\Component\HttpKernel\EventListener\TranslatorListener; use Symfony\Component\HttpKernel\HttpKernelInterface; +use Symfony\Contracts\Translation\TranslatorInterface; class TranslatorListenerTest extends TestCase { @@ -26,7 +27,7 @@ class TranslatorListenerTest extends TestCase protected function setUp() { - $this->translator = $this->getMockBuilder('Symfony\Component\Translation\TranslatorInterface')->getMock(); + $this->translator = $this->getMockBuilder(TranslatorInterface::class)->getMock(); $this->requestStack = $this->getMockBuilder('Symfony\Component\HttpFoundation\RequestStack')->getMock(); $this->listener = new TranslatorListener($this->translator, $this->requestStack); } diff --git a/src/Symfony/Component/HttpKernel/composer.json b/src/Symfony/Component/HttpKernel/composer.json index d4974664ec..8228377e04 100644 --- a/src/Symfony/Component/HttpKernel/composer.json +++ b/src/Symfony/Component/HttpKernel/composer.json @@ -37,7 +37,7 @@ "symfony/routing": "~3.4|~4.0", "symfony/stopwatch": "~3.4|~4.0", "symfony/templating": "~3.4|~4.0", - "symfony/translation": "~3.4|~4.0", + "symfony/translation": "~4.2", "symfony/var-dumper": "^4.1.1", "psr/cache": "~1.0" }, @@ -47,6 +47,7 @@ "conflict": { "symfony/config": "<3.4", "symfony/dependency-injection": "<4.2", + "symfony/translation": "<4.2", "symfony/var-dumper": "<4.1.1", "twig/twig": "<1.34|<2.4,>=2" }, diff --git a/src/Symfony/Component/Translation/CHANGELOG.md b/src/Symfony/Component/Translation/CHANGELOG.md index ac316b8bfa..6397c3c232 100644 --- a/src/Symfony/Component/Translation/CHANGELOG.md +++ b/src/Symfony/Component/Translation/CHANGELOG.md @@ -5,6 +5,8 @@ CHANGELOG ----- * Started using ICU parent locales as fallback locales. + * deprecated `TranslatorInterface` in favor of `Symfony\Contracts\Translation\TranslatorInterface` + * deprecated `MessageSelector`, `Interval` and `PluralizationRules`; use `IdentityTranslator` instead 4.1.0 ----- diff --git a/src/Symfony/Component/Translation/DataCollectorTranslator.php b/src/Symfony/Component/Translation/DataCollectorTranslator.php index 5d4d819e0a..9088bddeba 100644 --- a/src/Symfony/Component/Translation/DataCollectorTranslator.php +++ b/src/Symfony/Component/Translation/DataCollectorTranslator.php @@ -12,11 +12,13 @@ namespace Symfony\Component\Translation; use Symfony\Component\Translation\Exception\InvalidArgumentException; +use Symfony\Component\Translation\TranslatorInterface as LegacyTranslatorInterface; +use Symfony\Contracts\Translation\TranslatorInterface; /** * @author Abdellatif Ait boudad */ -class DataCollectorTranslator implements TranslatorInterface, TranslatorBagInterface +class DataCollectorTranslator implements LegacyTranslatorInterface, TranslatorBagInterface { const MESSAGE_DEFINED = 0; const MESSAGE_MISSING = 1; diff --git a/src/Symfony/Component/Translation/Formatter/MessageFormatter.php b/src/Symfony/Component/Translation/Formatter/MessageFormatter.php index e174be36c3..862e03aab1 100644 --- a/src/Symfony/Component/Translation/Formatter/MessageFormatter.php +++ b/src/Symfony/Component/Translation/Formatter/MessageFormatter.php @@ -11,21 +11,29 @@ namespace Symfony\Component\Translation\Formatter; +use Symfony\Component\Translation\IdentityTranslator; use Symfony\Component\Translation\MessageSelector; +use Symfony\Contracts\Translation\TranslatorInterface; /** * @author Abdellatif Ait boudad */ class MessageFormatter implements MessageFormatterInterface, ChoiceMessageFormatterInterface { - private $selector; + private $translator; /** - * @param MessageSelector|null $selector The message selector for pluralization + * @param TranslatorInterface|null $translator An identity translator to use as selector for pluralization */ - public function __construct(MessageSelector $selector = null) + public function __construct($translator = null) { - $this->selector = $selector ?: new MessageSelector(); + if ($translator instanceof MessageSelector) { + $translator = new IdentityTranslator($translator); + } elseif (null !== $translator && !$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 ?? new IdentityTranslator(); } /** @@ -43,6 +51,6 @@ class MessageFormatter implements MessageFormatterInterface, ChoiceMessageFormat { $parameters = array_merge(array('%count%' => $number), $parameters); - return $this->format($this->selector->choose($message, (int) $number, $locale), $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 82b247015b..9cdcd2fcb2 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\Contracts\Translation\TranslatorTrait; + /** * IdentityTranslator does not translate anything. * @@ -18,39 +20,22 @@ namespace Symfony\Component\Translation; */ class IdentityTranslator implements TranslatorInterface { + use TranslatorTrait { + transChoice as private doTransChoice; + } + private $selector; - private $locale; /** * @param MessageSelector|null $selector The message selector for pluralization */ public function __construct(MessageSelector $selector = null) { - $this->selector = $selector ?: new MessageSelector(); - } + $this->selector = $selector; - /** - * {@inheritdoc} - */ - public function setLocale($locale) - { - $this->locale = $locale; - } - - /** - * {@inheritdoc} - */ - public function getLocale() - { - return $this->locale ?: \Locale::getDefault(); - } - - /** - * {@inheritdoc} - */ - public function trans($id, array $parameters = array(), $domain = null, $locale = null) - { - return strtr((string) $id, $parameters); + if (\get_class($this) !== __CLASS__) { + @trigger_error(sprintf('Calling "%s()" is deprecated since Symfony 4.2.'), E_USER_DEPRECATED); + } } /** @@ -58,6 +43,15 @@ class IdentityTranslator implements TranslatorInterface */ public function transChoice($id, $number, array $parameters = array(), $domain = null, $locale = null) { - return strtr($this->selector->choose((string) $id, (int) $number, $locale ?: $this->getLocale()), $parameters); + if ($this->selector) { + return strtr($this->selector->choose((string) $id, (int) $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/Interval.php b/src/Symfony/Component/Translation/Interval.php index 9e2cae648c..a0e484da86 100644 --- a/src/Symfony/Component/Translation/Interval.php +++ b/src/Symfony/Component/Translation/Interval.php @@ -11,6 +11,8 @@ namespace Symfony\Component\Translation; +@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.2, use IdentityTranslator instead.', Interval::class), E_USER_DEPRECATED); + use Symfony\Component\Translation\Exception\InvalidArgumentException; /** @@ -32,6 +34,7 @@ use Symfony\Component\Translation\Exception\InvalidArgumentException; * @author Fabien Potencier * * @see http://en.wikipedia.org/wiki/Interval_%28mathematics%29#The_ISO_notation + * @deprecated since Symfony 4.2, use IdentityTranslator instead */ class Interval { diff --git a/src/Symfony/Component/Translation/LoggingTranslator.php b/src/Symfony/Component/Translation/LoggingTranslator.php index 0145670863..0c3f515583 100644 --- a/src/Symfony/Component/Translation/LoggingTranslator.php +++ b/src/Symfony/Component/Translation/LoggingTranslator.php @@ -13,11 +13,13 @@ namespace Symfony\Component\Translation; use Psr\Log\LoggerInterface; use Symfony\Component\Translation\Exception\InvalidArgumentException; +use Symfony\Component\Translation\TranslatorInterface as LegacyTranslatorInterface; +use Symfony\Contracts\Translation\TranslatorInterface; /** * @author Abdellatif Ait boudad */ -class LoggingTranslator implements TranslatorInterface, TranslatorBagInterface +class LoggingTranslator implements LegacyTranslatorInterface, TranslatorBagInterface { /** * @var TranslatorInterface|TranslatorBagInterface diff --git a/src/Symfony/Component/Translation/MessageSelector.php b/src/Symfony/Component/Translation/MessageSelector.php index 5de171c5f4..867a345ced 100644 --- a/src/Symfony/Component/Translation/MessageSelector.php +++ b/src/Symfony/Component/Translation/MessageSelector.php @@ -11,6 +11,8 @@ namespace Symfony\Component\Translation; +@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.2, use IdentityTranslator instead.', MessageSelector::class), E_USER_DEPRECATED); + use Symfony\Component\Translation\Exception\InvalidArgumentException; /** @@ -18,6 +20,8 @@ use Symfony\Component\Translation\Exception\InvalidArgumentException; * * @author Fabien Potencier * @author Bernhard Schussek + * + * @deprecated since Symfony 4.2, use IdentityTranslator instead. */ class MessageSelector { diff --git a/src/Symfony/Component/Translation/PluralizationRules.php b/src/Symfony/Component/Translation/PluralizationRules.php index 3aca0ba963..362f6a1f1f 100644 --- a/src/Symfony/Component/Translation/PluralizationRules.php +++ b/src/Symfony/Component/Translation/PluralizationRules.php @@ -15,6 +15,8 @@ namespace Symfony\Component\Translation; * Returns the plural rules for a given locale. * * @author Fabien Potencier + * + * @deprecated since Symfony 4.2, use IdentityTranslator instead */ class PluralizationRules { @@ -28,8 +30,12 @@ class PluralizationRules * * @return int The plural position */ - public static function get($number, $locale) + public static function get($number, $locale/*, bool $triggerDeprecation = true*/) { + if (3 > \func_num_args() || \func_get_arg(2)) { + @trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.2.', __CLASS__), E_USER_DEPRECATED); + } + if ('pt_BR' === $locale) { // temporary set a locale for brazilian $locale = 'xbr'; @@ -196,6 +202,8 @@ class PluralizationRules */ public static function set(callable $rule, $locale) { + @trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.2.', __CLASS__), E_USER_DEPRECATED); + if ('pt_BR' === $locale) { // temporary set a locale for brazilian $locale = 'xbr'; diff --git a/src/Symfony/Component/Translation/Tests/IdentityTranslatorTest.php b/src/Symfony/Component/Translation/Tests/IdentityTranslatorTest.php index 78288da40e..be0a548aa1 100644 --- a/src/Symfony/Component/Translation/Tests/IdentityTranslatorTest.php +++ b/src/Symfony/Component/Translation/Tests/IdentityTranslatorTest.php @@ -11,86 +11,13 @@ namespace Symfony\Component\Translation\Tests; -use PHPUnit\Framework\TestCase; -use Symfony\Component\Intl\Util\IntlTestHelper; use Symfony\Component\Translation\IdentityTranslator; +use Symfony\Contracts\Tests\Translation\TranslatorTest; -class IdentityTranslatorTest extends TestCase +class IdentityTranslatorTest extends TranslatorTest { - /** - * @dataProvider getTransTests - */ - public function testTrans($expected, $id, $parameters) + public function getTranslator() { - $translator = new IdentityTranslator(); - - $this->assertEquals($expected, $translator->trans($id, $parameters)); - } - - /** - * @dataProvider getTransChoiceTests - */ - public function testTransChoiceWithExplicitLocale($expected, $id, $number, $parameters) - { - $translator = new IdentityTranslator(); - $translator->setLocale('en'); - - $this->assertEquals($expected, $translator->transChoice($id, $number, $parameters)); - } - - /** - * @dataProvider getTransChoiceTests - */ - public function testTransChoiceWithDefaultLocale($expected, $id, $number, $parameters) - { - \Locale::setDefault('en'); - - $translator = new IdentityTranslator(); - - $this->assertEquals($expected, $translator->transChoice($id, $number, $parameters)); - } - - public function testGetSetLocale() - { - $translator = new IdentityTranslator(); - $translator->setLocale('en'); - - $this->assertEquals('en', $translator->getLocale()); - } - - public function testGetLocaleReturnsDefaultLocaleIfNotSet() - { - // in order to test with "pt_BR" - IntlTestHelper::requireFullIntl($this, false); - - $translator = new IdentityTranslator(); - - \Locale::setDefault('en'); - $this->assertEquals('en', $translator->getLocale()); - - \Locale::setDefault('pt_BR'); - $this->assertEquals('pt_BR', $translator->getLocale()); - } - - public function getTransTests() - { - return array( - array('Symfony is great!', 'Symfony is great!', array()), - 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)), - ); + return new IdentityTranslator(); } } diff --git a/src/Symfony/Component/Translation/Tests/IntervalTest.php b/src/Symfony/Component/Translation/Tests/IntervalTest.php index 99b209a708..a8dd18caa2 100644 --- a/src/Symfony/Component/Translation/Tests/IntervalTest.php +++ b/src/Symfony/Component/Translation/Tests/IntervalTest.php @@ -14,6 +14,9 @@ namespace Symfony\Component\Translation\Tests; use PHPUnit\Framework\TestCase; use Symfony\Component\Translation\Interval; +/** + * @group legacy + */ class IntervalTest extends TestCase { /** diff --git a/src/Symfony/Component/Translation/Tests/MessageSelectorTest.php b/src/Symfony/Component/Translation/Tests/MessageSelectorTest.php index 42b7e0a3fa..5f85c5b235 100644 --- a/src/Symfony/Component/Translation/Tests/MessageSelectorTest.php +++ b/src/Symfony/Component/Translation/Tests/MessageSelectorTest.php @@ -14,6 +14,9 @@ namespace Symfony\Component\Translation\Tests; use PHPUnit\Framework\TestCase; use Symfony\Component\Translation\MessageSelector; +/** + * @group legacy + */ class MessageSelectorTest extends TestCase { /** diff --git a/src/Symfony/Component/Translation/Tests/PluralizationRulesTest.php b/src/Symfony/Component/Translation/Tests/PluralizationRulesTest.php index 5eb6c01f5d..0da8757aaa 100644 --- a/src/Symfony/Component/Translation/Tests/PluralizationRulesTest.php +++ b/src/Symfony/Component/Translation/Tests/PluralizationRulesTest.php @@ -26,6 +26,8 @@ use Symfony\Component\Translation\PluralizationRules; * The goal to cover all languages is to far fetched so this test case is smaller. * * @author Clemens Tolboom clemens@build2be.nl + * + * @group legacy */ class PluralizationRulesTest extends TestCase { diff --git a/src/Symfony/Component/Translation/TranslatorInterface.php b/src/Symfony/Component/Translation/TranslatorInterface.php index 9fcfd5bcf4..829c6f3d14 100644 --- a/src/Symfony/Component/Translation/TranslatorInterface.php +++ b/src/Symfony/Component/Translation/TranslatorInterface.php @@ -11,57 +11,13 @@ namespace Symfony\Component\Translation; -use Symfony\Component\Translation\Exception\InvalidArgumentException; +use Symfony\Contracts\Translation\TranslatorInterface as BaseTranslatorInterface; /** - * TranslatorInterface. - * * @author Fabien Potencier + * + * @deprecated since Symfony 4.2, use the same interface from the Symfony\Contracts\Translation namespace */ -interface TranslatorInterface +interface TranslatorInterface extends BaseTranslatorInterface { - /** - * Translates the given message. - * - * @param string $id The message id (may also be an object that can be cast to string) - * @param array $parameters An array of parameters for the message - * @param string|null $domain The domain for the message or null to use the default - * @param string|null $locale The locale or null to use the default - * - * @return string The translated string - * - * @throws InvalidArgumentException If the locale contains invalid characters - */ - public function trans($id, array $parameters = array(), $domain = null, $locale = null); - - /** - * Translates the given choice message by choosing a translation according to a number. - * - * @param string $id The message id (may also be an object that can be cast to string) - * @param int $number The number to use to find the indice of the message - * @param array $parameters An array of parameters for the message - * @param string|null $domain The domain for the message or null to use the default - * @param string|null $locale The locale or null to use the default - * - * @return string The translated string - * - * @throws InvalidArgumentException If the locale contains invalid characters - */ - public function transChoice($id, $number, array $parameters = array(), $domain = null, $locale = null); - - /** - * Sets the current locale. - * - * @param string $locale The locale - * - * @throws InvalidArgumentException If the locale contains invalid characters - */ - public function setLocale($locale); - - /** - * Returns the current locale. - * - * @return string The locale - */ - public function getLocale(); } diff --git a/src/Symfony/Component/Translation/composer.json b/src/Symfony/Component/Translation/composer.json index 2991949987..56b9772523 100644 --- a/src/Symfony/Component/Translation/composer.json +++ b/src/Symfony/Component/Translation/composer.json @@ -17,6 +17,7 @@ ], "require": { "php": "^7.1.3", + "symfony/contracts": "^1.0", "symfony/polyfill-mbstring": "~1.0" }, "require-dev": { diff --git a/src/Symfony/Component/Validator/CHANGELOG.md b/src/Symfony/Component/Validator/CHANGELOG.md index 9d5e17d8b4..3c9be13a5d 100644 --- a/src/Symfony/Component/Validator/CHANGELOG.md +++ b/src/Symfony/Component/Validator/CHANGELOG.md @@ -5,6 +5,9 @@ CHANGELOG ----- * added `DivisibleBy` constraint + * decoupled from `symfony/translation` by using `Symfony\Contracts\Translation\TranslatorInterface` + * deprecated `ValidatorBuilderInterface` + * made `ValidatorBuilder` final 4.1.0 ----- diff --git a/src/Symfony/Component/Validator/Context/ExecutionContext.php b/src/Symfony/Component/Validator/Context/ExecutionContext.php index 7e074863d1..03f56e9175 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContext.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContext.php @@ -11,7 +11,6 @@ namespace Symfony\Component\Validator\Context; -use Symfony\Component\Translation\TranslatorInterface; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintViolation; use Symfony\Component\Validator\ConstraintViolationList; @@ -22,6 +21,7 @@ use Symfony\Component\Validator\Mapping\PropertyMetadataInterface; use Symfony\Component\Validator\Util\PropertyPath; use Symfony\Component\Validator\Validator\ValidatorInterface; use Symfony\Component\Validator\Violation\ConstraintViolationBuilder; +use Symfony\Contracts\Translation\TranslatorInterface; /** * The context used and created by {@link ExecutionContextFactory}. diff --git a/src/Symfony/Component/Validator/Context/ExecutionContextFactory.php b/src/Symfony/Component/Validator/Context/ExecutionContextFactory.php index a8369a4955..0bb1be767e 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContextFactory.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContextFactory.php @@ -11,8 +11,8 @@ namespace Symfony\Component\Validator\Context; -use Symfony\Component\Translation\TranslatorInterface; use Symfony\Component\Validator\Validator\ValidatorInterface; +use Symfony\Contracts\Translation\TranslatorInterface; /** * Creates new {@link ExecutionContext} instances. diff --git a/src/Symfony/Component/Validator/DependencyInjection/AddValidatorInitializersPass.php b/src/Symfony/Component/Validator/DependencyInjection/AddValidatorInitializersPass.php index 9dabb58c4f..6021906cb1 100644 --- a/src/Symfony/Component/Validator/DependencyInjection/AddValidatorInitializersPass.php +++ b/src/Symfony/Component/Validator/DependencyInjection/AddValidatorInitializersPass.php @@ -11,9 +11,13 @@ namespace Symfony\Component\Validator\DependencyInjection; +use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\Translation\TranslatorInterface as LegacyTranslatorInterface; +use Symfony\Component\Validator\Util\LegacyTranslatorProxy; /** * @author Fabien Potencier @@ -42,5 +46,27 @@ class AddValidatorInitializersPass implements CompilerPassInterface } $container->getDefinition($this->builderService)->addMethodCall('addObjectInitializers', array($initializers)); + + // @deprecated logic, to be removed in Symfony 5.0 + $builder = $container->getDefinition($this->builderService); + $calls = []; + + foreach ($builder->getMethodCalls() as list($method, $arguments)) { + if ('setTranslator' === $method) { + $translator = $arguments[0] instanceof Reference ? $container->findDefinition($arguments[0]) : $arguments[0]; + + while (!($class = $translator->getClass()) && $translator instanceof ChildDefinition) { + $translator = $translator->getParent(); + } + + if (!is_subclass_of($class, LegacyTranslatorInterface::class)) { + $arguments[0] = (new Definition(LegacyTranslatorProxy::class))->addArgument($arguments[0]); + } + } + + $calls[] = array($method, $arguments); + } + + $builder->setMethodCalls($calls); } } diff --git a/src/Symfony/Component/Validator/Test/ConstraintValidatorTestCase.php b/src/Symfony/Component/Validator/Test/ConstraintValidatorTestCase.php index 087a90a185..60a1cbde0a 100644 --- a/src/Symfony/Component/Validator/Test/ConstraintValidatorTestCase.php +++ b/src/Symfony/Component/Validator/Test/ConstraintValidatorTestCase.php @@ -21,6 +21,7 @@ use Symfony\Component\Validator\Context\ExecutionContext; use Symfony\Component\Validator\Context\ExecutionContextInterface; use Symfony\Component\Validator\Mapping\ClassMetadata; use Symfony\Component\Validator\Mapping\PropertyMetadata; +use Symfony\Contracts\Translation\TranslatorInterface; /** * A test case to ease testing Constraint Validators. @@ -95,7 +96,7 @@ abstract class ConstraintValidatorTestCase extends TestCase protected function createContext() { - $translator = $this->getMockBuilder('Symfony\Component\Translation\TranslatorInterface')->getMock(); + $translator = $this->getMockBuilder(TranslatorInterface::class)->getMock(); $validator = $this->getMockBuilder('Symfony\Component\Validator\Validator\ValidatorInterface')->getMock(); $contextualValidator = $this->getMockBuilder('Symfony\Component\Validator\Validator\ContextualValidatorInterface')->getMock(); diff --git a/src/Symfony/Component/Validator/Tests/DependencyInjection/AddValidatorInitializersPassTest.php b/src/Symfony/Component/Validator/Tests/DependencyInjection/AddValidatorInitializersPassTest.php index 61c13d25e7..26d5e6646d 100644 --- a/src/Symfony/Component/Validator/Tests/DependencyInjection/AddValidatorInitializersPassTest.php +++ b/src/Symfony/Component/Validator/Tests/DependencyInjection/AddValidatorInitializersPassTest.php @@ -13,8 +13,12 @@ namespace Symfony\Component\Validator\Tests\DependencyInjection; use PHPUnit\Framework\TestCase; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\Validator\DependencyInjection\AddValidatorInitializersPass; +use Symfony\Component\Validator\Util\LegacyTranslatorProxy; +use Symfony\Contracts\Translation\TranslatorInterface; +use Symfony\Contracts\Translation\TranslatorTrait; class AddValidatorInitializersPassTest extends TestCase { @@ -41,4 +45,34 @@ class AddValidatorInitializersPassTest extends TestCase $container->getDefinition('validator.builder')->getMethodCalls() ); } + + /** + * @group legacy + */ + public function testLegacyTranslatorProxy() + { + $translator = new TestTranslator(); + $proxy = new LegacyTranslatorProxy($translator); + $this->assertSame($translator, $proxy->getTranslator()); + + $container = new ContainerBuilder(); + $container + ->register('validator.builder') + ->addMethodCall('setTranslator', array(new Reference('translator'))) + ; + + $container->register('translator', TestTranslator::class); + + (new AddValidatorInitializersPass())->process($container); + + $this->assertEquals( + array(array('setTranslator', array((new Definition(LegacyTranslatorProxy::class))->addArgument(new Reference('translator'))))), + $container->getDefinition('validator.builder')->removeMethodCall('addObjectInitializers')->getMethodCalls() + ); + } +} + +class TestTranslator implements TranslatorInterface +{ + use TranslatorTrait; } diff --git a/src/Symfony/Component/Validator/Tests/ValidatorBuilderTest.php b/src/Symfony/Component/Validator/Tests/ValidatorBuilderTest.php index 77d586412b..155c4f8210 100644 --- a/src/Symfony/Component/Validator/Tests/ValidatorBuilderTest.php +++ b/src/Symfony/Component/Validator/Tests/ValidatorBuilderTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Validator\Tests; use PHPUnit\Framework\TestCase; +use Symfony\Component\Validator\Util\LegacyTranslatorProxy; use Symfony\Component\Validator\ValidatorBuilder; use Symfony\Component\Validator\ValidatorBuilderInterface; @@ -105,6 +106,14 @@ class ValidatorBuilderTest extends TestCase ); } + public function testLegacyTranslatorProxy() + { + $proxy = $this->getMockBuilder(LegacyTranslatorProxy::class)->disableOriginalConstructor()->getMock(); + $proxy->expects($this->once())->method('getTranslator'); + + $this->builder->setTranslator($proxy); + } + public function testSetTranslationDomain() { $this->assertSame($this->builder, $this->builder->setTranslationDomain('TRANS_DOMAIN')); diff --git a/src/Symfony/Component/Validator/Util/LegacyTranslatorProxy.php b/src/Symfony/Component/Validator/Util/LegacyTranslatorProxy.php new file mode 100644 index 0000000000..077d37931f --- /dev/null +++ b/src/Symfony/Component/Validator/Util/LegacyTranslatorProxy.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Validator\Util; + +use Symfony\Component\Translation\TranslatorInterface as LegacyTranslatorInterface; +use Symfony\Contracts\Translation\TranslatorInterface; + +/** + * @internal to be removed in Symfony 5.0. + */ +class LegacyTranslatorProxy implements LegacyTranslatorInterface +{ + private $translator; + + public function __construct(TranslatorInterface $translator) + { + $this->translator = $translator; + } + + public function getTranslator(): TranslatorInterface + { + return $this->translator; + } + + /** + * {@inheritdoc} + */ + public function setLocale($locale) + { + $this->translator->setLocale($locale); + } + + /** + * {@inheritdoc} + */ + public function getLocale() + { + return $this->translator->getLocale(); + } + + /** + * {@inheritdoc} + */ + public function trans($id, array $parameters = array(), $domain = null, $locale = null) + { + return $this->translator->trans($id, $parameters, $domain, $locale); + } + + /** + * {@inheritdoc} + */ + public function transChoice($id, $number, array $parameters = array(), $domain = null, $locale = null) + { + return $this->translator->transChoice($id, $number, $parameters, $domain, $locale); + } +} diff --git a/src/Symfony/Component/Validator/ValidatorBuilder.php b/src/Symfony/Component/Validator/ValidatorBuilder.php index 11c5052f3b..047f1827ba 100644 --- a/src/Symfony/Component/Validator/ValidatorBuilder.php +++ b/src/Symfony/Component/Validator/ValidatorBuilder.php @@ -15,8 +15,7 @@ use Doctrine\Common\Annotations\AnnotationReader; use Doctrine\Common\Annotations\CachedReader; use Doctrine\Common\Annotations\Reader; use Doctrine\Common\Cache\ArrayCache; -use Symfony\Component\Translation\IdentityTranslator; -use Symfony\Component\Translation\TranslatorInterface; +use Symfony\Component\Translation\TranslatorInterface as LegacyTranslatorInterface; use Symfony\Component\Validator\Context\ExecutionContextFactory; use Symfony\Component\Validator\Exception\ValidatorException; use Symfony\Component\Validator\Mapping\Cache\CacheInterface; @@ -28,16 +27,22 @@ use Symfony\Component\Validator\Mapping\Loader\LoaderInterface; use Symfony\Component\Validator\Mapping\Loader\StaticMethodLoader; use Symfony\Component\Validator\Mapping\Loader\XmlFileLoader; use Symfony\Component\Validator\Mapping\Loader\YamlFileLoader; +use Symfony\Component\Validator\Util\LegacyTranslatorProxy; use Symfony\Component\Validator\Validator\RecursiveValidator; +use Symfony\Contracts\Translation\TranslatorInterface; +use Symfony\Contracts\Translation\TranslatorTrait; /** * The default implementation of {@link ValidatorBuilderInterface}. * * @author Bernhard Schussek + * + * @final since Symfony 4.2 */ class ValidatorBuilder implements ValidatorBuilderInterface { private $initializers = array(); + private $loaders = array(); private $xmlMappings = array(); private $yamlMappings = array(); private $methodMappings = array(); @@ -249,9 +254,9 @@ class ValidatorBuilder implements ValidatorBuilderInterface /** * {@inheritdoc} */ - public function setTranslator(TranslatorInterface $translator) + public function setTranslator(LegacyTranslatorInterface $translator) { - $this->translator = $translator; + $this->translator = $translator instanceof LegacyTranslatorProxy ? $translator->getTranslator() : $translator; return $this; } @@ -266,6 +271,16 @@ class ValidatorBuilder implements ValidatorBuilderInterface return $this; } + /** + * @return $this + */ + public function addLoader(LoaderInterface $loader) + { + $this->loaders[] = $loader; + + return $this; + } + /** * @return LoaderInterface[] */ @@ -289,7 +304,7 @@ class ValidatorBuilder implements ValidatorBuilderInterface $loaders[] = new AnnotationLoader($this->annotationReader); } - return $loaders; + return array_merge($loaders, $this->loaders); } /** @@ -316,7 +331,9 @@ class ValidatorBuilder implements ValidatorBuilderInterface $translator = $this->translator; if (null === $translator) { - $translator = new IdentityTranslator(); + $translator = new class() implements TranslatorInterface { + use TranslatorTrait; + }; // Force the locale to be 'en' when no translator is provided rather than relying on the Intl default locale // This avoids depending on Intl or the stub implementation being available. It also ensures that Symfony // validation messages are pluralized properly even when the default locale gets changed because they are in diff --git a/src/Symfony/Component/Validator/ValidatorBuilderInterface.php b/src/Symfony/Component/Validator/ValidatorBuilderInterface.php index d7cd16836b..1c764d1796 100644 --- a/src/Symfony/Component/Validator/ValidatorBuilderInterface.php +++ b/src/Symfony/Component/Validator/ValidatorBuilderInterface.php @@ -21,6 +21,8 @@ use Symfony\Component\Validator\Validator\ValidatorInterface; * A configurable builder for ValidatorInterface objects. * * @author Bernhard Schussek + * + * @deprecated since Symfony 4.2, use the ValidatorBuilder class instead */ interface ValidatorBuilderInterface { diff --git a/src/Symfony/Component/Validator/Violation/ConstraintViolationBuilder.php b/src/Symfony/Component/Validator/Violation/ConstraintViolationBuilder.php index 0d96ffe894..621c38d9af 100644 --- a/src/Symfony/Component/Validator/Violation/ConstraintViolationBuilder.php +++ b/src/Symfony/Component/Validator/Violation/ConstraintViolationBuilder.php @@ -11,11 +11,11 @@ namespace Symfony\Component\Validator\Violation; -use Symfony\Component\Translation\TranslatorInterface; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintViolation; use Symfony\Component\Validator\ConstraintViolationList; use Symfony\Component\Validator\Util\PropertyPath; +use Symfony\Contracts\Translation\TranslatorInterface; /** * Default implementation of {@link ConstraintViolationBuilderInterface}. diff --git a/src/Symfony/Component/Validator/Violation/ConstraintViolationBuilderInterface.php b/src/Symfony/Component/Validator/Violation/ConstraintViolationBuilderInterface.php index 811b4842e8..d25b3a10f9 100644 --- a/src/Symfony/Component/Validator/Violation/ConstraintViolationBuilderInterface.php +++ b/src/Symfony/Component/Validator/Violation/ConstraintViolationBuilderInterface.php @@ -64,7 +64,7 @@ interface ConstraintViolationBuilderInterface * * @return $this * - * @see \Symfony\Component\Translation\TranslatorInterface + * @see \Symfony\Contracts\Translation\TranslatorInterface */ public function setTranslationDomain($translationDomain); @@ -85,7 +85,7 @@ interface ConstraintViolationBuilderInterface * * @return $this * - * @see \Symfony\Component\Translation\TranslatorInterface::transChoice() + * @see \Symfony\Contracts\Translation\TranslatorInterface::transChoice() */ public function setPlural($number); diff --git a/src/Symfony/Component/Validator/composer.json b/src/Symfony/Component/Validator/composer.json index 0e5253905e..442d7c67b4 100644 --- a/src/Symfony/Component/Validator/composer.json +++ b/src/Symfony/Component/Validator/composer.json @@ -19,8 +19,7 @@ "php": "^7.1.3", "symfony/contracts": "^1.0", "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-mbstring": "~1.0", - "symfony/translation": "~3.4|~4.0" + "symfony/polyfill-mbstring": "~1.0" }, "require-dev": { "symfony/http-foundation": "~4.1", @@ -33,6 +32,7 @@ "symfony/expression-language": "~3.4|~4.0", "symfony/cache": "~3.4|~4.0", "symfony/property-access": "~3.4|~4.0", + "symfony/translation": "~4.2", "doctrine/annotations": "~1.0", "doctrine/cache": "~1.0", "egulias/email-validator": "^1.2.8|~2.0" @@ -42,6 +42,7 @@ "symfony/dependency-injection": "<3.4", "symfony/http-kernel": "<3.4", "symfony/intl": "<4.1", + "symfony/translation": "<4.2", "symfony/yaml": "<3.4" }, "suggest": { @@ -50,6 +51,7 @@ "doctrine/cache": "For using the default cached annotation reader and metadata cache.", "symfony/http-foundation": "", "symfony/intl": "", + "symfony/translation": "For translating validation errors.", "symfony/yaml": "", "symfony/config": "", "egulias/email-validator": "Strict (RFC compliant) email validation", diff --git a/src/Symfony/Contracts/CHANGELOG.md b/src/Symfony/Contracts/CHANGELOG.md index 6e82b5c5bc..a6c997d80c 100644 --- a/src/Symfony/Contracts/CHANGELOG.md +++ b/src/Symfony/Contracts/CHANGELOG.md @@ -5,3 +5,4 @@ CHANGELOG ----- * added `Service\ResetInterface` to provide a way to reset an object to its initial state + * added `Translation\TranslatorInterface` and `Translation\TranslatorTrait` diff --git a/src/Symfony/Contracts/Tests/Translation/TranslatorTest.php b/src/Symfony/Contracts/Tests/Translation/TranslatorTest.php new file mode 100644 index 0000000000..e7af8e4da3 --- /dev/null +++ b/src/Symfony/Contracts/Tests/Translation/TranslatorTest.php @@ -0,0 +1,353 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\Tests\Translation; + +use PHPUnit\Framework\TestCase; +use Symfony\Contracts\Translation\TranslatorInterface; +use Symfony\Contracts\Translation\TranslatorTrait; + +/** + * Test should cover all languages mentioned on http://translate.sourceforge.net/wiki/l10n/pluralforms + * and Plural forms mentioned on http://www.gnu.org/software/gettext/manual/gettext.html#Plural-forms. + * + * See also https://developer.mozilla.org/en/Localization_and_Plurals which mentions 15 rules having a maximum of 6 forms. + * The mozilla code is also interesting to check for. + * + * As mentioned by chx http://drupal.org/node/1273968 we can cover all by testing number from 0 to 199 + * + * The goal to cover all languages is to far fetched so this test case is smaller. + * + * @author Clemens Tolboom clemens@build2be.nl + */ +class TranslatorTest extends TestCase +{ + public function getTranslator() + { + return new class() implements TranslatorInterface { + use TranslatorTrait; + }; + } + + /** + * @dataProvider getTransTests + */ + public function testTrans($expected, $id, $parameters) + { + $translator = $this->getTranslator(); + + $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(); + $translator->setLocale('en'); + + $this->assertEquals('en', $translator->getLocale()); + } + + /** + * @requires extension intl + */ + public function testGetLocaleReturnsDefaultLocaleIfNotSet() + { + $translator = $this->getTranslator(); + + \Locale::setDefault('pt_BR'); + $this->assertEquals('pt_BR', $translator->getLocale()); + + \Locale::setDefault('en'); + $this->assertEquals('en', $translator->getLocale()); + } + + public function getTransTests() + { + return array( + array('Symfony is great!', 'Symfony is great!', array()), + 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 new file mode 100644 index 0000000000..a54718733b --- /dev/null +++ b/src/Symfony/Contracts/Translation/TranslatorInterface.php @@ -0,0 +1,93 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\Translation; + +/** + * @author Fabien Potencier + */ +interface TranslatorInterface +{ + /** + * Translates the given message. + * + * @param string $id The message id (may also be an object that can be cast to string) + * @param array $parameters An array of parameters for the message + * @param string|null $domain The domain for the message or null to use the default + * @param string|null $locale The locale or null to use the default + * + * @return string The translated string + * + * @throws \InvalidArgumentException If the locale contains invalid characters + */ + public function trans($id, array $parameters = array(), $domain = null, $locale = null); + + /** + * Translates the given choice message by choosing a translation according to a number. + * + * Given a message with different plural translations separated by a + * pipe (|), this method returns the correct portion of the message based + * on the given number, locale and the pluralization rules in the message + * itself. + * + * The message supports two different types of pluralization rules: + * + * interval: {0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples + * indexed: There is one apple|There are %count% apples + * + * The indexed solution can also contain labels (e.g. one: There is one apple). + * This is purely for making the translations more clear - it does not + * affect the functionality. + * + * The two methods can also be mixed: + * {0} There are no apples|one: There is one apple|more: There are %count% apples + * + * An interval can represent a finite set of numbers: + * {1,2,3,4} + * + * An interval can represent numbers between two numbers: + * [1, +Inf] + * ]-1,2[ + * + * The left delimiter can be [ (inclusive) or ] (exclusive). + * The right delimiter can be [ (exclusive) or ] (inclusive). + * Beside numbers, you can use -Inf and +Inf for the infinite. + * + * @see https://en.wikipedia.org/wiki/ISO_31-11 + * + * @param string $id The message id (may also be an object that can be cast to string) + * @param int $number The number to use to find the indice of the message + * @param array $parameters An array of parameters for the message + * @param string|null $domain The domain for the message or null to use the default + * @param string|null $locale The locale or null to use the default + * + * @return string The translated string + * + * @throws \InvalidArgumentException If the locale contains invalid characters + */ + public function transChoice($id, $number, array $parameters = array(), $domain = null, $locale = null); + + /** + * Sets the current locale. + * + * @param string $locale The locale + * + * @throws \InvalidArgumentException If the locale contains invalid characters + */ + public function setLocale($locale); + + /** + * Returns the current locale. + * + * @return string The locale + */ + public function getLocale(); +} diff --git a/src/Symfony/Contracts/Translation/TranslatorTrait.php b/src/Symfony/Contracts/Translation/TranslatorTrait.php new file mode 100644 index 0000000000..7c392b2bd4 --- /dev/null +++ b/src/Symfony/Contracts/Translation/TranslatorTrait.php @@ -0,0 +1,258 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\Translation; + +use Symfony\Component\Translation\Exception\InvalidArgumentException; + +/** + * A trait to help implement TranslatorInterface. + * + * @author Fabien Potencier + */ +trait TranslatorTrait +{ + private $locale; + + /** + * {@inheritdoc} + */ + public function setLocale($locale) + { + $this->locale = (string) $locale; + } + + /** + * {@inheritdoc} + */ + public function getLocale() + { + return $this->locale ?: \Locale::getDefault(); + } + + /** + * {@inheritdoc} + */ + public function trans($id, array $parameters = array(), $domain = null, $locale = null) + { + return strtr((string) $id, $parameters); + } + + /** + * {@inheritdoc} + */ + public function transChoice($id, $number, array $parameters = array(), $domain = null, $locale = null) + { + $id = (string) $id; + $number = (float) $number; + $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; + } + } +}