Skip to content
Merged
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
3.5.0
=====

* (feature) Add `TranslationHelper`.
* (improvement) Allow `TranslatableInterface` instead of only `TranslatableMessage` everywhere.


3.4.6
=====

Expand Down
6 changes: 3 additions & 3 deletions src/Api/ApiResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
*/
Expand Down Expand Up @@ -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;
Expand Down
4 changes: 2 additions & 2 deletions src/Api/ApiResponseNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
],
Expand Down
50 changes: 50 additions & 0 deletions src/Translation/TranslationHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php declare(strict_types=1);

namespace Torr\Rad\Translation;

use Symfony\Contracts\Translation\TranslatableInterface;
use Symfony\Contracts\Translation\TranslatorInterface;

/**
* @final
*
* @api
*
* Translator helper that solves typical translation workflows
*/
readonly class TranslationHelper
{
/**
*/
public function __construct (
private TranslatorInterface $translator,
) {}

/**
* Regular translator, like {@see TranslatorInterface::trans()}
*/
public function trans (string $id, array $parameters = [], ?string $domain = null, ?string $locale = null) : string
{
return $this->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;
}
}
111 changes: 111 additions & 0 deletions tests/Translation/TranslationHelperTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
<?php declare(strict_types=1);

namespace Tests\Torr\Rad\Translation;

use PHPUnit\Framework\TestCase;
use Symfony\Contracts\Translation\TranslatableInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use Torr\Rad\Translation\TranslationHelper;

/**
* @internal
*/
final class TranslationHelperTest extends TestCase
{
public function testTransDelegatesToTranslator () : void
{
$translator = $this->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 {}
}
Loading