Skip to content

Translations

Edit this page

The term "internationalization" (often abbreviated i18n) refers to the process of abstracting strings and other locale-specific pieces out of your application into a layer where they can be translated and converted based on the user's locale (i.e. language and country). For text, this means wrapping each with a function capable of translating the text (or "message") into the language of the user:

1
2
3
4
5
6
// text will *always* print out in English
echo 'Hello World';

// text can be translated into the end-user's language or
// default to English
echo $translator->trans('Hello World');

Note

The term locale refers roughly to the user's language and country. It can be any string that your application uses to manage translations and other format differences (e.g. currency format). The ISO 639-1 language code, an underscore (_), then the ISO 3166-1 alpha-2 country code (e.g. fr_FR for French/France) is recommended.

Translations can be organized into groups, called domains. By default, all messages use the default messages domain:

1
echo $translator->trans('Hello World', domain: 'messages');

The translation process has several steps:

  1. Enable and configure Symfony's translation service;
  2. Abstract strings (i.e. "messages") by wrapping them in calls to the Translator;
  3. Create translation resources/files for each supported locale that translate each message in the application;
  4. Determine, set and manage the user's locale for the request and optionally on the user's entire session.

Installation

First, run this command to install the translator before using it:

1
$ composer require symfony/translation

Configuration

The previous command creates an initial config file where you can define the default locale of the application and the directory where the translation files are located:

1
2
3
4
5
# config/packages/translation.yaml
framework:
    default_locale: 'en'
    translator:
        default_path: '%kernel.project_dir%/translations'

Tip

You can also define the enabled_locales option to restrict the locales that your application is available in.

Basic Translation

Translation of text is done through the translator service (Translator). To translate a block of text (called a message), use the trans() method. Suppose, for example, that you're translating a static message from inside a controller:

1
2
3
4
5
6
7
8
9
// ...
use Symfony\Contracts\Translation\TranslatorInterface;

public function index(TranslatorInterface $translator): Response
{
    $translated = $translator->trans('Symfony is great');

    // ...
}

When this code is run, Symfony will attempt to translate the message "Symfony is great" based on the locale of the user. For this to work, you need to tell Symfony how to translate the message via a "translation resource", which is usually a file that contains a collection of translations for a given locale. This "dictionary" of translations can be created in several different formats:

1
2
# translations/messages.fr.yaml
Symfony is great: J'aime Symfony

You can find more information on where these files should be located.

Now, if the language of the user's locale is French (e.g. fr_FR or fr_BE), the message will be translated into J'aime Symfony. You can also translate the message inside your templates.

Using Real or Keyword Messages

This example illustrates the two different philosophies when creating messages to be translated:

1
2
3
$translator->trans('Symfony is great');

$translator->trans('symfony.great');

In the first method, messages are written in the language of the default locale (English in this case). That message is then used as the "id" when creating translations.

In the second method, messages are actually "keywords" that convey the idea of the message. The keyword message is then used as the "id" for any translations. In this case, translations must be made for the default locale (i.e. to translate symfony.great to Symfony is great).

The second method is handy because the message key won't need to be changed in every translation file if you decide that the message should actually read "Symfony is really great" in the default locale.

The choice of which method to use is entirely up to you, but the "keyword" format is often recommended for multi-language applications, whereas for shared bundles that contain translation resources we recommend the real message, so your application can choose to disable the translator layer and you will see a readable message.

Additionally, the php and yaml file formats support nested ids to avoid repeating yourself if you use keywords instead of real text for your ids:

1
2
3
4
5
6
7
8
9
10
11
12
symfony:
    is:
        # id is symfony.is.great
        great: Symfony is great
        # id is symfony.is.amazing
        amazing: Symfony is amazing
    has:
        # id is symfony.has.bundles
        bundles: Symfony has bundles
user:
    # id is user.login
    login: Login

The Translation Process

