ICU Message Formatting Guide
derafu/translation doesn’t implement its own message formatter. ICU
formatting comes directly from PHP’s intl extension (\MessageFormatter),
used in two places:
TranslatableMessage::__toString()— formats the raw message eagerly, without a translator. This is what makesgetMessage()on a translatable exception return a fully formatted string with zero setup.symfony/translation’s own ICU formatter — used when translating through a realTranslator, for any domain whose name ends in+intl-icu(see Resource Providers and the API Reference).
Both paths accept the exact same ICU MessageFormat syntax described below.
A message with no entry in the translator’s catalogues is formatted by the
first path: TranslatableMessage::trans() returns the same text as
__toString() (and so as getMessage()) when the translator knows its
catalogues (TranslatorBagInterface, like Symfony’s) and has no entry for
the message, in its locale or in the fallback ones, within the message’s
domain. symfony/translation does not apply ICU to ids it does not have: it
would replace the name of each parameter inside the braces (Hello {John}).
Basic Placeholders
The simplest form uses named placeholders:
throw new ValidationException(
'The field {field} is required.',
['field' => 'email'],
);
// getMessage(): "The field email is required."
Multiple placeholders are supported:
throw new ValidationException(
'Value {value} for field {field} is invalid.',
['value' => 'test@', 'field' => 'email'],
);
// getMessage(): "Value test@ for field email is invalid."
Pluralization
$id = '{count, plural, =0{No messages} one{# message} other{# messages}}';
new TranslatableMessage($id, ['count' => 2]); // "2 messages"
new TranslatableMessage($id, ['count' => 1]); // "1 message"
new TranslatableMessage($id, ['count' => 0]); // "No messages"
Available plural categories:
zero: for languages with a special zero form.one: singular form.two: dual form (for languages that have it).few/many: for languages with special handling of small/large numbers.other: default form (required — always provide it).=n: exact number matches (e.g.=0).
Gender / Select
$id = '{gender, select, female {She liked your post} male {He liked your post} other {They liked your post}}';
(string) new TranslatableMessage($id, ['gender' => 'female']);
// "She liked your post"
Nested placeholders work too:
$id = '{gender, select, female {{name} added her comment} male {{name} added his comment} other {{name} added their comment}}';
(string) new TranslatableMessage($id, ['gender' => 'female', 'name' => 'Alice']);
// "Alice added her comment"
Number Formatting
'{value, number}' // Plain number.
'{value, number, percent}' // Percentage.
'{value, number, currency}' // Currency, using the message's own locale.
Currency formatting depends on the locale carrying a specific currency
association (e.g. en_US, not just en):
new TranslatableMessage(
'Balance must be greater than {min, number, currency}.',
['min' => 100],
null,
'en_US',
);
// "Balance must be greater than $100.00."
Nested Plural + Select
Patterns can be nested for combined scenarios:
$id = '{gender, select,
female {{count, plural, =0{She has no messages} one{She has # message} other{She has # messages}}}
male {{count, plural, =0{He has no messages} one{He has # message} other{He has # messages}}}
other {{count, plural, =0{They have no messages} one{They have # message} other{They have # messages}}}
}';
(string) new TranslatableMessage($id, ['gender' => 'female', 'count' => 5]);
// "She has 5 messages"
Common Patterns
// Range validation.
'Value must be between {min} and {max}.'
// List validation.
'{count, plural, =0{List cannot be empty} one{At least one item is required} other{At least # items are required}}'
// Status messages.
'{status, select, pending{Waiting for approval} approved{Approved on {date}} rejected{Rejected: {reason}} other{Unknown status}}'
Troubleshooting
-
Missing the
othercategory — required for everyselect/pluralpattern. Without it, a value that doesn’t match any listed category falls back to the raw, unformatted message (same as an invalid pattern, see below):// Wrong. '{gender, select, male{He} female{She}}' // Correct. '{gender, select, male{He} female{She} other{They}}' -
Unmatched braces:
// Wrong. 'Hello {name' // Correct. 'Hello {name}'TranslatableMessage::__toString()catches this: an invalid ICU pattern returns the raw message unchanged rather than throwing. -
Missing parameters don’t throw either — the placeholder is simply left unexpanded:
(string) new TranslatableMessage('{count} items'); // "{count} items"
For more on ICU MessageFormat syntax: