Table of Contents
Table of Contents
- Pluralization Rules Across Languages: A Developer's Guide
- Why Pluralization Matters More Than You Think
- English Plurals: Deceptively Simple
- How Other Languages Handle Plurals
- Arabic: Six Forms
- Russian and Polish: Three to Four Forms with Complex Rules
- Japanese, Chinese, Korean: No Plurals at All
- Other Notable Cases
- The CLDR Plural Rules Standard
- ICU MessageFormat: The Right Abstraction
- Implementing Pluralization with i18next
- Basic Setup
- React Integration
- Handling Multiple CLDR Forms
- Next.js with next-i18next
- Common Pitfalls
- 1. Wrong Variable Name
- 2. Missing Plural Forms for Target Locale
- 3. String Concatenation Instead of Plural Keys
- 4. Hardcoding Ordinals with Plurals
- 5. Treating Zero as a Special Plural Form
- Testing Plural Translations
- Boundary Testing
- Snapshot Tests for Each Locale
- Lint for Missing Forms
- Where Type Safety Changes the Game
- Practical Summary
- Take your app global with better-i18n
Pluralization Rules Across Languages: A Developer's Guide
You've shipped a feature. Translations are in. Everything looks great in English. Then someone files a bug: "The app says '1 items in cart' and '5 item in cart'." You fix it with a quick ternary. Six months later, a user in Poland reports that the app is showing grammatically incorrect text throughout. You didn't break pluralization — you never actually solved it.
Pluralization is one of the most underestimated problems in internationalization. Most developers treat it as a binary English problem: singular vs. plural. But natural languages are far more complex than that, and production i18n is where that complexity bites back hard. This guide walks through how pluralization actually works across languages, how to implement it correctly, and how to avoid the mistakes that slip through code review every time.
If you are new to internationalization concepts more broadly, the guide to localisation and internationalisation provides useful grounding before diving into language-specific pluralization mechanics.
Why Pluralization Matters More Than You Think
Here is a common pattern in codebases:
const label = count === 1 ? 'item' : 'items';
This works for English. For exactly two languages: English and a handful of similar ones. The moment you ship to Turkish, Arabic, Russian, or Polish, this approach produces nonsense — or worse, text that is subtly wrong in ways that feel disrespectful to native speakers.
Pluralization errors in production have real consequences:
- Trust erosion: Native speakers immediately recognize bad grammar. It signals the product wasn't built for them.
- Legal risk: In some locales, quantity expressions in contracts, invoices, or compliance copy must be grammatically correct.
- Accessibility failures: Screen readers and assistive tools depend on grammatically correct text.
The root cause is almost always the same: developers hardcode English plural logic, and then translators get a string with no context about what form to use.
English Plurals: Deceptively Simple
English has two plural forms: singular (1) and plural (everything else). This maps cleanly to a ternary expression, which is why developers default to it.
// English: two forms, singular and plural
`${count} ${count === 1 ? 'file' : 'files'} uploaded`
But even in English, this gets complicated with edge cases:
- Zero: "0 files" reads naturally, but some UIs prefer "No files". This is a design decision, not a translation one — but it still requires plural form support.
- Fractions: "1.5 files" is grammatically ambiguous. English typically uses plural for non-integer values, but this varies by domain.
- Irregular nouns: "1 person" / "2 people", "1 child" / "2 children". These aren't handled by simple suffix rules.
English feels simple because it is, relatively speaking. The moment you leave it, the complexity scales dramatically.
How Other Languages Handle Plurals
Arabic: Six Forms
Arabic has six grammatically distinct plural forms depending on the number:
| Form | Numbers |
|---|---|
| zero | 0 |
| one | 1 |
| two | 2 |
| few | 3–10 |
| many | 11–99 |
| other | 100+ (and decimals) |
Each form requires a different word or suffix. The string "You have X messages" needs six translations in Arabic, not two. Shipping only singular and plural to an Arabic translator is asking them to guess — and they can't, because the message structure differs for each form.
Russian and Polish: Three to Four Forms with Complex Rules
Russian uses three forms: singular (1), few (2–4 and numbers ending in 2–4, except 12–14), and many (everything else).
The rule for Russian is non-trivial:
n % 10 === 1 && n % 100 !== 11 → singular
n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) → few
everything else → many
So: "21 файл" (singular), "22 файла" (few), "25 файлов" (many), "11 файлов" (many — not singular, because 11 is an exception).
Polish adds further complexity with four forms and slightly different boundaries. If you get this wrong, a Russian or Polish user notices immediately. These aren't obscure edge cases — they're numbers that appear in everyday UIs.
Japanese, Chinese, Korean: No Plurals at All
These languages don't grammatically distinguish singular from plural. "1 file" and "100 files" use the same noun form. Instead, quantity is expressed through numerals, counters (classifiers), or context.
This means:
- You should not send plural forms to translators for these languages — you're asking them to translate distinctions that don't exist.
- The number is still displayed, but it does not inflect the noun.
- Incorrect plural handling here usually manifests as translators duplicating the "other" form, which is technically fine but can produce redundant or unnatural phrasing.
Other Notable Cases
- Slavic languages generally (Czech, Slovak, Croatian): Three to four forms, complex modulo rules.
- Welsh: Six forms with highly irregular boundaries.
- Gaelic (Scottish and Irish): Forms that split on 1, 2, 3–10, 11–19, and 20+.
- Hebrew: Distinct forms for singular, dual (exactly 2), and plural.
The CLDR Plural Rules Standard
The Unicode Common Locale Data Repository (CLDR) defines plural rules for every major language. These are the canonical rules used by browsers, operating systems, and i18n libraries. The CLDR categorizes plural forms into six named categories:
zeroonetwofewmanyother
Every language uses some subset of these. English uses one and other. Arabic uses all six. Japanese uses only other.
These rules are available in machine-readable form and are already embedded in most mature i18n libraries. You do not need to implement the math yourself. You do need to understand that these categories exist and that your translation workflow must account for all the forms a given language requires.
ICU MessageFormat: The Right Abstraction
ICU MessageFormat is the most widely supported standard for expressing pluralization in translation strings. It embeds the plural logic inside the message itself, so translators can handle each form independently.
The syntax for English:
{count, plural,
one {# file uploaded}
other {# files uploaded}
}
For Russian (as supplied by a translator who knows the language):
{count, plural,
one {Загружен # файл}
few {Загружено # файла}
many {Загружено # файлов}
other {Загружено # файлов}
}
The # is replaced by the actual number. The library evaluates the plural rule for the active locale and picks the right form.
This approach has critical advantages over string concatenation:
- Each form is a complete sentence, so translators have full grammatical context.
- No runtime string assembly — the message is resolved to a final string before display.
- Language-appropriate forms — translators provide exactly the forms their language needs.
- Tooling support — linters, extraction tools, and translation platforms understand this format.
Implementing Pluralization with i18next
i18next is one of the most widely used i18n libraries in the JavaScript ecosystem, and it handles CLDR plural rules natively.
Basic Setup
import i18next from 'i18next';
i18next.init({
lng: 'en',
resources: {
en: {
translation: {
file_count: '{{count}} file',
file_count_other: '{{count}} files',
},
},
},
});
i18next uses a key suffix convention: key for the singular form, key_other for the default plural, and key_zero, key_one, key_two, key_few, key_many for additional CLDR forms. You pass count as the interpolation variable, and the library selects the right key automatically.
i18next.t('file_count', { count: 1 }); // "1 file"
i18next.t('file_count', { count: 5 }); // "5 files"
React Integration
With react-i18next:
import { useTranslation } from 'react-i18next';
function FileCount({ count }: { count: number }) {
const { t } = useTranslation();
return <span>{t('file_count', { count })}</span>;
}
For /i18n/react setups, this is the recommended pattern. The count variable drives both interpolation (displaying the number) and plural form selection.
Handling Multiple CLDR Forms
For Russian, your translation file needs all required forms:
{
"file_count_one": "{{count}} файл",
"file_count_few": "{{count}} файла",
"file_count_many": "{{count}} файлов",
"file_count_other": "{{count}} файлов"
}
i18next applies the correct CLDR rule for the locale and picks the right key. The rule evaluation is built in — you don't write the modulo logic yourself.
Next.js with next-i18next
For /i18n/nextjs apps using next-i18next, the pattern is the same but translations live in public/locales/{lng}/{ns}.json:
// public/locales/ru/common.json
{
"file_count_one": "{{count}} файл",
"file_count_few": "{{count}} файла",
"file_count_many": "{{count}} файлов",
"file_count_other": "{{count}} файлов"
}
import { useTranslation } from 'next-i18next';
export function FileCount({ count }: { count: number }) {
const { t } = useTranslation('common');
return <p>{t('file_count', { count })}</p>;
}
Common Pitfalls
1. Wrong Variable Name
i18next uses count specifically to trigger plural form selection. Using any other variable name skips the plural logic entirely:
// WRONG — plural selection will not trigger
t('file_count', { number: 5 });
// CORRECT
t('file_count', { count: 5 });
This is a silent failure. The fallback form (_other) is used, so English may appear fine while other languages silently break.
2. Missing Plural Forms for Target Locale
If you define only key and key_other for a Russian locale, the library falls back to key_other for all forms. Russian users get grammatically incorrect text and there is no error in the console. This is the most common pluralization bug in shipped software.
The fix is to require all CLDR forms for each locale before shipping. Automate this check — do not rely on manual review.
3. String Concatenation Instead of Plural Keys
// WRONG
const message = t('you_have') + ' ' + count + ' ' + t('messages');
// CORRECT — count and surrounding words are one translatable unit
t('you_have_messages', { count });
Concatenation makes it structurally impossible to handle languages where word order, noun form, or verb agreement change with the count. The entire phrase containing the count must be a single translation key.
4. Hardcoding Ordinals with Plurals
Ordinal numbers ("1st", "2nd", "3rd") follow completely different rules than cardinal plurals. Do not conflate them. i18next has separate ordinal support via the ordinal: true option:
t('position', { count: 1, ordinal: true }); // "1st place"
t('position', { count: 2, ordinal: true }); // "2nd place"
Ordinal rules are also locale-specific — French, for example, uses "1er" for first and then the same suffix for all subsequent ordinals.
5. Treating Zero as a Special Plural Form
Some designs call for "No files" instead of "0 files". This is not a plural form — it is a UI decision. Handle it explicitly in code before calling the translation function, or use a separate translation key:
const key = count === 0 ? 'no_files' : 'file_count';
t(key, { count });
Do not rely on the zero CLDR form for UI copy decisions. The zero form exists for languages that grammatically distinguish zero from other values, not as a design hook.
Testing Plural Translations
Plural handling is notoriously undertested. A few patterns that catch most bugs. For a broader look at how to structure your i18n quality process, the guide to i18n testing tools and automation strategies covers tooling that can be integrated alongside these patterns.
Boundary Testing
Test every boundary value for the locales you ship:
const testCounts = [0, 1, 2, 3, 4, 5, 11, 12, 21, 22, 100, 101];
for (const count of testCounts) {
console.log(`${count}: ${t('file_count', { count })}`);
}
For Russian specifically: 1, 2, 5, 11, 12, 21, 22, 25, 100, 101, 111, 121.
Snapshot Tests for Each Locale
describe('file_count pluralization', () => {
it.each([
['en', 1, '1 file'],
['en', 5, '5 files'],
['ru', 1, '1 файл'],
['ru', 2, '2 файла'],
['ru', 5, '5 файлов'],
['ru', 11, '11 файлов'],
['ru', 21, '21 файл'],
['ar', 1, '1 ملف'],
['ar', 3, '3 ملفات'],
])('locale %s, count %d → %s', (lng, count, expected) => {
i18next.changeLanguage(lng);
expect(i18next.t('file_count', { count })).toBe(expected);
});
});
This catches missing forms, wrong CLDR boundaries, and translator errors before they reach users.
Lint for Missing Forms
Write a script that reads your source locale keys, identifies all plural keys (those ending in _other), and then checks every target locale for the CLDR forms required by that locale. Fail CI if forms are missing. This prevents the silent-fallback class of bugs entirely.
Where Type Safety Changes the Game
One weakness of key-string-based i18n is that nothing enforces correct usage at the call site. You can pass number instead of count, omit the count entirely, or reference a key that doesn't exist — and all of these fail silently at runtime.
Type-safe i18n tools generate TypeScript types from your translation keys, so the compiler catches:
- Missing required interpolation variables
- Using the wrong variable name (
numbervscount) - Referencing non-existent keys
Better i18n builds on this pattern with SDKs that understand your translation schema and enforce plural completeness at publish time. When you add a new plural key, the SDK generates updated types — and any call site that doesn't supply the required count variable becomes a type error. For teams managing dozens of locales with hundreds of keys, this catches a whole class of pluralization bugs before code review.
The platform's features that matter most for pluralization are: enforced form completeness (all required CLDR forms must be present before publish), AI translation that understands context ("this is a file count, use the appropriate plural form"), and glossary enforcement so domain-specific nouns are inflected consistently.
Pluralization correctness is also deeply tied to a well-structured website translation and localization workflow — when plural forms are collected and reviewed as part of the same process as the rest of the translation, they are far less likely to slip through incomplete.
Practical Summary
Pluralization done correctly requires:
Use ICU MessageFormat or a library that implements CLDR rules. Do not write your own plural logic.
Always use
countas the interpolation variable in i18next. Other names bypass plural selection.Provide all CLDR forms for each locale. Check what forms a language needs before sending strings to translators.
Never concatenate strings with counts. The entire phrase is one translation key.
Test boundary values for each locale. Especially Russian, Polish, Arabic, and other languages with complex rules.
Enforce form completeness in CI. Missing plural forms cause silent fallbacks that are invisible in English testing.
Distinguish ordinals from cardinals. They follow different rules and need separate handling.
The tools exist to get this right. The CLDR rules are standardized. The libraries implement them. The failure mode in most codebases is not technical — it is assuming English plural logic generalizes, and not building the workflow to catch when it doesn't.
Take your app global with better-i18n
better-i18n combines AI-powered translations, git-native workflows, and global CDN delivery into one developer-first platform. Stop managing spreadsheets and start shipping in every language.