Skip to Content
Course content

133: Using the Intl API for Pluralization

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I can't tell you how many times I've walked into a code review and seen a line like ${count} ${count === 1 ? 'message' : 'messages'}. In the moment, it feels efficient. It's one line of code, it's readable, and for those of us working primarily in English, it's technically correct. But if your app ever needs to scale beyond English-speaking markets, this pattern becomes a liability.

The temptation of the ternary operator

When we think about plurals in English, we think in binaries: it's either singular or it isn't. This leads us to write logic that assumes every language on earth follows this same rule. Let's look at a typical notification counter implementation:

const getMessageText = (count) => {
  return `${count} ${count === 1 ? 'unread message' : 'unread messages'}`;
};

console.log(getMessageText(1)); // "1 unread message"
console.log(getMessageText(2)); // "2 unread messages"
console.log(getMessageText(0)); // "0 unread messages"

This is what I call "naive pluralization." It works for English, and it might even work for a few other languages, but it's fundamentally based on a false premise about how human language works.

When the binary assumption fails

The problem is that "plural" isn't a toggle switch in most of the world. Take Polish, for example. They have different noun forms depending on whether the number ends in 2, 3, or 4, versus numbers ending in 5 through 21. Arabic is even more complex, with distinct categories for zero, one, two, numbers from 3 to 10, and numbers from 11 to 99. If you try to force these languages into a count === 1 ternary, your UI will look broken or, worse, grammatically illiterate to your users.

I've seen teams realize this far too late in the development cycle, forcing them to go back and hunt down every single ternary plural across the entire codebase. It's a nightmare to refactor once the app is large.

Letting the browser handle the linguistic heavy lifting

This is where Intl.PluralRules comes in. Instead of you trying to memorize the grammatical rules of 100 different languages, you ask the browser to categorize the number for you. The API doesn't return the final string; instead, it returns a "category" (like 'one', 'two', 'few', 'many', or 'other') based on the locale provided.

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

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

By using select(), you're decoupling the logic of pluralization (which is handled by the browser) from the content of the pluralization (which is handled by your translation files).

Adding a bit of mapping overhead for global correctness

The trade-off here is that you can't just write a one-liner anymore. You need a mapping object that provides the correct string for each category. It's a bit more boilerplate, but it's the only way to be truly internationalized.

Here is how I would actually implement that notification counter in a production environment:

const messages = {
  'en-US': {
    one: 'unread message',
    other: 'unread messages',
  },
  'ar': {
    zero: 'رسالة غير مقروءة',
    one: 'رسالة واحدة غير مقروءة',
    two: 'رسالتان غير مقروءتين',
    few: 'رسائل غير مقروءة',
    many: 'رسائل غير مقروءة',
    other: 'رسائل غير مقروءة',
  }
};

function formatNotification(count, locale) {
  const pr = new Intl.PluralRules(locale);
  const category = pr.select(count);
  const term = messages[locale][category] || messages[locale]['other'];
  
  return `${count} ${term}`;
}

console.log(formatNotification(2, 'en-US')); // "2 unread messages"
console.log(formatNotification(2, 'ar'));    // "2 رسالتان غير مقروءتين"

You'll notice I added a fallback to 'other'. This is important because 'other' is the only category guaranteed to exist across all locales. If your translation file is missing a specific category, falling back to 'other' ensures the app doesn't crash and still provides a reasonably understandable message.




📋 Practical Task

Implementing a Multi-Locale File Upload Counter

You are building a file upload component that needs to support both English (en-US) and Russian (ru). Russian pluralization is notoriously tricky: it uses one, few, and many.

Your Task: Create a function called formatFileCount(count, locale).

  • Use Intl.PluralRules to determine the correct plural category.
  • Create a translation map for en-US (using one and other) and ru (using one, few, and many).
  • The Russian terms should be:
    • one: "файл"
    • few: "файла"
    • many: "файлов"
  • Ensure the function returns a string combining the number and the correct term (e.g., "1 файл" or "5 файлов").
  • Include a fallback to 'other' for the English locale to prevent errors.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.