Skip to content

toLocaleOrdinal applies English ordinal rules to every locale, and the ordinal translation data is wrong #98089

Description

@shubham1206agra

Problem

toLocaleOrdinal (src/libs/LocaleDigitUtils.ts) renders a number as a localized ordinal (1st, 2nd, …). It is broken in every locale except English, for three independent reasons:

  1. The selection logic hardcodes English's ordinal rule and applies it to all locales.
  2. The translation data is wrong — most locales have weekday or month abbreviations sitting in their ordinal keys.
  3. The output model is suffix-only (${number}${suffix}), which cannot express locales where the ordinal is a prefix.

Non-English users currently see strings such as 4Do., 4czw., 4gio, 4juil. and 1第第.

Where it is used

  • src/pages/workspace/workflows/WorkspaceAutoReportingMonthlyOffsetPage.tsx — day-of-month picker
  • src/pages/workspace/workflows/WorkspaceAutoReportingFrequencyPage.tsx — reporting frequency
  • src/pages/workspace/workflows/approvals/ApprovalWorkflowEditor.tsx — approver position
  • src/components/ApprovalWorkflowSection.tsx — approver position
  • src/libs/ReportActionsUtils.ts — auto-reporting offset in report actions

Cause 1 — the selection logic is English's rule, hand-rolled

const lastDigit = number % 10;
const lastTwoDigits = number % 100;

if (lastDigit === 1 && lastTwoDigits !== 11) {
    suffixKey = 'workflowsPage.frequencies.ordinals.one';
} else if (lastDigit === 2 && lastTwoDigits !== 12) {
    suffixKey = 'workflowsPage.frequencies.ordinals.two';
} else if (lastDigit === 3 && lastTwoDigits !== 13) {
    suffixKey = 'workflowsPage.frequencies.ordinals.few';
}

This is a transcription of English's CLDR ordinal rule, and it runs for every locale. So German, Polish, Japanese and the rest all get one for 1, two for 2, few for 3 — categories their grammar does not define.

Intl.PluralRules(locale, {type: 'ordinal'}) already encodes the correct rule for each locale and should be used instead. Verified: for English it produces an identical category for every number from 1 to 120, so replacing the hand-rolled logic is behaviour-preserving for English.

Ordinal categories actually defined per locale

locale        1     2     3     4     8    11    21    22    31
en          one   two   few other other other   one   two   few
fr          one other other other other other other other other
it        other other other other  many  many other other other
es        other other other other other other other other other
de        other other other other other other other other other
el        other other other other other other other other other
ja        other other other other other other other other other
nl        other other other other other other other other other
pl        other other other other other other other other other
pt-BR     other other other other other other other other other
zh-hans   other other other other other other other other other
Locale Ordinal categories
en one, two, few, other
it many, other
fr one, other
es, de, el, ja, nl, pl, pt-BR, zh-hans other only

Eight of eleven locales need a single form. Their one / two / few entries are unreachable.

Cause 2 — the translation data is wrong

Current values of workflowsPage.frequencies.ordinals:

en        one: 'st'    two: 'nd'      few: 'rd'      other: 'th'
es        one: 'º'     two: 'º'       few: 'º'       other: 'º'
fr        one: 'er'    two: 'eᵉ'      few: 'ᵉ'       other: 'juil.'
de        one: 'st'    two: 'nd'      few: 'rd.'     other: 'Do.'
el        one: 'στ'    two: '2η'      few: 'ημ.'     other: 'ημ.'
it        one: 'st'    two: 'nd'      few: 'rd'      other: 'gio'
ja        one: 'st'    two: '番目'     few: 'rd'      other: '第'
nl        one: 'ste'   two: 'nd'      few: 'rd'      other: 'doe'
pl        one: 'śr'    two: 'nd'      few: 'rd'      other: 'czw.'
pt-BR     one: 'º'     two: 'nd'      few: 'rd'      other: 'º'
zh-hans   one: '第第'   two: '第几位'   few: '第 rd'   other: '第'

Several of these are not ordinal suffixes at all — they are weekday and month abbreviations:

Locale Value What it actually means
de Do. Donnerstag — Thursday
pl czw. czwartek — Thursday
pl śr środa — Wednesday
it gio giovedì — Thursday
fr juil. juillet — July
nl doe the verb "doe"
el ημ. ημέρα — day

Others leak raw English (de, it, nl, pl, ja all have 'st' / 'nd' / 'rd' in some slot), and two contain doubled text that produces visible corruption:

  • el two: '2η' → the number 22 renders as 222η
  • zh-hans one: '第第' → the number 1 renders as 1第第

Cause 3 — the suffix model cannot express prefix locales

The function returns `${number}${suffix}`. Japanese and Chinese ordinals are formed with a prefix: 第1, 第22. No suffix value can produce that, so ja and zh-hans cannot be made correct without changing the model — currently they render 1第 instead of 第1.

Similarly, German and Polish ordinals are just the number followed by a period (1., 22.), which a suffix can express, but only if the value is corrected.

Comparison: current output vs a corrected implementation

Using Intl.PluralRules(locale, {type: 'ordinal'}) for selection, and per-locale data that returns the full string rather than a suffix:

          n =    1     2     3     4     8    11    21    22    31
en  current    1st   2nd   3rd   4th   8th  11th  21st  22nd  31st
    corrected  1st   2nd   3rd   4th   8th  11th  21st  22nd  31st   <- unchanged

fr  current    1er  2eᵉ    3ᵉ  4juil. 8juil. 11juil. 21er 22eᵉ 31er
    corrected  1er   2e    3e    4e    8e   11e   21e   22e   31e

de  current    1st   2nd  3rd.  4Do.  8Do. 11Do.  21st  22nd  31st
    corrected   1.    2.    3.    4.    8.   11.   21.   22.   31.

el  current    1στ   22η  3ημ.  4ημ.  8ημ. 11ημ.  21στ  222η  31στ
    corrected   1η    2η    3η    4η    8η   11η   21η   22η   31η

it  current    1st   2nd   3rd  4gio  8gio 11gio  21st  22nd  31st
    corrected   1º    2º    3º    4º    8º   11º   21º   22º   31º

ja  current    1st  2番目   3rd    4第    8第   11第  21st 22番目  31st
    corrected   第1    第2    第3    第4    第8   第11   第21   第22   第31

nl  current   1ste   2nd   3rd  4doe  8doe 11doe 21ste  22nd 31ste
    corrected   1e    2e    3e    4e    8e   11e   21e   22e   31e

pl  current    1śr   2nd   3rd 4czw. 8czw. 11czw. 21śr  22nd  31śr
    corrected   1.    2.    3.    4.    8.   11.   21.   22.   31.

zh  current   1第第 2第几位 3第 rd   4第    8第   11第  21第第 22第几位 31第第
    corrected   第1    第2    第3    第4    第8   第11   第21   第22   第31

The corrected values above are proposals and need native-speaker review before use. Two in particular are context-dependent:

  • Greek ordinals inflect for gender. η is feminine, which suits ημέρα (day) in the day-of-month usage, but would be wrong for a masculine or neuter noun.
  • Dutch uses both 1e and 1ste conventions; the existing file mixes them.

Spanish and Brazilian Portuguese already produce correct output for most numbers (º), so they need the least work.

Suggested fix

Use Intl.PluralRules(locale, {type: 'ordinal'}) instead of hand written rules for each locale. This will make the solution scalable for other locales.

The writtenOrdinals path (numeric keys '1''10' returning "First", "Second", …) is a separate lookup and is not affected by any of this.

Verification

// selection logic is unchanged for English
for (let n = 1; n <= 120; n++) {
    new Intl.PluralRules('en', {type: 'ordinal'}).select(n); // compare against the current rule
}

// categories each locale actually defines
new Intl.PluralRules('it', {type: 'ordinal'}).resolvedOptions().pluralCategories; // ['many', 'other']
Issue OwnerCurrent Issue Owner: @shubham1206agra

Metadata

Metadata

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions