From 02205d3a93245edf9afe8550afc37d9c3dbd7228 Mon Sep 17 00:00:00 2001 From: Jannik Zschiesche Date: Wed, 27 May 2026 10:13:29 +0200 Subject: [PATCH 1/4] Allow `TranslatableInterface` instead of only `TranslatableMessage` everywhere. --- CHANGELOG.md | 6 ++++++ src/Api/ApiResponse.php | 6 +++--- src/Api/ApiResponseNormalizer.php | 4 ++-- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75f37ea..493584f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +3.4.7 +===== + +* (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, ], From 2af5b9da3fe76ea2a83a3d5943dc95ee6099f44d Mon Sep 17 00:00:00 2001 From: Jannik Zschiesche Date: Wed, 27 May 2026 10:13:38 +0200 Subject: [PATCH 2/4] Add TranslationHelper --- src/Translation/TranslationHelper.php | 49 ++++++++++++ tests/Translation/TranslationHelperTest.php | 89 +++++++++++++++++++++ 2 files changed, 138 insertions(+) create mode 100644 src/Translation/TranslationHelper.php create mode 100644 tests/Translation/TranslationHelperTest.php diff --git a/src/Translation/TranslationHelper.php b/src/Translation/TranslationHelper.php new file mode 100644 index 0000000..312cc04 --- /dev/null +++ b/src/Translation/TranslationHelper.php @@ -0,0 +1,49 @@ +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..76476c3 --- /dev/null +++ b/tests/Translation/TranslationHelperTest.php @@ -0,0 +1,89 @@ +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 = $this->createStub(TranslatorInterface::class); + $helper = new TranslationHelper($translator); + + self::assertSame("already translated", $helper->resolve("already translated", "de")); + } + + public function testResolveTranslatesTranslatable () : void + { + $translator = $this->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 = $this->createStub(TranslatorInterface::class); + $helper = new TranslationHelper($translator); + + self::assertNull($helper->resolve(null, "de")); + } + + public function testResolveTranslatableUsesNullLocaleByDefault () : void + { + $translator = $this->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); + } +} From 37ceb1fa9191e026c6bc74654044159c33839bf1 Mon Sep 17 00:00:00 2001 From: Jannik Zschiesche Date: Wed, 27 May 2026 10:16:48 +0200 Subject: [PATCH 3/4] Fix CS + add phpstan type test --- src/Translation/TranslationHelper.php | 3 +- tests/Translation/TranslationHelperTest.php | 38 ++++++++++++++++----- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/src/Translation/TranslationHelper.php b/src/Translation/TranslationHelper.php index 312cc04..2b607ef 100644 --- a/src/Translation/TranslationHelper.php +++ b/src/Translation/TranslationHelper.php @@ -7,6 +7,7 @@ /** * @final + * * @api * * Translator helper that solves typical translation workflows @@ -22,7 +23,7 @@ public function __construct ( /** * Regular translator, like {@see TranslatorInterface::trans()} */ - public function trans (string $id, array $parameters = [], ?string $domain = null, ?string $locale = null): string + public function trans (string $id, array $parameters = [], ?string $domain = null, ?string $locale = null) : string { return $this->translator->trans($id, $parameters, $domain, $locale); } diff --git a/tests/Translation/TranslationHelperTest.php b/tests/Translation/TranslationHelperTest.php index 76476c3..26c062a 100644 --- a/tests/Translation/TranslationHelperTest.php +++ b/tests/Translation/TranslationHelperTest.php @@ -28,7 +28,7 @@ public function testTransDelegatesToTranslator () : void public function testResolveReturnsStringAsIsForStringInput () : void { - $translator = $this->createStub(TranslatorInterface::class); + $translator = self::createStub(TranslatorInterface::class); $helper = new TranslationHelper($translator); self::assertSame("already translated", $helper->resolve("already translated", "de")); @@ -36,11 +36,10 @@ public function testResolveReturnsStringAsIsForStringInput () : void public function testResolveTranslatesTranslatable () : void { - $translator = $this->createStub(TranslatorInterface::class); + $translator = self::createStub(TranslatorInterface::class); $helper = new TranslationHelper($translator); - $translatable = new class() implements TranslatableInterface - { + $translatable = new class() implements TranslatableInterface { public ?TranslatorInterface $receivedTranslator = null; public ?string $receivedLocale = null; @@ -60,7 +59,7 @@ public function trans (TranslatorInterface $translator, ?string $locale = null) public function testResolveReturnsNullForNull () : void { - $translator = $this->createStub(TranslatorInterface::class); + $translator = self::createStub(TranslatorInterface::class); $helper = new TranslationHelper($translator); self::assertNull($helper->resolve(null, "de")); @@ -68,11 +67,10 @@ public function testResolveReturnsNullForNull () : void public function testResolveTranslatableUsesNullLocaleByDefault () : void { - $translator = $this->createStub(TranslatorInterface::class); + $translator = self::createStub(TranslatorInterface::class); $helper = new TranslationHelper($translator); - $translatable = new class() implements TranslatableInterface - { + $translatable = new class() implements TranslatableInterface { public ?string $receivedLocale = "initial"; public function trans (TranslatorInterface $translator, ?string $locale = null) : string @@ -86,4 +84,28 @@ public function trans (TranslatorInterface $translator, ?string $locale = null) 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 {} } From 7668a17b8548275b4b9f9de660d0b0e25d261546 Mon Sep 17 00:00:00 2001 From: Jannik Zschiesche Date: Wed, 27 May 2026 10:25:25 +0200 Subject: [PATCH 4/4] Finalize changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 493584f..ac8e97f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ -3.4.7 +3.5.0 ===== +* (feature) Add `TranslationHelper`. * (improvement) Allow `TranslatableInterface` instead of only `TranslatableMessage` everywhere.