Symfony 8.2 brings many improvements to the Translation component and its
integration with Twig and forms, including better XLIFF files and a
translation:extract command that finds many more messages.
Meaningful XLIFF Source Elements
In XLIFF files, the <source> element is meant to contain the original text
to translate, which is what translation tools and translators display. However,
the XLIFF files generated by translation:extract store the translation key
in it:
1 2 3 4 5
<!-- translations/messages.fr.xlf -->
<trans-unit id="hVNhFLX" resname="navbar.home">
<source>navbar.home</source>
<target>__navbar.home</target>
</trans-unit>
First, Symfony 8.2 now keeps the <source> element when loading and dumping
XLIFF files, instead of replacing it with the translation key. Second, a new
command fills the <source> elements of your XLIFF files with the messages of
the default locale:
1 2 3 4
$ php bin/console translation:update-xliff-sources
# you can also restrict the update to some locales and domains
$ php bin/console translation:update-xliff-sources --locales=fr --domains=messages
If the English translation of navbar.home is Home (and en is the
default locale), the <source> element of the previous example now contains
Home. The command only updates files stored in your translations/ directory
and in the paths configured in framework.translator.paths; it never changes
the translation files of your vendors or bundles.
A File-Wide Default Domain in Twig
The trans_default_domain Twig tag applies to the current template scope.
That scope doesn't include the body of {% embed %} tags, because Twig compiles
them as separate templates. In the following example, product.title is
translated in the catalog domain, but product.add_to_cart uses messages:
1 2 3 4 5 6 7
{% trans_default_domain 'catalog' %}
<h1>{{ 'product.title'|trans }}</h1>
{% embed 'components/card.html.twig' %}
{% block footer %}{{ 'product.add_to_cart'|trans }}{% endblock %}
{% endembed %}
In Symfony 8.2, add the new for _self modifier to apply the domain to the
whole template file, including embed bodies and macros:
1
{% trans_default_domain 'catalog' for _self %}
The domain must be a literal string and you can only declare it once per template,
outside of any block and before any embed tag. It doesn't apply to other templates (e.g.
the ones included or extended), and a regular trans_default_domain tag still
takes precedence where it applies.
Translatable Choice Group Labels
The group_by option of ChoiceType creates <optgroup> elements to group
choices. Their labels were already translated, but the callable could only
return strings. In Symfony 8.2, it can also return TranslatableInterface
objects, like the choice_label option already allows:
1 2 3 4 5 6 7 8 9 10
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Translation\TranslatableMessage;
$builder->add('country', ChoiceType::class, [
'choices' => $countries,
'group_by' => fn (Country $country): TranslatableMessage => new TranslatableMessage(
'continent.'.$country->getContinent(),
domain: 'countries',
),
]);
If your custom form theme overrides the choice_widget_options block, render
the group label from choice.label (falling back to the loop key) because the
array key is no longer the label when using translatable objects.
Smarter Extraction of Messages in PHP
The translation:extract command only found messages passed to trans(),
t() or new TranslatableMessage() as literal strings, concatenations of
literals or class constants. In Symfony 8.2, the extractor also resolves ternary
and null-coalescing expressions, match expressions, interpolated strings,
variables assigned earlier and values returned by methods:
1 2 3 4 5 6 7 8 9
// extracts 'quote.amended' and 'quote.new'
$this->translator->trans($isAmended ? 'quote.amended' : 'quote.new');
// extracts 'status.draft' and 'status.published'
$label = match ($status) {
Status::Draft => 'status.draft',
Status::Published => 'status.published',
};
$this->translator->trans($label);
Methods are resolved when the extractor knows the class of the object ($this,
typed parameters and properties, enum cases, return types, etc.) and when they
are defined in the extracted paths:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
enum Status
{
case Draft;
case Published;
public function label(): string
{
return match ($this) {
self::Draft => 'status.draft',
self::Published => 'status.published',
};
}
}
// extracts 'status.draft' and 'status.published'
$this->translator->trans($article->getStatus()->label());
Extracting Form Labels and Help Messages in Twig
When rendering forms in Twig, you can override the label and help messages
defined in the form type. In Symfony 8.2, translation:extract finds the
messages that you pass to the form_label(), form_widget(), form_help()
and form_row() functions:
1 2 3 4 5 6 7
{{ form_label(form.apiToken, 'admin.company.api_token') }}
{{ form_row(form.email, {
label: 'user.email',
help: 'user.email_help',
translation_domain: 'admin',
}) }}
These messages are extracted in the domain defined by the translation_domain
variable or in the default domain otherwise. The trans_default_domain tag is
ignored on purpose, because form themes don't use it to translate labels and help
messages.