feature #28210 [Contracts] Add Translation\TranslatorInterface + decouple symfony/validator from symfony/translation (nicolas-grekas)

This PR was merged into the 4.2-dev branch.

Discussion
----------

[Contracts] Add Translation\TranslatorInterface + decouple symfony/validator from symfony/translation

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | yes
| Tests pass?   | yes
| Fixed tickets | #15714, #18930
| License       | MIT
| Doc PR        | -

Let's decouple Validator from Translation component \o/!

TODO:
- [x] add `TranslatorInterface`, deprecate it from Translation
- [x] add `TranslatorTrait`, deprecating `MessageSelector`, `Internal` and `PluralizationRules`
- [x] deprecate `ValidatorBuilderInterface(LegacyTranslatorInterface)`
- [x] inject a new `identity_translator` into `translator.formatter.default`, deprecate `translator.selector`
- [x] copy tests in the Contracts namespace to ensure the `TranslatorTrait` behaves properly
- [x] figure out a way to keep throwing `InvalidArgumentException` from the component
- [x] update UPGRADING and CHANGELOG files
- [x] polish the deprecation layer (ensure all needed runtime deprecations are here)

Reviews welcome already.

Commits
-------

064e369e06 [Contracts] Add Translation\TranslatorInterface + decouple symfony/validator from symfony/translation
This commit is contained in:
Fabien Potencier 2018-09-03 22:20:47 +02:00
commit 59fad59886
58 changed files with 1014 additions and 206 deletions

View File

@ -149,3 +149,15 @@ Serializer
* Relying on the default value (false) of the "as_collection" option is deprecated since 4.2. * 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. 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

View File

@ -146,6 +146,8 @@ Translation
* The `FileDumper::setBackup()` method has been removed. * The `FileDumper::setBackup()` method has been removed.
* The `TranslationWriter::disableBackup()` 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 TwigBundle
---------- ----------
@ -158,6 +160,8 @@ Validator
* The `Email::__construct()` 'strict' property has been removed. Use 'mode'=>"strict" instead. * 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. * 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. * 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 Workflow
-------- --------

View File

@ -16,7 +16,7 @@ use Symfony\Bridge\Twig\NodeVisitor\TranslationNodeVisitor;
use Symfony\Bridge\Twig\TokenParser\TransChoiceTokenParser; use Symfony\Bridge\Twig\TokenParser\TransChoiceTokenParser;
use Symfony\Bridge\Twig\TokenParser\TransDefaultDomainTokenParser; use Symfony\Bridge\Twig\TokenParser\TransDefaultDomainTokenParser;
use Symfony\Bridge\Twig\TokenParser\TransTokenParser; use Symfony\Bridge\Twig\TokenParser\TransTokenParser;
use Symfony\Component\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
use Twig\Extension\AbstractExtension; use Twig\Extension\AbstractExtension;
use Twig\NodeVisitor\NodeVisitorInterface; use Twig\NodeVisitor\NodeVisitorInterface;
use Twig\TokenParser\AbstractTokenParser; use Twig\TokenParser\AbstractTokenParser;

View File

@ -11,7 +11,7 @@
namespace Symfony\Bridge\Twig\Tests\Extension\Fixtures; namespace Symfony\Bridge\Twig\Tests\Extension\Fixtures;
use Symfony\Component\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
class StubTranslator implements TranslatorInterface class StubTranslator implements TranslatorInterface
{ {

View File

@ -15,6 +15,7 @@ use PHPUnit\Framework\TestCase;
use Symfony\Bridge\Twig\Extension\TranslationExtension; use Symfony\Bridge\Twig\Extension\TranslationExtension;
use Symfony\Bridge\Twig\Translation\TwigExtractor; use Symfony\Bridge\Twig\Translation\TwigExtractor;
use Symfony\Component\Translation\MessageCatalogue; use Symfony\Component\Translation\MessageCatalogue;
use Symfony\Contracts\Translation\TranslatorInterface;
use Twig\Environment; use Twig\Environment;
use Twig\Error\Error; use Twig\Error\Error;
use Twig\Loader\ArrayLoader; use Twig\Loader\ArrayLoader;
@ -33,7 +34,7 @@ class TwigExtractorTest extends TestCase
'cache' => false, 'cache' => false,
'autoescape' => 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 = new TwigExtractor($twig);
$extractor->setPrefix('prefix'); $extractor->setPrefix('prefix');
@ -82,7 +83,7 @@ class TwigExtractorTest extends TestCase
public function testExtractSyntaxError($resources) public function testExtractSyntaxError($resources)
{ {
$twig = new Environment($this->getMockBuilder('Twig\Loader\LoaderInterface')->getMock()); $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); $extractor = new TwigExtractor($twig);
@ -124,7 +125,7 @@ class TwigExtractorTest extends TestCase
'cache' => false, 'cache' => false,
'autoescape' => 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 = new TwigExtractor($twig);
$catalogue = new MessageCatalogue('en'); $catalogue = new MessageCatalogue('en');

View File

@ -29,7 +29,7 @@
"symfony/polyfill-intl-icu": "~1.0", "symfony/polyfill-intl-icu": "~1.0",
"symfony/routing": "~3.4|~4.0", "symfony/routing": "~3.4|~4.0",
"symfony/templating": "~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/yaml": "~3.4|~4.0",
"symfony/security": "~3.4|~4.0", "symfony/security": "~3.4|~4.0",
"symfony/security-acl": "~2.8|~3.0", "symfony/security-acl": "~2.8|~3.0",
@ -41,8 +41,9 @@
"symfony/workflow": "~3.4|~4.0" "symfony/workflow": "~3.4|~4.0"
}, },
"conflict": { "conflict": {
"symfony/console": "<3.4",
"symfony/form": "<4.1.2", "symfony/form": "<4.1.2",
"symfony/console": "<3.4" "symfony/translation": "<4.2"
}, },
"suggest": { "suggest": {
"symfony/finder": "", "symfony/finder": "",

View File

@ -15,7 +15,7 @@ use Psr\Container\ContainerInterface;
use Symfony\Component\DependencyInjection\ServiceSubscriberInterface; use Symfony\Component\DependencyInjection\ServiceSubscriberInterface;
use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface; use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface;
use Symfony\Component\HttpKernel\CacheWarmer\WarmableInterface; use Symfony\Component\HttpKernel\CacheWarmer\WarmableInterface;
use Symfony\Component\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
/** /**
* Generates the catalogues for translations. * Generates the catalogues for translations.

View File

@ -26,7 +26,7 @@ use Symfony\Component\Translation\LoggingTranslator;
use Symfony\Component\Translation\MessageCatalogue; use Symfony\Component\Translation\MessageCatalogue;
use Symfony\Component\Translation\Reader\TranslationReaderInterface; use Symfony\Component\Translation\Reader\TranslationReaderInterface;
use Symfony\Component\Translation\Translator; 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 * Helps finding unused or missing translation messages in a given locale

View File

@ -15,7 +15,7 @@ use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\Translation\TranslatorBagInterface; use Symfony\Component\Translation\TranslatorBagInterface;
use Symfony\Component\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
/** /**
* @author Abdellatif Ait boudad <a.aitboudad@gmail.com> * @author Abdellatif Ait boudad <a.aitboudad@gmail.com>

View File

@ -6,11 +6,13 @@
<services> <services>
<defaults public="false" /> <defaults public="false" />
<service id="translator" class="Symfony\Component\Translation\IdentityTranslator" public="true"> <service id="translator" class="Symfony\Component\Translation\IdentityTranslator" public="true" />
<argument type="service" id="translator.selector" />
</service>
<service id="Symfony\Component\Translation\TranslatorInterface" alias="translator" /> <service id="Symfony\Component\Translation\TranslatorInterface" alias="translator" />
<service id="Symfony\Contracts\Translation\TranslatorInterface" alias="translator" />
<service id="translator.selector" class="Symfony\Component\Translation\MessageSelector" /> <service id="identity_translator" class="Symfony\Component\Translation\IdentityTranslator" />
<service id="translator.selector" class="Symfony\Component\Translation\MessageSelector">
<deprecated>The "%service_id%" service is deprecated since Symfony 4.2, use "identity_translator" instead.</deprecated>
</service>
</services> </services>
</container> </container>

View File

@ -21,6 +21,7 @@
</call> </call>
</service> </service>
<service id="Symfony\Component\Translation\TranslatorInterface" alias="translator" /> <service id="Symfony\Component\Translation\TranslatorInterface" alias="translator" />
<service id="Symfony\Contracts\Translation\TranslatorInterface" alias="translator" />
<service id="translator.logging" class="Symfony\Component\Translation\LoggingTranslator"> <service id="translator.logging" class="Symfony\Component\Translation\LoggingTranslator">
<argument type="service" id="translator.logging.inner" /> <argument type="service" id="translator.logging.inner" />
@ -29,7 +30,7 @@
</service> </service>
<service id="translator.formatter.default" class="Symfony\Component\Translation\Formatter\MessageFormatter"> <service id="translator.formatter.default" class="Symfony\Component\Translation\Formatter\MessageFormatter">
<argument type="service" id="translator.selector" /> <argument type="service" id="identity_translator" />
</service> </service>
<service id="translation.loader.php" class="Symfony\Component\Translation\Loader\PhpFileLoader"> <service id="translation.loader.php" class="Symfony\Component\Translation\Loader\PhpFileLoader">

View File

@ -12,7 +12,7 @@
namespace Symfony\Bundle\FrameworkBundle\Templating\Helper; namespace Symfony\Bundle\FrameworkBundle\Templating\Helper;
use Symfony\Component\Templating\Helper\Helper; use Symfony\Component\Templating\Helper\Helper;
use Symfony\Component\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
/** /**
* @author Fabien Potencier <fabien@symfony.com> * @author Fabien Potencier <fabien@symfony.com>

View File

@ -15,7 +15,7 @@ use PHPUnit\Framework\TestCase;
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\DataCollectorTranslatorPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\DataCollectorTranslatorPass;
use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
class DataCollectorTranslatorPassTest extends TestCase class DataCollectorTranslatorPassTest extends TestCase
{ {

View File

@ -11,7 +11,7 @@
namespace Symfony\Bundle\FrameworkBundle\Tests\Templating\Helper\Fixtures; namespace Symfony\Bundle\FrameworkBundle\Tests\Templating\Helper\Fixtures;
use Symfony\Component\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
class StubTranslator implements TranslatorInterface class StubTranslator implements TranslatorInterface
{ {

View File

@ -47,7 +47,7 @@
"symfony/security-csrf": "~3.4|~4.0", "symfony/security-csrf": "~3.4|~4.0",
"symfony/serializer": "^4.1", "symfony/serializer": "^4.1",
"symfony/stopwatch": "~3.4|~4.0", "symfony/stopwatch": "~3.4|~4.0",
"symfony/translation": "~3.4|~4.0", "symfony/translation": "~4.2",
"symfony/templating": "~3.4|~4.0", "symfony/templating": "~3.4|~4.0",
"symfony/validator": "^4.1", "symfony/validator": "^4.1",
"symfony/var-dumper": "~3.4|~4.0", "symfony/var-dumper": "~3.4|~4.0",
@ -71,7 +71,7 @@
"symfony/property-info": "<3.4", "symfony/property-info": "<3.4",
"symfony/serializer": "<4.1", "symfony/serializer": "<4.1",
"symfony/stopwatch": "<3.4", "symfony/stopwatch": "<3.4",
"symfony/translation": "<3.4", "symfony/translation": "<4.2",
"symfony/twig-bridge": "<4.1.1", "symfony/twig-bridge": "<4.1.1",
"symfony/validator": "<4.1", "symfony/validator": "<4.1",
"symfony/workflow": "<4.1" "symfony/workflow": "<4.1"

View File

@ -19,6 +19,7 @@ use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\HttpKernel\DependencyInjection\Extension; use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\Translation\Translator;
use Twig\Extension\ExtensionInterface; use Twig\Extension\ExtensionInterface;
use Twig\Extension\RuntimeExtensionInterface; use Twig\Extension\RuntimeExtensionInterface;
use Twig\Loader\LoaderInterface; use Twig\Loader\LoaderInterface;
@ -48,7 +49,7 @@ class TwigExtension extends Extension
$loader->load('console.xml'); $loader->load('console.xml');
} }
if (!interface_exists('Symfony\Component\Translation\TranslatorInterface')) { if (!class_exists(Translator::class)) {
$container->removeDefinition('twig.translation.extractor'); $container->removeDefinition('twig.translation.extractor');
} }

View File

@ -41,7 +41,8 @@
}, },
"conflict": { "conflict": {
"symfony/dependency-injection": "<4.1", "symfony/dependency-injection": "<4.1",
"symfony/framework-bundle": "<4.1" "symfony/framework-bundle": "<4.1",
"symfony/translation": "<4.2"
}, },
"autoload": { "autoload": {
"psr-4": { "Symfony\\Bundle\\TwigBundle\\": "" }, "psr-4": { "Symfony\\Bundle\\TwigBundle\\": "" },

View File

@ -13,7 +13,7 @@ namespace Symfony\Component\Form\Extension\Csrf;
use Symfony\Component\Form\AbstractExtension; use Symfony\Component\Form\AbstractExtension;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; 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. * This extension protects forms by using a CSRF token.

View File

@ -18,7 +18,7 @@ use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\Util\ServerParams; use Symfony\Component\Form\Util\ServerParams;
use Symfony\Component\Security\Csrf\CsrfToken; use Symfony\Component\Security\Csrf\CsrfToken;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Component\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
/** /**
* @author Bernhard Schussek <bschussek@gmail.com> * @author Bernhard Schussek <bschussek@gmail.com>

View File

@ -19,7 +19,7 @@ use Symfony\Component\Form\FormView;
use Symfony\Component\Form\Util\ServerParams; use Symfony\Component\Form\Util\ServerParams;
use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Component\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
/** /**
* @author Bernhard Schussek <bschussek@gmail.com> * @author Bernhard Schussek <bschussek@gmail.com>

View File

@ -14,7 +14,7 @@ namespace Symfony\Component\Form\Extension\Validator\Type;
use Symfony\Component\Form\AbstractTypeExtension; use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
/** /**
* @author Abdellatif Ait boudad <a.aitboudad@gmail.com> * @author Abdellatif Ait boudad <a.aitboudad@gmail.com>

View File

@ -17,6 +17,8 @@ use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormError; use Symfony\Component\Form\FormError;
use Symfony\Component\Form\Test\TypeTestCase; use Symfony\Component\Form\Test\TypeTestCase;
use Symfony\Component\Security\Csrf\CsrfToken; use Symfony\Component\Security\Csrf\CsrfToken;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
class FormTypeCsrfExtensionTest_ChildType extends AbstractType class FormTypeCsrfExtensionTest_ChildType extends AbstractType
{ {
@ -42,8 +44,8 @@ class FormTypeCsrfExtensionTest extends TypeTestCase
protected function setUp() protected function setUp()
{ {
$this->tokenManager = $this->getMockBuilder('Symfony\Component\Security\Csrf\CsrfTokenManagerInterface')->getMock(); $this->tokenManager = $this->getMockBuilder(CsrfTokenManagerInterface::class)->getMock();
$this->translator = $this->getMockBuilder('Symfony\Component\Translation\TranslatorInterface')->getMock(); $this->translator = $this->getMockBuilder(TranslatorInterface::class)->getMock();
parent::setUp(); parent::setUp();
} }

View File

@ -15,12 +15,13 @@ use Symfony\Component\Form\Extension\Validator\Type\UploadValidatorExtension;
use Symfony\Component\Form\Test\TypeTestCase; use Symfony\Component\Form\Test\TypeTestCase;
use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Contracts\Translation\TranslatorInterface;
class UploadValidatorExtensionTest extends TypeTestCase class UploadValidatorExtensionTest extends TypeTestCase
{ {
public function testPostMaxSizeTranslation() public function testPostMaxSizeTranslation()
{ {
$translator = $this->getMockBuilder('Symfony\Component\Translation\TranslatorInterface')->getMock(); $translator = $this->getMockBuilder(TranslatorInterface::class)->getMock();
$translator->expects($this->any()) $translator->expects($this->any())
->method('trans') ->method('trans')

View File

@ -33,7 +33,7 @@
"symfony/http-foundation": "~3.4|~4.0", "symfony/http-foundation": "~3.4|~4.0",
"symfony/http-kernel": "~3.4|~4.0", "symfony/http-kernel": "~3.4|~4.0",
"symfony/security-csrf": "~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" "symfony/var-dumper": "~3.4|~4.0"
}, },
"conflict": { "conflict": {
@ -42,6 +42,7 @@
"symfony/doctrine-bridge": "<3.4", "symfony/doctrine-bridge": "<3.4",
"symfony/framework-bundle": "<3.4", "symfony/framework-bundle": "<3.4",
"symfony/http-kernel": "<3.4", "symfony/http-kernel": "<3.4",
"symfony/translation": "<4.2",
"symfony/twig-bridge": "<3.4.5|<4.0.5,>=4.0" "symfony/twig-bridge": "<3.4.5|<4.0.5,>=4.0"
}, },
"suggest": { "suggest": {

View File

@ -17,7 +17,7 @@ use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel\Event\FinishRequestEvent; use Symfony\Component\HttpKernel\Event\FinishRequestEvent;
use Symfony\Component\HttpKernel\Event\GetResponseEvent; use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
/** /**
* Synchronizes the locale between the request and the translator. * Synchronizes the locale between the request and the translator.

View File

@ -17,6 +17,7 @@ use Symfony\Component\HttpKernel\Event\FinishRequestEvent;
use Symfony\Component\HttpKernel\Event\GetResponseEvent; use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\EventListener\TranslatorListener; use Symfony\Component\HttpKernel\EventListener\TranslatorListener;
use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
class TranslatorListenerTest extends TestCase class TranslatorListenerTest extends TestCase
{ {
@ -26,7 +27,7 @@ class TranslatorListenerTest extends TestCase
protected function setUp() 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->requestStack = $this->getMockBuilder('Symfony\Component\HttpFoundation\RequestStack')->getMock();
$this->listener = new TranslatorListener($this->translator, $this->requestStack); $this->listener = new TranslatorListener($this->translator, $this->requestStack);
} }

View File

@ -37,7 +37,7 @@
"symfony/routing": "~3.4|~4.0", "symfony/routing": "~3.4|~4.0",
"symfony/stopwatch": "~3.4|~4.0", "symfony/stopwatch": "~3.4|~4.0",
"symfony/templating": "~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", "symfony/var-dumper": "^4.1.1",
"psr/cache": "~1.0" "psr/cache": "~1.0"
}, },
@ -47,6 +47,7 @@
"conflict": { "conflict": {
"symfony/config": "<3.4", "symfony/config": "<3.4",
"symfony/dependency-injection": "<4.2", "symfony/dependency-injection": "<4.2",
"symfony/translation": "<4.2",
"symfony/var-dumper": "<4.1.1", "symfony/var-dumper": "<4.1.1",
"twig/twig": "<1.34|<2.4,>=2" "twig/twig": "<1.34|<2.4,>=2"
}, },

View File

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

View File

@ -12,11 +12,13 @@
namespace Symfony\Component\Translation; namespace Symfony\Component\Translation;
use Symfony\Component\Translation\Exception\InvalidArgumentException; use Symfony\Component\Translation\Exception\InvalidArgumentException;
use Symfony\Component\Translation\TranslatorInterface as LegacyTranslatorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
/** /**
* @author Abdellatif Ait boudad <a.aitboudad@gmail.com> * @author Abdellatif Ait boudad <a.aitboudad@gmail.com>
*/ */
class DataCollectorTranslator implements TranslatorInterface, TranslatorBagInterface class DataCollectorTranslator implements LegacyTranslatorInterface, TranslatorBagInterface
{ {
const MESSAGE_DEFINED = 0; const MESSAGE_DEFINED = 0;
const MESSAGE_MISSING = 1; const MESSAGE_MISSING = 1;

View File

@ -11,21 +11,29 @@
namespace Symfony\Component\Translation\Formatter; namespace Symfony\Component\Translation\Formatter;
use Symfony\Component\Translation\IdentityTranslator;
use Symfony\Component\Translation\MessageSelector; use Symfony\Component\Translation\MessageSelector;
use Symfony\Contracts\Translation\TranslatorInterface;
/** /**
* @author Abdellatif Ait boudad <a.aitboudad@gmail.com> * @author Abdellatif Ait boudad <a.aitboudad@gmail.com>
*/ */
class MessageFormatter implements MessageFormatterInterface, ChoiceMessageFormatterInterface 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); $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);
} }
} }

