I ran into this problem several times over the last years and always found a good enough workaround. This time my getIdentifyingName()
methods that are heavily used across the whole project (like an explicit __toString()
) had to translate some keywords used in the data layer, so there was no elegant workaround.
My solution this time is a TranslateObject
and a corresponding helper service. The TranslateObject
is a plain object holding a translation key and an array of placeholders which also can be TranslateObjects to allow multi level translation (like a getIdentifyingNameTranslateObject()
calling another related object's getIdentifyingNameTranslateObject()
within one of the placeholders):
namespace App\Utils;
class TranslateObject
{
/** @var string */
protected $transKey;
/** @var array */
protected $placeholders;
public function __construct(string $transKey, array $placeholders = [])
{
$this->transKey = $transKey;
$this->placeholders = self::normalizePlaceholders($placeholders);
}
public static function normalizePlaceholders(array $placeholders): array
{
foreach ($placeholders as $key => &$placeholder) {
if (substr($key, 0, 1) !== '%' || substr($key, -1, 1) !== '%') {
throw new \InvalidArgumentException('The $placeholder attribute must only contain keys in format "%placeholder%".');
}
if ($placeholder instanceof TranslateObject) {
continue;
}
if (is_scalar($placeholder)) {
$placeholder = ['value' => $placeholder];
}
if (!isset($placeholder['value']) || !is_scalar($placeholder['value'])) {
throw new \InvalidArgumentException('$placeholders[\'%placeholder%\'][\'value\'] must be present and a scalar value.');
}
if (!isset($placeholder['translate'])) {
$placeholder['translate'] = false;
}
if (!is_bool($placeholder['translate'])) {
throw new \InvalidArgumentException('$placeholders[\'%placeholder%\'][\'translate\'] must be a boolean.');
}
}
return $placeholders;
}
public function getTransKey(): string
{
return $this->transKey;
}
public function getPlaceholders(): array
{
return $this->placeholders;
}
}
The helper looks like this and does the work:
namespace App\Services;
use App\Utils\TranslateObject;
use Symfony\Contracts\Translation\TranslatorInterface;
class TranslateObjectHelper
{
/** @var TranslatorInterface */
protected $translator;
public function __construct(TranslatorInterface $translator)
{
$this->translator = $translator;
}
public function trans(TranslateObject $translateObject): string
{
$placeholders = $translateObject->getPlaceholders();
foreach ($placeholders as $key => &$placeholder) {
if ($placeholder instanceof TranslateObject) {
$placeholder = $this->trans($placeholder);
}
elseif (true === $placeholder['translate']) {
$placeholder = $this->translator->trans($placeholder['value']);
}
else {
$placeholder = $placeholder['value'];
}
}
return $this->translator->trans($translateObject->getTransKey(), $placeholders);
}
}
And then within the Entity there is a getIdentifyingNameTranslateObject()
method returning a TranslateObject
.
/**
* Get an identifying name as a TranslateObject (for use with TranslateObjectHelper)
*/
public function getIdentifyingNameTranslateObject(): TranslateObject
{
return new TranslateObject('my.whatever.translation.key', [
'%placeholder1%' => $this->myEntityProperty1,
'%placeholderWithANeedOfTranslation%' => [
'value' => 'my.whatever.translation.values.' . $this->myPropertyWithANeedOfTranslation,
'translate' => true,
],
'%placeholderWithCascadingTranslationNeeds%' => $this->getRelatedEntity()->getIdentifyingNameTranslateObject(),
]);
}
When I need to return such a translated property, I need access to my injected TranslateObjectHelper
service and use its trans()
method like in a controller or any other service:
$this->translateObjectHelper->trans($myObject->getIdentifyingNameTranslateObject());
Then I created a twig filter as a simple helper like this:
namespace App\Twig;
use App\Services\TranslateObjectHelper;
use App\Utils\TranslateObject;
class TranslateObjectExtension extends \Twig_Extension
{
/** @var TranslateObjectHelper */
protected $translateObjectHelper;
public function __construct(TranslateObjectHelper $translateObjectHelper)
{
$this->translateObjectHelper = $translateObjectHelper;
}
public function getFilters()
{
return array(
new \Twig_SimpleFilter('translateObject', [$this, 'translateObject']),
);
}
/**
* sends a TranslateObject through a the translateObjectHelper->trans() method
*/
public function translateObject(TranslateObject $translateObject): string
{
return $this->translateObjectHelper->trans($translateObject);
}
public function getName(): string
{
return 'translate_object_twig_extension';
}
}
So in Twig I can translate like this:
{{ myObject.getIdentifyingNameTranslateObject()|translateObject }}
In the end, I "just" needed to find all getIdentifyingName()
calls (or .identifyingName
in Twig) on that entities and replace them with getIdentifyingNameTranslateObject()
with a call to the trans()
method of the TranslateObjectHelper
(or the translateObject
filter in Twig).