To actually translate the message, Symfony uses the following process when using the trans() method:

  1. The locale of the current user, which is stored on the request is determined; this is typically set via a _locale attribute on your routes;
  2. A catalog of translated messages is loaded from translation resources defined for the locale (e.g. fr_FR). Messages from the fallback locale and the enabled locales are also loaded and added to the catalog if they don't already exist. The end result is a large "dictionary" of translations.
  3. If the message is located in the catalog, the translation is returned. If not, the translator returns the original message.

Message Format

Sometimes, a message containing a variable needs to be translated:

1
2
// ...
$translated = $translator->trans('Hello '.$name);

However, creating a translation for this string is impossible since the translator will try to look up the message including the variable portions (e.g. "Hello Ryan" or "Hello Fabien").

Another complication is when you have translations that may or may not be plural, based on some variable:

1
2
There is one apple.
There are 5 apples.

To manage these situations, Symfony follows the ICU MessageFormat syntax by using PHP's MessageFormatter class. Read more about this in How to Translate Messages using the ICU MessageFormat.

Translatable Objects

Sometimes translating contents in templates is cumbersome because you need the original message, the translation parameters and the translation domain for each content. Making the translation in the controller or services simplifies your templates, but requires injecting the translator service in different parts of your application and mocking it in your tests.

Instead of translating a string at the time of creation, you can use a "translatable object", which is an instance of the TranslatableMessage class. This object stores all the information needed to fully translate its contents when needed:

1
2
3
4
5
6
7
use Symfony\Component\Translation\TranslatableMessage;

// the first argument is required and it's the original message
$message = new TranslatableMessage('Symfony is great!');
// the optional second argument defines the translation parameters and
// the optional third argument is the translation domain
$status = new TranslatableMessage('order.status', ['%status%' => $order->getStatus()], 'store');

Templates are now much simpler because you can pass translatable objects to the trans filter:

1
2
<h1>{{ message|trans }}</h1>
<p>{{ status|trans }}</p>

Tip

The translation parameters can also be a TranslatableMessage.

Tip

There's also a function called t(), available both in Twig and PHP, as a shortcut to create translatable objects.

Translations in Templates

Most of the time, translation occurs in templates. Symfony provides native support for both Twig and PHP templates.

Using Twig Filters

The trans filter can be used to translate variable texts and complex expressions:

1
2
3
{{ message|trans }}

{{ message|trans({'%name%': 'Fabien'}, 'app') }}

Tip

You can set the translation domain for an entire Twig template with a single tag:

1
{% trans_default_domain 'app' %}

Note that this only influences the current template, not any "included" template (in order to avoid side effects).

By default, the translated messages are output escaped; apply the raw filter after the translation filter to avoid the automatic escaping:

1
2
3
4
5
{% set message = '<h3>foo</h3>' %}