View File

@ -11,6 +11,8 @@
namespace Symfony\Component\Translation; namespace Symfony\Component\Translation;
use Symfony\Contracts\Translation\TranslatorTrait;
/** /**
* IdentityTranslator does not translate anything. * IdentityTranslator does not translate anything.
* *
@ -18,39 +20,22 @@ namespace Symfony\Component\Translation;
*/ */
class IdentityTranslator implements TranslatorInterface class IdentityTranslator implements TranslatorInterface
{ {
use TranslatorTrait {
transChoice as private doTransChoice;
}
private $selector; private $selector;
private $locale;
/** /**
* @param MessageSelector|null $selector The message selector for pluralization * @param MessageSelector|null $selector The message selector for pluralization
*/ */
public function __construct(MessageSelector $selector = null) public function __construct(MessageSelector $selector = null)
{ {
$this->selector = $selector ?: new MessageSelector(); $this->selector = $selector;
}
/** if (\get_class($this) !== __CLASS__) {
* {@inheritdoc} @trigger_error(sprintf('Calling "%s()" is deprecated since Symfony 4.2.'), E_USER_DEPRECATED);
*/ }
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);
} }
/** /**
@ -58,6 +43,15 @@ class IdentityTranslator implements TranslatorInterface
*/ */
public function transChoice($id, $number, array $parameters = array(), $domain = null, $locale = null) 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);
} }
} }

