Software localization

Pluralization: A Guide to Localizing Plurals

Learn more about how to get pluralization right in your multilingual app and ensure a smooth user experience for your global user base.
Software localization guide featured image | Phrase

Pluralization in multilingual apps—a seemingly simple concept that can quickly spiral into a maze of linguistic intricacies. From the straightforward “apple” vs “apples” in English to the multifaceted plural rules of Russian and Arabic, developers and translators often grapple with the challenge of representing quantity across cultures.

In this guide, we will unravel the complexities of plural localization, showing code in JavaScript with the popular internationalization (i18n) library, i18next. For those working with other programming languages and frameworks, we’ll provide handy links to related plural i18n resources.

💡 Learn more » Internationalization (i18n) and localization (l10n) allow us to make our apps available in different languages and to different regions, often for more profit. If you’re new to i18n and l10n, check out our guide to internationalization.

String Management UI visual | Phrase

Phrase Strings

Take your web or mobile app global without any hassle

Adapt your software, website, or video game for global audiences with the leanest and most realiable software localization platform.

Explore Phrase Strings

It’s not just singular and plural

English has two plural forms: singular and plural, “one tree” and “five trees.” Many languages share this simple duality, but quite a few don’t. Chinese has one plural form, and so does Japanese. Russian has four, and Arabic has six!

🔗 Resource » The CLDR (Common Language Data Repository) Language Plural Rules listing is the canonical source for each language’s plural forms.

When localizing plurals, we often have a dynamic count integer that we use to determine which form to pick e.g. 1 → “one tree”, 2 → “two trees”.

Let’s take an example. Here’s a message for a fictional tree-planting organization.

An app message screen containing numbers | Phrase

Looking at English, we have our two forms, called one and other in localization lingo. Here we would need two versions of the message:

  • one → “We’ve planted 1 tree so far!”
  • other → “We’ve planted 20,000 trees so far!”

🗒️ Note » Normally we use the other form for the zero case in English. We’ll see how we can override this later.

What about a language like Arabic? Remember, Arabic has six plural forms. If we want accurate translations for the above message, we need six versions:

  • zero → “لم نزرع أي شجرة حتى الآن”
  • one → “لقد زرعنا شجرة ١ حتى الآن”
  • two → “لقد زرعنا شجرتين ٢ حتى الآن”
  • few → “لقد زرعنا ٣ شجرات حتى الآن”
  • many → “لقد زرعنا ١١ شجرة حتى الآن”
  • other → “لقد زرعنا ١٠٠ شجرة حتى الآن”

So there’s no one-size-fits-all answer to plural translation. We need a solution that allows selecting the correct plural form for any given language, not just “pick from singular and plural”.

🤿 Go deeper » The astute reader will have noticed that our Arabic translations above are not using Western Arabic numerals (1, 2, 3). Many Arabic regions use Eastern Arabic numerals instead (١، ٢، ٣). Read our Concise Guide to Number Localization for this and a lot more about number localization.

Use an i18n library

If we’re building a simple JavaScript app with a couple of languages, we could get away with rolling our own pluralization solution. We could even use the standard Intl.PluralRules object to make our lives easier. Prebuilt i18n libraries make this work much easier, however, especially as we support more languages.

We’ll use the immensely popular i18next JavaScript framework in this article to demonstrate. But we’ll try to stay as tech-agnostic as possible, and we’ll provide links to our other programming language and framework articles a bit later.

Assuming we’ve installed and configured i18next, we add our translation messages to it as follows:

i18next
  .init({
    // Default language
    lng: "en",
    resources: {
      // English translations
      en: {
        translation: {
          countLabel: "Count",
          messageLabel: "Message",
        },
      },
      // Arabic translations
      ar: {
        translation: {
          countLabel: "العدد",
          messageLabel: "الرسالة",
        },
      },
    },
  })Code language: JavaScript (javascript)

We can then use these translations in our app like this:

i18next.t("countLabel");

// => "Count" when active locale is English
// => "العدد" when active locale is ArabicCode language: JavaScript (javascript)

🗒️ Note » In production, we would likely house each language’s translations in a separate JSON file and load the file when needed. We’re skipping this here to keep our focus on plurals.

So how do we add a translation message with plural forms? Well, any i18n library worth using supports plurals out of the box, and i18next is no exception.

i18next uses a suffix naming convention for plurals: Each plural form for a message called foo would get its own entry e.g. foo_one, foo_other.

Let’s revisit our above tree-planting example; say we wanted to give its translation message a key of message. We’d add the plural forms to our translations as follows. (Remember, English has two plural forms and Arabic has six).

