Localize is tested against conformance data that Unicode generates by running ICU. Most of the time the two agree and a mismatch means Localize has a defect. Sometimes they do not agree, because ICU is an implementation with its own API surface and defaults, not the specification itself. This guide records where Localize deliberately differs from ICU, why, and how each case can be checked.

The rule

Where the format CLDR ships and ICU's behaviour conflict, Localize follows CLDR.

CLDR is the data. ICU is one consumer of it, and a conformance file generated from ICU records ICU's answer — including where ICU overrides, ignores, or supplements what the data says. Localize renders what the locale's data specifies, so a locale maintainer who changes a pattern in CLDR sees that change in Localize's output.

This is not a claim that ICU is wrong. In several cases below ICU's output is arguably the friendlier one. It is a statement about which source is authoritative when they disagree, so that the answer is predictable rather than decided case by case.

Distinguishing a divergence from a defect

Four different things can make a conformance case fail, and only the first is a divergence:

  • Divergence — CLDR's data says one thing, ICU does another, and Localize follows CLDR. Recorded, asserted, and never "fixed".
  • Contract mismatch — the fixture was generated through an ICU API whose defaults differ from the CLDR pattern's. Not a divergence: the test has to ask the same question ICU was asked.
  • Upstream fixture issue — the conformance file itself disagrees with CLDR's own data.
  • Defect — Localize is wrong. Fix it.

The distinction matters because they are easy to confuse, and mislabelling a defect as a divergence hides a bug behind a comment. Before recording anything as a divergence, read the locale's data and confirm that Localize is rendering what CLDR ships.

Contract mismatch, in practice

CLDR 49's decimal conformance suite is generated by org.unicode.cldr.tool.GenerateDecimalFormatTestData, which builds an ICU NumberFormatter with no explicit precision. NumberFormatter's default is up to six fraction digits, where the CLDR decimal pattern #,##0.### allows three. Run the fixtures against the pattern default and thousands of cases fail — none of them because the formatting is wrong.

test/localize/number/decimal_conformance_test.exs therefore passes max_fractional_digits: 6 for the standard dimensions, matching the generator. This is why the generator source is worth reading before wiring up any new conformance suite: the fixture encodes the API call that produced it, not just the expected strings.

Recorded divergences

Number formatting

Enumerated in @divergences in test/localize/number/decimal_conformance_test.exs, which asserts the count of each rather than merely skipping them — a divergence that resolves upstream, or one that grows, moves the number and the test reports it.

