What is ICU MessageFormat?
ICU MessageFormat is a syntax for translatable messages that need logic: plurals, gender or other select branches, and typed placeholders for numbers and dates. It comes from ICU (International Components for Unicode), the library that implements much of the CLDR data, and it has been adopted well beyond C and Java, notably in the JavaScript ecosystem.
The core idea is that the branching lives inside the translation, not in code. A message like "{count, plural, one {# file} other {# files}}" lets a Russian translator supply the extra plural forms Russian needs without any code change. Select branches do the same for gender or arbitrary enumerations, and arguments can carry formatting types like number or date.
In PHP, the intl extension exposes this as MessageFormatter, so you can use ICU messages inside a Laravel app even though the framework native syntax is the simpler pipe format of trans_choice(). Teams usually adopt ICU when they localize into languages with rich plural systems or need gendered messages.
$msg = '{count, plural, one {# file} other {# files}} deleted';
MessageFormatter::formatMessage('en', $msg, ['count' => 1]); // "1 file deleted"
MessageFormatter::formatMessage('en', $msg, ['count' => 4]); // "4 files deleted"
Using ICU messages in a Laravel app
Laravel's translator does not parse ICU syntax, but it does not get in the way either. Store the ICU pattern as an ordinary value in a lang file, fetch it with __() without replacements so it comes back untouched, and pass it to MessageFormatter with the active locale. Laravel's own replacement only rewrites words that start with a colon, so the braces survive even if someone passes replacements by mistake.
A small helper keeps call sites short. MessageFormatter's constructor throws an IntlException when the pattern is invalid, which is what you want: a translation with broken syntax fails loudly in development instead of rendering half a sentence in production.
// lang/en/inbox.php
return [
'likes' => '{count, plural, =0 {No likes yet} one {# like} other {# likes}}',
];
function icu(string $key, array $args = []): string
{
return (new MessageFormatter(app()->getLocale(), __($key)))->format($args);
}
icu('inbox.likes', ['count' => 0]); // "No likes yet"
icu('inbox.likes', ['count' => 1]); // "1 like"
icu('inbox.likes', ['count' => 1250]); // "1,250 likes"
Plural, select and selectordinal
Inside a plural block, # prints the number formatted for the locale, so 1250 becomes 1,250 in English and 1 250 in French. Branches named zero, one, two, few, many and other are CLDR categories; branches written as =0 or =1 match exact values and are checked first. The difference matters: in Russian the one category also covers 21, 31 and 101, so wording meant for exactly one item belongs under =1.
offset:1 subtracts one from the number before the category is chosen and # is printed, which is how the classic You and 3 others message is written. select chooses between arbitrary keywords such as a gender or a role, and selectordinal chooses ordinal categories for 1st, 2nd and 3rd. Every plural, select and selectordinal block needs an other branch; without it the pattern does not compile.
$likes = '{count, plural, offset:1 =0 {No one liked this} =1 {You liked this}'
.' one {You and # other liked this} other {You and # others liked this}}';
MessageFormatter::formatMessage('en', $likes, ['count' => 2]); // "You and 1 other liked this"
MessageFormatter::formatMessage('en', $likes, ['count' => 5]); // "You and 4 others liked this"
$place = '{n, selectordinal, one {#st} two {#nd} few {#rd} other {#th}}';
MessageFormatter::formatMessage('en', $place, ['n' => 22]); // "22nd"
$files = '{count, plural, =1 {Один файл} one {# файл} few {# файла} many {# файлов} other {# файла}}';
MessageFormatter::formatMessage('ru', $files, ['count' => 21]); // "21 файл"
Common mistakes
Apostrophes are the classic trap. In ICU syntax, an apostrophe followed by a brace starts a quoted literal, so '{name}' prints {name} instead of the value. A lone apostrophe in ordinary text, as in It's, prints as is, and two apostrophes always produce one. Typographic apostrophes avoid the question entirely.
Missing arguments are silent: forget to pass count and the output contains {count} literally instead of raising an error. Using the same argument as a plural selector and again as plain {count} inside a branch can make formatting fail and return false, so print the number with # inside plural branches.
Finally, ICU strings are easy for translators to break, since one missing brace invalidates the whole message. Validate every ICU value in CI by constructing a MessageFormatter for it in each locale, and give translators an editor that shows the branch structure instead of raw braces.
ICU MessageFormat or trans_choice()?
trans_choice() covers most Laravel apps. Its pipe syntax is compact and the plural rules are built into the framework. Its limits are that forms are positional, with nothing in the string saying which segment is few and which is many, and that it has no equivalent of select for gender or other variants.
ICU names every branch and keeps all the variation inside one string, at the cost of a syntax translators must handle carefully. Teams localizing into Slavic or Arabic languages, or with gendered UI text, are the ones that usually find the switch worth it.