Hugo Alliaume
Contributed by Hugo Alliaume in #66690

Sometimes, a translated message needs some HTML inside it, such as a link in the middle of a sentence. The usual solution is to put the HTML in the translation and render it with the raw filter:

1
2
3
4
{# translations/messages.en.yaml:
   auth_prompt: 'Please <a href="/login">sign in</a> to continue.' #}

{{ 'auth_prompt'|trans|raw }}

This has several problems: URLs are hardcoded in the translation files, any HTML added by translators is rendered as is, and the message parameters are no longer escaped. If one of them contains user input, you have an XSS vulnerability.

The trans_html Filter

Symfony 8.2 adds a trans_html Twig filter to solve this. Translations no longer contain real HTML, but pseudo-tags (<name>...</name>) without attributes:

1
2
# translations/messages+intl-icu.en.yaml
auth_prompt: 'Hi {name}! Please <signin_link>sign in</signin_link> or <signup_link>create an account</signup_link>.'

In the template, you map each pseudo-tag to the HTML element to render. Elements are defined as data (the tag name and its attributes), and you can pass a list of elements to nest them (from the outermost to the innermost):

1
2
3
4
{{ 'auth_prompt'|trans_html(tags: {
    signin_link: { tag: 'a', attr: { href: path('login') } },
    signup_link: [{ tag: 'strong' }, { tag: 'a', attr: { href: path('register') } }],
}, arguments: { name: user.name }) }}

Symfony renders the declared tags and escapes everything else: the message text, the parameters, the attribute values and any tag that you didn't declare. If the translation contains unbalanced tags, Symfony escapes the entire message. Tags such as script and style are not allowed.

The output of this filter is already safe, so you don't need raw. The filter also works with translatable messages created with the t() function.

Translating HTML Messages in PHP

You can also translate these messages in PHP with the new HtmlTranslator class of the Translation component. For example, you could use it when building HTML emails:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Translation\HtmlTranslator;

class AuthPromptRenderer
{
    public function __construct(
        private HtmlTranslator $htmlTranslator,
        private UrlGeneratorInterface $urlGenerator,
    ) {
    }

    public function render(User $user): string
    {
        return $this->htmlTranslator->trans('auth_prompt', [
            'signin_link' => ['tag' => 'a', 'attr' => ['href' => $this->urlGenerator->generate('login')]],
            'signup_link' => ['tag' => 'a', 'attr' => ['href' => $this->urlGenerator->generate('register')]],
        ], ['name' => $user->getName()]);
    }
}
Published in #Living on the edge