LocalesDimensionCLDR shipsICU renders
gu, hi, hi-Latn, lo, mr, pa, siscientificthe pattern [#E0], brackets includedignores the locale pattern and builds the notation itself
urpercentsymbols-numberSystem-latn.percentSign is ٪ (U+066A)%, because the generator formats percent as a measure unit and ur's unit pattern is {0}%
gldecimal, percentgroup separator U+00A0.
bncompact long00000 কোটি, carrying no groupingapplies the locale's standard grouping to the mantissa
afcompact short0 m'.', whose quoted literal is a full stopdrops the full stop
en-IN, hi-Latncompact long0 million12 lakh
ig, ps, tacompactlocalised patterns 0 nde, 0 میلیونه, 0Mroot forms, or no compact form at all

The ur case is the subtlest: both ٪ and % come from CLDR, one from the number symbols and one from the unit data. Localize formats percent through the number pattern, so it uses the symbol; the generator formats it as a measure unit, so it uses the unit pattern.

Where ICU4C is used directly

Localize.Nif.number_format/3 is ICU4C's NumberFormatter and therefore produces ICU's answers, not Localize's, for every case in the table above. The NIF is built for cross-validation and for collation sort keys, not as a drop-in for Localize.Number.to_string/2; the two are not expected to agree in these locales. See the NIF section of the number formatting guide.

Date and time patterns

TR35 is CLDR's specification, and the rule applies to it as to the data. Its Date Field Symbol Table lists the widths each pattern letter takes, and its Handling Invalid Patterns recommends U+FFFD for a field at any other width and an error for a letter the table does not define. ICU4C 78.3 does neither; Localize follows TR35, asserted in test/localize/datetime/invalid_field_length_test.exs.

PatternTR35 and LocalizeICU4C renders
A numeric field past its widths: dddd, MMMMMM, HHHU+FFFDthe number zero-padded to the width, "0006"
A name field past its widths: GGGGGG, EEEEEEE, aaaaaaU+FFFDa defined width: "AD", "Sun", "AM"
zzzzz and ZZZZZZU+FFFDthe long forms, "Eastern Daylight Time" and "GMT-04:00"
Other zone widths TR35 does not list: OO, vv, xxxxxxU+FFFDnothing
An undefined letter such as nan errornothing

Week numbers (Y, w, W) are the calendar's own for a calendar that numbers its weeks (user, 2026-10-01), where ICU4C numbers every calendar's weeks by the locale's firstDay and minDays. Calendar.ISO has no weeks of its own, so Localize numbers its weeks by the locale's week data as ICU4C does (user, 2026-10-02): Y, w and the numeric weekday e agree with ICU4C 78.3 on every day of 2019 to 2030 in seventeen locales, the -u-fw-, -u-rg-, -u-sd- and -u-ca-iso8601 keys included.

W differs at a month's ends, and Localize keeps the difference (user, 2026-10-02). TR35 says a month's weeks are "similarly calculated" to a year's, with minDays "the minimum number of days in the new month for a week to count as part of that month". So Localize gives a week to the month that holds at least the locale's minimum days of it, and a pattern with W writes its month and year as the week's, as Y writes the year w belongs to. ICU4C numbers a week within its date's own month: 0 for the days before the month's week 1, and 5 or 6 for the days after its last whole week. MMMMW is "'week' W 'of' MMMM" in both locales:

DateLocaleLocalizeICU4C 78.3
1 October 2021en-GB"week 5 of September""week 0 of October"
30 September 2021en"week 1 of October""week 5 of September"
30 June 2026en"week 1 of July""week 5 of June"
30 June 2026en-GB"week 1 of July""week 5 of June"

The first row is a week holding four days of September under ISO 8601's weeks, and the others are weeks holding enough days of the month after: one in en, four in en-GB. Wherever the week is in its date's own month the two agree, which over 2019 to 2030 is every day but 432 of 4,383 in en and every day but 246 in en-GB. Localize's values are asserted from TR35's rule for every day of 2015 to 2026 in test/localize/calendar_callbacks_test.exs.

The extended year (u) is the calendar's own answer, its extended_year/3. TR35 calls it "a single number designating the year of this calendar system, encompassing all supra-year fields", with the Julian calendar's 1 BCE as year 0 for its example and 4601 for its sample value. ICU4C 78.3 writes the same number in the calendars whose years it counts as Calendrical does: the Gregorian, Julian, Japanese, Coptic, Ethiopic, Hebrew, Indian, Persian and Islamic calendars. In four others it writes a Gregorian year, where Localize writes the calendar's own (user, 2026-10-02), asserted in Calendrical's test/localize_extended_year_test.exs:

CalendarLocalize, 15 June 2026ICU4C 78.3
Buddhist25692026, the Gregorian year
ROC1152026, the Gregorian year
Chinese46632026, the related Gregorian year r writes
Dangi43592026, the related Gregorian year r writes

The year y writes in those last two calendars is no difference: Localize and ICU both write the year's place in the sixty-year cycle. TR35's U names "the year value" and, where it has no name for it, is written as y writes it, so the two letters write one number. ICU4C 78.3 writes the Chinese and Dangi year that began in 2026 as 43 with y and 0043 with yyyy, and 16 June 2026 in ko's Dangi short date "y. M. d." as "43. 5. 2.", and so does Localize (user, 2026-10-04: follow TR35), asking the calendar's cyclic_year/3 where the locale's data names the calendar's years by a cycle; asserted in test/localize/date_parse_lunisolar_test.exs. It is u alone that differs, the year's number here and a Gregorian year there.

The related year (r) keeps Latin digits whatever digits the rest of the date is written in. TR35 says of it that it "is usually displayed using the "latn" numbering system, regardless of what numbering systems may be used for other parts of the formatted date". ICU4C 78.3 writes it in the locale's digits, so the fifth month of the Chinese year that began in 2026 at root's yyyyM, "r-MM", in ar-EG is "2026-٠٥" here and "٢٠٢٦-٠٥" there, and so in every locale that writes its dates in digits of its own; asserted in test/localize/datetime/cyclic_year_symbol_test.exs.

A Hebrew year in Hebrew numerals keeps its thousands, as CLDR's hebr numbering system writes any number: 5786 is "ה׳תשפ״ו". TR35 has a pattern's numbers attribute render its "numeric quantities" in the numbering system named, and says nothing of shortening a year. ICU4C 78.3 takes 5,000 from a year of the current millennium before it writes it, "תשפ״ו", and writes 4999 in full, "ד׳תתקצ״ט"; Localize reads both. Asserted in test/localize/hebrew_numeral_date_parse_test.exs and test/localize/interval_calendar_test.exs.

A skeleton is matched against the locale's availableFormats alone, as CLDR's own skeleton conformance data (common/testData/datetime/skeletons.tsv) is made. TR35 has the list a skeleton is matched against hold "dateFormatItem elements as well as the predefined patterns", and ICU4C 78.3 adds the standard date and time formats' patterns to it: pl's Chinese yMd is root's yyyyMd, "2026-05-02", here and "2.5.43" there, its short date "dd.MM.y" being the nearer. CLDR's reference generator has those patterns behind a useStock flag its conformance data is made without, and with them 43 of that data's 279 locales fail (ru's yMdd becomes "06.7.2024"), so Localize keeps to the data (user, 2026-10-06); asserted in test/localize/datetime/skeleton_conformance_test.exs.

