Skip to content

Commit 9176d3d

Browse files
committed
Improve translation docs and developer experience
The `Template` class now offers a static façade to quickly obtain the template of a message: `Template::from($class, $mode)`. This replaces the old way of referencing messages in 2.x (`FooException::$defaultTemplates`), making existing translation setups easier to migrate. The documents on translation were updated to feature symfony with an array provider. Duplicated container notes were extracted to a single configuration.md file.
1 parent 570ba48 commit 9176d3d

12 files changed

Lines changed: 265 additions & 77 deletions

docs/case-sensitiveness.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ SPDX-FileContributor: Henrique Moody <henriquemoody@gmail.com>
77

88
# Case Insensitive Validation
99

10-
For most simple cases, you can use `v::call` wrappers to perform
10+
For most simple cases, you can use `v::after` wrappers to perform
1111
case normalization before comparison.
1212

1313
For strings:

docs/configuration.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<!--
2+
SPDX-License-Identifier: MIT
3+
SPDX-FileCopyrightText: (c) Respect Project Contributors
4+
SPDX-FileContributor: Alexandre Gomes Gaigalas <alganet@gmail.com>
5+
-->
6+
7+
# Configuration
8+
9+
## Container configuration
10+
11+
The `ContainerRegistry::createContainer()` method returns a [PHP-DI](https://php-di.org/) container. The definitions array follows the [PHP-DI definitions format](https://php-di.org/doc/php-definitions.html).
12+
13+
If you prefer to use a different container, `ContainerRegistry::setContainer()` accepts any [PSR-11](https://www.php-fig.org/psr/psr-11/) compatible container:
14+
15+
```php
16+
use Respect\Validation\ContainerRegistry;
17+
18+
ContainerRegistry::setContainer($yourPsr11Container);
19+
```

docs/messages/placeholder-conversion.md

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -35,19 +35,7 @@ ContainerRegistry::setContainer(
3535
);
3636
```
3737

38-
See [PlaceholderFormatter][] documentation for more information on creating custom modifiers.
39-
40-
## Container configuration
41-
42-
The `ContainerRegistry::createContainer()` method returns a [PHP-DI](https://php-di.org/) container. The definitions array follows the [PHP-DI definitions format](https://php-di.org/doc/php-definitions.html).
43-
44-
If you prefer to use a different container, `ContainerRegistry::setContainer()` accepts any [PSR-11](https://www.php-fig.org/psr/psr-11/) compatible container:
45-
46-
```php
47-
use Respect\Validation\ContainerRegistry;
48-
49-
ContainerRegistry::setContainer($yourPsr11Container);
50-
```
38+
See [PlaceholderFormatter][] documentation for more information on creating custom modifiers and the [configuration](../configuration.md) section for more details on container setup.
5139

5240
[PlaceholderFormatter]: https://github.com/Respect/StringFormatter/blob/main/docs/PlaceholderFormatter.md
5341
[Respect\StringFormatter]: https://github.com/Respect/StringFormatter

docs/messages/translation.md

Lines changed: 34 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,63 @@
11
<!--
22
SPDX-License-Identifier: MIT
33
SPDX-FileCopyrightText: (c) Respect Project Contributors
4+
SPDX-FileContributor: Alexandre Gomes Gaigalas <alganet@gmail.com>
45
SPDX-FileContributor: Henrique Moody <henriquemoody@gmail.com>
56
-->
67

78
# Message translation
89

9-
Validation uses [symfony/translation](https://symfony.com/doc/current/translation.html) for message translation, providing interoperability with the Symfony ecosystem and other PHP projects.
10+
Validation provides full translation capabilities, but they are not enabled by default nor
11+
do we provide official translations for our messages other than English.
1012

11-
By default, validation messages are not translated. To enable translation, provide a `Symfony\Contracts\Translation\TranslatorInterface` implementation to `ContainerRegistry::createContainer()`:
13+
Therefore, if you want to use it with translation, you must provide the translations yourself
14+
using a compatible [translation contract](https://github.com/symfony/translation-contracts).
15+
16+
Here's a quick setup using [symfony/translation](https://symfony.com/doc/current/translation.html):
1217

1318
```php
1419
use Respect\Validation\ContainerRegistry;
20+
use Respect\Validation\Message\TemplateRegistry;
21+
use Respect\Validation\Validators as vs;
22+
use Symfony\Component\Translation\Loader\ArrayLoader;
1523
use Symfony\Component\Translation\Translator;
1624
use Symfony\Contracts\Translation\TranslatorInterface;
1725

18-
// Create your Symfony Translator instance
19-
// See: https://symfony.com/doc/current/translation.html
26+
$templates = new TemplateRegistry();
2027
$translator = new Translator('pt_BR');
21-
// ... configure loaders and resources
28+
$translator->addLoader('array', new ArrayLoader()); // Choose the loader of your preference
29+
$translator->addResource('array', [
30+
// Reference standard template by class (StringVal, Intval, ...) and mode (default or inverted)
31+
$templates->get(vs\IntVal::class)->default => '{{subject}} DEVE ser um inteiro.',
32+
$templates->get(vs\IntVal::class)->inverted => '{{subject}} NÃO DEVE ser um inteiro.',
33+
34+
// Reference alternative templates by their id (second argument)
35+
$templates->get(vs\AllOf::class, vs\AllOf::TEMPLATE_ALL)->default => 'Todas as regras requeridas DEVEM passar para {{subject}}',
36+
37+
// You can also just translate messages directly
38+
'{{subject}} must be a URL' => '{{subject}} DEVE ser uma URL'
39+
]);
2240

2341
$container = ContainerRegistry::createContainer([
2442
TranslatorInterface::class => $translator,
43+
TemplateRegistry::class => $templates
2544
]);
26-
2745
ContainerRegistry::setContainer($container);
2846
```
2947

30-
After setting up the container, all messages produced by Validation will your translator.
48+
You only need to do this once before you perform any validation, and messages will start
49+
being produced with your translation setup. If you're using a framework, you can configure
50+
this in the service provider of your choice.
51+
52+
Check out the documentation for each validator for its available modes and existing messages
53+
and the [configuration](../configuration.md) section.
3154

3255
## Translating dynamic values
3356

3457
Validation messages contain placeholders like `{{subject}}` and `{{minValue}}` that are replaced with actual values. Some of these values may also need translation.
3558

36-
Use the `|trans` modifier to translate parameter values:
59+
You will encounter several messages with `|trans` in different validators. Those enable the
60+
translation of such dynamic values automatically.
3761

3862
```php
3963
// Message template
@@ -44,6 +68,8 @@ Use the `|trans` modifier to translate parameter values:
4468
'Palestine' => 'Palestina',
4569
```
4670

71+
The `|trans` modifier will also work with custom templates defined by [Templated](../validators/Templated.md) or provided by [`assert`](../handling-exceptions.md).
72+
4773
## Translating lists
4874

4975
When using validators that display lists of values, use the `|list:or` or `|list:and` modifiers. These modifiers also require translating the conjunctions:
@@ -63,15 +89,3 @@ When using validators that display lists of values, use the `|list:or` or `|list
6389
'{{haystack|list:and}} are the only possible names' => '{{haystack|list:and}} são os únicos nomes possíveis',
6490
'and' => 'e',
6591
```
66-
67-
## Container configuration
68-
69-
The `ContainerRegistry::createContainer()` method returns a [PHP-DI](https://php-di.org/) container. The definitions array follows the [PHP-DI definitions format](https://php-di.org/doc/php-definitions.html).
70-
71-
If you prefer to use a different container, `ContainerRegistry::setContainer()` accepts any [PSR-11](https://www.php-fig.org/psr/psr-11/) compatible container:
72-
73-
```php
74-
use Respect\Validation\ContainerRegistry;
75-
76-
ContainerRegistry::setContainer($yourPsr11Container);
77-
```

docs/migrating-from-v2-to-v3.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -932,6 +932,13 @@ v::email()->assert(
932932
);
933933
```
934934

935+
### Translation
936+
937+
The project now uses Symfony [translation contracts](https://github.com/symfony/translation-contracts)
938+
instead of a custom callback.
939+
940+
See [messages/translation.md](messages/translation.md) for more info.
941+
935942
### Placeholder pipes
936943

937944
Customize how values are rendered in templates using pipes:

src/ContainerRegistry.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
use Respect\Validation\Message\Parameters\PathHandler;
3939
use Respect\Validation\Message\Parameters\ResultHandler;
4040
use Respect\Validation\Message\Renderer;
41+
use Respect\Validation\Message\TemplateRegistry;
4142
use Respect\Validation\Transformers\Prefix;
4243
use Respect\Validation\Transformers\Transformer;
4344
use Symfony\Contracts\Translation\TranslatorInterface;
@@ -56,7 +57,8 @@ public static function createContainer(array $definitions = []): Container
5657
return new Container($definitions + [
5758
PhoneNumberUtil::class => factory(static fn() => PhoneNumberUtil::getInstance()),
5859
Transformer::class => create(Prefix::class),
59-
TemplateResolver::class => create(TemplateResolver::class),
60+
TemplateRegistry::class => create(TemplateRegistry::class),
61+
TemplateResolver::class => autowire(TemplateResolver::class),
6062
TranslatorInterface::class => autowire(BypassTranslator::class),
6163
Renderer::class => autowire(InterpolationRenderer::class),
6264
ResultFilter::class => create(OnlyFailedChildrenResultFilter::class),

src/Message/Formatter/TemplateResolver.php

Lines changed: 6 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,9 @@
1111

1212
namespace Respect\Validation\Message\Formatter;
1313

14-
use ReflectionClass;
15-
use Respect\Validation\Message\Template;
14+
use Respect\Validation\Message\TemplateRegistry;
1615
use Respect\Validation\Path;
1716
use Respect\Validation\Result;
18-
use Respect\Validation\Validator;
1917

2018
use function array_reduce;
2119
use function array_reverse;
@@ -24,8 +22,10 @@
2422

2523
final class TemplateResolver
2624
{
27-
/** @var array<string, array<Template>> */
28-
private array $templates = [];
25+
public function __construct(
26+
private TemplateRegistry $templateRegistry,
27+
) {
28+
}
2929

3030
/** @param array<string|int, mixed> $templates */
3131
public function getGivenTemplate(Result $result, array $templates): string|null
@@ -53,7 +53,7 @@ public function getGivenTemplate(Result $result, array $templates): string|null
5353

5454
public function getValidatorTemplate(Result $result): string
5555
{
56-
foreach ($this->extractTemplates($result->validator) as $template) {
56+
foreach ($this->templateRegistry->getTemplates($result->validator::class) as $template) {
5757
if ($template->id !== $result->template) {
5858
continue;
5959
}
@@ -68,19 +68,6 @@ public function getValidatorTemplate(Result $result): string
6868
return $result->template;
6969
}
7070

71-
/** @return array<Template> */
72-
private function extractTemplates(Validator $validator): array
73-
{
74-
if (!isset($this->templates[$validator::class])) {
75-
$reflection = new ReflectionClass($validator);
76-
foreach ($reflection->getAttributes(Template::class) as $attribute) {
77-
$this->templates[$validator::class][] = $attribute->newInstance();
78-
}
79-
}
80-
81-
return $this->templates[$validator::class] ?? [];
82-
}
83-
8471
/**
8572
* @param array<string|int> $nodes
8673
*

src/Message/TemplateRegistry.php

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?php
2+
3+
/*
4+
* SPDX-License-Identifier: MIT
5+
* SPDX-FileCopyrightText: (c) Respect Project Contributors
6+
* SPDX-FileContributor: Alexandre Gomes Gaigalas <alganet@gmail.com>
7+
*/
8+
9+
declare(strict_types=1);
10+
11+
namespace Respect\Validation\Message;
12+
13+
use InvalidArgumentException;
14+
use ReflectionClass;
15+
use Respect\Validation\Validator;
16+
17+
use function sprintf;
18+
19+
final class TemplateRegistry
20+
{
21+
/** @var array<class-string<Validator>, array<Template>> */
22+
private array $templates = [];
23+
24+
/**
25+
* @param class-string<Validator> $validatorClass
26+
*
27+
* @return array<Template>
28+
*/
29+
public function getTemplates(string $validatorClass): array
30+
{
31+
if (!isset($this->templates[$validatorClass])) {
32+
$reflection = new ReflectionClass($validatorClass);
33+
foreach ($reflection->getAttributes(Template::class) as $attribute) {
34+
$this->templates[$validatorClass][] = $attribute->newInstance();
35+
}
36+
}
37+
38+
return $this->templates[$validatorClass] ?? [];
39+
}
40+
41+
/** @param class-string<Validator> $validatorClass */
42+
public function get(string $validatorClass, string $id = Validator::TEMPLATE_STANDARD): Template
43+
{
44+
foreach ($this->getTemplates($validatorClass) as $template) {
45+
if ($template->id === $id) {
46+
return $template;
47+
}
48+
}
49+
50+
throw new InvalidArgumentException(sprintf(
51+
'Template with id "%s" not found in validator "%s".',
52+
$id,
53+
$validatorClass,
54+
));
55+
}
56+
}

tests/feature/TranslatorTest.php

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,30 +10,38 @@
1010
declare(strict_types=1);
1111

1212
use Respect\Validation\ContainerRegistry;
13+
use Respect\Validation\Message\TemplateRegistry;
14+
use Respect\Validation\Validators as vs;
1315
use Symfony\Component\Translation\Loader\ArrayLoader;
1416
use Symfony\Component\Translation\Translator;
1517
use Symfony\Contracts\Translation\TranslatorInterface;
1618

17-
$translator = new Translator('en');
19+
$templates = new TemplateRegistry();
20+
$translator = new Translator('pt_BR');
1821
$translator->addLoader('array', new ArrayLoader());
1922
$translator->addResource(
2023
'array',
2124
[
22-
'{{subject}} must pass all the rules' => 'Todas as regras requeridas devem passar para {{subject}}',
23-
'The length of' => 'O comprimento de',
24-
'{{subject}} must be a string' => '{{subject}} deve ser uma string',
25-
'{{subject}} must be between {{minValue}} and {{maxValue}}' => '{{subject}} deve possuir de {{minValue}} a {{maxValue}} caracteres',
26-
'{{subject}} must be a phone number for country {{countryName|trans}}' => '{{subject}} deve ser um número de telefone válido para o país {{countryName|trans}}',
25+
// Directly translating validator messages
26+
$templates->get(vs\AllOf::class, vs\AllOf::TEMPLATE_ALL)->default => 'Todas as regras requeridas devem passar para {{subject}}',
27+
$templates->get(vs\Length::class)->default => 'O comprimento de',
28+
$templates->get(vs\StringVal::class)->default => '{{subject}} deve ser uma string',
29+
$templates->get(vs\Between::class)->default => '{{subject}} deve possuir de {{minValue}} a {{maxValue}} caracteres',
30+
$templates->get(vs\Phone::class, vs\Phone::TEMPLATE_FOR_COUNTRY)->default => '{{subject}} deve ser um número de telefone válido para o país {{countryName|trans}}',
31+
$templates->get(vs\DateTimeDiff::class)->default => 'O número de {{type|trans}} entre agora e',
32+
$templates->get(vs\Equals::class)->default => '{{subject}} deve ser igual a {{compareTo}}',
33+
34+
// Custom templates set during runtime
35+
'Your name must be {{haystack|list:or}}' => 'Seu nome deve ser {{haystack|list:or}}',
36+
'{{haystack|list:and}} are the only possible names' => '{{haystack|list:and}} são os únicos nomes possíveis',
37+
38+
// Miscellaneous translations
2739
'United States' => 'Estados Unidos',
2840
'years' => 'anos',
29-
'The number of {{type|trans}} between now and' => 'O número de {{type|trans}} entre agora e',
30-
'{{subject}} must be equal to {{compareTo}}' => '{{subject}} deve ser igual a {{compareTo}}',
31-
'Your name must be {{haystack|list:or}}' => 'Seu nome deve ser {{haystack|list:or}}',
3241
'or' => 'ou',
33-
'{{haystack|list:and}} are the only possible names' => '{{haystack|list:and}} são os únicos nomes possíveis',
3442
'and' => 'e',
3543
],
36-
'en',
44+
'pt_BR',
3745
);
3846
$container = ContainerRegistry::createContainer();
3947
$container->set(TranslatorInterface::class, $translator);

tests/unit/Message/Formatter/TemplateResolverTest.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use PHPUnit\Framework\Attributes\CoversClass;
1515
use PHPUnit\Framework\Attributes\Test;
16+
use Respect\Validation\Message\TemplateRegistry;
1617
use Respect\Validation\Path;
1718
use Respect\Validation\Test\Builders\ResultBuilder;
1819
use Respect\Validation\Test\TestCase;
@@ -25,7 +26,7 @@ public function itShouldReturnResultWithTemplateWhenKeyExists(): void
2526
{
2627
$result = (new ResultBuilder())->path(new Path('foo-path'))->build();
2728
$templates = ['foo-path' => 'My custom template'];
28-
$sut = new TemplateResolver();
29+
$sut = new TemplateResolver(new TemplateRegistry());
2930
$template = $sut->getGivenTemplate($result, $templates);
3031

3132
self::assertSame('My custom template', $template);

0 commit comments

Comments
 (0)