Symfony Translation & i18n with ICU Message Format
AI generated
SF
{ }
Symfony · Translation · ICU · i18n · Localization
Symfony Translation & i18n
done properly with the ICU Message Format

Simple string substitution is not enough for real multilingual support. Plural forms, gender agreement, locale-aware date and number formats, and context-dependent messages need the ICU Message Format, which the Symfony translation component has fully supported since version 5.

17 min read ICU · XLIFF · Translator · Pluralization · Extraction Symfony 7.x · PHP 8.3+ · intl extension

1. Why simple translations are not enough

Many Symfony projects start with plain string replacement: a key is mapped to a sentence, and variables are embedded with %count%. That works fine for English, since English only has two plural forms, has barely any case system, and gets by with very little grammatical inflection. As soon as German, Russian, Arabic, or Polish enter the picture, this system hits hard limits. Polish has four plural forms, Arabic has six, and many languages adjust articles and adjectives to match the gender of the subject. With classic Symfony translation and %count% variables, these requirements cannot be solved elegantly.

The ICU Message Format is the international standard for exactly these problems. It was specified by the Unicode organization, is used across Android, iOS, Java and every modern frontend framework, and is available in PHP through the intl extension. Symfony translation integrates the ICU Message Format as a native formatter, translation catalogs can mix both formats, and older messages do not need to be migrated right away. The decisive advantage: translators describe the linguistic logic directly in the translation, not in PHP code.

2. Setting up and configuring Symfony translation

The Symfony translation component is already included in the standard Symfony skeleton via the Flex recipe. For the ICU Message Format you additionally need the PHP intl extension, which is active in most PHP installations. In config/packages/translation.yaml you set the default locale, the fallback, and the path to the catalog files. The formatter type is determined per filename via the file extension: files with the suffix +intl-icu.xlf or +intl-icu.yaml automatically use the ICU formatter, while all other files use the classic Symfony formatter.

The recommended directory structure keeps translations in translations/ at the project root, split by domain and locale: messages+intl-icu.de.xlf for the default domain in German, validators+intl-icu.de.xlf for validation messages. The command bin/console translation:extract de --format=xlf20 --output-format=xliff2 --force automatically scans all Twig templates and PHP classes for translation keys in use and adds missing entries to the catalogs. That saves considerable manual work when maintaining large translation files.


<?php
// config/packages/translation.yaml
// framework:
//   default_locale: en
//   translator:
//     default_path: '%kernel.project_dir%/translations'
//     fallbacks: [en]
//     providers: []

// Directory structure for ICU-enabled translations:
// translations/
//   messages+intl-icu.de.xlf   ← ICU formatter activated by filename suffix
//   messages+intl-icu.en.xlf
//   validators+intl-icu.de.xlf
//   emails+intl-icu.de.xlf

// Extract all translation keys from Twig + PHP automatically:
// bin/console translation:extract de --format=xlf20 --force
// bin/console translation:extract en --format=xlf20 --force

// Check for missing translations:
// bin/console debug:translation de
// bin/console debug:translation de --only-missing

// Verify intl extension is available (required for ICU):
// php -m | grep intl

3. ICU Message Format: syntax and fundamentals