A skeleton for a date of another calendar is matched against that calendar's formats and then against the Gregorian calendar's. TR35 has most calendars "inherit general date format data (in the <dateFormats> and <dateTimeFormats> elements) from the "generic" calendar format data in the same locale, which in turn inherits from Gregorian", and appends the fields no format carries to "the one with the greatest number of matching fields (but no extra fields)". CLDR's root data has no alias from the generic calendar's formats to the Gregorian ones, and ICU4C 78.3 follows the data: en's Hebrew yw is the Gregorian format here, "week 39 of 5786", and the calendar's year with the week appended there, "5786 (week: 39)"; Yw is the same format here and "39 5786 AM" there; and ywE is "Tue, week 39 of 5786" here, where ICU writes no weekday. Where the calendar has a format of the other fields the two agree: yMMMdw is "1 Tamuz 5786 (week: 39)" in both. The Chinese and Dangi calendars, which TR35 excepts, are matched against their own formats alone (user, 2026-10-06), so their yw is "2026(bing-wu) (week: 18)" in both; of two formats that lack as many fields TR35 ranks the one with the month before the one with the week, so their MMMMW is "Fifth Month (week: 1)" here and "1 (month: Fifth Month)" there. Asserted in test/localize/datetime/append_items_test.exs.

Date parsing

TR35 makes y the year of the era G names, and leaves the reading of a two-digit year to the implementation. ICU4C 78.3 reads any two-digit year as one within 80 years before and 20 after today, even beside an era, so it cannot read back the "44 BC" it formats itself. Localize takes a year its era qualifies as written where the format writes the year in full (y), and applies the pivot to a year written without an era and, as ICU does, to one the format writes as yy, its two low-order digits; asserted in test/localize/date_parse_era_test.exs and test/localize/date_parse_lunisolar_test.exs.

Input in enTR35 and LocalizeICU4C parses
"Jun 1, 44 BC"44 BC, ~D[-0043-06-01]2044 BC
"Jun 1, 44 AD"AD 44, ~D[0044-06-01]2044

The two agree on everything else here: "Jun 1, 1 BC" is year 0, and "Jun 1, 44", with no era, is 2044.

A week the year does not have is an error in Localize and a later week in ICU4C, and Localize keeps the difference (user, 2026-10-02). ICU4C's parser is lenient: in en, where 2026 has 52 weeks, it reads "week 53 of 2026" as the week beginning 27 December 2026, which is week 1 of 2027, and "week 60 of 2026" as a week of February 2027. TR35's note on week data recommends the first of those readings, saying a week 53 parsed for a year without one "should be treated as in the first week of the following year". Localize reads only a week the year has, in the weeks it writes, and returns a Localize.DateParseError for any other, as it does for every field value no date has, so text that names no week is reported rather than moved.

InputLocaleLocalizeICU4C 78.3 parses
"week 53 of 2022"en~D[2022-12-25]25 December 2022
"week 53 of 2026"enan error27 December 2026
"week 60 of 2026"enan error14 February 2027
"Woche 53 des Jahres 2026"de~D[2026-12-28]28 December 2026
"Woche 53 des Jahres 2025"dean error29 December 2025

Calendar.ISO's weeks are the locale's, so the weeks a year has depend on the locale: 2026 has 53 weeks in de and 52 in en. Asserted in test/localize/impossible_fields_parse_test.exs.

Time parsing

TR35 has a flexible day period (B) the locale gives no name formatted with its AM and PM names, and ICU4C 78.3 formats it so, but it does not read those names back in a B field: oc, which has no day-period rules, writes 23:59 in its Bhm pattern "h:mm B" as "11:59 PM", which ICU cannot parse. Localize reads a B field's AM and PM names as a reads them, so the time parses back; asserted in test/localize/time_parse_test.exs.

