Pluralisation Rules Across Languages
debt(d7/e5/b5/t9)
Closest to 'only careful code review or runtime testing' (d7). The detection_hints indicate automated detection is 'no', and while phpstan and php-intl are listed as tools, they don't automatically flag the hardcoded English ternary pattern (`$count === 1 ? 'item' : 'items'`) as a pluralisation bug. The issue only surfaces when a Russian or Arabic user encounters garbled output — it won't crash or warn, it just silently produces wrong text for non-English locales.
Closest to 'touches multiple files / significant refactor in one component' (e5). The quick_fix says to switch to ICU MessageFormat, but the common_mistakes reveal this isn't a one-liner: every pluralised string across the codebase needs to be identified, all locale translation files need plural-form variants added (e.g. Russian needs 3 forms per string), and helper functions with hardcoded English rules must be replaced. This touches translation files, code call-sites, and potentially a pluralisation helper — spanning multiple files but typically within the i18n layer rather than a full cross-cutting refactor.
Closest to 'persistent productivity tax' (b5). The applies_to covers both web and cli contexts, meaning any developer adding a new user-facing string must understand plural form requirements. Without a correct ICU/ngettext foundation, every new translatable string with a count carries the risk of being done wrong for non-English locales, slowing down internationalisation work across the codebase. It's not quite b7 (it doesn't reshape every change) but it does persistently tax multiple work streams.
Closest to 'catastrophic trap — the obvious way is always wrong' (t9). The misconception is explicitly: 'All languages have two plural forms like English.' The common_mistakes confirm the ternary `$count === 1 ? 'item' : 'items'` is the natural, obvious solution every PHP developer reaches for — and it is silently wrong for Polish (4 forms), Arabic (6 forms), Russian (3 forms), and returns no error or warning whatsoever. The 'obvious' approach works for English and fails globally, making this a textbook t9 trap.
Also Known As
TL;DR
Explanation
English pluralisation (1 item / 2 items) is deceptively simple. Russian has three plural forms based on the last two digits. Arabic has six forms. Polish has four. Slavic languages distinguish: 1, 2-4, 5+. ICU MessageFormat (used by PHP Intl extension's MessageFormatter) handles this universally: {count, plural, one {# item} other {# items}}. gettext uses ngettext() with locale-specific plural form rules. Never write if ($count === 1) 'item' else 'items' for an i18n app — it breaks every non-English locale.
Common Misconception
Why It Matters
Common Mistakes
- Ternary pluralisation: $count === 1 ? 'item' : 'items' — breaks for Russian, Arabic, Polish.
- Not using ngettext() for gettext plurals — gettext() alone doesn't handle plural forms.
- Hardcoding English plural rule in a helper function shared across locales.
- Not including plural strings for all required forms — Russian needs 3 translations per string.
Code Examples
// Hardcoded English — wrong for most languages:
function formatCount(int $count, string $noun): string {
return $count . ' ' . ($count === 1 ? $noun : $noun . 's');
}
echo formatCount(5, 'element'); // '5 elements' in English
// Russian: wrong — needs locale-specific plural form logic
// PHP Intl MessageFormatter — handles all locales:
$pattern = '{count, plural, one {# item} other {# items}}';
$fmt = new MessageFormatter('en_US', $pattern);
echo $fmt->format(['count' => 5]); // '5 items'
// gettext plural:
ngettext('%d item', '%d items', $count);
// Laravel trans_choice with locale files:
// en.json: { "items": "{1} :count item|[2,*] :count items" }
// ru.json: { "items": "{1} :count элемент|[2,4] :count элемента|[5,*] :count элементов" }