View File

@ -11,6 +11,8 @@
namespace Symfony\Component\Translation; 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; use Symfony\Component\Translation\Exception\InvalidArgumentException;
/** /**
@ -32,6 +34,7 @@ use Symfony\Component\Translation\Exception\InvalidArgumentException;
* @author Fabien Potencier <fabien@symfony.com> * @author Fabien Potencier <fabien@symfony.com>
* *
* @see http://en.wikipedia.org/wiki/Interval_%28mathematics%29#The_ISO_notation * @see http://en.wikipedia.org/wiki/Interval_%28mathematics%29#The_ISO_notation
* @deprecated since Symfony 4.2, use IdentityTranslator instead
*/ */
class Interval class Interval
{ {

View File

@ -13,11 +13,13 @@ namespace Symfony\Component\Translation;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Symfony\Component\Translation\Exception\InvalidArgumentException; use Symfony\Component\Translation\Exception\InvalidArgumentException;
use Symfony\Component\Translation\TranslatorInterface as LegacyTranslatorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
/** /**
* @author Abdellatif Ait boudad <a.aitboudad@gmail.com> * @author Abdellatif Ait boudad <a.aitboudad@gmail.com>
*/ */
class LoggingTranslator implements TranslatorInterface, TranslatorBagInterface class LoggingTranslator implements LegacyTranslatorInterface, TranslatorBagInterface
{ {
/** /**
* @var TranslatorInterface|TranslatorBagInterface * @var TranslatorInterface|TranslatorBagInterface

View File

@ -11,6 +11,8 @@
namespace Symfony\Component\Translation; 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; use Symfony\Component\Translation\Exception\InvalidArgumentException;
/** /**
@ -18,6 +20,8 @@ use Symfony\Component\Translation\Exception\InvalidArgumentException;
* *
* @author Fabien Potencier <fabien@symfony.com> * @author Fabien Potencier <fabien@symfony.com>
* @author Bernhard Schussek <bschussek@gmail.com> * @author Bernhard Schussek <bschussek@gmail.com>
*
* @deprecated since Symfony 4.2, use IdentityTranslator instead.
*/ */
class MessageSelector class MessageSelector
{ {

View File

@ -15,6 +15,8 @@ namespace Symfony\Component\Translation;
* Returns the plural rules for a given locale. * Returns the plural rules for a given locale.
* *
* @author Fabien Potencier <fabien@symfony.com> * @author Fabien Potencier <fabien@symfony.com>
*
* @deprecated since Symfony 4.2, use IdentityTranslator instead
*/ */
class PluralizationRules class PluralizationRules
{ {
@ -28,8 +30,12 @@ class PluralizationRules
* *
* @return int The plural position * @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) { if ('pt_BR' === $locale) {
// temporary set a locale for brazilian // temporary set a locale for brazilian
$locale = 'xbr'; $locale = 'xbr';
@ -196,6 +202,8 @@ class PluralizationRules
*/ */
public static function set(callable $rule, $locale) 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) { if ('pt_BR' === $locale) {
// temporary set a locale for brazilian // temporary set a locale for brazilian
$locale = 'xbr'; $locale = 'xbr';

View File

@ -11,86 +11,13 @@
namespace Symfony\Component\Translation\Tests; namespace Symfony\Component\Translation\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Intl\Util\IntlTestHelper;
use Symfony\Component\Translation\IdentityTranslator; use Symfony\Component\Translation\IdentityTranslator;
use Symfony\Contracts\Tests\Translation\TranslatorTest;
class IdentityTranslatorTest extends TestCase class IdentityTranslatorTest extends TranslatorTest
{ {
/** public function getTranslator()
* @dataProvider getTransTests
*/
public function testTrans($expected, $id, $parameters)
{ {
$translator = new IdentityTranslator(); return 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)),
);
} }
} }

View File

@ -14,6 +14,9 @@ namespace Symfony\Component\Translation\Tests;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Symfony\Component\Translation\Interval; use Symfony\Component\Translation\Interval;
/**
* @group legacy
*/
class IntervalTest extends TestCase class IntervalTest extends TestCase
{ {
/** /**

View File

@ -14,6 +14,9 @@ namespace Symfony\Component\Translation\Tests;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Symfony\Component\Translation\MessageSelector; use Symfony\Component\Translation\MessageSelector;
/**
* @group legacy
*/
class MessageSelectorTest extends TestCase class MessageSelectorTest extends TestCase
{ {
/** /**

View File

@ -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. * The goal to cover all languages is to far fetched so this test case is smaller.
* *
* @author Clemens Tolboom clemens@build2be.nl * @author Clemens Tolboom clemens@build2be.nl
*
* @group legacy
*/ */
class PluralizationRulesTest extends TestCase class PluralizationRulesTest extends TestCase
{ {

View File

@ -11,57 +11,13 @@
namespace Symfony\Component\Translation; namespace Symfony\Component\Translation;
use Symfony\Component\Translation\Exception\InvalidArgumentException; use Symfony\Contracts\Translation\TranslatorInterface as BaseTranslatorInterface;
/** /**
* TranslatorInterface.
*
* @author Fabien Potencier <fabien@symfony.com> * @author Fabien Potencier <fabien@symfony.com>
*
* @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();
} }

View File

@ -17,6 +17,7 @@
], ],
"require": { "require": {
"php": "^7.1.3", "php": "^7.1.3",
"symfony/contracts": "^1.0",
"symfony/polyfill-mbstring": "~1.0" "symfony/polyfill-mbstring": "~1.0"
}, },
"require-dev": { "require-dev": {

View File

@ -5,6 +5,9 @@ CHANGELOG
----- -----
* added `DivisibleBy` constraint * added `DivisibleBy` constraint
* decoupled from `symfony/translation` by using `Symfony\Contracts\Translation\TranslatorInterface`
* deprecated `ValidatorBuilderInterface`
* made `ValidatorBuilder` final
4.1.0 4.1.0
----- -----

View File

@ -11,7 +11,6 @@
namespace Symfony\Component\Validator\Context; namespace Symfony\Component\Validator\Context;
use Symfony\Component\Translation\TranslatorInterface;
use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintViolation; use Symfony\Component\Validator\ConstraintViolation;
use Symfony\Component\Validator\ConstraintViolationList; 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\Util\PropertyPath;
use Symfony\Component\Validator\Validator\ValidatorInterface; use Symfony\Component\Validator\Validator\ValidatorInterface;
use Symfony\Component\Validator\Violation\ConstraintViolationBuilder; use Symfony\Component\Validator\Violation\ConstraintViolationBuilder;
use Symfony\Contracts\Translation\TranslatorInterface;
/** /**
* The context used and created by {@link ExecutionContextFactory}. * The context used and created by {@link ExecutionContextFactory}.

View File

@ -11,8 +11,8 @@
namespace Symfony\Component\Validator\Context; namespace Symfony\Component\Validator\Context;
use Symfony\Component\Translation\TranslatorInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface; use Symfony\Component\Validator\Validator\ValidatorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
/** /**
* Creates new {@link ExecutionContext} instances. * Creates new {@link ExecutionContext} instances.

View File

@ -11,9 +11,13 @@
namespace Symfony\Component\Validator\DependencyInjection; namespace Symfony\Component\Validator\DependencyInjection;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\Translation\TranslatorInterface as LegacyTranslatorInterface;
use Symfony\Component\Validator\Util\LegacyTranslatorProxy;
/** /**
* @author Fabien Potencier <fabien@symfony.com> * @author Fabien Potencier <fabien@symfony.com>
@ -42,5 +46,27 @@ class AddValidatorInitializersPass implements CompilerPassInterface
} }
$container->getDefinition($this->builderService)->addMethodCall('addObjectInitializers', array($initializers)); $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);
} }
} }

View File

@ -21,6 +21,7 @@ use Symfony\Component\Validator\Context\ExecutionContext;
use Symfony\Component\Validator\Context\ExecutionContextInterface; use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Component\Validator\Mapping\ClassMetadata; use Symfony\Component\Validator\Mapping\ClassMetadata;
use Symfony\Component\Validator\Mapping\PropertyMetadata; use Symfony\Component\Validator\Mapping\PropertyMetadata;
use Symfony\Contracts\Translation\TranslatorInterface;
/** /**
* A test case to ease testing Constraint Validators. * A test case to ease testing Constraint Validators.
@ -95,7 +96,7 @@ abstract class ConstraintValidatorTestCase extends TestCase
protected function createContext() 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(); $validator = $this->getMockBuilder('Symfony\Component\Validator\Validator\ValidatorInterface')->getMock();
$contextualValidator = $this->getMockBuilder('Symfony\Component\Validator\Validator\ContextualValidatorInterface')->getMock(); $contextualValidator = $this->getMockBuilder('Symfony\Component\Validator\Validator\ContextualValidatorInterface')->getMock();

View File

@ -13,8 +13,12 @@ namespace Symfony\Component\Validator\Tests\DependencyInjection;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\Validator\DependencyInjection\AddValidatorInitializersPass; 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 class AddValidatorInitializersPassTest extends TestCase
{ {
@ -41,4 +45,34 @@ class AddValidatorInitializersPassTest extends TestCase
$container->getDefinition('validator.builder')->getMethodCalls() $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;
} }

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\Validator\Tests; namespace Symfony\Component\Validator\Tests;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Symfony\Component\Validator\Util\LegacyTranslatorProxy;
use Symfony\Component\Validator\ValidatorBuilder; use Symfony\Component\Validator\ValidatorBuilder;
use Symfony\Component\Validator\ValidatorBuilderInterface; 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() public function testSetTranslationDomain()
{ {
$this->assertSame($this->builder, $this->builder->setTranslationDomain('TRANS_DOMAIN')); $this->assertSame($this->builder, $this->builder->setTranslationDomain('TRANS_DOMAIN'));

View File

@ -0,0 +1,65 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\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);
}
}

View File

@ -15,8 +15,7 @@ use Doctrine\Common\Annotations\AnnotationReader;
use Doctrine\Common\Annotations\CachedReader; use Doctrine\Common\Annotations\CachedReader;
use Doctrine\Common\Annotations\Reader; use Doctrine\Common\Annotations\Reader;
use Doctrine\Common\Cache\ArrayCache; use Doctrine\Common\Cache\ArrayCache;
use Symfony\Component\Translation\IdentityTranslator; use Symfony\Component\Translation\TranslatorInterface as LegacyTranslatorInterface;
use Symfony\Component\Translation\TranslatorInterface;
use Symfony\Component\Validator\Context\ExecutionContextFactory; use Symfony\Component\Validator\Context\ExecutionContextFactory;
use Symfony\Component\Validator\Exception\ValidatorException; use Symfony\Component\Validator\Exception\ValidatorException;
use Symfony\Component\Validator\Mapping\Cache\CacheInterface; 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\StaticMethodLoader;
use Symfony\Component\Validator\Mapping\Loader\XmlFileLoader; use Symfony\Component\Validator\Mapping\Loader\XmlFileLoader;
use Symfony\Component\Validator\Mapping\Loader\YamlFileLoader; use Symfony\Component\Validator\Mapping\Loader\YamlFileLoader;
use Symfony\Component\Validator\Util\LegacyTranslatorProxy;
use Symfony\Component\Validator\Validator\RecursiveValidator; use Symfony\Component\Validator\Validator\RecursiveValidator;
use Symfony\Contracts\Translation\TranslatorInterface;
use Symfony\Contracts\Translation\TranslatorTrait;
/** /**
* The default implementation of {@link ValidatorBuilderInterface}. * The default implementation of {@link ValidatorBuilderInterface}.
* *
* @author Bernhard Schussek <bschussek@gmail.com> * @author Bernhard Schussek <bschussek@gmail.com>
*
* @final since Symfony 4.2
*/ */
class ValidatorBuilder implements ValidatorBuilderInterface class ValidatorBuilder implements ValidatorBuilderInterface
{ {
private $initializers = array(); private $initializers = array();
private $loaders = array();
private $xmlMappings = array(); private $xmlMappings = array();
private $yamlMappings = array(); private $yamlMappings = array();
private $methodMappings = array(); private $methodMappings = array();
@ -249,9 +254,9 @@ class ValidatorBuilder implements ValidatorBuilderInterface
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public function setTranslator(TranslatorInterface $translator) public function setTranslator(LegacyTranslatorInterface $translator)
{ {
$this->translator = $translator; $this->translator = $translator instanceof LegacyTranslatorProxy ? $translator->getTranslator() : $translator;
return $this; return $this;
} }
@ -266,6 +271,16 @@ class ValidatorBuilder implements ValidatorBuilderInterface
return $this; return $this;
} }
/**
* @return $this
*/
public function addLoader(LoaderInterface $loader)
{
$this->loaders[] = $loader;
return $this;
}
/** /**
* @return LoaderInterface[] * @return LoaderInterface[]
*/ */
@ -289,7 +304,7 @@ class ValidatorBuilder implements ValidatorBuilderInterface
$loaders[] = new AnnotationLoader($this->annotationReader); $loaders[] = new AnnotationLoader($this->annotationReader);
} }
return $loaders; return array_merge($loaders, $this->loaders);
} }
/** /**
@ -316,7 +331,9 @@ class ValidatorBuilder implements ValidatorBuilderInterface
$translator = $this->translator; $translator = $this->translator;
if (null === $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 // 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 // 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 // validation messages are pluralized properly even when the default locale gets changed because they are in

View File

@ -21,6 +21,8 @@ use Symfony\Component\Validator\Validator\ValidatorInterface;
* A configurable builder for ValidatorInterface objects. * A configurable builder for ValidatorInterface objects.
* *
* @author Bernhard Schussek <bschussek@gmail.com> * @author Bernhard Schussek <bschussek@gmail.com>
*
* @deprecated since Symfony 4.2, use the ValidatorBuilder class instead
*/ */
interface ValidatorBuilderInterface interface ValidatorBuilderInterface
{ {

View File

@ -11,11 +11,11 @@
namespace Symfony\Component\Validator\Violation; namespace Symfony\Component\Validator\Violation;
use Symfony\Component\Translation\TranslatorInterface;
use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintViolation; use Symfony\Component\Validator\ConstraintViolation;
use Symfony\Component\Validator\ConstraintViolationList; use Symfony\Component\Validator\ConstraintViolationList;
use Symfony\Component\Validator\Util\PropertyPath; use Symfony\Component\Validator\Util\PropertyPath;
use Symfony\Contracts\Translation\TranslatorInterface;
/** /**
* Default implementation of {@link ConstraintViolationBuilderInterface}. * Default implementation of {@link ConstraintViolationBuilderInterface}.

View File

@ -64,7 +64,7 @@ interface ConstraintViolationBuilderInterface
* *
* @return $this * @return $this
* *
* @see \Symfony\Component\Translation\TranslatorInterface * @see \Symfony\Contracts\Translation\TranslatorInterface
*/ */
public function setTranslationDomain($translationDomain); public function setTranslationDomain($translationDomain);
@ -85,7 +85,7 @@ interface ConstraintViolationBuilderInterface
* *
* @return $this * @return $this
* *
* @see \Symfony\Component\Translation\TranslatorInterface::transChoice() * @see \Symfony\Contracts\Translation\TranslatorInterface::transChoice()
*/ */
public function setPlural($number); public function setPlural($number);

View File

@ -19,8 +19,7 @@
"php": "^7.1.3", "php": "^7.1.3",
"symfony/contracts": "^1.0", "symfony/contracts": "^1.0",
"symfony/polyfill-ctype": "~1.8", "symfony/polyfill-ctype": "~1.8",
"symfony/polyfill-mbstring": "~1.0", "symfony/polyfill-mbstring": "~1.0"
"symfony/translation": "~3.4|~4.0"
}, },
"require-dev": { "require-dev": {
"symfony/http-foundation": "~4.1", "symfony/http-foundation": "~4.1",
@ -33,6 +32,7 @@
"symfony/expression-language": "~3.4|~4.0", "symfony/expression-language": "~3.4|~4.0",
"symfony/cache": "~3.4|~4.0", "symfony/cache": "~3.4|~4.0",
"symfony/property-access": "~3.4|~4.0", "symfony/property-access": "~3.4|~4.0",
"symfony/translation": "~4.2",
"doctrine/annotations": "~1.0", "doctrine/annotations": "~1.0",
"doctrine/cache": "~1.0", "doctrine/cache": "~1.0",
"egulias/email-validator": "^1.2.8|~2.0" "egulias/email-validator": "^1.2.8|~2.0"
@ -42,6 +42,7 @@
"symfony/dependency-injection": "<3.4", "symfony/dependency-injection": "<3.4",
"symfony/http-kernel": "<3.4", "symfony/http-kernel": "<3.4",
"symfony/intl": "<4.1", "symfony/intl": "<4.1",
"symfony/translation": "<4.2",
"symfony/yaml": "<3.4" "symfony/yaml": "<3.4"
}, },
"suggest": { "suggest": {
@ -50,6 +51,7 @@
"doctrine/cache": "For using the default cached annotation reader and metadata cache.", "doctrine/cache": "For using the default cached annotation reader and metadata cache.",
"symfony/http-foundation": "", "symfony/http-foundation": "",
"symfony/intl": "", "symfony/intl": "",
"symfony/translation": "For translating validation errors.",
"symfony/yaml": "", "symfony/yaml": "",
"symfony/config": "", "symfony/config": "",
"egulias/email-validator": "Strict (RFC compliant) email validation", "egulias/email-validator": "Strict (RFC compliant) email validation",

View File

@ -5,3 +5,4 @@ CHANGELOG
----- -----
* added `Service\ResetInterface` to provide a way to reset an object to its initial state * added `Service\ResetInterface` to provide a way to reset an object to its initial state
* added `Translation\TranslatorInterface` and `Translation\TranslatorTrait`

View File

@ -0,0 +1,353 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\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;
}
}

View File

@ -0,0 +1,93 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Contracts\Translation;
/**
* @author Fabien Potencier <fabien@symfony.com>
*/
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();
}

View File

@ -0,0 +1,258 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Contracts\Translation;
use Symfony\Component\Translation\Exception\InvalidArgumentException;
/**
* A trait to help implement TranslatorInterface.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
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<interval>
({\s*
(\-?\d+(\.\d+)?[\s*,\s*\-?\d+(\.\d+)?]*)
\s*})
|
(?P<left_delimiter>[\[\]])
\s*
(?P<left>-Inf|\-?\d+(\.\d+)?)
\s*,\s*
(?P<right>\+?Inf|\-?\d+(\.\d+)?)
\s*
(?P<right_delimiter>[\[\]])
)\s*(?P<message>.*?)$/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;
}
}
}