TR35's time zone parsing reads a zone written in any form — a specific or generic name, a location, a city, a zone ID or the localized GMT format — and ICU4C 78.3's TimeZoneFormat does too, but its date formats read in a zone field only the forms of that field's letter and the GMT format: "Eastern Time" in the zzzz field of en's full time is a parse error. Localize reads every form in every zone field, as TR35's process does, and reads a name whatever apostrophe or spacing it is written with, as TR35 allows a parse to ("heure d'été de l'Est nord-américain" with ASCII apostrophes), where ICU requires the name's own; asserted in test/localize/zone_parse_test.exs.

Input in enTR35 and LocalizeICU4C parses
"10:00:00 AM Eastern Time" in h:mm:ss a zzzz10:00 in New Yorkan error
"10:00:00 AM New York Time" in h:mm:ss a z10:00 in New Yorkan error

Reading every form in every field, Localize reads a string one way where ICU reads it by the field. en names the Kyrgyzstan metazone "Kyrgyzstan Time" and nothing else, which is Bishkek's location format too; by TR35's type fallback a metazone with no daylight name needs none and its standard name stands for all three types, so Localize reads it as Bishkek's own time in the summers the zone kept daylight time. ICU4C 78.3 reads it so in a VVVV or vvvv field, and as standard time, an hour later, in a zzzz field; asserted in test/localize/zone_parse_test.exs.

"1990-07-15 10:00:00 Kyrgyzstan Time"LocalizeICU4C parses
in y-MM-dd HH:mm:ss VVVV03:00 UTC03:00 UTC
in y-MM-dd HH:mm:ss zzzz03:00 UTC04:00 UTC

TR35's parsing reads the localized GMT format with "non-Latin numbers", and ICU4C 78.3 reads any of Unicode's decimal digits in it, in any locale. Localize reads the digits of every CLDR numbering system in any locale, among them those of hanidec, which are no decimal digits to Unicode and which ICU reads only where the locale asks for them. It reads an ISO 8601 offset in other digits too, which TR35 does not speak of and ICU reads in 0 to 9 alone: Localize's date and time parsers read all of a time in the locale's digits, its offset with it, and the zone reader reads the same text alone. Asserted in test/localize/zone_parse_test.exs.

InputLocalizeICU4C parses
"GMT+٠٥:٣٠" in en+05:30+05:30
"GMT+五:三〇" in en+05:30an error
"+٠٥:٣٠" in en or ar-EG+05:30an error

TR35's parsing has the localized GMT format matched leniently: its number as "03, 3, 330, 3:30, 33045 or 3:30:45", with "+, -, or nothing" before it and spaces after "GMT", after the sign and before the number. ICU4C 78.3 reads the number as the locale's hourFormat writes it, so in en it wants the sign, the colons and no spaces, and reads digits with nothing between them only in a locale that writes them so, as am does. Localize reads each of TR35's forms in every locale. A number with no sign is an offset east of the literal it follows, in a zone read on its own (Localize.DateTime.Timezone.parse_zone/2, parse_offset/2, resolve/3), which is how TR35 describes its parsing; a zone that is a field of a date or time keeps needing the sign, since the number after it can be the year or the hour that follows ("12:00:00 UTC 2026", zh's "GMT 12:00:00"), and a number before a literal that follows it needs one everywhere, being as much a time of day ("10:30 GMT"). Both write an offset's seconds, which TR35 makes an optional field without saying where it goes: after the minutes, behind what the locale's hourFormat has between its hours and its minutes, as ICU places them. Asserted in test/localize/zone_parse_test.exs and test/localize/datetime/timezone_format_test.exs.

Input in enTR35 and LocalizeICU4C parses
"GMT+3:30:45"+03:30:45+03:30:45
"GMT+33045"+03:30:45an error
"GMT+330"+03:30an error
"GMT +3"+03:00an error
"GMT 3"+03:00an error

TR35 gives each hour field its range, h 1 to 12, H 0 to 23, K 0 to 11 and k 1 to 24, and its parsing notes take a number beyond a field's range for no value of that field. ICU4C 78.3's parser, lenient unless told otherwise, carries such an hour on into the hours that follow, and into the next day where they run out; told not to be lenient, it refuses it. Localize refuses it, and refuses the three values at a field's edge that TR35's ranges leave out: k's 0 and h's 0, which ICU reads as midnight either way, and K's 12, which ICU reads as half past noon when it is lenient. Asserted in test/localize/time_parse_test.exs.

InputPatternLocalizeICU4C, lenientICU4C, not lenient
"13:30 PM"h:mm aan error01:30 the next dayan error
"13:30 AM"h:mm aan error13:30an error
"24:00"H:mman error00:00 the next dayan error
"0:30"k:mman error00:3000:30
"12:30 AM"K:mm aan error12:30an error
"0:30 AM"h:mm aan error00:3000:30