i18next
  .init({
    lng: "en",
    debug: true,
    resources: {
      en: {
        translation: {
          countLabel: "Count",
          messageLabel: "Message",
+         message_one: "🌳 We've planted {{count, number}} tree so far!",
+         message_other: "🌳 We've planted {{count, number}} trees so far!",
        },
      },
      ar: {
        translation: {
          countLabel: "العدد",
          messageLabel: "الرسالة",
+         message_zero: "🌳 لم نزرع أي شجرة حتى الآن!",
+         message_one: "🌳 لقد زرعنا شجرة {{count, number}} حتى الآن!",
+         message_two: "🌳 لقد زرعنا شجرتين {{count, number}} حتى الآن!",
+         message_few: "🌳 لقد زرعنا {{count, number}} شجرات حتى الآن!",
+         message_many: "🌳 لقد زرعنا {{count, number}} شجرة حتى الآن!",
+         message_other: "🌳 لقد زرعنا {{count, number}} شجرة حتى الآن!",
        },
      },
    },
  })Code language: Diff (diff)

Heads up » Generally speaking, the other form is always required.

To use these plural forms, we provide the message key without any suffix, and a count variable:

i18next.t("message", { count: 3 })

// => (en) "🌳 We've planted 3 trees so far!"
// => (ar) "🌳 لقد زرعنا ٣ شجرات حتى الآن!"Code language: JavaScript (javascript)

Of course, the count can be dynamic and provided at runtime.

Heads up » The plural counter variable must be called count.

🗒️ Note » i18next uses a {{variable}} syntax to interpolate runtime values into a message. We’re making use of this above — note the {{count, number}} — to format the count as a number with proper localized formatting. Read more about interpolation and formatting in the i18next docs.

With that in place, we have a localized plural solution that adapts to any language.

Demo app screen displaying a localized plural solution | Phrase
Our plural messages shown in English and Arabic

🔗 Resource » Get all the code for this demo app from our GitHub repository.

i18next provides a special _zero case for all languages: It overrides the language’s normal plural form resolution. We could use it to provide a special zero message in English.

i18next
  .init({
    lng: "en",
    debug: true,
    resources: {
      en: {
        translation: {
          countLabel: "Count",
          messageLabel: "Message",
+         message_zero: "🌳 We haven't planted any trees yet.",
          message_one: "🌳 We've planted {{count, number}} tree so far!",
          message_other: "🌳 We've planted {{count, number}} trees so far!",
        },
      },
      ar: {
        // ...
      },
    },
  })Code language: Diff (diff)
App screen displaying a count of zero | Phrase
Our zero override shown when the count is zero

Use the ICU message format

ICU (International Components for Unicode) is a set of portable, widely used i18n libraries. i18next itself has an ICU plugin, which we’ll demo in a moment. Many other i18n libraries across programming languages have built-in ICU support. One of the most important parts of the ICU is its translation message format, which has excellent plural support.

🤿 Go deeper » We’ve written extensively about ICU in The Missing Guide to the ICU Message Format.

An ICU message is a string, much like our translation strings above, with special syntaxes for interpolating runtime values, plurals, and more. We’ll focus on plurals here, of course.

Assuming the official i18next ICU plugin is installed and set up, here’s how we can refactor our messages to the ICU message format.

i18next
  .use(window.i18nextICU)
  .init({
    lng: "en",
    debug: true,
    resources: {
      en: {
        translation: {
          countLabel: "Count",
          messageLabel: "Message",
          message: `
            {count, plural,
              one {🌳 We've planted one tree so far!}
              other {🌳 We've planted # trees so far!}
            }`,
        },
      },
      ar: {
        translation: {
          countLabel: "العدد",
          messageLabel: "الرسالة",
          message: `{count, plural,
                zero {🌳 لم نزرع أي شجرة حتى الآن!}
                one {🌳 لقد زرعنا شجرة # حتى الآن!}
                two {🌳 لقد زرعنا شجرتين # حتى الآن!}
                few {🌳 لقد زرعنا # شجرات حتى الآن!}
                many {🌳 لقد زرعنا # شجرة حتى الآن!}
                other {🌳 لقد زرعنا # شجرة حتى الآن!}
            }`,
        },
      },
    },
  })Code language: JavaScript (javascript)

ICU plurals are all part of the same message. The syntax is basically:

{countVariable, plural,
  firstPluralForm {content}
  secondPluralForm {content}
  ... 
}Code language: plaintext (plaintext)

The CLDR plural forms are used here as before (one, other, etc.). You can find the forms for your language in the CLDR Language Plural Rules listing.