The ICU Message Format extends simple variable substitution with a full expression language for linguistic variants. Variables are written in curly braces: {name} substitutes the variable directly. For more complex expressions, the variable name is followed by a comma and the expression type: {count, plural, one {# item} other {# items}}. The hash sign # stands for the formatted value of the current variable. This syntax is readable enough for translators to edit without any PHP knowledge, an important factor for professional translation workflows involving external agencies.

The Symfony translator passes parameters as an associative array to the trans() method. In the classic format, placeholders like %name% get replaced. In the ICU Message Format you pass the values directly without percent signs: $translator->trans('greeting', ['name' => 'Maria'], domain: 'messages'). The ICU engine takes care of type checking, formatting, and selecting the correct language variant itself. Mixing both formats in one project is possible, old files with the classic extension use the old formatter, new ICU files with the +intl-icu suffix use the ICU formatter.

4. Getting pluralization right

Plural forms are the most common problem in Symfony translation. The classic format uses its own pipe syntax (One article|{count} articles), which quickly becomes unwieldy for complex plural rules. The ICU Message Format solves plural forms with the plural expression, which loads the locale-specific plural rules of the current locale from the intl extension. For English there are the keys one (singular) and other (plural). For Russian, few and many are added. The ICU Message Format knows the rules for every language, the team only has to supply the wording, not program the logic.

Particularly powerful is combining pluralization with variables in the same message. A typical e-commerce message: "You have 3 items in your cart. The total is 89.90 EUR." would need to use the singular for one item and format the price correctly per locale. In the ICU Message Format, that is a single translation unit that declaratively covers every case, without if-else constructs in the PHP code.


<?php

declare(strict_types=1);

namespace App\Service;

use Symfony\Contracts\Translation\TranslatorInterface;

/**
 * Demonstrates ICU Message Format usage in a Symfony service.
 */
final readonly class OrderSummaryService
{
    public function __construct(
        private TranslatorInterface $translator,
    ) {}

    /**
     * Build a human-readable order summary using ICU pluralization.
     */
    public function getSummaryMessage(int $itemCount, float $total, string $locale): string
    {
        // ICU plural, the translator selects the correct plural rule for the locale
        return $this->translator->trans(
            id: 'order.summary',
            parameters: [
                'count' => $itemCount,
                'total' => $total,
            ],
            locale: $locale,
        );
    }
}

// translations/messages+intl-icu.de.xlf entry (simplified):
// <trans-unit id="order.summary">
//   <source>order.summary</source>
//   <target>{count, plural,
//     one   {Sie haben # Artikel im Warenkorb. Gesamt: {total, number, ::currency/EUR}.}
//     other {Sie haben # Artikel im Warenkorb. Gesamt: {total, number, ::currency/EUR}.}
//   }</target>
// </trans-unit>

// English variant in messages+intl-icu.en.xlf:
// {count, plural,
//   one   {You have # item in your cart. Total: {total, number, ::currency/EUR}.}
//   other {You have # items in your cart. Total: {total, number, ::currency/EUR}.}
// }

5. Select expressions for gender and variants

The select expression in the ICU Message Format chooses between predefined variants based on a string value. The classic use case is adapting salutation and possessive pronouns to gender: {gender, select, female {Her order} male {His order} other {Their order}}. The other branch is mandatory and serves as a fallback for any value that is not explicitly named. This also covers the case of "diverse" or unknown gender without a separate translation unit.

Nested select and plural expressions are allowed in the ICU Message Format and cover complex linguistic cases. One example: in Russian, a noun following a numeral must be inflected not only by plural form but also by gender. This logic lives entirely in the translation file, the PHP code stays unchanged. For teams working with external translation agencies, this is an important advantage, the agencies work within their own domain of expertise without needing any developer intervention.

6. Localizing dates, times and numbers

The ICU Message Format comes with its own formatters for date, time and numbers, drawn from the intl standard. A date is formatted with {date, date, medium} for a medium-length format (for example "May 9, 2026" in English, "9. Mai 2026" in German). short, long and full are further levels. For numbers, {amount, number, ::currency/EUR} is available, formatting the number correctly per locale with a currency symbol: "€89.90" in English, "89,90 €" in German. These formatters automatically use the active locale of the Symfony translator.

For times, {time, time, short} gives "2:30 PM" (English-US) or "14:30" (German). Relative time phrases like "5 minutes ago" or "in 2 days" can be combined: a translation unit contains a plural expression for the number and a select expression for past/future. This is more expressive than the classic Symfony choice format and needs no additional Twig filter. The Symfony translation component passes all parameters as native PHP values, \DateTimeInterface objects are automatically converted correctly by the ICU formatter.

7. Managing and extracting XLIFF catalogs

XLIFF 2.0 is the recommended file format for Symfony translation in production projects. It is the ISO standard for exchanging translation data, is supported by professional CAT tools (Computer-Aided Translation) such as SDL Trados, MemoQ and Memsource, and carries metadata about the translation status of individual units. That means: if the team works with an external translation agency, XLIFF files can be handed over directly without needing a proprietary format.

The extraction command bin/console translation:extract scans Twig templates for trans tags and PHP files for $translator->trans() calls. Newly found keys are inserted into the existing XLIFF files without overwriting existing translations. The --sort flag sorts the units alphabetically, which improves diff readability in the Git repository. With --clean, the command removes entries that no longer appear in the code, ideal for regularly cleaning up stale translation keys in actively developed projects.


<?xml version="1.0" encoding="utf-8"?>
<!--
  translations/messages+intl-icu.de.xlf
  XLIFF 2.0 with ICU Message Format content
  The +intl-icu suffix in the filename activates the ICU formatter automatically.
-->
<xliff xmlns="urn:oasis:names:tc:xliff:document:2.0" version="2.0"
       srcLang="en" trgLang="de">
  <file id="messages+intl-icu.de">

    <!-- Simple variable substitution -->
    <unit id="greeting.user">
      <segment state="translated">
        <source>Hello, {name}!</source>
        <target>Hallo, {name}!</target>
      </segment>
    </unit>

    <!-- ICU plural with currency formatting -->
    <unit id="cart.summary">
      <segment state="translated">
        <source>{count, plural, one {# item} other {# items}}, total: {total, number, ::currency/EUR}</source>
        <target>{count, plural, one {# Artikel} other {# Artikel}}, Gesamt: {total, number, ::currency/EUR}</target>
      </segment>
    </unit>

    <!-- ICU select for gender-aware salutation -->
    <unit id="order.salutation">
      <segment state="translated">
        <source>Dear {gender, select, female {Ms.} male {Mr.} other {}} {name},</source>
        <target>{gender, select, female {Sehr geehrte Frau} male {Sehr geehrter Herr} other {Sehr geehrte/r}} {name},</target>
      </segment>
    </unit>

    <!-- Date and time formatting via ICU -->
    <unit id="invoice.date">
      <segment state="translated">
        <source>Invoice date: {date, date, long}</source>
        <target>Rechnungsdatum: {date, date, long}</target>
      </segment>
    </unit>

  </file>
</xliff>

8. Using translations in Twig templates

In Twig templates you use the trans filter and the {% trans %} tag for Symfony translations. With the ICU Message Format, you pass parameters as a hash: {{ 'cart.summary'|trans({count: items|length, total: cartTotal}) }}. The filter automatically detects the active locale and routes the call to the ICU formatter if the key lives in a +intl-icu file. A domain is specified as the second parameter: {{ 'form.submit'|trans({}, 'forms') }}.

For longer translation blocks with embedded HTML, the {% trans %} tag is recommended. It supports ICU variables and returns the entire block as a single translation unit. Important: HTML inside translation strings should be kept to a minimum, because it makes the work harder for translators. It is better to keep the HTML wrapper in the template and translate only the text. The translation:extract command recognizes both notations and inserts both correctly into the XLIFF catalog, without any manual intervention.

9. ICU vs. the classic Symfony format compared

The comparison shows where the ICU Message Format wins against the classic Symfony translation format and where the difference is minor.

Requirement Classic Symfony format ICU Message Format Recommendation
Simple variable %name% {name} ICU: cleaner, standards-compliant
Pluralization Pipe syntax, limited to 2 to 3 forms All plural classes of the locale ICU: essential for Slavic/Arabic
Gender / select Not natively supported {gender, select, …} ICU: the only clean solution
Numbers/currency Twig filter needed {amount, number, ::currency/EUR} ICU: integrated into the translation
CAT tool compatibility XLIFF is compatible XLIFF + ICU: industry standard ICU: preferred by agencies

For simple projects with few languages and no complex grammatical requirements, the classic Symfony translation format still works fine. As soon as Russian, Arabic, Polish or Asian languages come into play, the ICU Message Format is the only solution that gets by without hacks in the PHP code. The migration path is gradual: new catalogs get the +intl-icu extension, old catalogs keep running in parallel.

Mironsoft

Symfony development, i18n architecture and multilingual strategy

Ready to make your Symfony application properly multilingual?

We implement complete i18n solutions with Symfony translation and the ICU Message Format, from the XLIFF catalog structure through plural forms and gender variants to automatic extraction and CAT-tool-compatible workflows.

i18n architecture

XLIFF catalog structure, ICU integration and extraction workflow for Symfony projects of any size

Migration

Migrating existing Symfony translation projects gradually to the ICU Message Format without breaking changes

Translation workflow

CAT tool integration, XLIFF export and automatic synchronization with translation agencies

10. Summary

The ICU Message Format in the Symfony translation component solves the most important weaknesses of classic string-replacement systems: plural forms for every language, gender agreement via select, locale-correct date, time and number formatting directly in the translation unit, without any PHP code changes. Activation happens via the filename suffix +intl-icu, and migration from existing projects can happen gradually. XLIFF 2.0 as a file format ensures compatibility with professional CAT tools and enables smooth collaboration with translation agencies.

The biggest leverage lies in moving linguistic logic out of the PHP code and into the translation file. A developer does not need to know how many plural forms Russian has, the translator knows it and enters it directly into the XLIFF catalog. The Symfony translation system processes this information correctly without requiring a deployment cycle for linguistic changes. In projects with active translation teams, this is a fundamental efficiency gain.

Symfony Translation with ICU, the essentials at a glance

Activating ICU

File extension +intl-icu.xlf or +intl-icu.yaml, the ICU formatter is activated automatically. The PHP intl extension is a prerequisite.

Plural & select

{count, plural, one {# item} other {# items}} and {gender, select, female {…} male {…} other {…}} cover every linguistic case.

Numbers & dates

{amount, number, ::currency/EUR} and {date, date, long} format correctly per locale without a Twig filter or PHP formatter.

Extraction

bin/console translation:extract de --format=xlf20 --force finds every key automatically and maintains XLIFF catalogs with no manual effort.

11. FAQ: Symfony Translation & i18n with ICU Message Format

1What is the ICU Message Format in Symfony?
An international standard for translations with pluralization, gender, and date/number formatting. Activation via the filename suffix +intl-icu. The PHP intl extension is required.
2How do I activate ICU in Symfony?
Name the catalog file as messages+intl-icu.de.xlf. Symfony recognizes the suffix automatically and switches on the ICU formatter, no further configuration needed.
3Plural forms in the ICU Message Format?
zero, one, two, few, many, other, all locale-dependent. English: one + other. Russian: one, few, many. Arabic: all six. The rules come automatically from the intl extension.
4Mixing ICU and the classic format?
Yes. Classic files without the suffix run alongside ICU files with the +intl-icu suffix. Migration can be gradual, old catalogs do not need to be converted right away.
5Extracting keys automatically?
bin/console translation:extract de --format=xlf20 --force, finds every key in Twig and PHP, inserts missing entries into XLIFF, leaves existing translations untouched.
6Currency formatting with ICU?
{amount, number, ::currency/EUR} in the translation unit. Correct per locale: in English "€89.90", in German "89,90 €". No Twig filter or PHP formatter needed.
7Passing DateTimeInterface directly?
Yes. Symfony converts DateTimeInterface objects automatically. {date, date, long} in the ICU unit formats the date correctly per locale, no manual date() or Twig filter needed.
8Is XLIFF compatible with translation tools?
Yes. XLIFF 2.0 is an ISO standard, supported by SDL Trados, MemoQ and Memsource. The ICU Message Format in XLIFF is the industry standard for professional i18n workflows with agencies.
9Select expression for gender?
{gender, select, female {Dear Ms.} male {Dear Mr.} other {Dear}} {name}. The other branch is mandatory and catches every unrecognized value.
10Which PHP extension do I need?
The PHP intl extension. Check with php -m | grep intl. In Docker: apt-get install php-intl. Active by default in most PHP installations.