TR35 says a 24-hour pattern should have no day period, and that in parsing "the dayperiod is checked for consistency with the hour". One pattern in CLDR 49 has both, ksh's Hmsv, "H:mm:ss a v". Localize reads a 24-hour hour beside a day period where the two agree and refuses it where they do not, which leaves "4:00:00 n.M.", four in the afternoon, to the 12-hour pattern that wrote it. ICU4C 78.3 takes the day period and drops the hour, lenient or not. Asserted in test/localize/time_parse_test.exs.

InputPatternLocalizeICU4C
"16:00 PM"H:mm a16:0012:00
"4:00 PM"H:mm aan error12:00
"4:00 AM"H:mm a04:0000:00
"16:00 AM"H:mm aan error00:00

Time zone formatting

TR35's generic non-location format (v, vvvv) qualifies a metazone name, unless the zone is the metazone's preferred zone for the locale's country, with the zone's country where it is that country's preferred zone and with its city otherwise; CLDR's own TimezoneFormatter does exactly that. ICU4C 78.3 qualifies a metazone name only where the zone's offset at that moment differs from the preferred zone's, and writes a zone keeping no daylight time within 184 days by its standard name, so even TR35's worked example "Pacific Time (Canada)" for Vancouver in en_MX is "Pacific Time" in ICU. Localize follows TR35, and both read back to the same instant; asserted in test/localize/datetime/generic_zone_format_test.exs.

vvvv in en, July 2023TR35 and LocalizeICU4C formats
Europe/BerlinCentral European Time (Germany)Central European Time
America/VancouverPacific Time (Canada)Pacific Time
America/PhoenixMountain Time (Phoenix)Mountain Standard Time
America/New_York in en-JM, JanuaryEastern Time (United States)Eastern Time

The specific format (z, zzzz) takes the same steps, which TR35 gives for "the non-location formats (generic or specific)", and CLDR's TimezoneFormatter qualifies a specific name as it does a generic one. ICU4C 78.3 never qualifies a specific name, so Vancouver's and Los Angeles's are one string. Localize follows TR35 and CLDR's formatter (user, 2026-10-04), and a qualified name reads back as its own zone; asserted in test/localize/datetime/specific_zone_format_test.exs.

zzzz in en, July 2026TR35, CLDR's formatter and LocalizeICU4C formats
Europe/BerlinCentral European Summer Time (Germany)Central European Summer Time
America/VancouverPacific Daylight Time (Canada)Pacific Daylight Time
America/PhoenixMountain Standard Time (Phoenix)Mountain Standard Time
Africa/AbidjanGreenwich Mean Time (Côte d’Ivoire)Greenwich Mean Time

Where a locale has no name for a zone, Localize and ICU both write the localized GMT format, the short one for z and the long one for zzzz. TR35 49's symbol table gives zzzz the short one, while its text on semantic skeletons has the offset follow the length; Localize keeps the long, as ICU4C and ICU4X write it (user, 2026-10-04). CLDR's TimezoneFormatter is not followed there: it writes a region format ("Jordan Time"), which TR35 49 removed, for a long name and the long GMT format for a short one.