{# strings and variables translated via a filter are escaped by default #}
{{ message|trans|raw }}
{{ '<h3>bar</h3>'|trans|raw }}

Using Twig Tags

Symfony provides a specialized Twig tag trans to help with message translation of static blocks of text:

1
{% trans %}Hello %name%{% endtrans %}

Warning

The %var% notation of placeholders is required when translating in Twig templates using the tag.

Tip

If you need to use the percent character (%) in a string, escape it by doubling it: {% trans %}Percent: %percent%%%{% endtrans %}

You can also specify the message domain and pass some additional variables:

1
2
3
{% trans with {'%name%': 'Fabien'} from 'app' %}Hello %name%{% endtrans %}

{% trans with {'%name%': 'Fabien'} from 'app' into 'fr' %}Hello %name%{% endtrans %}

Warning

Using the translation tag has the same effect as the filter, but with one major difference: automatic output escaping is not applied to translations using a tag.

Forcing the Translator Locale

When translating a message, the translator uses the specified locale or the fallback locale if necessary. You can also manually specify the locale to use for translation:

1
$translator->trans('Symfony is great', locale: 'fr_FR');

Extracting Translation Contents and Updating Catalogs Automatically

The most time-consuming task when translating an application is to extract all the template contents to be translated and to keep all the translation files in sync. Symfony includes a command called translation:extract that helps you with these tasks:

1
2
3
4
5
6
7
8
# shows all the messages that should be translated for the French language
$ php bin/console translation:extract --dump-messages fr

# updates the French translation files with the missing strings for that locale
$ php bin/console translation:extract --force fr

# check out the command help to see its options (prefix, output format, domain, sorting, etc.)
$ php bin/console translation:extract --help

The translation:extract command looks for missing translations in:

  • Templates stored in the templates/ directory (or any other directory defined in the twig.default_path and twig.paths config options);
  • Any PHP file/class that injects or autowires the translator service and makes calls to the trans() method;
  • Any PHP file/class stored in the src/ directory that creates translatable objects using the constructor or the t() method or calls the trans() method;
  • Any PHP file/class stored in the src/ directory that uses Constraints Attributes with *message named argument(s).

6.2

The support of PHP files/classes that use constraint attributes was introduced in Symfony 6.2.

Tip

Install the nikic/php-parser package in your project to improve the results of the translation:extract command. This package enables an AST parser that can find many more translatable items:

1
$ composer require nikic/php-parser

6.2

The AST parser support was introduced in Symfony 6.2.

By default, when the translation:extract command creates new entries in the translation file, it uses the same content as both the source and the pending translation. The only difference is that the pending translation is prefixed by __. You can customize this prefix using the --prefix option:

1
$ php bin/console translation:extract --force --prefix="NEW_" fr

Translation Resource/File Names and Locations

Symfony looks for message files (i.e. translations) in the following default locations:

  • the translations/ directory (at the root of the project);
  • the translations/ directory inside of any bundle (and also their Resources/translations/ directory, which is no longer recommended for bundles).

The locations are listed here with the highest priority first. That is, you can override the translation messages of a bundle in the first directory.

The override mechanism works at a key level: only the overridden keys need to be listed in a higher priority message file. When a key is not found in a message file, the translator will automatically fall back to the lower priority message files.

The filename of the translation files is also important: each message file must be named according to the following path: domain.locale.loader:

  • domain: The translation domain;
  • locale: The locale that the translations are for (e.g. en_GB, en, etc);
  • loader: How Symfony should load and parse the file (e.g. xlf, php, yaml, etc).

The loader can be the name of any registered loader. By default, Symfony provides many loaders which are selected based on the following file extensions:

6.1

The .xliff file extension support was introduced in Symfony 6.1.

The choice of which loader to use is entirely up to you and is a matter of taste. The recommended option is to use YAML for simple projects and use XLIFF if you're generating translations with specialized programs or teams.

Warning

Each time you create a new message catalog (or install a bundle that includes a translation catalog), be sure to clear your cache so that Symfony can discover the new translation resources:

1
$ php bin/console cache:clear

Note

You can add other directories with the paths option in the configuration:

1
2
3
4
5
# config/packages/translation.yaml
framework:
    translator:
        paths:
            - '%kernel.project_dir%/custom/path/to/translations'

Translations of Doctrine Entities

Unlike the contents of templates, it's not practical to translate the contents stored in Doctrine Entities using translation catalogs. Instead, use the Doctrine Translatable Extension or the Translatable Behavior. For more information, read the documentation of those libraries.

Custom Translation Resources

If your translations use a format not supported by Symfony or you store them in a special way (e.g. not using files or Doctrine entities), you need to provide a custom class implementing the LoaderInterface interface. See the Built-in Symfony Service Tags tag for more information.

Translation Providers

When using external translators to translate your application, you must send them the new contents to translate frequently and merge the results back in the application.

Instead of doing this manually, Symfony provides integration with several third-party translation services. You can upload and download (called "push" and "pull") translations to/from these services and merge the results automatically in the application.

Installing and Configuring a Third Party Provider

Before pushing/pulling translations to a third-party provider, you must install the package that provides integration with that provider:

Provider Install with
Crowdin composer require symfony/crowdin-translation-provider
Loco (localise.biz) composer require symfony/loco-translation-provider
Lokalise composer require symfony/lokalise-translation-provider
Phrase composer require symfony/phrase-translation-provider

6.4

The Phrase translation provider was introduced in Symfony 6.4.

Each library includes a Symfony Flex recipe that will add a configuration example to your .env file. For example, suppose you want to use Loco. First, install it:

1
$ composer require symfony/loco-translation-provider

You'll now have a new line in your .env file that you can uncomment:

1
2
# .env
LOCO_DSN=loco://API_KEY@default

The LOCO_DSN isn't a real address: it's a convenient format that offloads most of the configuration work to Symfony. The loco scheme activates the Loco provider that you just installed, which knows all about how to push and pull translations via Loco. The only part you need to change is the API_KEY placeholder.

This table shows the full list of available DSN formats for each provider:

Provider DSN
Crowdin crowdin://PROJECT_ID:API_TOKEN@ORGANIZATION_DOMAIN.default
Loco (localise.biz) loco://API_KEY@default
Lokalise lokalise://PROJECT_ID:API_KEY@default
Phrase phrase://PROJECT_ID:API_TOKEN@default?userAgent=myProject

To enable a translation provider, customize the DSN in your .env file and configure the providers option:

1
2
3
4
5
6
7
8
# config/packages/translation.yaml
framework:
    translator:
        providers:
            loco:
                dsn: '%env(LOCO_DSN)%'
                domains: ['messages']
                locales: ['en', 'fr']

Important

If you use Phrase as a provider you must configure a user agent in your dsn. See Identification via User-Agent for reasoning and some examples.

Also make the locale _names_ in Phrase should be as defined in RFC4646 (e.g. pt-BR rather than pt_BR). Not doing so will result in Phrase creating a new locale for the imported keys.

Tip

If you use Crowdin as a provider and some of your locales are different from the Crowdin Language Codes, you have to set the Custom Language Codes in the Crowdin project for each of your locales, in order to override the default value. You need to select the "locale" placeholder and specify the custom code in the "Custom Code" field.

Tip

If you use Lokalise as a provider and a locale format following the ISO 639-1 (e.g. "en" or "fr"), you have to set the Custom Language Name setting in Lokalise for each of your locales, in order to override the default value (which follow the ISO 639-1 succeeded by a sub-code in capital letters that specifies the national variety (e.g. "GB" or "US" according to ISO 3166-1 alpha-2)).

Tip

The Phrase provider uses Phrase's tag feature to map translations to Symfony's translation domains. If you need some assistance with organising your tags in Phrase, you might want to consider the Phrase Tag Bundle which provides some commands helping you with that.

Pushing and Pulling Translations

After configuring the credentials to access the translation provider, you can now use the following commands to push (upload) and pull (download) translations:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# push all local translations to the Loco provider for the locales and domains
# configured in config/packages/translation.yaml file.
# it will update existing translations already on the provider.
$ php bin/console translation:push loco --force

# push new local translations to the Loco provider for the French locale
# and the validators domain.
# it will **not** update existing translations already on the provider.
$ php bin/console translation:push loco --locales fr --domains validators

# push new local translations and delete provider's translations that not
# exists anymore in local files for the French locale and the validators domain.
# it will **not** update existing translations already on the provider.
$ php bin/console translation:push loco --delete-missing --locales fr --domains validators

# check out the command help to see its options (format, domains, locales, etc.)
$ php bin/console translation:push --help
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# pull all provider's translations to local files for the locales and domains
# configured in config/packages/translation.yaml file.
# it will overwrite completely your local files.
$ php bin/console translation:pull loco --force

# pull new translations from the Loco provider to local files for the French
# locale and the validators domain.
# it will **not** overwrite your local files, only add new translations.
$ php bin/console translation:pull loco --locales fr --domains validators

# check out the command help to see its options (format, domains, locales, intl-icu, etc.)
$ php bin/console translation:pull --help

# the "--as-tree" option will write YAML messages as a tree-like structure instead
# of flat keys
$ php bin/console translation:pull loco --force --as-tree

6.4

The --as-tree option of the translation:pull command was introduced in Symfony 6.4.

Creating Custom Providers

In addition to using Symfony's built-in translation providers, you can create your own providers. To do so, you need to create two classes:

  1. The first class must implement ProviderInterface;
  2. The second class needs to be a factory which will create instances of the first class. It must implement

ProviderFactoryInterface (you can extend AbstractProviderFactory to simplify its creation).

After creating these two classes, you need to register your factory as a service and tag it with translation.provider_factory.

Handling the User's Locale

Translating happens based on the user's locale. The locale of the current user is stored in the request and is accessible via the Request object:

1
2
3
4
5
6
use Symfony\Component\HttpFoundation\Request;

public function index(Request $request): void
{
    $locale = $request->getLocale();
}

To set the user's locale, you may want to create a custom event listener so that it's set before any other parts of the system (i.e. the translator) need it:

1
2
3
4
5
6
7
public function onKernelRequest(RequestEvent $event): void
{
    $request = $event->getRequest();

    // some logic to determine the $locale
    $request->setLocale($locale);
}

Note

The custom listener must be called before LocaleListener, which initializes the locale based on the current request. To do so, set your listener priority to a higher value than LocaleListener priority (which you can obtain by running the debug:event kernel.request command).

Read Sessions for more information on making the user's locale "sticky" to their session.

Note

Setting the locale using $request->setLocale() in the controller is too late to affect the translator. Either set the locale via a listener (like above), the URL (see next) or call setLocale() directly on the translator service.

See the Translations section below about setting the locale via routing.

The Locale and the URL

Since you can store the locale of the user in the session, it may be tempting to use the same URL to display a resource in different languages based on the user's locale. For example, http://www.example.com/contact could show content in English for one user and French for another user. Unfortunately, this violates a fundamental rule of the Web: that a particular URL returns the same resource regardless of the user. To further muddy the problem, which version of the content would be indexed by search engines?

A better policy is to include the locale in the URL using the special _locale parameter:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// src/Controller/ContactController.php
namespace App\Controller;

// ...
class ContactController extends AbstractController
{
    #[Route(
        path: '/{_locale}/contact',
        name: 'contact',
        requirements: [
            '_locale' => 'en|fr|de',
        ],
    )]
    public function contact(): Response
    {
        // ...
    }
}

