plural Plural Parameter

Complete Guide to ICU Plural Rules - Zero/One/Few/Many/Other Categories

Overview

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.

Basic Example

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:

ICU Plural Categories

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

⚠️ Important: Always Include "other"

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.

Language-Specific Rules

English (Simplest)

English only uses one and other:

English Plural Definition

{
  "paramName": "count",
  "type": "plural",
  "list": [
    { "value": "one", "text": "# item" },
    { "value": "other", "text": "# items" }
  ]
}

Arabic (Most Complex)

Arabic uses all six categories with complex rules:

Arabic Plural Definition

{
  "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 (Russian, Polish, Czech)

Slavic languages have complex rules involving the last digit of the number:

Russian Example

Russian Plural Definition

{
  "paramName": "items",
  "type": "plural",
  "list": [
    { "value": "one", "text": "# товар" },
    { "value": "few", "text": "# товара" },
    { "value": "many", "text": "# товаров" },
    { "value": "other", "text": "# товара" }
  ]
}

Chinese (No Plurals)

Chinese doesn't have grammatical plurals. The same form is used for all numbers.

Chinese Definition

{
  "paramName": "count",
  "type": "plural",
  "list": [
    { "value": "other", "text": "# 个苹果" }
  ]
}

French

French uses one and other, but with a twist for 0:

French Plural Definition

{
  "paramName": "items",
  "type": "plural",
  "list": [
    { "value": "one", "text": "# élément" },
    { "value": "other", "text": "# éléments" }
  ]
}

Language Implementation Comparison

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

ℹ️ Locale Format Note

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.

Generated Code Implementation

TypeScript/JavaScript Implementation

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

Swift Implementation

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
    }

Python Implementation

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

Best Practices

✅ Do: Define All Relevant Categories

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": "# штук" }
  ]
}

❌ Don't: Assume English Plural Rules Apply Everywhere

Never assume that just because English uses "one" and "other", other languages follow the same pattern. Always research the target language's plural rules.

Complete Import Example

{
  "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": "عنصر" }
              ]
            }
          ]
        }
      }
    }
  ]
}