Intl.PluralRules API
How to handle plural forms correctly in JavaScript
Introduction
Pluralization is the process of displaying different text based on a count. In English, you might show "1 item" for a single item and "2 items" for multiple items. Most developers handle this with a simple conditional that adds an "s" for counts other than one.
This approach breaks for languages other than English. Polish uses different forms for 1, 2-4, and 5 or more. Arabic has forms for zero, one, two, few, and many. Welsh has six distinct forms. Even staying in English, irregular plurals like "person" to "people" require special handling.
The Intl.PluralRules API solves this by providing the plural form category for any number in any language. You provide a count, and the API tells you which form to use based on the rules of the target language. This lets you write internationalization-ready code that works correctly across languages without manually encoding language-specific rules.
How languages handle plural forms
Languages differ widely in how they express quantity. English has two forms: singular for one, plural for everything else. This seems straightforward until you encounter languages with different systems.
Russian and Polish use three forms. The singular applies to one item. A special form applies to counts ending in 2, 3, or 4 (but not 12, 13, or 14). All other counts use a third form.
Arabic uses six forms: zero, one, two, few (3-10), many (11-99), and other (100+). Welsh also has six forms with different numeric boundaries.
Some languages like Chinese and Japanese do not distinguish between singular and plural at all. The same form works for any count.
The Intl.PluralRules API abstracts these differences using standardized category names based on Unicode CLDR plural rules. The six categories are: zero, one, two, few, many, and other. Not every language uses all six categories. English only uses one and other. Arabic uses all six.
Create a PluralRules instance for a locale
The Intl.PluralRules constructor takes a locale identifier and returns an object that can determine which plural category applies to a given number.
const enRules = new Intl.PluralRules('en-US');
Create one instance per locale and reuse it. Constructing a new instance for every pluralization is wasteful. Store the instance in a variable or use a caching mechanism.
The default type is cardinal, which handles counting objects. You can also create rules for ordinal numbers by passing an options object.
const enOrdinalRules = new Intl.PluralRules('en-US', { type: 'ordinal' });
Cardinal rules apply to counts like "1 apple, 2 apples". Ordinal rules apply to positions like "1st place, 2nd place".
Use select() to get the plural category for a number
The select() method takes a number and returns which plural category it belongs to in the target language.
const enRules = new Intl.PluralRules('en-US');
enRules.select(0); // 'other'
enRules.select(1); // 'one'
enRules.select(2); // 'other'
enRules.select(5); // 'other'
The return value is always one of the six category names: zero, one, two, few, many, or other. English only returns one and other because those are the only forms English uses.
For Arabic, which has more complex rules, you see all six categories in use:
const arRules = new Intl.PluralRules('ar-EG');
arRules.select(0); // 'zero'
arRules.select(1); // 'one'
arRules.select(2); // 'two'
arRules.select(6); // 'few'
arRules.select(18); // 'many'
arRules.select(100); // 'other'
Map categories to localized strings
The API only tells you which category applies. You provide the actual text for each category. Store the text forms in a Map or object, keyed by category name.
const enRules = new Intl.PluralRules('en-US');
const enForms = new Map([
['one', 'item'],
['other', 'items'],
]);
function formatItems(count) {
const category = enRules.select(count);
const form = enForms.get(category);
return `${count} ${form}`;
}
formatItems(1); // '1 item'
formatItems(5); // '5 items'
This pattern separates logic from data. The PluralRules instance handles the rules. The Map holds the translations. The function combines them.
For languages with more categories, add more entries to the Map:
const arRules = new Intl.PluralRules('ar-EG');
const arForms = new Map([
['zero', 'عناصر'],
['one', 'عنصر واحد'],
['two', 'عنصران'],
['few', 'عناصر'],
['many', 'عنصرًا'],
['other', 'عنصر'],
]);
function formatItems(count) {
const category = arRules.select(count);
const form = arForms.get(category);
return `${count} ${form}`;
}
Always provide entries for every category the language uses. Missing categories cause undefined lookups. If you are unsure which categories a language uses, check the Unicode CLDR plural rules or test with the API across different numbers.
Handle decimal and fractional counts
The select() method works with decimal numbers. English treats decimals as plural, even for values between 0 and 2.
const enRules = new Intl.PluralRules('en-US');
enRules.select(1); // 'one'
enRules.select(1.0); // 'one'
enRules.select(1.5); // 'other'
enRules.select(0.5); // 'other'
Other languages have different rules for decimals. Some treat any decimal as plural, while others use more nuanced rules based on the fractional part.
If your UI displays fractional quantities like "1.5 GB" or "2.7 miles", pass the fractional number directly to select(). Do not round first unless your UI rounds the display value.
Format ordinal numbers like 1st, 2nd, 3rd
Ordinal numbers indicate position or rank. English forms ordinals by adding suffixes: 1st, 2nd, 3rd, 4th. The pattern is not simply "add th" because 1, 2, and 3 have special forms, and numbers ending in 1, 2, or 3 follow special rules (21st, 22nd, 23rd) except when ending in 11, 12, or 13 (11th, 12th, 13th).
The Intl.PluralRules API handles these rules when you specify type: 'ordinal'.
const enOrdinalRules = new Intl.PluralRules('en-US', { type: 'ordinal' });
enOrdinalRules.select(1); // 'one'
enOrdinalRules.select(2); // 'two'
enOrdinalRules.select(3); // 'few'
enOrdinalRules.select(4); // 'other'
enOrdinalRules.select(11); // 'other'
enOrdinalRules.select(21); // 'one'
enOrdinalRules.select(22); // 'two'
enOrdinalRules.select(23); // 'few'
Map the categories to ordinal suffixes:
const enOrdinalRules = new Intl.PluralRules('en-US', { type: 'ordinal' });
const enOrdinalSuffixes = new Map([
['one', 'st'],
['two', 'nd'],
['few', 'rd'],
['other', 'th'],
]);
function formatOrdinal(n) {
const category = enOrdinalRules.select(n);
const suffix = enOrdinalSuffixes.get(category);
return `${n}${suffix}`;
}
formatOrdinal(1); // '1st'
formatOrdinal(2); // '2nd'
formatOrdinal(3); // '3rd'
formatOrdinal(4); // '4th'
formatOrdinal(11); // '11th'
formatOrdinal(21); // '21st'
Other languages have entirely different ordinal systems. French uses "1er" for first and "2e" for all others. Spanish has gender-specific ordinals. The API provides the category, and you provide the localized forms.
Handle ranges with selectRange()
The selectRange() method determines the plural category for a range of numbers, like "1-5 items" or "10-20 results". Some languages have different plural rules for ranges than for individual counts.
const enRules = new Intl.PluralRules('en-US');
enRules.selectRange(1, 5); // 'other'
enRules.selectRange(0, 1); // 'other'
In English, ranges are almost always plural, even when the range starts at 1. Other languages have more complex range rules.
const slRules = new Intl.PluralRules('sl');
slRules.selectRange(102, 201); // 'few'
const ptRules = new Intl.PluralRules('pt');
ptRules.selectRange(102, 102); // 'other'
Use selectRange() when displaying ranges explicitly in your UI. For single counts, use select().
Combine with Intl.NumberFormat for localized number display
Plural forms often appear alongside formatted numbers. Use Intl.NumberFormat to format the number according to locale conventions, then use Intl.PluralRules to choose the correct text.
const locale = 'en-US';
const numberFormat = new Intl.NumberFormat(locale);
const pluralRules = new Intl.PluralRules(locale);
const forms = new Map([
['one', 'item'],
['other', 'items'],
]);
function formatCount(count) {
const formattedNumber = numberFormat.format(count);
const category = pluralRules.select(count);
const form = forms.get(category);
return `${formattedNumber} ${form}`;
}
formatCount(1); // '1 item'
formatCount(1000); // '1,000 items'
formatCount(1.5); // '1.5 items'
For German, which uses periods as thousand separators and commas as decimal separators:
const locale = 'de-DE';
const numberFormat = new Intl.NumberFormat(locale);
const pluralRules = new Intl.PluralRules(locale);
const forms = new Map([
['one', 'Artikel'],
['other', 'Artikel'],
]);
function formatCount(count) {
const formattedNumber = numberFormat.format(count);
const category = pluralRules.select(count);
const form = forms.get(category);
return `${formattedNumber} ${form}`;
}
formatCount(1); // '1 Artikel'
formatCount(1000); // '1.000 Artikel'
formatCount(1.5); // '1,5 Artikel'
This pattern ensures both the number formatting and the text form match user expectations for the locale.
Handle the zero case explicitly when needed
How zero is pluralized varies by language. English typically uses the plural form: "0 items", "0 results". Some languages use the singular form for zero. Others have a distinct zero category.
The Intl.PluralRules API returns the appropriate category for zero based on the language rules. In English, zero returns 'other', which maps to the plural form:
const enRules = new Intl.PluralRules('en-US');
enRules.select(0); // 'other'
In Arabic, zero has its own category:
const arRules = new Intl.PluralRules('ar-EG');
arRules.select(0); // 'zero'
Your text should account for this. For English, you might want to show "No items" instead of "0 items" for better UX. Handle this before calling the plural rules:
function formatItems(count) {
if (count === 0) {
return 'No items';
}
const category = enRules.select(count);
const form = enForms.get(category);
return `${count} ${form}`;
}
For Arabic, provide a specific zero form in your translations:
const arForms = new Map([
['zero', 'لا توجد عناصر'],
['one', 'عنصر واحد'],
['two', 'عنصران'],
['few', 'عناصر'],
['many', 'عنصرًا'],
['other', 'عنصر'],
]);
This respects the linguistic conventions of each language while allowing you to customize the zero case for better user experience.
Reuse PluralRules instances for performance
Creating a PluralRules instance involves parsing the locale and loading plural rule data. Do this once per locale, not on every function call or render cycle.
// Good: create once, reuse
const enRules = new Intl.PluralRules('en-US');
const enForms = new Map([
['one', 'item'],
['other', 'items'],
]);
function formatItems(count) {
const category = enRules.select(count);
const form = enForms.get(category);
return `${count} ${form}`;
}
If you support multiple locales, create instances for each locale and store them in a Map or cache:
const rulesCache = new Map();
function getPluralRules(locale) {
if (!rulesCache.has(locale)) {
rulesCache.set(locale, new Intl.PluralRules(locale));
}
return rulesCache.get(locale);
}
const rules = getPluralRules('en-US');
This pattern amortizes the initialization cost across many calls.
Browser support and compatibility
Intl.PluralRules is supported in all modern browsers since 2019. This includes Chrome 63+, Firefox 58+, Safari 13+, and Edge 79+. It is not supported in Internet Explorer.
For applications targeting modern browsers, you can use Intl.PluralRules without a polyfill. If you need to support older browsers, polyfills are available through packages like intl-pluralrules on npm.
The selectRange() method is newer and has slightly more limited support. It is available in Chrome 106+, Firefox 116+, Safari 15.4+, and Edge 106+. Check compatibility if you use selectRange() and need to support older browser versions.
Avoid hardcoding plural forms in logic
Do not check the count and branch in code to select a plural form. This approach does not scale to languages with more than two forms and couples your logic to English rules.
// Avoid this pattern
function formatItems(count) {
if (count === 1) {
return `${count} item`;
}
return `${count} items`;
}
Use Intl.PluralRules and a data structure to store forms. This keeps your code language-agnostic and makes it easy to add new languages by providing new translations.
// Prefer this pattern
const rules = new Intl.PluralRules('en-US');
const forms = new Map([
['one', 'item'],
['other', 'items'],
]);
function formatItems(count) {
const category = rules.select(count);
const form = forms.get(category);
return `${count} ${form}`;
}
This pattern works identically for any language. Only the rules instance and forms Map change.
Test with multiple locales and edge cases
Plural rules have edge cases that are easy to miss when testing only in English. Test your pluralization logic with at least one language that uses more than two forms, like Polish or Arabic.
Test counts that trigger different categories:
- Zero
- One
- Two
- A few (3-10 in Arabic)
- Many (11-99 in Arabic)
- Large numbers (100+)
- Decimal values (0.5, 1.5, 2.3)
- Negative numbers if your UI displays them
If you use ordinal rules, test numbers that trigger different suffixes: 1, 2, 3, 4, 11, 21, 22, 23. This ensures you handle the special cases correctly.
Testing with multiple locales early prevents surprises when you add new languages later. It also validates that your data structure includes all necessary categories and that your logic handles them correctly.