From 9412a9e9204da673b728630a04fe37a7cac7fe93 Mon Sep 17 00:00:00 2001 From: Henrique Moody Date: Thu, 5 Dec 2024 14:15:30 +0100 Subject: [PATCH] Upgrade translation mechanism Currently, defining translations is quite cumbersome, and the translator callback is passed to the constructor of multiple classes, which makes it quite ugly and could make translations inconsistent. This commit completely changes how translations are done in Validation. Instead of using a callback, it uses a specific class, and `Validator` will pass that object through the objects that render the messages. --- docs/04-message-translation.md | 55 +++- library/Factory.php | 66 +---- library/Message/Formatter.php | 6 +- library/Message/Parameter/Processor.php | 15 -- library/Message/Parameter/Raw.php | 30 --- library/Message/Parameter/Stringify.php | 25 -- library/Message/Parameter/Trans.php | 35 --- library/Message/Renderer.php | 2 +- library/Message/StandardFormatter.php | 30 ++- library/Message/StandardRenderer.php | 60 +++-- library/Message/Translator.php | 15 ++ .../Message/Translator/ArrayTranslator.php | 32 +++ .../Message/Translator/DummyTranslator.php | 20 ++ .../Message/Translator/GettextTranslator.php | 31 +++ library/Validator.php | 20 +- library/ValidatorDefaults.php | 68 +++++ phpstan.neon.dist | 4 + tests/integration/translator-assert.phpt | 34 --- tests/integration/translator-check.phpt | 21 -- tests/integration/translator.phpt | 26 ++ .../Message/Parameter/TestingProcessor.php | 27 -- .../Message/TestingMessageRenderer.php | 3 +- tests/library/Message/TestingStringifier.php | 23 ++ tests/unit/FactoryTest.php | 20 -- tests/unit/Message/Parameter/RawTest.php | 65 ----- .../unit/Message/Parameter/StringifyTest.php | 41 --- tests/unit/Message/Parameter/TransTest.php | 61 ----- tests/unit/Message/StandardFormatterTest.php | 13 +- tests/unit/Message/StandardRendererTest.php | 254 ++++++++++++------ .../Translator/ArrayTranslatorTest.php | 47 ++++ 30 files changed, 556 insertions(+), 593 deletions(-) delete mode 100644 library/Message/Parameter/Processor.php delete mode 100644 library/Message/Parameter/Raw.php delete mode 100644 library/Message/Parameter/Stringify.php delete mode 100644 library/Message/Parameter/Trans.php create mode 100644 library/Message/Translator.php create mode 100644 library/Message/Translator/ArrayTranslator.php create mode 100644 library/Message/Translator/DummyTranslator.php create mode 100644 library/Message/Translator/GettextTranslator.php create mode 100644 library/ValidatorDefaults.php delete mode 100644 tests/integration/translator-assert.phpt delete mode 100644 tests/integration/translator-check.phpt create mode 100644 tests/integration/translator.phpt delete mode 100644 tests/library/Message/Parameter/TestingProcessor.php create mode 100644 tests/library/Message/TestingStringifier.php delete mode 100644 tests/unit/Message/Parameter/RawTest.php delete mode 100644 tests/unit/Message/Parameter/StringifyTest.php delete mode 100644 tests/unit/Message/Parameter/TransTest.php create mode 100644 tests/unit/Message/Translator/ArrayTranslatorTest.php diff --git a/docs/04-message-translation.md b/docs/04-message-translation.md index a52b73601..c6775ffc0 100644 --- a/docs/04-message-translation.md +++ b/docs/04-message-translation.md @@ -1,17 +1,52 @@ # Message translation -You're also able to translate your message to another language with Validation. -The only thing one must do is to define the param `translator` as a callable that -will handle the translation overwriting the default factory: +You're able to translate message templates with Validation. ```php -Factory::setDefaultInstance( - (new Factory())->withTranslator('gettext') -); +use Respect\Validation\Message\Translator\GettextTranslator; +use Respect\Validation\ValidatorDefaults; + +ValidatorDefaults::setTranslator(new GettextTranslator()); +``` + +After that, if you call the methods `getMessage()`, `getMessages()`, or `getFullMessage()` from the `ValidationException`, the messages will be translated. The example above will work if you have `gettext` properly configured. + +For non-static usage, pass the translator directly to the `Validator` constructor: + +```php +use Respect\Validation\Factory; +use Respect\Validation\Message\StandardFormatter; +use Respect\Validation\Message\Translator\GettextTranslator; +use Respect\Validation\Validator; + +$translator = new GettextTranslator(); + +$validator = new Validator(new Factory(), new StandardFormatter(), $translator); ``` -The example above uses `gettext()` but you can use any other callable value, like -`[$translator, 'trans']` or `you_custom_function()`. +## Supported translators -After that, if you call `getMessage()`, `getMessages()`, or `getFullMessage()`, -the message will be translated. +- `ArrayTranslator`: Translates messages using an array of messages. +- `GettextTranslator`: Translates messages using the `gettext` extension. + +## Custom translators + +You can implement custom translators by creating a class that implements the `Translator` interface. Here's an example using the `stichoza/google-translate-php` package: + +```php +use Respect\Validation\Message\Translator; +use Stichoza\GoogleTranslate\GoogleTranslate; + +final class MyTranslator implements Translator +{ + public function __construct( + private readonly GoogleTranslate $googleTranslate + ) { + } + + public function translate(string $message): string + { + return $this->googleTranslate->translate($message); + } +} +``` diff --git a/library/Factory.php b/library/Factory.php index 2628a8dbb..22c01de1c 100644 --- a/library/Factory.php +++ b/library/Factory.php @@ -13,10 +13,6 @@ use ReflectionException; use Respect\Validation\Exceptions\ComponentException; use Respect\Validation\Exceptions\InvalidClassException; -use Respect\Validation\Message\Parameter\Processor; -use Respect\Validation\Message\Parameter\Raw; -use Respect\Validation\Message\Parameter\Stringify; -use Respect\Validation\Message\Parameter\Trans; use Respect\Validation\Transformers\Aliases; use Respect\Validation\Transformers\DeprecatedAge; use Respect\Validation\Transformers\DeprecatedAttribute; @@ -42,22 +38,8 @@ final class Factory */ private array $rulesNamespaces = ['Respect\\Validation\\Rules']; - /** - * @var callable - */ - private $translator; - - private Processor $processor; - - private Transformer $transformer; - - private static Factory $defaultInstance; - - public function __construct() - { - $this->translator = static fn (string $message) => $message; - $this->processor = new Raw(new Trans($this->translator, new Stringify())); - $this->transformer = new DeprecatedAttribute( + public function __construct( + private readonly Transformer $transformer = new DeprecatedAttribute( new DeprecatedKey( new DeprecatedKeyValue( new DeprecatedMinAndMax( @@ -67,16 +49,8 @@ public function __construct() ) ) ) - ); - } - - public static function getDefaultInstance(): self - { - if (!isset(self::$defaultInstance)) { - self::$defaultInstance = new self(); - } - - return self::$defaultInstance; + ) + ) { } public function withRuleNamespace(string $rulesNamespace): self @@ -87,33 +61,6 @@ public function withRuleNamespace(string $rulesNamespace): self return $clone; } - public function withTranslator(callable $translator): self - { - $clone = clone $this; - $clone->translator = $translator; - $clone->processor = new Raw(new Trans($translator, new Stringify())); - - return $clone; - } - - public function withParameterProcessor(Processor $processor): self - { - $clone = clone $this; - $clone->processor = $processor; - - return $clone; - } - - public function getTranslator(): callable - { - return $this->translator; - } - - public function getParameterProcessor(): Processor - { - return $this->processor; - } - /** * @param mixed[] $arguments */ @@ -122,11 +69,6 @@ public function rule(string $ruleName, array $arguments = []): Validatable return $this->createRuleSpec($this->transformer->transform(new RuleSpec($ruleName, $arguments))); } - public static function setDefaultInstance(self $defaultInstance): void - { - self::$defaultInstance = $defaultInstance; - } - private function createRuleSpec(RuleSpec $ruleSpec): Validatable { $rule = $this->createRule($ruleSpec->name, $ruleSpec->arguments); diff --git a/library/Message/Formatter.php b/library/Message/Formatter.php index 4d6af775d..33e93678b 100644 --- a/library/Message/Formatter.php +++ b/library/Message/Formatter.php @@ -16,17 +16,17 @@ interface Formatter /** * @param array $templates */ - public function main(Result $result, array $templates): string; + public function main(Result $result, array $templates, Translator $translator): string; /** * @param array $templates */ - public function full(Result $result, array $templates, int $depth = 0): string; + public function full(Result $result, array $templates, Translator $translator, int $depth = 0): string; /** * @param array $templates * * @return array */ - public function array(Result $result, array $templates): array; + public function array(Result $result, array $templates, Translator $translator): array; } diff --git a/library/Message/Parameter/Processor.php b/library/Message/Parameter/Processor.php deleted file mode 100644 index 1eef366fd..000000000 --- a/library/Message/Parameter/Processor.php +++ /dev/null @@ -1,15 +0,0 @@ - - * SPDX-License-Identifier: MIT - */ - -declare(strict_types=1); - -namespace Respect\Validation\Message\Parameter; - -interface Processor -{ - public function process(string $name, mixed $value, ?string $modifier = null): string; -} diff --git a/library/Message/Parameter/Raw.php b/library/Message/Parameter/Raw.php deleted file mode 100644 index e9a573e5c..000000000 --- a/library/Message/Parameter/Raw.php +++ /dev/null @@ -1,30 +0,0 @@ - - * SPDX-License-Identifier: MIT - */ - -declare(strict_types=1); - -namespace Respect\Validation\Message\Parameter; - -use function is_bool; -use function is_scalar; - -final class Raw implements Processor -{ - public function __construct( - private readonly Processor $nextProcessor, - ) { - } - - public function process(string $name, mixed $value, ?string $modifier = null): string - { - if ($modifier === 'raw' && is_scalar($value)) { - return is_bool($value) ? (string) (int) $value : (string) $value; - } - - return $this->nextProcessor->process($name, $value, $modifier); - } -} diff --git a/library/Message/Parameter/Stringify.php b/library/Message/Parameter/Stringify.php deleted file mode 100644 index 6b1e9f608..000000000 --- a/library/Message/Parameter/Stringify.php +++ /dev/null @@ -1,25 +0,0 @@ - - * SPDX-License-Identifier: MIT - */ - -declare(strict_types=1); - -namespace Respect\Validation\Message\Parameter; - -use function is_string; -use function Respect\Stringifier\stringify; - -final class Stringify implements Processor -{ - public function process(string $name, mixed $value, ?string $modifier = null): string - { - if ($name === 'name' && is_string($value)) { - return $value; - } - - return stringify($value); - } -} diff --git a/library/Message/Parameter/Trans.php b/library/Message/Parameter/Trans.php deleted file mode 100644 index 8a485e9e4..000000000 --- a/library/Message/Parameter/Trans.php +++ /dev/null @@ -1,35 +0,0 @@ - - * SPDX-License-Identifier: MIT - */ - -declare(strict_types=1); - -namespace Respect\Validation\Message\Parameter; - -use function call_user_func; -use function is_string; - -final class Trans implements Processor -{ - /** @var callable */ - private $translator; - - public function __construct( - callable $translator, - private readonly Processor $nextProcessor, - ) { - $this->translator = $translator; - } - - public function process(string $name, mixed $value, ?string $modifier = null): string - { - if ($modifier === 'trans' && is_string($value)) { - return call_user_func($this->translator, $value); - } - - return $this->nextProcessor->process($name, $value, $modifier); - } -} diff --git a/library/Message/Renderer.php b/library/Message/Renderer.php index 75ca5367c..78187dd3b 100644 --- a/library/Message/Renderer.php +++ b/library/Message/Renderer.php @@ -13,5 +13,5 @@ interface Renderer { - public function render(Result $result, ?string $template = null): string; + public function render(Result $result, Translator $translator, ?string $template = null): string; } diff --git a/library/Message/StandardFormatter.php b/library/Message/StandardFormatter.php index 7c93a7bbc..41d7951e9 100644 --- a/library/Message/StandardFormatter.php +++ b/library/Message/StandardFormatter.php @@ -29,29 +29,29 @@ final class StandardFormatter implements Formatter { public function __construct( - private readonly Renderer $renderer, + private readonly Renderer $renderer = new StandardRenderer(), ) { } /** * @param array $templates */ - public function main(Result $result, array $templates): string + public function main(Result $result, array $templates, Translator $translator): string { $selectedTemplates = $this->selectTemplates($result, $templates); if (!$this->isFinalTemplate($result, $selectedTemplates)) { foreach ($this->extractDeduplicatedChildren($result) as $child) { - return $this->main($child, $selectedTemplates); + return $this->main($child, $selectedTemplates, $translator); } } - return $this->renderer->render($this->getTemplated($result, $selectedTemplates)); + return $this->renderer->render($this->getTemplated($result, $selectedTemplates), $translator); } /** * @param array $templates */ - public function full(Result $result, array $templates, int $depth = 0): string + public function full(Result $result, array $templates, Translator $translator, int $depth = 0): string { $selectedTemplates = $this->selectTemplates($result, $templates); $isFinalTemplate = $this->isFinalTemplate($result, $selectedTemplates); @@ -62,14 +62,14 @@ public function full(Result $result, array $templates, int $depth = 0): string $rendered .= sprintf( '%s- %s' . PHP_EOL, $indentation, - $this->renderer->render($this->getTemplated($result, $selectedTemplates)), + $this->renderer->render($this->getTemplated($result, $selectedTemplates), $translator), ); $depth++; } if (!$isFinalTemplate) { foreach ($this->extractDeduplicatedChildren($result) as $child) { - $rendered .= $this->full($child, $selectedTemplates, $depth); + $rendered .= $this->full($child, $selectedTemplates, $translator, $depth); $rendered .= PHP_EOL; } } @@ -82,17 +82,23 @@ public function full(Result $result, array $templates, int $depth = 0): string * * @return array */ - public function array(Result $result, array $templates): array + public function array(Result $result, array $templates, Translator $translator): array { $selectedTemplates = $this->selectTemplates($result, $templates); $deduplicatedChildren = $this->extractDeduplicatedChildren($result); if (count($deduplicatedChildren) === 0 || $this->isFinalTemplate($result, $selectedTemplates)) { - return [$result->id => $this->renderer->render($this->getTemplated($result, $selectedTemplates))]; + return [ + $result->id => $this->renderer->render($this->getTemplated($result, $selectedTemplates), $translator), + ]; } $messages = []; foreach ($deduplicatedChildren as $child) { - $messages[$child->id] = $this->array($child, $this->selectTemplates($child, $selectedTemplates)); + $messages[$child->id] = $this->array( + $child, + $this->selectTemplates($child, $selectedTemplates), + $translator + ); if (count($messages[$child->id]) !== 1) { continue; } @@ -101,7 +107,9 @@ public function array(Result $result, array $templates): array } if (count($messages) > 1) { - $self = ['__root__' => $this->renderer->render($this->getTemplated($result, $selectedTemplates))]; + $self = [ + '__root__' => $this->renderer->render($this->getTemplated($result, $selectedTemplates), $translator), + ]; return $self + $messages; } diff --git a/library/Message/StandardRenderer.php b/library/Message/StandardRenderer.php index ee71312bc..dbe9285ea 100644 --- a/library/Message/StandardRenderer.php +++ b/library/Message/StandardRenderer.php @@ -10,66 +10,55 @@ namespace Respect\Validation\Message; use ReflectionClass; -use Respect\Validation\Exceptions\ComponentException; -use Respect\Validation\Message\Parameter\Processor; +use Respect\Stringifier\Stringifier; +use Respect\Stringifier\Stringifiers\CompositeStringifier; use Respect\Validation\Mode; use Respect\Validation\Result; use Respect\Validation\Validatable; -use Throwable; -use function call_user_func; +use function is_bool; +use function is_scalar; +use function is_string; use function preg_replace_callback; -use function sprintf; +use function print_r; final class StandardRenderer implements Renderer { /** @var array> */ private array $templates = []; - /** @var callable */ - private $translator; + private readonly Stringifier $stringifier; - public function __construct( - callable $translator, - private readonly Processor $processor - ) { - $this->translator = $translator; + public function __construct(?Stringifier $stringifier = null) + { + $this->stringifier = $stringifier ?? CompositeStringifier::createDefault(); } - public function render(Result $result, ?string $template = null): string + public function render(Result $result, Translator $translator, ?string $template = null): string { $parameters = $result->parameters; - $parameters['name'] ??= $result->name ?? $this->processor->process('input', $result->input); + $parameters['name'] ??= $result->name ?? $this->placeholder('input', $result->input, $translator); $parameters['input'] = $result->input; $rendered = (string) preg_replace_callback( '/{{(\w+)(\|([^}]+))?}}/', - function (array $matches) use ($parameters) { + function (array $matches) use ($parameters, $translator) { if (!isset($parameters[$matches[1]])) { return $matches[0]; } - return $this->processor->process($matches[1], $parameters[$matches[1]], $matches[3] ?? null); + return $this->placeholder($matches[1], $parameters[$matches[1]], $translator, $matches[3] ?? null); }, - $this->translate($template ?? $this->getTemplateMessage($result)) + $translator->translate($template ?? $this->getTemplateMessage($result)) ); if (!$result->hasCustomTemplate() && $result->nextSibling !== null) { - $rendered .= ' ' . $this->render($result->nextSibling); + $rendered .= ' ' . $this->render($result->nextSibling, $translator); } return $rendered; } - private function translate(string $message): string - { - try { - return call_user_func($this->translator, $message); - } catch (Throwable $throwable) { - throw new ComponentException(sprintf('Failed to translate "%s"', $message), 0, $throwable); - } - } - /** @return array