🗒️ Note » In fact the {count, plural, ...} segment could be embedded in a longer message. For example, We planted {count, plural, ...}!. However, it’s considered good practice to keep each plural form separate: It’s easier to maintain the message that way.

Heads up » Generally speaking, we can’t mix and match non-ICU plurals, interpolation, etc. with ICU ones when using i18next. It’s a good idea to choose one kind of format and stick to it.

The special # character above will be replaced by the count variable when we resolve the message.

i18next.t("message", { count: 12 })

// => (en) "🌳 We've planted 12 trees so far!"
// => (ar) "🌳 لقد زرعنا ١٢ شجرة حتى الآن!"Code language: JavaScript (javascript)

🗒️ Note » Unlike regular i18next messages, we could have called count anything we wanted here, as long as we kept it consistent in our messages.

Instead of #, we can use the variable name itself along with ICU interpolation syntax ({variable}) to inject the counter in a message:

{count, plural,
  one {🌳 We've planted {count} tree so far!}
  other {🌳 We've planted {count} trees so far!}
}Code language: plaintext (plaintext)

Note that while # respects the number formatting of the current language, {count} won’t necessarily. For example, using {count} in Arabic messages results in Western Arabic numerals used (1, 2, 3), which we don’t want.

Arabic translation screen with a Western Arabic “1” embedded | Phrase
Our Arabic translation shown with a Western Arabic “1” embedded

To correct this, we can use the ICU number format.

{count, plural,
  zero {🌳 لم نزرع أي شجرة حتى الآن!}
  one {🌳 لقد زرعنا شجرة {count, number} حتى الآن!}
  two {🌳 لقد زرعنا شجرتين {count, number} حتى الآن!}
  ...
}Code language: plaintext (plaintext)

This ensures that the active language’s number formatting is respected.

Arabic translation screen with Eastern Arabic “١” embedded | Phrase
Our Arabic translation shown with the appropriate Eastern Arabic “١” embedded

🔗 Resource » To get the ICU message code in this article, check out the ICU branch in our GitHub repo.

ICU plurals allow us to override language plural rules for specific numbers. Unlike regular i18next plurals, the ICU format works for any number, not just zero. We just need to use the =n {} syntax for the override we want, where n is the specific count we’re overriding.

{count, plural,
  =0 {🌳 We haven't planted any trees yet!}
  one {🌳 We've planted one tree so far!}
  =2 {🌳 We've planted a couple of trees so far!}
  other {🌳 We've planted # trees so far!}
}Code language: plaintext (plaintext)
Zero and two number overrides displayed in an English message screen | Phrase
The zero and two number overrides displayed in our English message

🔗 Resource » I find the Online ICU Message Editor handy when I’m formatting my ICU messages.

Ordinals

An ordinal is a word that represents the rank of a number, e.g. first, second, third. Some languages, like English, have special representations for ordinals, e.g. 1st, 2nd, 3rd, 4th.

The plural forms we’ve covered so far in this article are cardinal plurals, relating to the natural numbers represented in a language. Note, however, that a language can have different forms for its cardinals and ordinals. As we’ve mentioned earlier, English, for example, has two forms for its cardinals: one and other. Yet it has four forms for its ordinals: one, two, few, and other.

The ICU message format has a syntax just for ordinals, using the selectordinal keyword. Here it is for English:

Yay! Our
{count, selectordinal,
  one {#st}
  two {#nd}
  few {#rd}
  other {#th}
}
tree!Code language: plaintext (plaintext)

Arabic has alphabetic ordinals (أول، ثاني، ثالث). Its numerical representation of ordinals is written just as the cardinal number, something like, “We’ve planted the tree 33”. So ICU affords Arabic the single other form to display these:

رائع! شجرتنا الـ
{count, selectordinal, other {#}}
!Code language: plaintext (plaintext)
Ordinal message screen | Phrase
Our ordinal messages

🔗 Resource » Check the CLDR Language Plural Rules listing for all languages’ supported ordinals.

Framework resources

We’ve covered i18next and JavaScript above. However, many programming languages and libraries implement the ICU message format. Environments that have no ICU support almost certainly have an alternative that solves complex plural localization.

We think you’ll find a good plural localization solution for your framework in one of these articles (we link to the plurals section directly where we can):

React and Next.js

Vue

Other web frameworks

Mobile

Server

Game engines

Pluralization doesn’t need to be a hassle

That about does it for this guide on localizing plurals. By following developer-tested best practices, you can ensure your multilingual app smoothly handles plurals in different languages, delivering a seamless UX to your global user base. We hope you’ve picked up some valuable insights and enjoyed yourself.