Get all plural forms available in a locale

Discover which plural categories you need to provide translations for

Introduction

When building multilingual applications, you need to provide different text forms for different quantities. In English, you write "1 item" and "2 items". This seems simple until you start supporting other languages.

Russian uses three different forms depending on the count. Arabic uses six. Some languages use the same form for all counts. Before you can provide translations for these forms, you need to know which forms exist in each language.

JavaScript provides a way to discover which plural categories a locale uses. The resolvedOptions() method on a PluralRules instance returns a pluralCategories property that lists all the plural forms the locale needs. This tells you exactly which translations to provide without guessing or maintaining language-specific rule tables.

What plural categories are

Plural categories are standardized names for different plural forms used across languages. The Unicode CLDR (Common Locale Data Repository) defines six categories: zero, one, two, few, many, and other.

Not every language uses all six categories. English uses only two: one and other. The category one applies to the count 1, and other applies to everything else.

Arabic uses all six categories. The category zero applies to 0, one to 1, two to 2, few to counts like 3-10, many to counts like 11-99, and other to counts like 100 and above.

Russian uses three categories: one for counts ending in 1 (except 11), few for counts ending in 2-4 (except 12-14), and many for everything else.

Japanese and Chinese use only the other category because these languages do not distinguish between singular and plural forms.

These categories represent the linguistic rules of each language. When you provide translations, you create one string for each category the language uses.

Getting plural categories with resolvedOptions

The resolvedOptions() method on a PluralRules instance returns an object containing information about the rules, including which plural categories the locale uses.

const enRules = new Intl.PluralRules('en-US');
const options = enRules.resolvedOptions();

console.log(options.pluralCategories);
// Output: ["one", "other"]

The pluralCategories property is an array of strings. Each string is one of the six standard category names. The array contains only the categories the locale actually uses.

For English, the array contains one and other because English distinguishes between singular and plural forms.

For a language with more complex rules, the array contains more categories:

const arRules = new Intl.PluralRules('ar-EG');
const options = arRules.resolvedOptions();

console.log(options.pluralCategories);
// Output: ["zero", "one", "two", "few", "many", "other"]

Arabic uses all six categories, so the array contains all six values.

Seeing plural categories for different locales

Different languages have different plural rules, which means they use different sets of categories. Compare several languages to see the variation:

const locales = ['en-US', 'ar-EG', 'ru-RU', 'pl-PL', 'ja-JP', 'zh-CN'];

locales.forEach(locale => {
  const rules = new Intl.PluralRules(locale);
  const categories = rules.resolvedOptions().pluralCategories;
  console.log(`${locale}: [${categories.join(', ')}]`);
});

// Output:
// en-US: [one, other]
// ar-EG: [zero, one, two, few, many, other]
// ru-RU: [one, few, many, other]
// pl-PL: [one, few, many, other]
// ja-JP: [other]
// zh-CN: [other]

English has two categories. Arabic has six. Russian and Polish have four each. Japanese and Chinese have only one because they do not distinguish plural forms at all.

This variation shows why you cannot assume every language works like English. You need to check which categories each locale uses and provide appropriate translations for each.

Understanding what categories mean for each locale

The same category name means different things in different languages. The one category in English applies only to the number 1. In Russian, one applies to numbers ending in 1 except 11, so it includes 1, 21, 31, 101, and so on.

Test which numbers map to which categories in different locales:

const enRules = new Intl.PluralRules('en-US');
const ruRules = new Intl.PluralRules('ru-RU');

const numbers = [0, 1, 2, 3, 5, 11, 21, 22, 100];

console.log('English:');
numbers.forEach(n => {
  console.log(`  ${n}: ${enRules.select(n)}`);
});

console.log('Russian:');
numbers.forEach(n => {
  console.log(`  ${n}: ${ruRules.select(n)}`);
});

// Output:
// English:
//   0: other
//   1: one
//   2: other
//   3: other
//   5: other
//   11: other
//   21: other
//   22: other
//   100: other
// Russian:
//   0: many
//   1: one
//   2: few
//   3: few
//   5: many
//   11: many
//   21: one
//   22: few
//   100: many

In English, only 1 uses the one category. In Russian, 1 and 21 both use one because they end in 1. The numbers 2, 3, and 22 use few because they end in 2-4. The numbers 0, 5, 11, and 100 use many.

This demonstrates that you cannot predict which category applies to a number without knowing the language rules. The pluralCategories array tells you which categories exist, and the select() method tells you which category applies to each number.

Getting categories for ordinal numbers

Ordinal numbers like 1st, 2nd, 3rd have their own plural rules that differ from cardinal numbers. Create a PluralRules instance with type: 'ordinal' to get the categories for ordinal numbers:

const enCardinalRules = new Intl.PluralRules('en-US', { type: 'cardinal' });
const enOrdinalRules = new Intl.PluralRules('en-US', { type: 'ordinal' });

console.log('Cardinal:', enCardinalRules.resolvedOptions().pluralCategories);
// Output: Cardinal: ["one", "other"]

