Software localization
Pluralization: A Guide to Localizing Plurals
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.
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.
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.
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 Arabic
Code 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.
🔗 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)
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.
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.
🔗 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)
🔗 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)
🔗 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
- React + i18next
- React + react-intl/FormatJS (first-class ICU message support)
- Next.js + next-intl (App Router)
Vue
- Vue 3 + Vue I18n (Composition API)
- Vue 2 + Vue I18n (Options API)
Other web frameworks
- Angular (first-class ICU message support)
- Svelte + svelte-i18n
- SolidJS + i18next
Mobile
- Flutter (first-class ICU message support)
- React Native + expo-localization + i18n-js
- Android
- iOS
Server
- PHP / Symfony (first-class ICU message support)
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.