TR35's location format (VVVV) names a zone's country by "short country name, if it exists, otherwise the country name". ICU4C 78.3 writes the full name, so the two differ wherever a locale gives such a country a short name. Localize follows TR35 and reads either name back; asserted in test/localize/datetime/location_zone_format_test.exs. Both write the code of a country the locale does not name ("CU" for Havana's zone in su).

VVVV in enTR35 and LocalizeICU4C formats
Europe/LondonUK TimeUnited Kingdom Time
Asia/Hong_KongHong Kong TimeHong Kong SAR China Time
Asia/YangonMyanmar TimeMyanmar (Burma) Time
Europe/SarajevoBosnia TimeBosnia & Herzegovina Time

Interval formatting

An interval takes the formats of its endpoints' calendar. ICU4C 78.3 differs from Localize in eleven places, asserted in test/localize/interval_calendar_test.exs, test/localize/interval_skeleton_test.exs, test/localize/interval_closest_match_test.exs, test/localize/interval_standard_format_test.exs and test/localize/date_parse_lunisolar_test.exs; the third follows ECMA-402 and the others TR35.

IntervalLocalizeICU4C renders
nl Buddhist medium, 10:00 to 10:30 on 1 April 2566"1 apr 2566 BE 10:00:00 – 1 apr 2566 BE 10:30:00""1 apr 2566 BE, 10:00:00 – 10:30:00"
en yMMMdHms, 12:00:05 to 14:30:07 on 8 April 2026"Apr 8, 2026, 12:00:05 – Apr 8, 2026, 14:30:07""Apr 8, 2026, 12:00:05 – 14:30:07"
en Chinese medium, rMMMd"Mo2bis 11 – 20, gui-mao""Mo2bis 11, 2023 – Mo2bis 20, 2023"
am medium, equal endpoints"1 ኤፕሪ 2023""ኤፕሪ 1 2023"
en Hm, 10:00 on 15 June to 14:30 on 16 June 2026"10:00 – 14:30""6/15/2026, 10:00 – 6/16/2026, 14:30"
en yMMMMdHm, the same two days"June 15, 2026, 10:00 – June 16, 2026, 14:30""June 15, 2026 at 10:00 – June 16, 2026 at 14:30"
am short, 1 to 10 April 2023"01/04/2023 – 10/04/2023""1/4/2023 – 10/4/2023"
en yMMdd, the same two days"04/01/2023 – 04/10/2023""4/1/2023 – 4/10/2023"
en yQQQ, 15 April to 20 May 2026"Q2 2026""Q2 2026 – Q2 2026"
en E, 15 to 22 June 2026, both Mondays"Mon""Mon – Mon"
en QQQ, 15 June 2026 to 15 June 2027"Q2""Q2 – Q2"
en MMMd, 28 December 2026 to 3 January 2027"Dec 28 – Jan 3""Dec 28, 2026 – Jan 3, 2027"
en MMMd, 15 June 2026 to 15 June 2027"Jun 15""Jun 15, 2026 – Jun 15, 2027"
en d, 28 June to 3 July 2026"28 – 3""6/28 – 7/3"
en yMMMHm, 10:00 on 15 June to 14:30 on 16 June 2026"Jun 2026, 10:00 – Jun 2026, 14:30""Jun 15, 2026, 10:00 – Jun 16, 2026, 14:30"
en MMMdHm, 10:00 on 15 June 2026 to 14:30 on 15 June 2027"Jun 15, 10:00 – Jun 15, 14:30""Jun 15, 2026, 10:00 – Jun 15, 2027, 14:30"
en medium, 31 December 1 BC to 1 January AD 1"Dec 31, 1 – Jan 1, 1""Dec 31, 1 BC – Jan 1, 1 AD"
ja long, 1 to 10 April 2023"2023年4月1日~10日""2023/04/01~2023/04/10"
zh Chinese medium, 14:30:45 to 15:30:45 on 16 June 2026"2026年五月初二 14:30:45–2026年五月初二 15:30:45""2026年五月2 14:30:45–15:30:45"
en Chinese Md, 16 June 2026 to 12 June 2086"5/2""5/2 – 5/2"
he Hebrew medium, 1 to 5 Tamuz 5786"א׳–ה׳ בתמוז ה׳תשפ״ו""1–5 בתמוז 5786"
  • The date-time pattern. TR35 joins a date to a time range with the calendar's dateTimeFormat. ICU's DateIntervalFormat reads calendar/gregorian/DateTimePatterns whatever the calendar, though its single date-time formatter uses the calendar's own.

  • Year symbols. TR35 finds the closest interval item "as in availableFormats", where symbols "representing the same type (year, month, day, etc)" are a distance apart and never a failed match, so the related year r and the cyclic year's name U of a Chinese or Dangi format take the items CLDR keys by y. The item's pattern is then adjusted as TR35 adjusts any: "adjustments should never convert a numeric element in the pattern to an alphabetic element, or the opposite". A numbered year in the item is written as the numbered year asked for, a name stays a name and a number a number: en's medium interval, from "MMM d – d, U", is "Mo5 2 – 6, bing-wu" beside the date "Mo5 2, 2026", and de's, from "y-MM-dd – y-MM-dd", "43-05-02 – 43-05-06" beside "02.05 bing-wu". ICU's interval matcher compares letters one for one, finds no item, and writes both dates in full, "Mo5 2, 2026 – Mo5 6, 2026" and "02.05 bing-wu – 06.05 bing-wu".

  • Years of another cycle. TR35 writes two dates in full about the fallback pattern where their greatest difference is a field the interval item has no pattern for, and as one date where no field of the pattern differs. It does not say whether the sixty-year cycle of the Chinese and Dangi calendars is such a field, and two dates sixty years apart have one year, month and day. ICU holds the cycle as an era, so they differ in a field no item is keyed by, and it writes both in full in the skeleton's own format: "5/2/2026 – 5/2/2086" for en's yMd, 16 June 2026 to 12 June 2086. Localize writes them in full too, TR35 leaving it open. A skeleton that writes no year writes none: two dates sixty years apart are alike in every field of Md, and are one date, "5/2", by TR35's step 4, where ICU, to which they differ in an era, writes "5/2 – 5/2".

  • Equal endpoints. Localize writes the requested standard format, as ECMA-402's formatRange and Localize.Date.to_string/2 do. ICU writes its pattern generator's pattern for the style's skeleton, which prefers an availableFormats entry with that skeleton over the standard format.

  • Whole datetimes across days. TR35 says an interval takes the standard date-time pattern, its example "March 15, 9:00 AM – March 16, 5:00 PM", and Localize joins each datetime's date and time with it (user, 2026-10-02). ICU does so for a date joined to a time range, but formats each whole datetime of a fallback with its single-date pattern, which takes the "at" pattern.

  • Two times of a day no interval item is keyed by. TR35's algorithm looks the time fields of a skeleton up as an interval item and joins the date, written once, to it; where there is none its last step applies, "format the start and end datetime using the fallback pattern", in which "{0} is replaced by the start datetime, and {1} is replaced by the end datetime". No interval item of CLDR's has seconds, so at a format that writes them, the :medium, :long and :full formats among them, two times of one day are each written whole. ICU writes the date once and the two times around the fallback pattern (user, 2026-10-06: TR35).

  • A time skeleton across days. TR35's interval algorithm, read as it is written, finds no day difference in a time item and formats both values with the skeleton around the fallback pattern. ICU adds the locale's yMd date to a skeleton of time fields alone when the days differ (user, 2026-10-01: not the ICU approach).

  • Numeric widths. TR35 finds the closest interval item "as in availableFormats", whose pattern takes the field lengths of the skeleton requested, so a numeric month and day are padded as the skeleton asks: am's short date is "dd/MM/y" with the skeleton yMMdd, and its yMd item "d/M/y – d/M/y" is written as the single date is, "01/04/2023". ICU adjusts the width of the year (yy) and of a month written as a name (MMMM), but writes a numeric month and day at the item's own widths (user, 2026-10-04: TR35).

  • Standard formats. An interval at a standard format takes the fields of the pattern the single date is written with, at the pattern's widths, where CLDR's datetimeSkeleton for the format is not the pattern's (vi's short date "d/M/yy" beside yMMdd), as ECMA-402's formatRange does with the skeleton ICU makes of a style's pattern (DateTimePatternGenerator::staticGetSkeleton). That skeleton is the pattern's letters as they stand, so a month written as a number beside a word is a numeric month to it, and ja's long date "y年M月d日" takes the yMd interval. Localize keeps such a month the name CLDR's skeleton gives it (yMMMd), whose interval writes "M月" as the date alone does.

  • Fields compared as written. TR35 formats one date where "there is no difference among any of the fields in the pattern", and Localize compares the value of each field a date interval's format writes, whether or not CLDR has an interval item for it: two dates in one quarter are one quarter, and two Mondays one weekday. ICU ranks calendar fields by size and writes both dates wherever they differ in a field as large as the smallest in the pattern, though the two halves read the same. It makes no pattern for a skeleton of weeks (yw, MMMMW), for which Localize writes both weeks around the fallback pattern, or one where the days are in the same week.

  • A field the skeleton does not write. TR35's algorithm writes the fields of the skeleton asked for and no others. Two values alike in every field of the pattern are "a single date"; where the item has no pattern for their greatest difference, "the start and end datetime" are written "using the fallback pattern", each with the skeleton; and for a date and a time "the result will be the same for each greatestDifference of a day or longer". So MMMd from 28 December to 3 January is "Dec 28 – Jan 3", the same day of two years "Jun 15", d from 28 June to 3 July "28 – 3", yMMMHm on two days of June "Jun 2026, 10:00 – Jun 2026, 14:30", and two dates of two eras at a format with no era are written without one. ICU adds the day, the month, the year or the era the two differ in, to a skeleton CLDR has an interval item for, and writes a skeleton it has none for as it stands (Ed, QQQ). Localize adds nothing (user, 2026-10-06: TR35).

  • The date's numbering. CLDR gives a date pattern the numbering its numeric fields are written in, the numbers attribute, and each date a date-time interval writes is the date as Localize.Date.to_string/2 writes it, in that numbering: zh's Chinese day is "初二" and he's Hebrew date is in Hebrew numerals. ICU's DateIntervalFormat works from the format's skeleton, which carries no numbering, and writes digits, though its single date-time formatter writes "2026年五月初二 14:30:45" as Localize does. An interval of two dates at a standard format takes the format's numbering too, since it is made from the format's skeleton, which CLDR gives the same numbers attribute, and TR35 has that attribute cover "all of the numeric fields in the date format": zh's Chinese medium interval is "2026年五月初二至初六" and he's Hebrew "א׳–ה׳ בתמוז ה׳תשפ״ו". ICU writes digits there as well. At a skeleton, which states no numbering, both write digits, "2026年五月2至6".

