Symfony 8.1 brings several improvements to the translation system: broader
XLIFF format support, a more flexible enabled_locales configuration, a
better translated placeholder for expanded choice fields, and a new helper
class to compute locale fallback chains.
Using Environment Variables in Enabled Locales
The framework.enabled_locales configuration option used to require a hard-coded list of locales. In Symfony 8.1 you can use environment variables in this list directly. Empty values are filtered out automatically, so you can declare a fixed number of slots and leave some of them empty:
1 2 3 4 5 6 7
# config/packages/translation.yaml
framework:
enabled_locales:
- '%env(LOCALE_1)%'
- '%env(LOCALE_2)%'
- '%env(LOCALE_3)%'
- '%env(LOCALE_4)%'
This is useful when the list of enabled locales depends on the deployment environment. For example, different tenants may enable different subsets of languages without requiring a separate configuration file per environment.
Translated Placeholder on Expanded Choice Fields
When a ChoiceType is rendered as a group of radio buttons or checkboxes
(expanded: true), its placeholder label used the choice_translation_domain
option for translation.
This caused problems with EntityType, whose choice_translation_domain
defaults to false. As a result, TranslatableMessage placeholders were
never translated even when an explicit translation_domain was configured.
In Symfony 8.1, placeholders for expanded choice fields now use
translation_domain, matching the behavior of non-expanded <select> fields:
1 2 3 4 5 6 7 8 9 10
use Symfony\Component\Translation\TranslatableMessage;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
$form = $this->createForm(EntityType::class, null, [
'class' => Country::class,
'expanded' => true,
// this placeholder is now translated using the 'forms' domain.
'placeholder' => new TranslatableMessage('select_a_country', [], 'forms'),
'translation_domain' => 'forms',
]);
New LocaleFallbackProvider Class
Applications with many regional locales often need to compute locale fallback chains consistently across different services and components.
Symfony resolves these fallback chains by following ICU parent mappings,
shortening locale sub-tags, and finally appending any configured fallback
locales. Until now, this logic lived inside the Translator class.
In Symfony 8.1, this logic is extracted into a dedicated reusable
LocaleFallbackProvider class:
1 2 3 4 5
use Symfony\Component\Translation\LocaleFallbackProvider;
$provider = new LocaleFallbackProvider(['en']);
$fallbacks = $provider->computeFallbackLocales('es_AR');
// ['es_419', 'es', 'en']
The class also provides a static helper to validate locale strings. It throws
an InvalidArgumentException when the locale contains invalid characters:
1
LocaleFallbackProvider::validateLocale($locale);
Support for XLIFF Versions 2.1 and 2.2
Symfony previously accepted only XLIFF 1.2 and 2.0 files. Translation files
declaring version="2.1" or version="2.2" were rejected even though
their structure is fully compatible with XLIFF 2.0.
This could become an issue when using newer localization platforms or translation tooling that already generate XLIFF 2.1 or 2.2 files by default. Symfony 8.1 now accepts these newer version numbers transparently:
1 2 3 4 5 6 7 8 9 10 11 12
<?xml version="1.0" encoding="UTF-8" ?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:2.0"
version="2.2" srcLang="en" trgLang="fr">
<file id="f1">
<unit id="welcome">
<segment>
<source>Welcome</source>
<target>Bienvenue</target>
</segment>
</unit>
</file>
</xliff>
Support for XLIFF PGS Module
XLIFF 2.2 introduces the PGS module (Plural, Gender and Select), which allows translators to express plural, gender, and select variations directly inside XLIFF files.
Symfony 8.1 understands these attributes, converts them internally to ICU
MessageFormat, and registers the result in the +intl-icu translation
domain. This means the translations work transparently with trans():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
<?xml version="1.0" encoding="UTF-8" ?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:2.0"
xmlns:pgs="urn:oasis:names:tc:xliff:pgs:1.0"
version="2.2" srcLang="en" trgLang="fr">
<file id="f1">
<unit id="file_deleted" pgs:switch="plural:file_count">
<segment id="s1" pgs:case="0">
<source>You deleted no file.</source>
<target>Vous n'avez supprimé aucun fichier.</target>
</segment>
<segment id="s2" pgs:case="1">
<source>You deleted one file.</source>
<target>Vous avez supprimé un fichier.</target>
</segment>
<segment id="s3" pgs:case="other">
<source>You deleted <ph id="1" disp="file_count"/> files.</source>
<target>Vous avez supprimé <ph id="1" disp="file_count"/> fichiers.</target>
</segment>
</unit>
</file>
</xliff>
The pgs:switch attribute defines the variable name and switch type
(plural, gender, or select), while each <segment> uses
pgs:case to match a value.
You can also combine multiple switches in a single attribute. For example,
pgs:switch="gender:host_gender plural:guest_count". Symfony automatically
nests the generated ICU structures as needed.