How do you choose the right plural form for different languages?

Use JavaScript's Intl.PluralRules to select between one item, two items, few items, many items based on language-specific rules

Introduction

When you display text with quantities, you need different messages for different counts. In English, you write "1 file" but "2 files". The simplest approach concatenates a number with a word and adds an "s" when needed.

function formatFileCount(count) {
  return count === 1 ? `${count} file` : `${count} files`;
}

This approach fails in three ways. First, it produces incorrect English for zero ("0 files" should arguably be "no files"). Second, it breaks down with complex plurals like "1 child, 2 children" or "1 person, 2 people". Third, and most critically, other languages have entirely different plural rules that this code cannot handle.

JavaScript provides Intl.PluralRules to solve this problem. This API determines which plural form to use for any number in any language, following the Unicode CLDR standard used by professional translation systems worldwide.

Why different languages need different plural forms

English uses two plural forms. You write "1 book" and "2 books". The word changes when the count is exactly one versus any other number.

Other languages work differently. Polish uses three forms based on complex rules. Russian uses four forms. Arabic uses six forms. Some languages use only one form for all quantities.

Here are examples showing how the word for "apple" changes based on quantity in different languages:

English: 1 apple, 2 apples, 5 apples, 0 apples

Polish: 1 jabłko, 2 jabłka, 5 jabłek, 0 jabłek

Russian: 1 яблоко, 2 яблока, 5 яблок, 0 яблок

Arabic: Uses six different forms depending on whether you have zero, one, two, a few, many, or other quantities

The Unicode CLDR defines the exact rules for when to use each form in every language. You cannot memorize these rules or hardcode them into your application. You need an API that knows them.

What are CLDR plural categories

The Unicode CLDR standard defines six plural categories that cover all languages:

  • zero: Used in some languages for exactly zero items
  • one: Used for singular forms
  • two: Used in languages with a dual form
  • few: Used for small quantities in some languages
  • many: Used for larger quantities or fractions in some languages
  • other: The default form, used when no other category applies

Every language uses the other category. Most languages use only two or three categories total. The categories do not directly correspond to quantities. For example, in Polish, the number 5 uses the many category, but so does 0, 25, and 1.5.

The specific rules for which numbers map to which categories differ by language. JavaScript handles this complexity through the Intl.PluralRules API.

How to determine which plural form to use

The Intl.PluralRules object determines which plural category a number belongs to in a specific language. You create a PluralRules object with a locale, then call its select() method with a number.

const rules = new Intl.PluralRules('en-US');
console.log(rules.select(0));  // "other"
console.log(rules.select(1));  // "one"
console.log(rules.select(2));  // "other"
console.log(rules.select(5));  // "other"

In English, select() returns "one" for the number 1 and "other" for everything else.

Polish uses three categories with more complex rules:

const rules = new Intl.PluralRules('pl-PL');
console.log(rules.select(0));   // "many"
console.log(rules.select(1));   // "one"
console.log(rules.select(2));   // "few"
console.log(rules.select(5));   // "many"
console.log(rules.select(22));  // "few"
console.log(rules.select(25));  // "many"

Arabic uses six categories:

const rules = new Intl.PluralRules('ar-EG');
console.log(rules.select(0));   // "zero"
console.log(rules.select(1));   // "one"
console.log(rules.select(2));   // "two"
console.log(rules.select(3));   // "few"
console.log(rules.select(11));  // "many"
console.log(rules.select(100)); // "other"

The select() method returns a string identifying the category. You use this string to choose the appropriate message for display.

How to map plural categories to messages

After determining the plural category, you need to select the correct message to show the user. Create an object that maps each category to its message, then use the category string to look up the message.

const messages = {
  one: '{count} file',
  other: '{count} files'
};

function formatFileCount(count, locale) {
  const rules = new Intl.PluralRules(locale);
  const category = rules.select(count);
  const message = messages[category];
  return message.replace('{count}', count);
}

console.log(formatFileCount(1, 'en-US'));  // "1 file"
console.log(formatFileCount(5, 'en-US'));  // "5 files"

This pattern works for any language. For Polish, you provide messages for all three categories the language uses:

const messages = {
  one: '{count} plik',
  few: '{count} pliki',
  many: '{count} plików'
};

function formatFileCount(count, locale) {
  const rules = new Intl.PluralRules(locale);
  const category = rules.select(count);
  const message = messages[category];
  return message.replace('{count}', count);
}

console.log(formatFileCount(1, 'pl-PL'));   // "1 plik"
console.log(formatFileCount(2, 'pl-PL'));   // "2 pliki"
console.log(formatFileCount(5, 'pl-PL'));   // "5 plików"
console.log(formatFileCount(22, 'pl-PL'));  // "22 pliki"

The code structure remains identical across languages. Only the messages object changes. This separation allows translators to provide the correct messages for their language without modifying code.

How to handle missing plural categories

Your messages object might not include all six possible categories. Most languages only use two or three. When select() returns a category not in your messages object, fall back to the other category.

function formatFileCount(count, locale, messages) {
  const rules = new Intl.PluralRules(locale);
  const category = rules.select(count);
  const message = messages[category] || messages.other;
  return message.replace('{count}', count);
}

const englishMessages = {
  one: '{count} file',
  other: '{count} files'
};

console.log(formatFileCount(1, 'en-US', englishMessages));  // "1 file"
console.log(formatFileCount(5, 'en-US', englishMessages));  // "5 files"

This pattern ensures your code works even when the messages object is incomplete. The other category always exists in every language, making it a safe fallback.

