Complete Guide to ICU Plural Rules - Zero/One/Few/Many/Other Categories
The plural parameter type selects appropriate text based on a numeric value following ICU plural rules. This is essential for grammatically correct translations in languages with complex pluralization systems.
Content: "You have @{count} @{itemCount}"
Params:
{
"paramName": "count",
"type": "simple"
},
{
"paramName": "itemCount",
"type": "plural",
"options": { "type": "cardinal" },
"list": [
{ "value": "one", "text": "item" },
{ "value": "other", "text": "items" }
]
}
Expected Output:
1 → "You have 1 item"5 → "You have 5 items"ICU defines six plural categories. Not all categories are used in every language.
| Category | Description | Example Languages |
|---|---|---|
zero |
For value 0 | Arabic, Polish, Russian, Lithuanian |
one |
For value 1 | English, French, German, Spanish, Chinese |
two |
For value 2 | Arabic, Slovenian, Scottish Gaelic |
few |
For small groups (3-6 in some Slavic languages) | Russian, Polish, Czech, Slovak, Ukrainian, Romanian |
many |
For larger numbers (varies by language) | Polish, Russian, Ukrainian |
other |
Fallback - required in all languages | All languages |
The other category is required as a fallback in all languages. Even if your target language doesn't use all categories, include other to ensure correct fallback behavior.
English only uses one and other:
{
"paramName": "count",
"type": "plural",
"list": [
{ "value": "one", "text": "# item" },
{ "value": "other", "text": "# items" }
]
}
Arabic uses all six categories with complex rules:
{
"paramName": "items",
"type": "plural",
"list": [
{ "value": "zero", "text": "# عناصر" },
{ "value": "one", "text": "# عنصر" },
{ "value": "two", "text": "# عنصران" },
{ "value": "few", "text": "# عناصر" },
{ "value": "many", "text": "# عنصر" },
{ "value": "other", "text": "# عنصر" }
]
}
Slavic languages have complex rules involving the last digit of the number:
{
"paramName": "items",
"type": "plural",
"list": [
{ "value": "one", "text": "# товар" },
{ "value": "few", "text": "# товара" },
{ "value": "many", "text": "# товаров" },
{ "value": "other", "text": "# товара" }
]
}
Chinese doesn't have grammatical plurals. The same form is used for all numbers.
{
"paramName": "count",
"type": "plural",
"list": [
{ "value": "other", "text": "# 个苹果" }
]
}
French uses one and other, but with a twist for 0:
{
"paramName": "items",
"type": "plural",
"list": [
{ "value": "one", "text": "# élément" },
{ "value": "other", "text": "# éléments" }
]
}
Different programming languages implement plural rules with varying levels of completeness.
| Language | Zero Support | One Support | Two Support | Few Support | Many Support | Notes |
|---|---|---|---|---|---|---|
| TypeScript | ✅ | ✅ | ✅ | ✅ | ✅ | Full ICU rules via isZero/One/Two/Few/Many |
| JavaScript | ✅ | ✅ | ✅ | ✅ | ✅ | Full ICU rules via isZero/One/Two/Few/Many |
| Swift | ✅ | ✅ | ✅ | ✅ | ✅ | Full ICU rules via isZero/One/Two/Few/Many |
| Python | ✅ | ✅ | ✅ | ✅ | ✅ | Full ICU rules via is_zero/one/two/few/many |
Generated code automatically extracts the language part only from locale strings. For example, "en-US", "en-GB", and "en-AU" are all treated as "en" for matching purposes.
This is correct because plural rules are defined at the language level, not the regional level. The English plural rule (one/other) is the same across all English-speaking countries.
The generated code includes helper functions for each plural category:
function isZero(locale: string, value: number, type: string): boolean {
if (type === "ordinal") return false;
const n = Math.abs(value);
const lang = locale.split("-")[0];
if (lang === "ar") return n === 0;
return false;
}
function isOne(locale: string, value: number, type: string): boolean {
const n = Math.abs(value);
const lang = locale.split("-")[0];
if (type === "ordinal") {
if (lang === "en") return n % 10 === 1 && n % 100 !== 11;
if (lang === "fr") return n === 1;
if (lang === "es" || lang === "it" || lang === "pt" || lang === "de" || lang === "nl" || lang === "sv" || lang === "no" || lang === "da" || lang === "fi") return n % 10 === 1 && n % 100 !== 11;
if (lang === "ko" || lang === "ja" || lang === "zh" || lang === "th" || lang === "vi") return false;
return n === 1;
}
if (lang === "fr") return n === 0 || n === 1;
if (lang === "ru" || lang === "uk" || lang === "be") return n % 10 === 1 && n % 100 !== 11;
if (lang === "pl") return n === 1;
if (lang === "cs" || lang === "sk") return n === 1;
if (lang === "ar") return n === 1;
if (lang === "zh" || lang === "ja" || lang === "ko" || lang === "th" || lang === "vi") return false;
return n === 1;
}
// Similar functions: isTwo, isFew, isMany
The generated Swift code includes helper functions for each plural category:
static func formatPlural(_ value: Int, _ p: I18NParam, locale: String) -> String {
let key: String
if isZero(locale, value, "cardinal") { key = "zero" }
else if isOne(locale, value, "cardinal") { key = "one" }
else if isTwo(locale, value, "cardinal") { key = "two" }
else if isFew(locale, value, "cardinal") { key = "few" }
else if isMany(locale, value, "cardinal") { key = "many" }
else { key = "other" }
guard let list = p.list else { return "\(value)" }
for item in list {
if item.value == key {
var text = item.text
text = text.replacingOccurrences(of: "#", with: "\(value)")
return text
}
}
return "\(value)"
}
private static func isZero(_ locale: String, _ value: Int, _ type: String) -> Bool {
if type == "ordinal" { return false }
let n = abs(value)
let lang = locale.contains("-") ? String(locale.split(separator: "-").first ?? "") : locale
if lang == "ar" { return n == 0 }
return false
}
private static func isOne(_ locale: String, _ value: Int, _ type: String) -> Bool {
let n = abs(value)
let lang = locale.contains("-") ? String(locale.split(separator: "-").first ?? "") : locale
if type == "ordinal" {
if lang == "en" { return n % 10 == 1 && n % 100 != 11 }
if lang == "fr" { return n == 1 }
if ["es", "it", "pt", "de", "nl", "sv", "no", "da", "fi"].contains(lang) { return n % 10 == 1 && n % 100 != 11 }
if ["ko", "ja", "zh", "th", "vi"].contains(lang) { return false }
return n == 1
}
if lang == "fr" { return n == 0 || n == 1 }
if ["ru", "uk", "be"].contains(lang) { return n % 10 == 1 && n % 100 != 11 }
if lang == "pl" { return n == 1 }
if ["cs", "sk"].contains(lang) { return n == 1 }
if lang == "ar" { return n == 1 }
if ["zh", "ja", "ko", "th", "vi"].contains(lang) { return false }
return n == 1
}
private static func isTwo(_ locale: String, _ value: Int, _ type: String) -> Bool {
let n = abs(value)
let lang = locale.contains("-") ? String(locale.split(separator: "-").first ?? "") : locale
if type == "ordinal" {
if lang == "en" { return n % 10 == 2 && n % 100 != 12 }
return false
}
if lang == "ar" { return n == 2 }
return false
}
private static func isFew(_ locale: String, _ value: Int, _ type: String) -> Bool {
let n = abs(value)
let lang = locale.contains("-") ? String(locale.split(separator: "-").first ?? "") : locale
if type == "ordinal" {
if lang == "en" { return n % 10 == 3 && n % 100 != 13 }
return false
}
if ["ru", "uk", "be"].contains(lang) { return n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14) }
if lang == "pl" { return n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14) }
if ["cs", "sk"].contains(lang) { return n >= 2 && n <= 4 }
if lang == "ar" { return n % 100 >= 3 && n % 100 <= 10 }
return false
}
private static func isMany(_ locale: String, _ value: Int, _ type: String) -> Bool {
let n = abs(value)
let lang = locale.contains("-") ? String(locale.split(separator: "-").first ?? "") : locale
if type == "ordinal" { return false }
if ["ru", "uk", "be"].contains(lang) { return n % 10 == 0 || (n % 10 >= 5 && n % 10 <= 9) || (n % 100 >= 11 && n % 100 <= 14) }
if lang == "pl" { return (n % 10 == 0 && n != 1) || (n % 10 >= 5 && n % 10 <= 9) || (n % 100 >= 12 && n % 100 <= 14) || (n % 10 == 1 && n != 1) }
if ["cs", "sk"].contains(lang) { return n == 0 || n >= 5 }
if lang == "ar" { return n % 100 >= 11 && n % 100 <= 99 }
return false
}
The generated Python code includes helper functions for each plural category:
@staticmethod
def format_plural(value: int, p: dict, locale: str) -> str:
key = "other"
if T.is_zero(locale, value, "cardinal"): key = "zero"
elif T.is_one(locale, value, "cardinal"): key = "one"
elif T.is_two(locale, value, "cardinal"): key = "two"
elif T.is_few(locale, value, "cardinal"): key = "few"
elif T.is_many(locale, value, "cardinal"): key = "many"
options = {}
if p.get("list"):
for item in p["list"]:
options[item["value"]] = item["text"]
opt = options.get(key)
return opt.replace("#", str(value)) if opt else str(value)
@staticmethod
def is_zero(locale: str, value: int, type: str) -> bool:
if type == "ordinal": return False
n = abs(value)
lang = locale.split("-")[0] if "-" in locale else locale
if lang == "ar": return n == 0
return False
@staticmethod
def is_one(locale: str, value: int, type: str) -> bool:
n = abs(value)
lang = locale.split("-")[0] if "-" in locale else locale
if type == "ordinal":
if lang == "en": return n % 10 == 1 and n % 100 != 11
if lang == "fr": return n == 1
if lang in ["es", "it", "pt", "de", "nl", "sv", "no", "da", "fi"]: return n % 10 == 1 and n % 100 != 11
if lang in ["ko", "ja", "zh", "th", "vi"]: return False
return n == 1
if lang == "fr": return n == 0 or n == 1
if lang in ["ru", "uk", "be"]: return n % 10 == 1 and n % 100 != 11
if lang == "pl": return n == 1
if lang in ["cs", "sk"]: return n == 1
if lang == "ar": return n == 1
if lang in ["zh", "ja", "ko", "th", "vi"]: return False
return n == 1
@staticmethod
def is_two(locale: str, value: int, type: str) -> bool:
n = abs(value)
lang = locale.split("-")[0] if "-" in locale else locale
if type == "ordinal":
if lang == "en": return n % 10 == 2 and n % 100 != 12
return False
if lang == "ar": return n == 2
return False
@staticmethod
def is_few(locale: str, value: int, type: str) -> bool:
n = abs(value)
lang = locale.split("-")[0] if "-" in locale else locale
if type == "ordinal":
if lang == "en": return n % 10 == 3 and n % 100 != 13
return False
if lang in ["ru", "uk", "be"]: return n % 10 >= 2 and n % 10 <= 4 and (n % 100 < 12 or n % 100 > 14)
if lang == "pl": return n % 10 >= 2 and n % 10 <= 4 and (n % 100 < 12 or n % 100 > 14)
if lang in ["cs", "sk"]: return n >= 2 and n <= 4
if lang == "ar": return n % 100 >= 3 and n % 100 <= 10
return False
@staticmethod
def is_many(locale: str, value: int, type: str) -> bool:
n = abs(value)
lang = locale.split("-")[0] if "-" in locale else locale
if type == "ordinal": return False
if lang in ["ru", "uk", "be"]: return n % 10 == 0 or (n % 10 >= 5 and n % 10 <= 9) or (n % 100 >= 11 and n % 100 <= 14)
if lang == "pl": return (n % 10 == 0 and n != 1) or (n % 10 >= 5 and n % 10 <= 9) or (n % 100 >= 12 and n % 100 <= 14) or (n % 10 == 1 and n != 1)
if lang in ["cs", "sk"]: return n == 0 or n >= 5
if lang == "ar": return n % 100 >= 11 and n % 100 <= 99
return False
When supporting multiple languages, include all the plural forms that those languages need. For a multilingual app supporting English, Russian, and Arabic:
{
"paramName": "count",
"type": "plural",
"list": [
{ "value": "zero", "text": "# штук" },
{ "value": "one", "text": "# штука" },
{ "value": "two", "text": "# штуки" },
{ "value": "few", "text": "# штуки" },
{ "value": "many", "text": "# штук" },
{ "value": "other", "text": "# штук" }
]
}
Never assume that just because English uses "one" and "other", other languages follow the same pattern. Always research the target language's plural rules.
{
"strings": [
{
"key": "cart.items",
"translations": {
"en-US": {
"content": "You have @{count} @{itemCount}",
"params": [
{
"paramName": "count",
"type": "simple"
},
{
"paramName": "itemCount",
"type": "plural",
"options": { "type": "cardinal" },
"list": [
{ "value": "one", "text": "item" },
{ "value": "other", "text": "items" }
]
}
]
},
"ar-SA": {
"content": "لديك @{count} @{itemCount}",
"params": [
{
"paramName": "count",
"type": "simple"
},
{
"paramName": "itemCount",
"type": "plural",
"options": { "type": "cardinal" },
"list": [
{ "value": "zero", "text": "عناصر" },
{ "value": "one", "text": "عنصر" },
{ "value": "two", "text": "عنصران" },
{ "value": "few", "text": "عناصر" },
{ "value": "many", "text": "عنصر" },
{ "value": "other", "text": "عنصر" }
]
}
]
}
}
}
]
}