Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/case-sensitiveness.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ SPDX-FileContributor: Henrique Moody <henriquemoody@gmail.com>

# Case Insensitive Validation

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

For strings:
Expand Down
19 changes: 19 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<!--
SPDX-License-Identifier: MIT
SPDX-FileCopyrightText: (c) Respect Project Contributors
SPDX-FileContributor: Alexandre Gomes Gaigalas <alganet@gmail.com>
-->

# Configuration

## Container configuration

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).

If you prefer to use a different container, `ContainerRegistry::setContainer()` accepts any [PSR-11](https://www.php-fig.org/psr/psr-11/) compatible container:

```php
use Respect\Validation\ContainerRegistry;

ContainerRegistry::setContainer($yourPsr11Container);
```
14 changes: 1 addition & 13 deletions docs/messages/placeholder-conversion.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,19 +35,7 @@ ContainerRegistry::setContainer(
);
```

See [PlaceholderFormatter][] documentation for more information on creating custom modifiers.

## Container configuration

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).

If you prefer to use a different container, `ContainerRegistry::setContainer()` accepts any [PSR-11](https://www.php-fig.org/psr/psr-11/) compatible container:

```php
use Respect\Validation\ContainerRegistry;

ContainerRegistry::setContainer($yourPsr11Container);
```
See [PlaceholderFormatter][] documentation for more information on creating custom modifiers and the [configuration](../configuration.md) section for more details on container setup.

[PlaceholderFormatter]: https://github.com/Respect/StringFormatter/blob/main/docs/PlaceholderFormatter.md
[Respect\StringFormatter]: https://github.com/Respect/StringFormatter
Expand Down
54 changes: 34 additions & 20 deletions docs/messages/translation.md
Original file line number Diff line number Diff line change
@@ -1,39 +1,63 @@
<!--
SPDX-License-Identifier: MIT
SPDX-FileCopyrightText: (c) Respect Project Contributors
SPDX-FileContributor: Alexandre Gomes Gaigalas <alganet@gmail.com>
SPDX-FileContributor: Henrique Moody <henriquemoody@gmail.com>
-->

# Message translation

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

By default, validation messages are not translated. To enable translation, provide a `Symfony\Contracts\Translation\TranslatorInterface` implementation to `ContainerRegistry::createContainer()`:
Therefore, if you want to use it with translation, you must provide the translations yourself
using a compatible [translation contract](https://github.com/symfony/translation-contracts).

Here's a quick setup using [symfony/translation](https://symfony.com/doc/current/translation.html):

```php
use Respect\Validation\ContainerRegistry;
use Respect\Validation\Message\TemplateRegistry;
use Respect\Validation\Validators as vs;
use Symfony\Component\Translation\Loader\ArrayLoader;
use Symfony\Component\Translation\Translator;
use Symfony\Contracts\Translation\TranslatorInterface;

// Create your Symfony Translator instance
// See: https://symfony.com/doc/current/translation.html
$templates = new TemplateRegistry();
$translator = new Translator('pt_BR');
// ... configure loaders and resources
$translator->addLoader('array', new ArrayLoader()); // Choose the loader of your preference
$translator->addResource('array', [
// Reference standard template by class (StringVal, Intval, ...) and mode (default or inverted)
$templates->get(vs\IntVal::class)->default => '{{subject}} DEVE ser um inteiro.',
$templates->get(vs\IntVal::class)->inverted => '{{subject}} NÃO DEVE ser um inteiro.',

// Reference alternative templates by their id (second argument)
$templates->get(vs\AllOf::class, vs\AllOf::TEMPLATE_ALL)->default => 'Todas as regras requeridas DEVEM passar para {{subject}}',

// You can also just translate messages directly
'{{subject}} must be a URL' => '{{subject}} DEVE ser uma URL'
]);

$container = ContainerRegistry::createContainer([
TranslatorInterface::class => $translator,
TemplateRegistry::class => $templates
]);

ContainerRegistry::setContainer($container);
```

After setting up the container, all messages produced by Validation will your translator.
You only need to do this once before you perform any validation, and messages will start
being produced with your translation setup. If you're using a framework, you can configure
this in the service provider of your choice.

Check out the documentation for each validator for its available modes and existing messages
and the [configuration](../configuration.md) section.

## Translating dynamic values

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

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

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

The `|trans` modifier will also work with custom templates defined by [Templated](../validators/Templated.md) or provided by [`assert`](../handling-exceptions.md).

## Translating lists

When using validators that display lists of values, use the `|list:or` or `|list:and` modifiers. These modifiers also require translating the conjunctions:
Expand All @@ -63,15 +89,3 @@ When using validators that display lists of values, use the `|list:or` or `|list
'{{haystack|list:and}} are the only possible names' => '{{haystack|list:and}} são os únicos nomes possíveis',
'and' => 'e',
```

## Container configuration

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).

If you prefer to use a different container, `ContainerRegistry::setContainer()` accepts any [PSR-11](https://www.php-fig.org/psr/psr-11/) compatible container:

```php
use Respect\Validation\ContainerRegistry;

ContainerRegistry::setContainer($yourPsr11Container);
```
7 changes: 7 additions & 0 deletions docs/migrating-from-v2-to-v3.md
Original file line number Diff line number Diff line change
Expand Up @@ -932,6 +932,13 @@ v::email()->assert(
);
```

### Translation

The project now uses Symfony [translation contracts](https://github.com/symfony/translation-contracts)
instead of a custom callback.

See [messages/translation.md](messages/translation.md) for more info.

### Placeholder pipes

Customize how values are rendered in templates using pipes:
Expand Down
4 changes: 3 additions & 1 deletion src/ContainerRegistry.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
use Respect\Validation\Message\Parameters\PathHandler;
use Respect\Validation\Message\Parameters\ResultHandler;
use Respect\Validation\Message\Renderer;
use Respect\Validation\Message\TemplateRegistry;
use Respect\Validation\Transformers\Prefix;
use Respect\Validation\Transformers\Transformer;
use Symfony\Contracts\Translation\TranslatorInterface;
Expand All @@ -56,7 +57,8 @@ public static function createContainer(array $definitions = []): Container
return new Container($definitions + [
PhoneNumberUtil::class => factory(static fn() => PhoneNumberUtil::getInstance()),
Transformer::class => create(Prefix::class),
TemplateResolver::class => create(TemplateResolver::class),
TemplateRegistry::class => create(TemplateRegistry::class),
TemplateResolver::class => autowire(TemplateResolver::class),
TranslatorInterface::class => autowire(BypassTranslator::class),
Renderer::class => autowire(InterpolationRenderer::class),
ResultFilter::class => create(OnlyFailedChildrenResultFilter::class),
Expand Down
25 changes: 6 additions & 19 deletions src/Message/Formatter/TemplateResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,9 @@

namespace Respect\Validation\Message\Formatter;

use ReflectionClass;
use Respect\Validation\Message\Template;
use Respect\Validation\Message\TemplateRegistry;
use Respect\Validation\Path;
use Respect\Validation\Result;
use Respect\Validation\Validator;

use function array_reduce;
use function array_reverse;
Expand All @@ -24,8 +22,10 @@

final class TemplateResolver
{
/** @var array<string, array<Template>> */
private array $templates = [];
public function __construct(
private TemplateRegistry $templateRegistry,
) {
}

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

public function getValidatorTemplate(Result $result): string
{
foreach ($this->extractTemplates($result->validator) as $template) {
foreach ($this->templateRegistry->getTemplates($result->validator::class) as $template) {
if ($template->id !== $result->template) {
continue;
}
Expand All @@ -68,19 +68,6 @@ public function getValidatorTemplate(Result $result): string
return $result->template;
}

/** @return array<Template> */
private function extractTemplates(Validator $validator): array
{
if (!isset($this->templates[$validator::class])) {
$reflection = new ReflectionClass($validator);
foreach ($reflection->getAttributes(Template::class) as $attribute) {
$this->templates[$validator::class][] = $attribute->newInstance();
}
}

return $this->templates[$validator::class] ?? [];
}

/**
* @param array<string|int> $nodes
*
Expand Down
62 changes: 62 additions & 0 deletions src/Message/TemplateRegistry.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php

/*
* SPDX-License-Identifier: MIT
* SPDX-FileCopyrightText: (c) Respect Project Contributors
* SPDX-FileContributor: Alexandre Gomes Gaigalas <alganet@gmail.com>
*/

declare(strict_types=1);

namespace Respect\Validation\Message;

use InvalidArgumentException;
use ReflectionClass;
use Respect\Validation\ContainerRegistry;
use Respect\Validation\Validator;

use function sprintf;

final class TemplateRegistry
{
/** @var array<class-string<Validator>, array<Template>> */
private array $templates = [];

public static function getInstance(): self
{
return ContainerRegistry::getContainer()->get(self::class);
}

/**
* @param class-string<Validator> $validatorClass
*
* @return array<Template>
*/
public function getTemplates(string $validatorClass): array
{
if (!isset($this->templates[$validatorClass])) {
$reflection = new ReflectionClass($validatorClass);
foreach ($reflection->getAttributes(Template::class) as $attribute) {
$this->templates[$validatorClass][] = $attribute->newInstance();
}
}

return $this->templates[$validatorClass] ?? [];
}

/** @param class-string<Validator> $validatorClass */
public function get(string $validatorClass, string $id = Validator::TEMPLATE_STANDARD): Template
{
foreach ($this->getTemplates($validatorClass) as $template) {
if ($template->id === $id) {
return $template;
}
}

throw new InvalidArgumentException(sprintf(
'Template with id "%s" not found in validator "%s".',
$id,
$validatorClass,
));
}
}
30 changes: 19 additions & 11 deletions tests/feature/TranslatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,30 +10,38 @@
declare(strict_types=1);

use Respect\Validation\ContainerRegistry;
use Respect\Validation\Message\TemplateRegistry;
use Respect\Validation\Validators as vs;
use Symfony\Component\Translation\Loader\ArrayLoader;
use Symfony\Component\Translation\Translator;
use Symfony\Contracts\Translation\TranslatorInterface;

$translator = new Translator('en');
$templates = new TemplateRegistry();
$translator = new Translator('pt_BR');
$translator->addLoader('array', new ArrayLoader());
$translator->addResource(
'array',
[
'{{subject}} must pass all the rules' => 'Todas as regras requeridas devem passar para {{subject}}',
'The length of' => 'O comprimento de',
'{{subject}} must be a string' => '{{subject}} deve ser uma string',
'{{subject}} must be between {{minValue}} and {{maxValue}}' => '{{subject}} deve possuir de {{minValue}} a {{maxValue}} caracteres',
'{{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}}',
// Directly translating validator messages
$templates->get(vs\AllOf::class, vs\AllOf::TEMPLATE_ALL)->default => 'Todas as regras requeridas devem passar para {{subject}}',
$templates->get(vs\Length::class)->default => 'O comprimento de',
$templates->get(vs\StringVal::class)->default => '{{subject}} deve ser uma string',
$templates->get(vs\Between::class)->default => '{{subject}} deve possuir de {{minValue}} a {{maxValue}} caracteres',
$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}}',
$templates->get(vs\DateTimeDiff::class)->default => 'O número de {{type|trans}} entre agora e',
$templates->get(vs\Equals::class)->default => '{{subject}} deve ser igual a {{compareTo}}',

// Custom templates set during runtime
'Your name must be {{haystack|list:or}}' => 'Seu nome deve ser {{haystack|list:or}}',
'{{haystack|list:and}} are the only possible names' => '{{haystack|list:and}} são os únicos nomes possíveis',

// Miscellaneous translations
'United States' => 'Estados Unidos',
'years' => 'anos',
'The number of {{type|trans}} between now and' => 'O número de {{type|trans}} entre agora e',
'{{subject}} must be equal to {{compareTo}}' => '{{subject}} deve ser igual a {{compareTo}}',
'Your name must be {{haystack|list:or}}' => 'Seu nome deve ser {{haystack|list:or}}',
'or' => 'ou',
'{{haystack|list:and}} are the only possible names' => '{{haystack|list:and}} são os únicos nomes possíveis',
'and' => 'e',
],
'en',
'pt_BR',
);
$container = ContainerRegistry::createContainer();
$container->set(TranslatorInterface::class, $translator);
Expand Down
3 changes: 2 additions & 1 deletion tests/unit/Message/Formatter/TemplateResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use Respect\Validation\Message\TemplateRegistry;
use Respect\Validation\Path;
use Respect\Validation\Test\Builders\ResultBuilder;
use Respect\Validation\Test\TestCase;
Expand All @@ -25,7 +26,7 @@ public function itShouldReturnResultWithTemplateWhenKeyExists(): void
{
$result = (new ResultBuilder())->path(new Path('foo-path'))->build();
$templates = ['foo-path' => 'My custom template'];
$sut = new TemplateResolver();
$sut = new TemplateResolver(new TemplateRegistry());
$template = $sut->getGivenTemplate($result, $templates);

self::assertSame('My custom template', $template);
Expand Down
Loading