How to use plural rules with ordinal numbers

The Intl.PluralRules constructor accepts a type option that changes how categories are determined. The default type is "cardinal", used for counting items. Set type: "ordinal" to determine plural forms for ordinal numbers like "1st", "2nd", "3rd".

const cardinalRules = new Intl.PluralRules('en-US', { type: 'cardinal' });
console.log(cardinalRules.select(1));  // "one"
console.log(cardinalRules.select(2));  // "other"
console.log(cardinalRules.select(3));  // "other"

const ordinalRules = new Intl.PluralRules('en-US', { type: 'ordinal' });
console.log(ordinalRules.select(1));   // "one"
console.log(ordinalRules.select(2));   // "two"
console.log(ordinalRules.select(3));   // "few"
console.log(ordinalRules.select(4));   // "other"

Cardinal rules determine "1 item, 2 items". Ordinal rules determine "1st place, 2nd place, 3rd place, 4th place". The categories returned differ because the grammatical patterns differ.

How to format fractional quantities

The select() method works with decimal numbers. Different languages have specific rules for how fractions map to plural categories.

const rules = new Intl.PluralRules('en-US');
console.log(rules.select(1));    // "one"
console.log(rules.select(1.0));  // "one"
console.log(rules.select(1.5));  // "other"
console.log(rules.select(0.5));  // "other"

In English, 1.0 uses the singular form, but 1.5 uses the plural. Some languages have different rules for fractions, treating them as a separate category.

const messages = {
  one: '{count} file',
  other: '{count} files'
};

const rules = new Intl.PluralRules('en-US');
const count = 1.5;
const category = rules.select(count);
const message = messages[category];
console.log(message.replace('{count}', count));  // "1.5 files"

Pass the decimal number directly to select(). The method applies the correct language rules automatically.

How to create a reusable plural formatter

Instead of repeating the same pattern throughout your application, create a reusable function that encapsulates the plural selection logic.

class PluralFormatter {
  constructor(locale) {
    this.locale = locale;
    this.rules = new Intl.PluralRules(locale);
  }

  format(count, messages) {
    const category = this.rules.select(count);
    const message = messages[category] || messages.other;
    return message.replace('{count}', count);
  }
}

const formatter = new PluralFormatter('en-US');

const fileMessages = {
  one: '{count} file',
  other: '{count} files'
};

const itemMessages = {
  one: '{count} item',
  other: '{count} items'
};

console.log(formatter.format(1, fileMessages));  // "1 file"
console.log(formatter.format(5, fileMessages));  // "5 files"
console.log(formatter.format(1, itemMessages));  // "1 item"
console.log(formatter.format(3, itemMessages));  // "3 items"

This class creates the PluralRules object once and reuses it for multiple format operations. You can extend it to support more advanced features like formatting the count with Intl.NumberFormat before inserting it into the message.

How to integrate plural rules with translation systems

Professional translation systems store messages with placeholders for plural categories. When you translate text, you provide all the plural forms your language needs.

const translations = {
  'en-US': {
    fileCount: {
      one: '{count} file',
      other: '{count} files'
    },
    downloadComplete: {
      one: 'Download of {count} file complete',
      other: 'Download of {count} files complete'
    }
  },
  'pl-PL': {
    fileCount: {
      one: '{count} plik',
      few: '{count} pliki',
      many: '{count} plików'
    },
    downloadComplete: {
      one: 'Pobieranie {count} pliku zakończone',
      few: 'Pobieranie {count} plików zakończone',
      many: 'Pobieranie {count} plików zakończone'
    }
  }
};

class Translator {
  constructor(locale, translations) {
    this.locale = locale;
    this.translations = translations[locale] || {};
    this.rules = new Intl.PluralRules(locale);
  }

  translate(key, count) {
    const messages = this.translations[key];
    if (!messages) return key;

    const category = this.rules.select(count);
    const message = messages[category] || messages.other;
    return message.replace('{count}', count);
  }
}

const translator = new Translator('en-US', translations);
console.log(translator.translate('fileCount', 1));         // "1 file"
console.log(translator.translate('fileCount', 5));         // "5 files"
console.log(translator.translate('downloadComplete', 1));  // "Download of 1 file complete"
console.log(translator.translate('downloadComplete', 5));  // "Download of 5 files complete"

const polishTranslator = new Translator('pl-PL', translations);
console.log(polishTranslator.translate('fileCount', 1));   // "1 plik"
console.log(polishTranslator.translate('fileCount', 2));   // "2 pliki"
console.log(polishTranslator.translate('fileCount', 5));   // "5 plików"

This pattern separates translation data from code logic. Translators provide the messages for each plural category their language uses. Your code applies the rules automatically.

How to check which plural categories a locale uses

The resolvedOptions() method returns information about the PluralRules object, but it does not list which categories the locale uses. To find all categories a locale uses, test a range of numbers and collect the unique categories returned.

function getPluralCategories(locale) {
  const rules = new Intl.PluralRules(locale);
  const categories = new Set();

  for (let i = 0; i <= 100; i++) {
    categories.add(rules.select(i));
    categories.add(rules.select(i + 0.5));
  }

  return Array.from(categories).sort();
}

console.log(getPluralCategories('en-US'));  // ["one", "other"]
console.log(getPluralCategories('pl-PL'));  // ["few", "many", "one"]
console.log(getPluralCategories('ar-EG'));  // ["few", "many", "one", "other", "two", "zero"]

This technique tests integers and half values across a range. It captures the categories your messages object needs to include for a given locale.