ICU Message Format
debt(d7/e3/b5/t5)
Closest to 'only careful code review or runtime testing' (d7). The detection_hints.tools field is not specified. The common mistakes — string concatenation instead of placeholders, missing ext-intl, mixing ICU with sprintf — are not caught by default linters or compilers. Missing ext-intl may throw a runtime error, but incorrect usage patterns (concatenation, mixed formats) only surface during runtime testing or careful review. No standard PHP linter rule targets ICU format misuse by default.
Closest to 'simple parameterised fix' (e3). The quick_fix describes enabling ext-intl and switching to MessageFormatter::formatMessage or a framework wrapper. This is more than a one-line patch — it requires replacing concatenation patterns and potentially updating composer.json — but it stays within a localised component or translation layer rather than spanning the whole codebase.
Closest to 'persistent productivity tax' (b5). The choice applies to web and cli contexts broadly. Adopting ICU format means every developer touching the i18n layer must understand ICU syntax, every translator must receive ICU-formatted strings, and the ext-intl dependency must be tracked. It's not architectural (b7+), but it does impose a consistent ongoing cost across multiple work streams touching translations.
Closest to 'notable trap — a documented gotcha most devs eventually learn' (t5). The misconception field explicitly states that developers believe ICU is only for plurals, when it also handles gender (select), choice-based text, and contextual formatting. Additionally, mixing ICU with sprintf-style placeholders is an incompatibility trap many developers hit. These are documented gotchas that intermediate developers encounter but don't predict on first use.
Also Known As
TL;DR
Explanation
ICU Message Format is a syntax developed by IBM's International Components for Unicode project and now maintained by the Unicode Consortium. It enables translators to express all variations of a message in a single string, rather than requiring multiple string keys for different grammatical forms. The syntax supports: plural (count-based forms), select (choice-based forms like gender), number (locale-aware number formatting), date, time, and spellout formatters. A message like 'You have {count, plural, one {# unread message} other {# unread messages}}' is a single translation key that a translator handles in full context. PHP's intl extension provides MessageFormatter::formatMessage(); Symfony's Translation and Laravel's localisation system both have ICU support. The alternative — multiple string keys like 'message_singular' and 'message_plural' — fragments context and forces translators to guess the surrounding UI.
Common Misconception
Why It Matters
Common Mistakes
- Using string concatenation instead of placeholders — 'You have ' . $count . ' messages' cannot be correctly translated in languages where word order changes.
- Not installing the intl PHP extension — MessageFormatter requires ext-intl, which must be listed as a composer.json require.
- Mixing ICU format with sprintf-style placeholders — they are incompatible; choose one approach consistently.
- Providing ICU messages only in English and expecting translators to infer the correct plural forms — always provide explicit plural categories for the source language.
Code Examples
// Fragmented strings + concatenation — untranslatable
$msg = 'You have ' . $count . ' ' . ($count === 1 ? 'message' : 'messages');
// Translator gets 3 separate strings with no context
// Word order fixed — wrong for Japanese, Arabic, German
// ICU format — full context, all variations in one string
$pattern = '{count, plural,
=0 {You have no messages.}
one {You have one message.}
other {You have {count} messages.}
}';
$result = MessageFormatter::formatMessage('en_US', $pattern, ['count' => $count]);
// With gender select
$pattern = '{gender, select, female {She} male {He} other {They}} submitted the form.';
$result = MessageFormatter::formatMessage($locale, $pattern, ['gender' => $userGender]);