Understanding CLDR Plural Rules
This Unicode CLDR plural rules overview explains how software decides whether a number maps to one, few, many, or another plural category before a translated message is rendered.
The Common Locale Data Repository (CLDR) is a Unicode project that provides standardized locale data for software internationalization. One of its most critical components is the plural rules system, which defines how different languages handle pluralization. Unlike English, which typically has just singular and plural forms, many languages have complex plural systems that require special handling. CLDR plural rules provide a standardized framework for handling these variations across 200+ languages, ensuring that your application displays grammatically correct messages regardless of the user's locale. The system defines six plural categories—zero, one, two, few, many, and other—which are selectively used by different languages based on their grammatical rules. Understanding these rules is essential for building truly internationalized applications that feel native to users worldwide, avoiding embarrassing grammatical errors like "1 items" or "5 fichier" that immediately signal poor localization quality.
The Six Plural Categories Explained
CLDR defines six distinct plural categories that languages can use, though no single language uses all six. The zero category is used by languages like Arabic when the count is exactly zero (e.g., "لا كتب" meaning "no books"). The one category applies to singular forms in most languages but has special rules in Slavic languages where it might apply to numbers ending in 1 but not 11. The two category is used by languages with a dual form, such as Arabic and Slovenian, when referring to exactly two items.
The few category is employed by languages like Polish, Russian, and Arabic for small quantities (typically 2-4 in Slavic languages, or 3-10 in Arabic). The many category covers larger quantities in languages like Polish (numbers ending in 2-4 except 12-14) or Arabic (11-99). Finally, the other category serves as the default fallback and is used for all numbers that don't fit the other categories—this is the only category guaranteed to exist in every language.
Practical Category Usage Examples
TypeScript1// English uses only 'one' and 'other' 2const englishRules = { 3 one: 'n = 1', // 1 item 4 other: '...' // 0, 2, 3, 4, 5, ... items 5}; 6 7// Arabic uses all six categories 8const arabicRules = { 9 zero: 'n = 0', // لا عناصر (no items) 10 one: 'n = 1', // عنصر واحد (one item) 11 two: 'n = 2', // عنصران (two items) 12 few: 'n % 100 = 3..10', // ٣ عناصر (3-10 items) 13 many: 'n % 100 = 11..99',// ١١ عنصرا (11-99 items) 14 other: '...' // ١٠٠ عنصر (100+ items) 15}; 16 17// Polish uses 'one', 'few', 'many', and 'other' 18const polishRules = { 19 one: 'n = 1', // 1 plik 20 few: 'n % 10 = 2..4 AND n % 100 != 12..14', // 2-4 pliki 21 many: 'n % 10 = 0..1 OR n % 10 = 5..9 OR n % 100 = 12..14', // 5-21 plików 22 other: '...' // 1.5 pliku (fractions) 23};
Language-Specific Plural Rules
Different languages have vastly different plural systems. English is one of the simplest, using only two categories: "one" for n=1 ("1 file") and "other" for everything else ("0 files", "2 files", "1.5 files"). Japanese, Chinese, Korean, and Vietnamese have even simpler rules—they use only the "other" category for all numbers, as these languages don't grammatically distinguish between singular and plural forms.
Arabic represents the most complex system, using all six categories. Numbers follow intricate rules: 0 uses "zero", 1 uses "one", 2 uses "two", 3-10 use "few", 11-99 use "many", and 100+ use "other". Additionally, the noun form changes with each category, making proper localization crucial.
Russian and Polish use four categories with rules based on the last digits of numbers. In Russian, numbers ending in 1 (except 11) use "one" (1 файл, 21 файл), numbers ending in 2-4 (except 12-14) use "few" (2 файла, 23 файла), and everything else uses "many" (5 файлов, 11 файлов). French has a unique rule where both 0 and 1 use the "one" category ("0 fichier", "1 fichier"), while all other numbers use "other" ("2 fichiers").
Comparison Table: Plural Rules by Language
| Language | Categories Used | Example Rules | Complexity |
|---|---|---|---|
| English | one, other | n=1 → one | ★☆☆☆☆ |
| Japanese | other | all → other | ☆☆☆☆☆ |
| French | one, other | n=0,1 → one | ★☆☆☆☆ |
| Russian | one, few, many, other | n%10=1 AND n%100≠11 → one | ★★★★☆ |
| Polish | one, few, many, other | Similar to Russian | ★★★★☆ |
| Arabic | zero, one, two, few, many, other | n=0 → zero, n=1 → one, n=2 → two | ★★★★★ |
| Welsh | zero, one, two, few, many, other | n=0 → zero, n=1 → one | ★★★★☆ |
| Slovenian | one, two, few, other | n%100=1 → one, n%100=2 → two | ★★★☆☆ |
ICU MessageFormat Implementation
ICU MessageFormat is the industry standard for implementing CLDR plural rules in your applications. It provides a powerful syntax for handling plurals, gender, and other locale-specific formatting. The basic plural syntax uses the {variable, plural, ...} pattern:
JavaScript1// Basic ICU MessageFormat plural example 2const message = '{count, plural, one {# file} other {# files}}'; 3 4// With Intl.MessageFormat (or similar library) 5import IntlMessageFormat from 'intl-messageformat'; 6 7const formatter = new IntlMessageFormat(message, 'en'); 8console.log(formatter.format({ count: 1 })); // "1 file" 9console.log(formatter.format({ count: 5 })); // "5 files" 10 11// Arabic example using all categories 12const arabicMessage = `{count, plural, 13 zero {لا عناصر} 14 one {عنصر واحد} 15 two {عنصران} 16 few {# عناصر} 17 many {# عنصرا} 18 other {# عنصر} 19}`; 20 21const arabicFormatter = new IntlMessageFormat(arabicMessage, 'ar'); 22console.log(arabicFormatter.format({ count: 0 })); // "لا عناصر" 23console.log(arabicFormatter.format({ count: 1 })); // "عنصر واحد" 24console.log(arabicFormatter.format({ count: 2 })); // "عنصران" 25console.log(arabicFormatter.format({ count: 5 })); // "٥ عناصر"
Advanced ICU Features
ICU MessageFormat supports offset for handling patterns like "you and 2 others":
JavaScript1const message = `{count, plural, offset:1 2 =0 {No followers} 3 =1 {Just you} 4 one {You and # other person} 5 other {You and # other people} 6}`; 7 8const formatter = new IntlMessageFormat(message, 'en'); 9console.log(formatter.format({ count: 0 })); // "No followers" 10console.log(formatter.format({ count: 1 })); // "Just you" 11console.log(formatter.format({ count: 2 })); // "You and 1 other person" 12console.log(formatter.format({ count: 5 })); // "You and 4 other people"
You can also combine plurals with select for complex scenarios:
JavaScript1const message = `{gender, select, 2 male {{count, plural, one {He has # item} other {He has # items}}} 3 female {{count, plural, one {She has # item} other {She has # items}}} 4 other {{count, plural, one {They have # item} other {They have # items}}} 5}`;
Library Support for CLDR Plurals
JavaScript/TypeScript Libraries
FormatJS (formerly React Intl) provides comprehensive ICU MessageFormat support with excellent TypeScript types and React integration:
TypeScript1import { IntlProvider, FormattedMessage } from 'react-intl'; 2 3const messages = { 4 itemCount: '{count, plural, one {# item} other {# items}}' 5}; 6 7function App() { 8 return ( 9 <IntlProvider locale="en" messages={messages}> 10 <FormattedMessage id="itemCount" values={{ count: 5 }} /> 11 </IntlProvider> 12 ); 13}
i18next supports plurals through a simpler key-based syntax:
TypeScript1import i18next from 'i18next'; 2 3i18next.init({ 4 lng: 'en', 5 resources: { 6 en: { 7 translation: { 8 'item_one': '{{count}} item', 9 'item_other': '{{count}} items' 10 } 11 } 12 } 13}); 14 15i18next.t('item', { count: 1 }); // "1 item" 16i18next.t('item', { count: 5 }); // "5 items"
Fluent (Mozilla's localization system) uses a different syntax but follows CLDR rules:
FLUENT1items = { $count -> 2 [one] { $count } item 3 *[other] { $count } items 4}
Backend Library Support
Go uses the golang.org/x/text package:
GO1import "golang.org/x/text/message" 2 3p := message.NewPrinter(language.English) 4p.Printf("%d files 5", plural.Selectf(1, "", 6 plural.One, "file", 7 plural.Other, "files", 8))
Java uses ICU4J:
JAVA1MessageFormat mf = new MessageFormat( 2 "{count, plural, one {# file} other {# files}}", 3 Locale.ENGLISH 4); 5System.out.println(mf.format(new Object[]{5}));
Python uses the babel library:
Python1from babel.plural import PluralRule 2 3rule = PluralRule({'one': 'n is 1'}) 4print(rule(1)) # 'one' 5print(rule(5)) # 'other'
Common Mistakes and Pitfalls
Mistake 1: Hardcoding Plural Logic
Never write conditional logic for plurals in your code:
TypeScript1// ❌ BAD: Hardcoded English-only logic 2const message = count === 1 ? `${count} item` : `${count} items`; 3 4// ❌ BAD: Incomplete logic that breaks in other languages 5const message = count === 0 ? 'no items' 6 : count === 1 ? '1 item' 7 : `${count} items`; 8 9// ✅ GOOD: Use ICU MessageFormat 10const message = formatMessage( 11 { id: 'itemCount', defaultMessage: '{count, plural, one {# item} other {# items}}' }, 12 { count } 13);
Mistake 2: Forgetting Zero Handling
Some languages have specific rules for zero that differ from other plural forms:
TypeScript1// ❌ BAD: Missing zero case for Arabic 2const message = `{count, plural, 3 one {عنصر واحد} 4 other {# عناصر} 5}`; 6 7// ✅ GOOD: Include zero for languages that need it 8const message = `{count, plural, 9 zero {لا عناصر} 10 one {عنصر واحد} 11 two {عنصران} 12 few {# عناصر} 13 many {# عنصرا} 14 other {# عنصر} 15}`;
Mistake 3: Using Plural Forms in Wrong Context
Don't use plural rules for non-countable nouns or when the number isn't displayed:
TypeScript1// ❌ BAD: Plural for non-countable 2const message = '{count, plural, one {information} other {informations}}'; // "informations" is wrong 3 4// ❌ BAD: Plural without number 5const message = '{count, plural, one {Delete item?} other {Delete items?}}'; // Missing count 6 7// ✅ GOOD: Include the count 8const message = '{count, plural, one {Delete # item?} other {Delete # items?}}';
Mistake 4: Not Testing All Plural Forms
Many developers only test with 0, 1, and 2, missing edge cases:
TypeScript1// Test these numbers for thorough coverage 2const testNumbers = [0, 1, 2, 3, 5, 11, 21, 22, 25, 100, 101, 1.5]; 3 4testNumbers.forEach(count => { 5 console.log(count, formatMessage({ id: 'items' }, { count })); 6});
Testing Plural Rules
Unit Testing Approach
Create comprehensive test suites that cover all plural categories:
TypeScript1import { IntlProvider } from 'react-intl'; 2import { render } from '@testing-library/react'; 3 4describe('Plural Rules', () => { 5 const messages = { 6 items: '{count, plural, one {# item} other {# items}}' 7 }; 8 9 it('handles singular correctly', () => { 10 const { getByText } = render( 11 <IntlProvider locale="en" messages={messages}> 12 <FormattedMessage id="items" values={{ count: 1 }} /> 13 </IntlProvider> 14 ); 15 expect(getByText('1 item')).toBeInTheDocument(); 16 }); 17 18 it('handles plural correctly', () => { 19 const { getByText } = render( 20 <IntlProvider locale="en" messages={messages}> 21 <FormattedMessage id="items" values={{ count: 5 }} /> 22 </IntlProvider> 23 ); 24 expect(getByText('5 items')).toBeInTheDocument(); 25 }); 26 27 it('handles zero correctly', () => { 28 const { getByText } = render( 29 <IntlProvider locale="en" messages={messages}> 30 <FormattedMessage id="items" values={{ count: 0 }} /> 31 </IntlProvider> 32 ); 33 expect(getByText('0 items')).toBeInTheDocument(); 34 }); 35});
Testing Multiple Locales
Test plural rules across different languages to catch locale-specific issues:
TypeScript1describe('Russian Plural Rules', () => { 2 const messages = { 3 ru: { 4 files: '{count, plural, one {# файл} few {# файла} many {# файлов} other {# файла}}' 5 } 6 }; 7 8 const testCases = [ 9 { count: 1, expected: '1 файл' }, // one 10 { count: 2, expected: '2 файла' }, // few 11 { count: 5, expected: '5 файлов' }, // many 12 { count: 21, expected: '21 файл' }, // one (ends in 1, not 11) 13 { count: 22, expected: '22 файла' }, // few (ends in 2, not 12) 14 { count: 25, expected: '25 файлов' }, // many 15 { count: 1.5, expected: '1.5 файла' } // other (fraction) 16 ]; 17 18 testCases.forEach(({ count, expected }) => { 19 it(`formats ${count} correctly`, () => { 20 const { getByText } = render( 21 <IntlProvider locale="ru" messages={messages.ru}> 22 <FormattedMessage id="files" values={{ count }} /> 23 </IntlProvider> 24 ); 25 expect(getByText(expected)).toBeInTheDocument(); 26 }); 27 }); 28});
Visual Testing with IntlPull
IntlPull provides a built-in plural preview feature that lets you visualize how your plural strings will appear across all categories and test numbers. This helps catch issues before they reach production:
TypeScript1// In your IntlPull dashboard, the plural preview shows: 2// count=0: "0 files" 3// count=1: "1 file" 4// count=2: "2 files" 5// count=5: "5 files" 6// count=21: "21 files" 7 8// For Russian, it shows all forms: 9// count=1: "1 файл" (one) 10// count=2: "2 файла" (few) 11// count=5: "5 файлов" (many) 12// count=21: "21 файл" (one)
Ordinal Plurals
CLDR also defines ordinal plural rules for numbers used in ordering (1st, 2nd, 3rd). These follow different rules than cardinal plurals:
TypeScript1// English ordinal rules 2const ordinalMessage = `{count, selectordinal, 3 one {#st} 4 two {#nd} 5 few {#rd} 6 other {#th} 7}`; 8 9// Examples: 10// 1 → "1st" 11// 2 → "2nd" 12// 3 → "3rd" 13// 4 → "4th" 14// 21 → "21st" 15// 22 → "22nd" 16// 23 → "23rd" 17 18// Full implementation 19import { IntlMessageFormat } from 'intl-messageformat'; 20 21const formatter = new IntlMessageFormat( 22 'You finished {place, selectordinal, one {#st} two {#nd} few {#rd} other {#th}}!', 23 'en' 24); 25 26console.log(formatter.format({ place: 1 })); // "You finished 1st!" 27console.log(formatter.format({ place: 22 })); // "You finished 22nd!"
Range Pluralization
Some scenarios require pluralizing ranges of numbers:
TypeScript1const rangeMessage = `{start}-{end} of {total, plural, 2 one {# item} 3 other {# items} 4}`; 5 6// Usage 7formatMessage(rangeMessage, { start: 1, end: 10, total: 100 }); 8// "1-10 of 100 items" 9 10formatMessage(rangeMessage, { start: 1, end: 1, total: 1 }); 11// "1-1 of 1 item"
Performance Considerations
CLDR plural rule evaluation is fast, but there are optimization techniques for high-volume scenarios:
TypeScript1// ❌ SLOW: Creating formatter on every call 2function formatItems(count: number) { 3 const formatter = new IntlMessageFormat( 4 '{count, plural, one {# item} other {# items}}', 5 'en' 6 ); 7 return formatter.format({ count }); 8} 9 10// ✅ FAST: Cache formatters 11const formatters = new Map(); 12 13function getFormatter(locale: string, message: string) { 14 const key = `${locale}:${message}`; 15 if (!formatters.has(key)) { 16 formatters.set(key, new IntlMessageFormat(message, locale)); 17 } 18 return formatters.get(key); 19} 20 21function formatItems(count: number, locale: string) { 22 const formatter = getFormatter( 23 locale, 24 '{count, plural, one {# item} other {# items}}' 25 ); 26 return formatter.format({ count }); 27}
Integration with Translation Management
When using a TMS like IntlPull, plural strings should be stored as single keys with ICU MessageFormat syntax:
JSON1{ 2 "itemCount": "{count, plural, one {# item} other {# items}}", 3 "deleteConfirm": "{count, plural, one {Delete # file?} other {Delete # files?}}", 4 "followers": "{count, plural, offset:1 =0 {No followers} =1 {Just you} one {You and # other} other {You and # others}}" 5}
IntlPull automatically detects ICU MessageFormat syntax and provides:
- Visual preview of all plural forms
- Warnings if translations are missing required plural categories
- Context screenshots for each plural variation
- Validation that plural syntax is correct across all languages
FAQ
Q: Do I need to provide all six plural categories for every language? A: No. Each language uses only a subset of the six categories. English uses only "one" and "other", while Arabic uses all six. Your translation management system should guide translators on which categories are required for their language.
Q: What happens if I'm missing a required plural category? A: Most libraries will fall back to the "other" category, which is required for all languages. However, this will result in grammatically incorrect text. Good TMS tools like IntlPull will warn you when required categories are missing.
Q: Can I use CLDR plural rules with template literals?
A: No. Template literals like ${count} item${count === 1 ? '' : 's'} only work for English. Use ICU MessageFormat or a similar library that implements CLDR rules for proper internationalization.
Q: How do I handle plurals for languages I don't speak? A: Use the CLDR plural rules specification and rely on native speakers for translation. Tools like IntlPull show translators the required plural forms automatically, so they can provide correct translations for each category.
Q: Are there any languages that don't use plurals? A: Yes. Japanese, Chinese, Korean, Vietnamese, and several other Asian languages don't grammatically distinguish between singular and plural, so they use only the "other" category for all counts.
Q: How do I test plural rules during development? A: Use test numbers that cover all categories: 0, 1, 2, 3, 5, 11, 21, 22, 100, 101, and 1.5. This ensures you catch edge cases like Russian numbers ending in 1 (but not 11) and fractions.
Q: Should I use ordinal or cardinal plurals for counts? A: Use cardinal plurals (one/few/many/other) for quantities ("5 files") and ordinal plurals (1st/2nd/3rd) for ordering ("finished 1st"). They follow different rules in many languages.