console.log('Ordinal:', enOrdinalRules.resolvedOptions().pluralCategories);
// Output: Ordinal: ["one", "two", "few", "other"]

English cardinal numbers use two categories. English ordinal numbers use four categories because ordinals need to distinguish between 1st, 2nd, 3rd, and all others.

The ordinal categories map to ordinal suffixes:

const enOrdinalRules = new Intl.PluralRules('en-US', { type: 'ordinal' });

const numbers = [1, 2, 3, 4, 11, 21, 22, 23];

numbers.forEach(n => {
  const category = enOrdinalRules.select(n);
  console.log(`${n}: ${category}`);
});

// Output:
// 1: one
// 2: two
// 3: few
// 4: other
// 11: other
// 21: one
// 22: two
// 23: few

The category one corresponds to the st suffix (1st, 21st), two to nd (2nd, 22nd), few to rd (3rd, 23rd), and other to th (4th, 11th).

Different languages have different ordinal categories:

const locales = ['en-US', 'es-ES', 'fr-FR'];

locales.forEach(locale => {
  const rules = new Intl.PluralRules(locale, { type: 'ordinal' });
  const categories = rules.resolvedOptions().pluralCategories;
  console.log(`${locale}: [${categories.join(', ')}]`);
});

// Output:
// en-US: [one, two, few, other]
// es-ES: [other]
// fr-FR: [one, other]

Spanish uses only one ordinal category because Spanish ordinals follow a simpler pattern. French uses two categories to distinguish first from all other positions.

Using plural categories to build translation maps

When you know which categories a locale uses, you can build a translation map with exactly the right number of entries:

function buildTranslationMap(locale, translations) {
  const rules = new Intl.PluralRules(locale);
  const categories = rules.resolvedOptions().pluralCategories;

  const map = new Map();

  categories.forEach(category => {
    if (translations[category]) {
      map.set(category, translations[category]);
    } else {
      console.warn(`Missing translation for category "${category}" in locale "${locale}"`);
    }
  });

  return map;
}

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

const arTranslations = {
  zero: 'لا توجد عناصر',
  one: 'عنصر واحد',
  two: 'عنصران',
  few: 'عناصر',
  many: 'عنصرًا',
  other: 'عنصر'
};

const enMap = buildTranslationMap('en-US', enTranslations);
const arMap = buildTranslationMap('ar-EG', arTranslations);

console.log(enMap);
// Output: Map(2) { 'one' => 'item', 'other' => 'items' }

console.log(arMap);
// Output: Map(6) { 'zero' => 'لا توجد عناصر', 'one' => 'عنصر واحد', ... }

This function checks that you provided translations for all required categories and warns you if any are missing. This prevents runtime errors when a category is used but has no translation.

Validating translation completeness

Use plural categories to verify that your translations include all necessary forms before deploying to production:

function validateTranslations(locale, translations) {
  const rules = new Intl.PluralRules(locale);
  const requiredCategories = rules.resolvedOptions().pluralCategories;
  const providedCategories = Object.keys(translations);

  const missing = requiredCategories.filter(cat => !providedCategories.includes(cat));
  const extra = providedCategories.filter(cat => !requiredCategories.includes(cat));

  if (missing.length > 0) {
    console.error(`Locale ${locale} is missing categories: ${missing.join(', ')}`);
    return false;
  }

  if (extra.length > 0) {
    console.warn(`Locale ${locale} has unused categories: ${extra.join(', ')}`);
  }

  return true;
}

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

const incompleteArTranslations = {
  one: 'عنصر واحد',
  other: 'عنصر'
};

validateTranslations('en-US', enTranslations);
// Output: true

validateTranslations('ar-EG', incompleteArTranslations);
// Output: Locale ar-EG is missing categories: zero, two, few, many
// Output: false

This validation catches missing translations during development instead of discovering them when users encounter untranslated text.

Building dynamic translation interfaces

When building tools for translators, query the plural categories to show exactly which forms need translation:

function generateTranslationForm(locale, key) {
  const rules = new Intl.PluralRules(locale);
  const categories = rules.resolvedOptions().pluralCategories;

  const form = document.createElement('div');
  form.className = 'translation-form';

  const heading = document.createElement('h3');
  heading.textContent = `Translate "${key}" for ${locale}`;
  form.appendChild(heading);

  categories.forEach(category => {
    const label = document.createElement('label');
    label.textContent = `${category}:`;

    const input = document.createElement('input');
    input.type = 'text';
    input.name = `${key}.${category}`;
    input.placeholder = `Enter ${category} form`;

    const wrapper = document.createElement('div');
    wrapper.appendChild(label);
    wrapper.appendChild(input);
    form.appendChild(wrapper);
  });

  return form;
}

const enForm = generateTranslationForm('en-US', 'items');
const arForm = generateTranslationForm('ar-EG', 'items');

document.body.appendChild(enForm);
document.body.appendChild(arForm);

This generates a form with the correct number of input fields for each locale. English gets two fields (one and other), while Arabic gets six fields (zero, one, two, few, many, and other).

Comparing categories across locales

When managing translations for multiple locales, compare which categories they use to understand the translation complexity:

function compareLocalePluralCategories(locales) {
  const comparison = {};

  locales.forEach(locale => {
    const rules = new Intl.PluralRules(locale);
    const categories = rules.resolvedOptions().pluralCategories;
    comparison[locale] = categories;
  });

  return comparison;
}

const locales = ['en-US', 'es-ES', 'ar-EG', 'ru-RU', 'ja-JP'];
const comparison = compareLocalePluralCategories(locales);

console.log(comparison);
// Output:
// {
//   'en-US': ['one', 'other'],
//   'es-ES': ['one', 'other'],
//   'ar-EG': ['zero', 'one', 'two', 'few', 'many', 'other'],
//   'ru-RU': ['one', 'few', 'many', 'other'],
//   'ja-JP': ['other']
// }

This shows that English and Spanish have the same plural categories, making it easy to reuse translation structures between them. Arabic requires significantly more translation work because it uses six categories.

Checking if a locale uses a specific category

Before using a specific plural category in your code, check whether the locale actually uses it:

function localeUsesCategory(locale, category) {
  const rules = new Intl.PluralRules(locale);
  const categories = rules.resolvedOptions().pluralCategories;
  return categories.includes(category);
}

console.log(localeUsesCategory('en-US', 'zero'));
// Output: false

console.log(localeUsesCategory('ar-EG', 'zero'));
// Output: true

console.log(localeUsesCategory('ja-JP', 'one'));
// Output: false

This prevents you from assuming every locale has a zero category or one category. Use this check to implement category-specific logic safely.

Understanding the other category

Every language uses the other category. This category serves as the default case when no other category applies.

In English, other covers all counts except 1. In Arabic, other covers large numbers like 100 and above. In Japanese, other covers all counts because Japanese does not distinguish plural forms.

Always provide a translation for the other category. This category is guaranteed to exist in every locale and will be used when no more specific category matches.

const locales = ['en-US', 'ar-EG', 'ru-RU', 'ja-JP'];

locales.forEach(locale => {
  const rules = new Intl.PluralRules(locale);
  const categories = rules.resolvedOptions().pluralCategories;
  const hasOther = categories.includes('other');
  console.log(`${locale} uses "other": ${hasOther}`);
});

// Output:
// en-US uses "other": true
// ar-EG uses "other": true
// ru-RU uses "other": true
// ja-JP uses "other": true

Getting all resolved options together

The resolvedOptions() method returns more than just plural categories. It includes information about the locale, type, and number formatting options:

const rules = new Intl.PluralRules('de-DE', {
  type: 'cardinal',
  minimumFractionDigits: 2,
  maximumFractionDigits: 2
});

const options = rules.resolvedOptions();

console.log(options);
// Output:
// {
//   locale: 'de-DE',
//   type: 'cardinal',
//   pluralCategories: ['one', 'other'],
//   minimumIntegerDigits: 1,
//   minimumFractionDigits: 2,
//   maximumFractionDigits: 2,
//   minimumSignificantDigits: undefined,
//   maximumSignificantDigits: undefined
// }

The pluralCategories property is one piece of information in the resolved options object. The other properties tell you the exact configuration the PluralRules instance uses, including any options that were set to default values.

Caching plural categories for performance

Creating PluralRules instances and calling resolvedOptions() has a cost. Cache the results for each locale instead of repeatedly querying them:

const categoriesCache = new Map();

function getPluralCategories(locale, type = 'cardinal') {
  const key = `${locale}:${type}`;

  if (categoriesCache.has(key)) {
    return categoriesCache.get(key);
  }

  const rules = new Intl.PluralRules(locale, { type });
  const categories = rules.resolvedOptions().pluralCategories;

  categoriesCache.set(key, categories);

  return categories;
}

const enCardinal = getPluralCategories('en-US', 'cardinal');
const enOrdinal = getPluralCategories('en-US', 'ordinal');
const arCardinal = getPluralCategories('ar-EG', 'cardinal');

console.log('en-US cardinal:', enCardinal);
console.log('en-US ordinal:', enOrdinal);
console.log('ar-EG cardinal:', arCardinal);

// Subsequent calls use cached results
const enCardinal2 = getPluralCategories('en-US', 'cardinal');
// No new PluralRules instance created

This pattern is especially important in applications that format many pluralized strings or support many locales.

Browser support and compatibility

The pluralCategories property on resolvedOptions() was added to JavaScript in 2020. It is supported in Chrome 106+, Firefox 116+, Safari 15.4+, and Edge 106+.

Older browsers that support Intl.PluralRules but not pluralCategories will return undefined for this property. Check for its existence before using it:

function getPluralCategories(locale) {
  const rules = new Intl.PluralRules(locale);
  const options = rules.resolvedOptions();

  if (options.pluralCategories) {
    return options.pluralCategories;
  }

  // Fallback for older browsers
  return ['one', 'other'];
}

This fallback assumes a simple two-category system, which works for English and many European languages but may not be correct for languages with more complex rules. For better compatibility, provide language-specific fallbacks or use a polyfill.