selectordinal Selectordinal Parameter

Complete Guide to ICU Ordinal Rules - 1st/2nd/3rd/4th Category Selection

Overview

The selectordinal parameter type selects appropriate text based on a numeric value following ICU ordinal rules. This is essential for grammatically correct translations of ordinal numbers like 1st, 2nd, 3rd, 4th in various languages.

Basic Example

Content: "You finished in @{position} @{ordinalText}"

Params:

{
  "paramName": "position",
  "type": "simple"
},
{
  "paramName": "ordinalText",
  "type": "selectordinal",
  "options": { "type": "ordinal" },
  "list": [
    { "value": "one", "text": "st" },
    { "value": "two", "text": "nd" },
    { "value": "few", "text": "rd" },
    { "value": "other", "text": "th" }
  ]
}

Expected Output:

⚠️ selectordinal vs select

selectordinal is specifically for ordinal numbers (1st, 2nd, 3rd). For non-numeric selections based on arbitrary values, use the select parameter instead.

ICU Ordinal Categories

ICU defines six ordinal categories. The key difference from plurals is that ordinal categories describe position in a sequence, not quantity.

Category Description Example Languages
zero For position 0 (rarely used in ordinals) Not commonly used
one For positions ending in 1 (1st, 21st, 31st...) English, French, German, Spanish
two For positions ending in 2 (2nd, 22nd, 32nd...) English, French
few For positions ending in 3 (3rd, 23rd, 33rd...) English, Polish, Lithuanian
many For positions with special rules (varies by language) Varies significantly by language
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 (1st, 2nd, 3rd, 4th...)

English ordinals have special rules for 11, 12, and 13:

English Ordinal Definition

{
  "paramName": "position",
  "type": "selectordinal",
  "list": [
    { "value": "one", "text": "#st" },
    { "value": "two", "text": "#nd" },
    { "value": "few", "text": "#rd" },
    { "value": "other", "text": "#th" }
  ]
}

French (1er, 2e, 3e, 4e...)

French ordinals are simpler - only 1st is special:

French Ordinal Definition

{
  "paramName": "position",
  "type": "selectordinal",
  "list": [
    { "value": "one", "text": "#er" },
    { "value": "other", "text": "#e" }
  ]
}

German (1., 2., 3., 4...)

German ordinals use a period suffix for all numbers:

German Ordinal Definition

{
  "paramName": "position",
  "type": "selectordinal",
  "list": [
    { "value": "other", "text": "#." }
  ]
}

Polish

Polish ordinal rules are simplified in the current implementation. For ordinals, only one (n === 1) is properly handled; other values fall through to other.

Polish Ordinal Definition

{
  "paramName": "position",
  "type": "selectordinal",
  "list": [
    { "value": "one", "text": "#y" },
    { "value": "other", "text": "#y" }
  ]
}

⚠️ Limited Polish Ordinal Support

Current implementation treats all non-1 values as "other" for Polish ordinals. Full Polish ordinal support (with proper forms for 2, 3, 4, etc.) requires additional language rules.

Chinese (第1, 第2, 第3...)

Chinese uses a prefix "第" for ordinals:

Chinese Ordinal Definition

{
  "paramName": "position",
  "type": "selectordinal",
  "list": [
    { "value": "other", "text": "第#位" }
  ]
}

Spanish (1.°, 2.°, 3.°, 4.°...)

Spanish uses a superscript degree symbol:

Spanish Ordinal Definition

{
  "paramName": "position",
  "type": "selectordinal",
  "list": [
    { "value": "one", "text": "#.°" },
    { "value": "other", "text": "#.°" }
  ]
}

Language Implementation Comparison

Different programming languages implement ordinal rules with varying levels of completeness.

Language Zero Support One Support Two Support Few Support Many Support Notes
TypeScript English ordinals (1st/2nd/3rd with 11/12/13 handling)
JavaScript English ordinals (1st/2nd/3rd with 11/12/13 handling)
Java English ordinals via ChoiceFormat
Kotlin English ordinals via PluralRules
Swift English ordinals (1st/2nd/3rd with 11/12/13 handling), other languages limited
Python English ordinals (1st/2nd/3rd with 11/12/13 handling), other languages limited
Go English ordinals via go-i18n
Rust English ordinals via fluent
Dart English ordinals via intl

ℹ️ 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 ordinal rules are defined at the language level, not the regional level. The English ordinal rule (1st/2nd/3rd with 11/12/13 exception) is the same across all English-speaking countries.

Generated Code Implementation

TypeScript/JavaScript Implementation

The generated code includes helper functions for each ordinal category, with special handling for English 11th, 12th, and 13th:

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;
    }
    // ... cardinal rules follow
}

function isTwo(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 === 2 && n % 100 !== 12;
        if (lang === "fr") return n % 10 === 2 && n % 100 !== 12;
        return false;
    }
    // ... cardinal rules follow
}

function isFew(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 === 3 && n % 100 !== 13;
        return false;  // Other languages don't distinguish "few" for ordinals
    }
    // ... cardinal rules follow
}

function isMany(locale: string, value: number, type: string): boolean {
    const n = Math.abs(value);
    const lang = locale.split("-")[0];
    if (type === "ordinal") return false;
    // ... cardinal rules follow
}

Swift Implementation

The generated Swift code includes helper functions for each ordinal category, with special handling for English 11th, 12th, and 13th:

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
    }
    // ... cardinal rules
}

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
    }
    // ... cardinal rules
}

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
    }
    // ... cardinal rules
}

private static func isMany(_ locale: String, _ value: Int, _ type: String) -> Bool {
    if type == "ordinal" { return false }
    // ... cardinal rules
}

Python Implementation

The generated Python code includes helper functions for each ordinal category, with special handling for English 11th, 12th, and 13th:

    @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
        # ... cardinal rules

    @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
        # ... cardinal rules

    @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
        # ... cardinal rules

    @staticmethod
    def is_many(locale: str, value: int, type: str) -> bool:
        if type == "ordinal": return False
        # ... cardinal rules

Best Practices

✅ Do: Account for 11th, 12th, 13th in English

When supporting English, remember that 11th, 12th, and 13th use the "th" suffix despite ending in 1, 2, or 3. The generated TypeScript code handles this correctly:

{
  "paramName": "position",
  "type": "selectordinal",
  "list": [
    { "value": "one", "text": "#st" },
    { "value": "two", "text": "#nd" },
    { "value": "few", "text": "#rd" },
    { "value": "other", "text": "#th" }
  ]
}

❌ Don't: Assume Ordinal Rules Are the Same as Plural Rules

Even though both use the same categories (zero, one, few, many, other), their meanings are different. For plurals, "one" means exactly 1 item. For ordinals, "one" means position ending in 1 (but not 11, 111, etc.).

Complete Import Example

{
  "strings": [
    {
      "key": "race.position",
      "translations": {
        "en-US": {
          "content": "You finished in @{position} place",
          "params": [
            {
              "paramName": "position",
              "type": "selectordinal",
              "list": [
                { "value": "one", "text": "st" },
                { "value": "two", "text": "nd" },
                { "value": "few", "text": "rd" },
                { "value": "other", "text": "th" }
              ]
            }
          ]
        },
        "fr-FR": {
          "content": "Vous avez terminé à la @{position} place",
          "params": [
            {
              "paramName": "position",
              "type": "selectordinal",
              "list": [
                { "value": "one", "text": "er" },
                { "value": "other", "text": "e" }
              ]
            }
          ]
        },
        "zh-CN": {
          "content": "你的排名是第@{position}位",
          "params": [
            {
              "paramName": "position",
              "type": "selectordinal",
              "list": [
                { "value": "other", "text": "位" }
              ]
            }
          ]
        }
      }
    }
  ]
}