Relative time

Without :unit, Localize.DateTime.Relative takes the largest unit of which a whole one lies between two moments, reckoned as ECMA-262 Temporal reckons it: its DifferenceZonedDateTime across a change of UTC offset, and its leap-to-common month rules in a lunisolar calendar. ICU4C 78.3's Calendar::fieldDifference differs in two places, asserted in test/localize/datetime/relative_test.exs. Localize agrees with a step-by-step implementation of the specification on 293,336 date-time pairs around 1,348 offset changes. On 42,035 date pairs in each of ten calendars it agrees with ICU except in the second case, 370 Chinese and 380 Dangi pairs, and in 39 Chinese pairs about the new year of 2027, which ICU starts on 7 February, a day after the Hong Kong Observatory and Calendrical.

From → toLocalize and TemporalICU4C
New York, 01:50 on 31 October 2026 → 01:10 EST on 1 November24 hours 20 minutes1 day
Chinese, the 2nd of the leap fourth month of 2020 → the 2nd of the fourth month of 20211 year12 months
  • A repeated hour. Temporal compares the wall clocks first, and 01:10 is short of 01:50. ICU reaches days and weeks by adding elapsed time, which lands on the first 01:50, but months and years through its fields, which resolve a repeated time at its second occurrence (UCAL_WALLTIME_LAST), so its answer turns on the unit.

  • A leap month. Temporal takes a leap month that the next year lacks as the ordinary month it doubles (its skip-backward rule for the Chinese and Dangi calendars). ICU takes the next new moon, the month after it.

  • A named form. TR35's relative is a name "for the current instance of the field, and one or two past and future instances", an instance being a whole number of the field away, and ECMA-402's numeric: "auto" takes the name for that number alone. So only a whole offset is named: 0.9999 days is "in 1 day", the number as it is displayed. ICU4C 78.3 takes an offset within half a hundredth of a whole number for the instance, so 0.996 days is "tomorrow" there and 0.004 days "today" (user, 2026-10-06).