When using the special _locale parameter in a route, the matched locale is automatically set on the Request and can be retrieved via the getLocale() method. In other words, if a user visits the URI /fr/contact, the locale fr will automatically be set as the locale for the current request.

You can now use the locale to create routes to other translated pages in your application.

Tip

Define the locale requirement as a container parameter to avoid hardcoding its value in all your routes.

Setting a Default Locale

What if the user's locale hasn't been determined? You can guarantee that a locale is set on each user's request by defining a default_locale for the framework:

1
2
3
# config/packages/translation.yaml
framework:
    default_locale: en

This default_locale is also relevant for the translator, as shown in the next section.

Selecting the Language Preferred by the User

If your application supports multiple languages, the first time a user visits your site it's common to redirect them to the best possible language according to their preferences. This is achieved with the getPreferredLanguage() method of the Request object:

1
2
3
4
5
// get the Request object somehow (e.g. as a controller argument)
$request = ...
// pass an array of the locales (their script and region parts are optional) supported
// by your application and the method returns the best locale for the current user
$locale = $request->getPreferredLanguage(['pt', 'fr_Latn_CH', 'en_US'] );

Symfony finds the best possible language based on the locales passed as argument and the value of the Accept-Language HTTP header. If it can't find a perfect match between them, this method returns the first locale passed as argument (that's why the order of the passed locales is important).

Fallback Translation Locales

Imagine that the user's locale is es_AR and that you're translating the key Symfony is great. To find the Spanish translation, Symfony actually checks translation resources for several locales:

  1. First, Symfony looks for the translation in a es_AR (Argentinean Spanish) translation resource (e.g. messages.es_AR.yaml);
  2. If it wasn't found, Symfony looks for the translation in the parent locale, which is automatically defined only for some locales. In this example, the parent locale is es_419 (Latin American Spanish);
  3. If it wasn't found, Symfony looks for the translation in a es (Spanish) translation resource (e.g. messages.es.yaml);
  4. If the translation still isn't found, Symfony uses the fallbacks option, which can be configured as follows. When this option is not defined, it defaults to the default_locale setting mentioned in the previous section.

    1
    2
    3
    4
    5
    # config/packages/translation.yaml
    framework:
        translator:
            fallbacks: ['en']
            # ...

Note

When Symfony can't find a translation in the given locale, it will add the missing translation to the log file. For details, see Framework Configuration Reference (FrameworkBundle).

Switch Locale Programmatically

6.1

The LocaleSwitcher was introduced in Symfony 6.1.

Sometimes you need to change the locale of the application dynamically just to run some code. Imagine a console command that renders Twig templates of emails in different languages. You need to change the locale only to render those templates.

