Complete Guide to ICU Ordinal Rules - 1st/2nd/3rd/4th Category Selection
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.
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:
1 → "You finished in 1st"2 → "You finished in 2nd"3 → "You finished in 3rd"4 → "You finished in 4th"selectordinal is specifically for ordinal numbers (1st, 2nd, 3rd). For non-numeric selections based on arbitrary values, use the select parameter instead.
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 |
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 ordinals have special rules for 11, 12, and 13:
{
"paramName": "position",
"type": "selectordinal",
"list": [
{ "value": "one", "text": "#st" },
{ "value": "two", "text": "#nd" },
{ "value": "few", "text": "#rd" },
{ "value": "other", "text": "#th" }
]
}
French ordinals are simpler - only 1st is special:
{
"paramName": "position",
"type": "selectordinal",
"list": [
{ "value": "one", "text": "#er" },
{ "value": "other", "text": "#e" }
]
}
German ordinals use a period suffix for all numbers:
{
"paramName": "position",
"type": "selectordinal",
"list": [
{ "value": "other", "text": "#." }
]
}
Polish ordinal rules are simplified in the current implementation. For ordinals, only one (n === 1) is properly handled; other values fall through to other.
{
"paramName": "position",
"type": "selectordinal",
"list": [
{ "value": "one", "text": "#y" },
{ "value": "other", "text": "#y" }
]
}
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 uses a prefix "第" for ordinals:
{
"paramName": "position",
"type": "selectordinal",
"list": [
{ "value": "other", "text": "第#位" }
]
}
Spanish uses a superscript degree symbol:
{
"paramName": "position",
"type": "selectordinal",
"list": [
{ "value": "one", "text": "#.°" },
{ "value": "other", "text": "#.°" }
]
}
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 |
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.
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
}
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
}
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
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" }
]
}
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.).
{
"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": "位" }
]
}
]
}
}
}
]
}