diff --git a/CHANGELOG.md b/CHANGELOG.md index 75f37ea..ac8e97f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +3.5.0 +===== + +* (feature) Add `TranslationHelper`. +* (improvement) Allow `TranslatableInterface` instead of only `TranslatableMessage` everywhere. + + 3.4.6 ===== diff --git a/src/Api/ApiResponse.php b/src/Api/ApiResponse.php index 594e597..68cc496 100644 --- a/src/Api/ApiResponse.php +++ b/src/Api/ApiResponse.php @@ -2,13 +2,13 @@ namespace Torr\Rad\Api; -use Symfony\Component\Translation\TranslatableMessage; +use Symfony\Contracts\Translation\TranslatableInterface; class ApiResponse { public int $statusCode; public ?string $error = null; - public TranslatableMessage|string|null $errorMessage = null; + public TranslatableInterface|string|null $errorMessage = null; /** */ @@ -58,7 +58,7 @@ public function withStatusCode (int $statusCode) : self */ public function withError ( ?string $error, - TranslatableMessage|string|null $errorMessage = null, + TranslatableInterface|string|null $errorMessage = null, ) : self { $this->error = $error; diff --git a/src/Api/ApiResponseNormalizer.php b/src/Api/ApiResponseNormalizer.php index bf34a30..d791dd1 100644 --- a/src/Api/ApiResponseNormalizer.php +++ b/src/Api/ApiResponseNormalizer.php @@ -3,7 +3,7 @@ namespace Torr\Rad\Api; use Symfony\Component\HttpFoundation\JsonResponse; -use Symfony\Component\Translation\TranslatableMessage; +use Symfony\Contracts\Translation\TranslatableInterface; use Symfony\Contracts\Translation\TranslatorInterface; final readonly class ApiResponseNormalizer @@ -26,7 +26,7 @@ public function normalize (ApiResponse $apiResponse) : array "ok" => $apiResponse->isOk(), "data" => $apiResponse->data, "error" => $apiResponse->error, - "errorMessage" => $apiResponse->errorMessage instanceof TranslatableMessage + "errorMessage" => $apiResponse->errorMessage instanceof TranslatableInterface ? $apiResponse->errorMessage->trans($this->translator) : $apiResponse->errorMessage, ], diff --git a/src/Translation/TranslationHelper.php b/src/Translation/TranslationHelper.php new file mode 100644 index 0000000..2b607ef --- /dev/null +++ b/src/Translation/TranslationHelper.php @@ -0,0 +1,50 @@ +translator->trans($id, $parameters, $domain, $locale); + } + + /** + * Resolves a translatable value: if given a translatable, it will be translated. If given a string, it will return as-is. + * + * @phpstan-return ($value is null ? null : string) + */ + public function resolve ( + TranslatableInterface|string|null $value, + ?string $locale = null, + ) : ?string + { + if (null === $value) + { + return null; + } + + return $value instanceof TranslatableInterface + ? $value->trans($this->translator, $locale) + : $value; + } +} diff --git a/tests/Translation/TranslationHelperTest.php b/tests/Translation/TranslationHelperTest.php new file mode 100644 index 0000000..26c062a --- /dev/null +++ b/tests/Translation/TranslationHelperTest.php @@ -0,0 +1,111 @@ +createMock(TranslatorInterface::class); + $translator + ->expects(self::once()) + ->method("trans") + ->with("greeting", ["name" => "Ada"], "messages", "de") + ->willReturn("Hallo Ada"); + + $helper = new TranslationHelper($translator); + + self::assertSame("Hallo Ada", $helper->trans("greeting", ["name" => "Ada"], "messages", "de")); + } + + public function testResolveReturnsStringAsIsForStringInput () : void + { + $translator = self::createStub(TranslatorInterface::class); + $helper = new TranslationHelper($translator); + + self::assertSame("already translated", $helper->resolve("already translated", "de")); + } + + public function testResolveTranslatesTranslatable () : void + { + $translator = self::createStub(TranslatorInterface::class); + $helper = new TranslationHelper($translator); + + $translatable = new class() implements TranslatableInterface { + public ?TranslatorInterface $receivedTranslator = null; + public ?string $receivedLocale = null; + + public function trans (TranslatorInterface $translator, ?string $locale = null) : string + { + $this->receivedTranslator = $translator; + $this->receivedLocale = $locale; + + return "translated value"; + } + }; + + self::assertSame("translated value", $helper->resolve($translatable, "fr")); + self::assertSame($translator, $translatable->receivedTranslator); + self::assertSame("fr", $translatable->receivedLocale); + } + + public function testResolveReturnsNullForNull () : void + { + $translator = self::createStub(TranslatorInterface::class); + $helper = new TranslationHelper($translator); + + self::assertNull($helper->resolve(null, "de")); + } + + public function testResolveTranslatableUsesNullLocaleByDefault () : void + { + $translator = self::createStub(TranslatorInterface::class); + $helper = new TranslationHelper($translator); + + $translatable = new class() implements TranslatableInterface { + public ?string $receivedLocale = "initial"; + + public function trans (TranslatorInterface $translator, ?string $locale = null) : string + { + $this->receivedLocale = $locale; + + return "translated without explicit locale"; + } + }; + + self::assertSame("translated without explicit locale", $helper->resolve($translatable)); + self::assertNull($translatable->receivedLocale); + } + + /** + * Runtime assertion plus static type check for PHPStan: + * `resolve("...")` must be inferred as `string`, and `resolve(null)` as `null`. + */ + public function testResolveConditionalReturnTypeForPhpStan () : void + { + $translator = self::createStub(TranslatorInterface::class); + $helper = new TranslationHelper($translator); + + $stringResult = $helper->resolve("value"); + $nullResult = $helper->resolve(null); + + self::assertSame("value", $stringResult); + self::assertNull($nullResult); + + // These calls are the actual PHPStan checks for the conditional return type. + self::expectString($stringResult); + self::expectNull($nullResult); + } + + private static function expectString (string $value) : void {} + + private static function expectNull (null $value) : void {} +}