Durations

Localize.Duration.new/2 measures two date-times in time zones as the same specification does (user, 2026-10-02): the later is moved to the earlier's time zone, whole days are counted on that wall clock, and the hours, minutes and seconds are the time that passes after them. It agrees with a step-by-step implementation of DifferenceZonedDateTime on 322,464 of 324,207 pairs about 36 changes of clocks in twelve time zones, and differs in one case, which it keeps. A sample of the pairs is asserted in test/localize/duration_zoned_matrix_test.exs.

From → toLocalizeThe specification's steps
New York, the second 01:30 on 3 November 2024 → 01:10 on 4 November23 hours 40 minutes24 hours 40 minutes
  • From the second occurrence of a repeated time. Where no whole day lies between the two, the specification finds the earlier moment again from its wall-clock time, which it resolves at its first occurrence, an hour before the moment itself. Localize counts from the earlier moment, so the hours are the time that passes, which is what DateTime.diff/3 gives.

Not divergences

These also appear as exclusions in the test suites and should not be read as deliberate differences:

  • Unimplemented features. test/localize/locale/locale_display_test.exs excludes line 47, the uu attribute in the -u- extension. That is a gap, not a decision.

  • Calendars that are not enabled. test/localize/datetime/conformance_test.exs keeps a @wrong_format list of non-Gregorian cases that never run; they are held for whenever the other calendars are switched on. The timezone-resolution and skeleton-matching exclusions that used to sit here are gone: zone name resolution landed, the date/time split path now adjusts the matched pattern's field widths, and :tz is a test dependency so a real offset is available.

  • Upstream fixture issues. The same file's @invalid_test_results and the locale-display suite's @invalid_test_results mark cases where the conformance data disagrees with CLDR's own data.

Collation carries no exclusions at all: all 210,155 pairs in both CLDR conformance files pass under both strengths.

Adding a divergence

  1. Read the locale's data. Open the relevant file in the CLDR JSON, under $CLDR_PRODUCTION/cldr-*-full/main/<locale>/, and find the pattern or symbol in question. If Localize is not rendering what is there, it is a defect — stop and fix it.
  2. Check what produced the fixture. Find the generator in $CLDR_REPO/tools/ and read the API call. A contract mismatch is fixed in the test, not recorded as a divergence.
  3. Record it with its evidence. Name what CLDR ships and what ICU renders, so the entry can be re-checked without repeating the investigation.
  4. Assert the count, not just the membership. An exclusion that silently absorbs new failures stops being a record and becomes a blind spot.