The LocaleSwitcher class allows you to change at once the locale of:

  • All the services that are tagged with kernel.locale_aware;
  • \Locale::setDefault();
  • If the RequestContext service is available, the _locale parameter (so urls are generated with the new locale):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    use Symfony\Component\Translation\LocaleSwitcher;
    
    class SomeService
    {
        public function __construct(
            private LocaleSwitcher $localeSwitcher,
        ) {
        }
    
        public function someMethod(): void
        {
            // you can get the current application locale like this:
            $currentLocale = $this->localeSwitcher->getLocale();
    
            // you can set the locale for the entire application like this:
            // (from now on, the application will use 'fr' (French) as the
            // locale; including the default locale used to translate Twig templates)
            $this->localeSwitcher->setLocale('fr');
    
            // reset the current locale of your application to the configured default locale
            // in config/packages/translation.yaml, by option 'default_locale'
            $this->localeSwitcher->reset();
    
            // you can also run some code with a certain locale, without
            // changing the locale for the rest of the application
            $this->localeSwitcher->runWithLocale('es', function() {
    
                // e.g. render here some Twig templates using 'es' (Spanish) locale
    
            });
    
            // you can optionally declare an argument in your callback to receive the
            // injected locale
            $this->localeSwitcher->runWithLocale('es', function(string $locale) {
    
                // here, the $locale argument will be set to 'es'
    
            });
    
            // ...
        }
    }

6.4

The support of declaring an argument in the callback to inject the locale being used in the runWithLocale() method was introduced in Symfony 6.4.

When using autowiring, type-hint any controller or service argument with the LocaleSwitcher class to inject the locale switcher service. Otherwise, configure your services manually and inject the translation.locale_switcher service.

How to Find Missing or Unused Translation Messages

When you work with many translation messages in different languages, it can be hard to keep track which translations are missing and which are not used anymore. The debug:translation command helps you to find these missing or unused translation messages templates:

1
2
3
4
{# messages can be found when using the trans filter and tag #}
{% trans %}Symfony is great{% endtrans %}

{{ 'Symfony is great'|trans }}

Warning

The extractors can't find messages translated outside templates (like form labels or controllers) unless using translatable objects or calling the trans() method on a translator (since Symfony 5.3). Dynamic translations using variables or expressions in templates are not detected either:

1
2
3
{# this translation uses a Twig variable, so it won't be detected #}
{% set message = 'Symfony is great' %}
{{ message|trans }}

Suppose your application's default_locale is fr and you have configured en as the fallback locale (see configuration and fallback for how to configure these). And suppose you've already set up some translations for the fr locale:

1
2
3
4
5
6
7
8
9
10
11
12
<!-- translations/messages.fr.xlf -->
<?xml version="1.0" encoding="UTF-8" ?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
    <file source-language="en" datatype="plaintext" original="file.ext">
        <body>
            <trans-unit id="1">
                <source>Symfony is great</source>
                <target>J'aime Symfony</target>
            </trans-unit>
        </body>
    </file>
</xliff>

and for the en locale:

1
2
3
4
5
6
7
8
9
10
11
12
<!-- translations/messages.en.xlf -->
<?xml version="1.0" encoding="UTF-8" ?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
    <file source-language="en" datatype="plaintext" original="file.ext">
        <body>
            <trans-unit id="1">
                <source>Symfony is great</source>
                <target>Symfony is great</target>
            </trans-unit>
        </body>
    </file>
</xliff>

To inspect all messages in the fr locale for the application, run:

1
2
3
4
5
6
7
$ php bin/console debug:translation fr

---------  ------------------  ----------------------  -------------------------------
 State      Id                  Message Preview (fr)    Fallback Message Preview (en)
---------  ------------------  ----------------------  -------------------------------
 unused     Symfony is great    J'aime Symfony          Symfony is great
---------  ------------------  ----------------------  -------------------------------

It shows you a table with the result when translating the message in the fr locale and the result when the fallback locale en would be used. On top of that, it will also show you when the translation is the same as the fallback translation (this could indicate that the message was not correctly translated). Furthermore, it indicates that the message Symfony is great is unused because it is translated, but you haven't used it anywhere yet.

Now, if you translate the message in one of your templates, you will get this output:

1
2
3
4
5
6
7
$ php bin/console debug:translation fr

---------  ------------------  ----------------------  -------------------------------
 State      Id                  Message Preview (fr)    Fallback Message Preview (en)
---------  ------------------  ----------------------  -------------------------------
            Symfony is great    J'aime Symfony          Symfony is great
---------  ------------------  ----------------------  -------------------------------

The state is empty which means the message is translated in the fr locale and used in one or more templates.

If you delete the message Symfony is great from your translation file for the fr locale and run the command, you will get:

1
2
3
4
5
6
7
$ php bin/console debug:translation fr

---------  ------------------  ----------------------  -------------------------------
 State      Id                  Message Preview (fr)    Fallback Message Preview (en)
---------  ------------------  ----------------------  -------------------------------
 missing    Symfony is great    Symfony is great        Symfony is great
---------  ------------------  ----------------------  -------------------------------

The state indicates the message is missing because it is not translated in the fr locale but it is still used in the template. Moreover, the message in the fr locale equals to the message in the en locale. This is a special case because the untranslated message id equals its translation in the en locale.

If you copy the content of the translation file in the en locale to the translation file in the fr locale and run the command, you will get:

1
2
3
4
5
6
7
$ php bin/console debug:translation fr

----------  ------------------  ----------------------  -------------------------------
 State       Id                  Message Preview (fr)    Fallback Message Preview (en)
----------  ------------------  ----------------------  -------------------------------
 fallback    Symfony is great    Symfony is great        Symfony is great
----------  ------------------  ----------------------  -------------------------------

You can see that the translations of the message are identical in the fr and en locales which means this message was probably copied from English to French and maybe you forgot to translate it.

By default, all domains are inspected, but it is possible to specify a single domain:

1
$ php bin/console debug:translation en --domain=messages

When the application has a lot of messages, it is useful to display only the unused or only the missing messages, by using the --only-unused or --only-missing options:

1
2
$ php bin/console debug:translation en --only-unused
$ php bin/console debug:translation en --only-missing

Debug Command Exit Codes

The exit code of the debug:translation command changes depending on the status of the translations. Use the following public constants to check it:

1
2
3
4
5
6
7
8
9
10
11
12
13
use Symfony\Bundle\FrameworkBundle\Command\TranslationDebugCommand;

// generic failure (e.g. there are no translations)
TranslationDebugCommand::EXIT_CODE_GENERAL_ERROR;

// there are missing translations
TranslationDebugCommand::EXIT_CODE_MISSING;

// there are unused translations
TranslationDebugCommand::EXIT_CODE_UNUSED;

// some translations are using the fallback translation
TranslationDebugCommand::EXIT_CODE_FALLBACK;

These constants are defined as "bit masks", so you can combine them as follows:

1
2
3
if (TranslationDebugCommand::EXIT_CODE_MISSING | TranslationDebugCommand::EXIT_CODE_UNUSED) {
    // ... there are missing and/or unused translations
}

How to Find Errors in Translation Files

Symfony processes all the application translation files as part of the process that compiles the application code before executing it. If there's an error in any translation file, you'll see an error message explaining the problem.

If you prefer, you can also validate the contents of any YAML and XLIFF translation file using the lint:yaml and lint:xliff commands:

1
2
3
4
5
6
7
8
9
10
11
# lint a single file
$ php bin/console lint:yaml translations/messages.en.yaml
$ php bin/console lint:xliff translations/messages.en.xlf

# lint a whole directory
$ php bin/console lint:yaml translations
$ php bin/console lint:xliff translations

# lint multiple files or directories
$ php bin/console lint:yaml translations path/to/trans
$ php bin/console lint:xliff translations/messages.en.xlf translations/messages.es.xlf

The linter results can be exported to JSON using the --format option:

1
2
$ php bin/console lint:yaml translations/ --format=json
$ php bin/console lint:xliff translations/ --format=json

When running these linters inside GitHub Actions, the output is automatically adapted to the format required by GitHub, but you can force that format too:

1
2
$ php bin/console lint:yaml translations/ --format=github
$ php bin/console lint:xliff translations/ --format=github

Tip

The Yaml component provides a stand-alone yaml-lint binary allowing you to lint YAML files without having to create a console application:

1
$ php vendor/bin/yaml-lint translations/

Pseudo-localization translator

Note

The pseudolocalization translator is meant to be used for development only.

The following image shows a typical menu on a webpage:

A menu showing multiple items nicely aligned next to eachother.

This other image shows the same menu when the user switches the language to Spanish. Unexpectedly, some text is cut and other contents are so long that they overflow and you can't see them:

In Spanish, some menu items contain more letters which result in them being cut.

These kind of errors are very common, because different languages can be longer or shorter than the original application language. Another common issue is to only check if the application works when using basic accented letters, instead of checking for more complex characters such as the ones found in Polish, Czech, etc.

These problems can be solved with pseudolocalization, a software testing method used for testing internationalization. In this method, instead of translating the text of the software into a foreign language, the textual elements of an application are replaced with an altered version of the original language.

For example, Account Settings is translated as [!!! Àççôûñţ Šéţţîñĝš !!!]. First, the original text is expanded in length with characters like [!!! !!!] to test the application when using languages more verbose than the original one. This solves the first problem.

In addition, the original characters are replaced by similar but accented characters. This makes the text highly readable, while allowing to test the application with all kinds of accented and special characters. This solves the second problem.

Full support for pseudolocalization was added to help you debug internationalization issues in your applications. You can enable and configure it in the translator configuration:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# config/packages/translation.yaml
framework:
    translator:
        pseudo_localization:
            # replace characters by their accented version
            accents: true
            # wrap strings with brackets
            brackets: true
            # controls how many extra characters are added to make text longer
            expansion_factor: 1.4
            # maintain the original HTML tags of the translated contents
            parse_html: true
            # also translate the contents of these HTML attributes
            localizable_html_attributes: ['title']

That's all. The application will now start displaying those strange, but readable, contents to help you internationalize it. See for example the difference in the Symfony Demo application. This is the original page:

The Symfony demo login page.

And this is the same page with pseudolocalization enabled:

The Symfony demo login page with pseudolocalization.

Summary

With the Symfony Translation component, creating an internationalized application no longer needs to be a painful process and boils down to these steps:

  • Abstract messages in your application by wrapping each in the trans() method;
  • Translate each message into multiple locales by creating translation message files. Symfony discovers and processes each file because its name follows a specific convention;
  • Manage the user's locale, which is stored on the request, but can also be set on the user's session.
This work, including the code samples, is licensed under a Creative Commons BY-SA 3.0 license.
TOC
    Version