Skip to content

Localize version 1.4.0

Choose a tag to compare

@kipcole9 kipcole9 released this 28 Oct 05:25
· 74 commits to main since this release

[1.4.0] — October 23rd, 2026

Release notes

  1. Built on CLDR 49 and Unicode 18. CLDR 49 no longer publishes 114 locales below Basic coverage, collation follows UCA 18, and its new data is supported, from dual and UTC offset formats to new units and the Unicode 18 currency signs.

  2. Significant work on date and time formatting towards TR35: semantic skeletons, append items, qualified metazone names, cyclic years, and a calendar's own weeks and quarters. Many outputs change; see the breaking changes below.

  3. Date, time, date-time and interval parsing move from Calendrical into Localize, so an application that uses only the proleptic Gregorian calendar no longer needs Calendrical.

  4. Time zones are parsed and resolved by Localize.DateTime.Timezone.parse_zone/2, resolve/3 and parse_offset/2, and Localize.TimeZoneDatabase handles fixed offsets.

  5. Calendars other than Calendar.ISO come from Calendrical 2.0, which ships the same day, and a locale's -u-ca- converts a value into its calendar.

  6. Localize.Inflection is added as a technical preview: inflected words, pronouns and quantities, MessageFormat 2's i: functions and inflected units.

  7. CLDR 49's technical previews are supported and marked as such: ordinal days, numeric date and time separators, the c12 and c24 hour cycles, semantic skeletons and rational number formats.

  8. Number parsing reads currency amounts and a locale's alternative separators (:currency, :separators).

  9. Locale data is verifiable and self-hostable: mix localize.verify_locales, Localize.cldr_repo_ref/0 and a self-hosting guide.

  10. Public functions return {:error, exception} for arguments and options of the wrong type, where they previously raised.

Breaking changes

Localize 1.4 includes a significant number of breaking changes but they are all a result of improved conformance with TR35 and would not, in typical use, be expected to be observed in the majority of current use cases.

Calendars

  • A calendar other than Calendar.ISO must implement the Calendrical 2.0 behaviour, and any other is refused with Localize.UnknownCalendarError. Use Calendrical 2.0 with Localize 1.4.

  • A locale's -u-ca- converts a value into that calendar before it is formatted: en-u-ca-hebrew writes a Gregorian date as a Hebrew one. The calendar comes from the provider registered with Localize.Calendar.register_provider/1, which Calendrical 2.0 registers when it starts.

  • MessageFormat 2's calendar option names the calendar on the message's locale, as -u-ca- does, rather than a calendar module.

  • The parse functions' :calendar option is a calendar module, and the date is returned in it. A CLDR calendar name such as :hebrew, or a module that is no calendar, returns Localize.UnknownCalendarError.

  • The parse functions no longer take :return_calendar. Use Date.convert/2 on the result.

  • Localize.DependencyRequiredError is removed. Nothing returns it, since Localize no longer reaches for Calendrical by name.

  • A numeric month is written as the calendar writes it (written_date/3): a Hebrew common year's Adar is "6", as ICU writes it, where it was "7".

  • y in the Chinese and Dangi calendars is the year's place in the 60-year cycle: the year that began in 2026 is "43", where it was "4663". u writes the year's number.

  • Quarters and week numbers are the calendar's own: a Coptic or Hebrew thirteenth month is in Q4, where it was Q5. Calendar.ISO keeps the locale's weeks.

  • W numbers a month's weeks as a year's are numbered, and its pattern writes the month that week belongs to: 1 October 2021 is "week 5 of September" in en-GB, where it was "week 0 of October".

  • A date in a calendar of weeks is written with CLDR's generic calendar, "M06 16, 2026 AD", where its week was written as its month.

  • A calendar's own formats are read before ISO 8601, so "2023-11-22" read in the Chinese calendar is a Chinese date, where it was a Gregorian date converted.

  • Text that none of a calendar's formats reads is read in the calendar its info/0 names as :parsing_calendar, then converted: "Feb 1, 2024" in Calendrical.ISOWeek is 1 February 2024.

  • Japanese eras before Meiji start on proleptic Gregorian dates, so each moves by days to weeks: CLDR recorded their lunisolar proclamation dates.

Formatting dates and times

  • Localize.DateTime.to_string/2 joins date and time with the locale's "at" pattern by default: en :long is "July 6, 2024 at 2:30:45 PM". style: :default restores the previous output; :medium and :short are unchanged.

  • An interval joins date and time with the locale's standard date-time pattern, not the "at" pattern: "June 15, 2026, 10:00 – 14:30". style: :at restores it.

  • An interval at a standard format writes the fields and widths of that format's single-value pattern: vi :short is "1/4/23 – 10/4/23", where it was "01/04/2023 – 10/04/2023".

  • Two times on one day at a format with seconds are each written whole: "Apr 8, 2026, 12:00:00 PM – Apr 8, 2026, 2:00:00 PM". time_format: :short, or a skeleton without seconds, writes the date once.

  • Skeletons match the locale's availableFormats only, as CLDR's pattern generator does, not its standard formats: ko :yMd is "2024/7/6".

  • A pattern or skeleton that asks for a field the value lacks returns {:error, %Localize.DateTimeInvalidInputError{}}, where it left the field blank: "Jun , 2026".

  • A partial date takes its skeleton from the fields it has: %{year: 2024, month: 6} is "Jun 2024", where it was "6/2024".

  • The numeric weekday fields e and c count from the locale's first day of the week: Saturday is 7 in en and 6 in de, where both gave 6. cc is one digit.

  • A pattern field at a width TR35 does not list, such as MMMMMM or HHH, formats as U+FFFD.

  • Localize.Calendar.localize/3 returns a Localize.DateTimeInvalidInputError for a value that lacks the field it names, where it gave the field's first value: localize(%{year: 2026}, :month) was "January".

Time zones

  • A metazone name is qualified by the zone's country or city unless the zone is the metazone's preferred zone for the locale's country: in en, Berlin is "Central European Time (Germany)" (v) and "Central European Summer Time (Germany)" (z).

  • A zone's standard name stands in for a missing generic name only where the zone keeps one offset for 184 days either side, so London's summer is no longer "Greenwich Mean Time".

  • A zero offset is spelled out in the localized GMT format, "GMT+00:00" long and "GMT+0" short. Localize.DateTime.Timezone.gmt_format/3 drops its :zero_format option.

  • EST5EDT, CST6CDT, MST7MDT and PST8PDT are zones of their own in CLDR 49, not aliases of New York, Chicago, Denver and Los Angeles, so they format by their offset.

Parsing

  • A date-time parsed with an offset keeps its wall time and offset: "2026-05-23T14:30:00+05:00" is 14:30 at +05:00, where it was 09:30 UTC. DateTime.shift_zone/3 gives the previous result.

  • The time zone of a date-time parsed with an offset is the offset itself, "+05:00", where it was "Etc/UTC". DateTime.add/4 and DateTime.shift/3 on it need the new Localize.TimeZoneDatabase.

  • A named zone in parsed text resolves to a DateTime through the configured time zone database, where it was dropped unless Calendrical was installed. A zone the locale does not write, such as "XQZV", is an error.

Relative time

  • Localize.DateTime.Relative counts the calendar periods between two dates with the calendar's diff/3 and shift/6, not a mean month or year: 1 February is "next month" from 31 January. Hours count the clock, so 00:01 is "in 1 hour" from 23:59.

  • Without :unit, the largest unit of which a whole one lies between the two values is chosen, as ECMA-262 Temporal chooses it. A number of seconds is counted in weeks at most.

MessageFormat 2

  • {$t :time} and {$d :datetime} give the time to the minute, as TR35 specifies; precision=second restores the seconds. A date or time option other than timeZone, hour12 and calendar set from a variable is an error.

Locale data and display names

  • CLDR 49 drops 114 locales below Basic coverage, among them aa, ab, an and ht. Such a locale resolves to a fallback, :und for aa.

  • An unsupported :prefer value is an error, where it resolved to :standard. :prefer now applies to Localize.Language, Localize.Territory and Localize.Script too.

  • Collation moves to Unicode 18 (UCA 18.0.0), so any stored Localize.Collation.sort_key/2 must be regenerated. compare/3 is unchanged for existing characters.

Added

  • An ISO 8601 week date parses in any of its forms, "2026-W25-2", "2026W252" or "2026-W25", as a week and day of the calendar it is read in.

  • Localize.Number.Parser.parse/2 takes :currency and reads an amount with that currency's separators: de-AT writes € 1.234,56 but 1 234,56 plain, so "1.234,56" parses only with the option.

  • Localize.Number.Parser.parse/2 takes :separators, naming the set of decimal and grouping separators to read with for a locale that has more than one: en-ZA writes 1 234,56 with its :standard set and 1,234.56 with its :us set. A locale without the named set reads with its standard one.

  • Localize.Locale.data_locale_id/1 returns the id of the locale whose data is loaded for a locale, which is its own unless a fallback supplied it. Language rules now follow that locale so they agree with its text, while territory preferences keep following the requested locale.

  • Localize.Number.Symbol.for_currency/2 returns the symbols an amount of a currency is written with, taking the currency's own decimal separator over the locale's currency separators over its plain ones.

  • The day of the month (d) is the day the calendar writes its date with, its written_date/3, so Calendrical's Julian year-start variants write 25 March 1750 from month 1 day 1.

  • Such a date reads back through the calendar's date_from_written_date/3: "March 25, 1750" is month 1 day 1 again, alone, in a map or at either end of an interval.

  • The months between two dates are counted by the calendar's diff/3, so Calendrical's March25 months 1 and 13, both named March, are two months.

  • Every parse function refuses text longer than :max_parse_length, 1,024 bytes by default, with a Localize.DateTimeParseLengthError. The longest text any locale writes is 666 bytes, and reading cost grows with the square of the length.

  • Localize.Territory.Hemisphere.hemisphere/1 returns the hemisphere of a territory or locale, :northern, :southern or :ambiguous where the equator crosses it: hemisphere(:AU) is {:ok, :southern}. A region is answered from the territories it contains.

  • Localize.Calendar.register_provider/1 registers the module that supplies calendar modules for a locale's -u-ca-, and calendar_provider/0 returns it. Calendrical 2.0 registers itself when it starts.

  • Localize.Date.parse/2, Localize.DateTime.parse/2 and Localize.Interval.parse/2 read a composite calendar's date in each calendar its parsing_calendars/0 names, so "Mo5 11, 1872" in Calendrical.Reform.Japan reads back.

  • Localize.Date.parse/2 takes :format, a standard format, skeleton or pattern, and reads the text with it alone: mt's :yMd "4/3/2024" is 3 April. Localize.Interval.parse/2 takes it as the interval's format.

  • Localize.DateTime.parse/2 reads the date and the time with the two halves of :format: "3/4/2024 22:05" with format: "d/M/y HH:mm". :date_format and :time_format set one half.

  • Localize.Time.parse/2 takes :format too, a standard format, a skeleton or a pattern, and reads the text with that format alone: "14h30" with format: "HH'h'mm". A :long or a :full format reads a time with its zone and, as it is written for a time that has none, without it.

  • A date's months, days and patterns are named from the CLDR calendar in effect on that date, the calendar's cldr_calendar_type/3: Japan's lunisolar dates before 1873 take the Chinese calendar's names.

  • Localize.Number.resolve_currency/2 and resolve_currencies/2 resolve a symbol several currencies share by the nearest locale that uses one, so "$" is the Canadian dollar in fr. It stays unknown where no locale is a clear fit, as in es.

  • Localize.Interval.to_string/3 and to_parts/3 take a skeleton or a pattern as a date interval's :format or :date_format, as CLDR keys interval formats by skeleton, so a format no standard format reaches can be named: format: :yMMMEd is "Mon, Jun 15 – Thu, Jun 18, 2026" in en.

  • Localize.DateTime.Timezone.parse_zone/2 reads a time zone in any form a locale writes one — a name, location, city, zone ID or localized GMT format — as TR35's time zone parsing does, and resolve/3 resolves it at a date and time through the configured time zone database.

  • Localize.Inflection, a technical preview, inflects words from CLDR's inflection data, with pronoun/2,3 and quantify/4: inflect("Haus", :de, %{case: "dative", number: "plural"}) is "Häusern". Data for 48 languages comes from mix localize.download_inflection.

  • MessageFormat 2 gains the Unicode inflection project's i: functions, :i:inflect, :i:pronoun, :i:quantify, :i:list and :i:numeral, with SSML output (output: :ssml). Localize.Message.Namespace lets an application register a namespace of its own.

  • Localize.Unit.to_string/2 takes :inflect, synthesizing a pattern when the requested :grammatical_case has no CLDR one — :safe uses attested paths only, :always also guesses from suffixes. Localize.Unit.grammatical_gender/2 returns a unit's gender.

  • Localize.Locale.LocaleDisplay.key_name/2 and type_name/3 return a BCP 47 key's localized name and the name of one of its type values — key_name(:ca) is "Calendar", type_name(:ca, :buddhist) is "Buddhist Calendar", and "Buddhist", CLDR's scope="core" name, with prefer: :menu. A value may be given in its BCP 47 form, :gregory or :level1.

  • Localize.Locale.LocaleDisplay.display_name/2 takes key_value_display: :separate, TR35's SeparateKeyValue, so en-u-ca-gregory is "English (Calendar: Gregorian)".

  • Localize.Calendar.display_name(:calendar, calendar) takes prefer: :menu for the short name a menu of calendars uses, "Gregorian", and accepts BCP 47 names such as :gregory.

  • Localize.affirmative_responses/1 and negative_responses/1 return CLDR's POSIX yesstr / nostr forms — {:ok, ["ja", "j"]} for :de — and affirmative?/2 and negative?/2 match a response against them, folding case as TR35 requires.

  • Localize.Locale.LocaleDisplay.type_value_name/2 returns the localized name for a boolean BCP 47 keyword value: "On" and "Off" in en, "Ein" and "Aus" in de.

  • CLDR 49's placeholderBoundarySpacing separates a text field from a neighbour its output would run digits into, as TR35 now specifies, so zh's "vHH:mm:ss" renders a nameless zone as "GMT+8 14:30:45" rather than "GMT+814:30:45". Numeric fields written together are left alone.

  • designation: :utc on Localize.DateTime.Timezone.gmt_format/3 writes CLDR 49's alt="utc" forms, "UTC+01:00" and "UTC+?" in en, and non_location_format/3 passes it to its GMT fallback. Both spellings read back through parse_offset/2.

  • CLDR 49's dualOffsetFormat joins the two offsets a zone keeps, so the localized GMT format describes a zone that moves between them: offsets: :dual writes Los Angeles as "GMT-8/-7" and Denver as "GMT-7/-6", where Phoenix keeps one offset and stays "GMT-7". A value with no zone writes its single offset.

  • A skeleton whose time half is only a zone joins its date through CLDR 49's Date-Timezone item, and one whose date half is only a weekday joins its time through Time-Day-Of-Week, as TR35 now specifies. en :yMMMdz renders "Jul 6, 2024 EDT", where it errored.

  • Localize.DateTime.SemanticSkeleton implements TR35 semantic skeletons, which ask for a date by meaning ("YMDE") rather than by field, and :format accepts one everywhere. :time_precision, :zone_length, :hour_cycle and alignment: :column shape the result.

  • MessageFormat 2's :date, :time and :datetime take TR35's options, from fields and length to timeZoneStyle, and the overrides timeZone, hour12 and calendar: {$d :date fields=month-day} in en-AU is "14 Jun".

  • Localize.DateTime.Timezone.parse_offset/2 reads a fixed UTC offset from a zone string — ISO 8601, or the localized GMT format in any locale's spelling — inverting gmt_format/3. Localize.DateTime.parse/2 uses it, so "May 16, 2026 2:30 PM GMT+10:30" parses without calendrical.

  • Localize.Date.parse/2, Localize.Time.parse/2 and Localize.DateTime.parse/2 parse natively, no longer requiring calendrical — parse("March 22, 2026", locale: :en) gives {:ok, ~D[2026-03-22]}. Localize.Interval.parse/2 and Localize.DateTime.Parser.parse/2 join them.

  • Localize.MinimalPairs exposes CLDR's minimal pairs — the phrases demonstrating a locale's plural, ordinal, case and gender forms. cardinal/1, ordinal/1, grammatical_case/1 and grammatical_gender/1 return them; format/3 picks the one a number selects.

  • CLDR's symbol-alt-variant currency symbols are ingested as Localize.Currency.variant_symbol and selected with currency_symbol: :variant — :TRY in en gives "TL". CLDR 49's new Unicode 18 signs resolve through it: :AED "⃃", :MVR "⃂", :OMR "⃄" and :SAR "⃁".

  • CLDR 49's numeric date and time separators, a technical preview: Localize.DateTime.numeric_separators/2 returns a locale's pair, and :numeric_date_separator and :numeric_time_separator set them on the date, time and interval formatters.

  • CLDR 49's -u-hc-c12 and -u-hc-c24, a technical preview, ask for the locale's own 12- or 24-hour cycle, resolved from the cycles its region allows: ja-u-hc-c12 writes 00:30 as "午前0:30".

  • Ordinal dates from CLDR 49, which CLDR designates a technical preview: the ddd field formats the day from the locale's dayOfMonths data, so en :yMMMddd renders "Jul 6th, 2024". Locales without that data, and patterns with a numeric month, format the plain day.

  • Date-time skeletons resolve through TR35's append items when no available format carries every requested field, appending the missing ones from the locale's appendItems templates. en has no quarter format, so :yMMMdQ now renders "Jul 6, 2024 (quarter: 3)" where it errored.

  • Localize.DateTime.Timezone.location_name/3 returns the place the generic location format names for a timezone — a country for a single-zone territory or a CLDR primary zone, a city otherwise — and generic_location_format/2 renders it through the locale's regionFormat.

  • The g pattern symbol formats the modified Julian day and the deprecated l is ignored, as TR35 specifies, where both returned a tokenize error.

  • mix localize.verify_locales checks a directory of locale files against the release's SHA-256 manifest, exiting non-zero on a mismatch so it can gate a build. It reports the CLDR tag the reference data came from, since the hashes are not overridable.

  • Localize.cldr_repo_ref/0 returns the CLDR repository tag the bundled data was generated from, such as "release-49", and the tag now ships in the hex package. Together with Localize.version/0 it traces a locale file to the source it came from.

  • A self-hosting guide covers provisioning, verifying and serving locale data from a private CDN. Configure the cache location before downloading, since the download target follows it.

Changed

  • Localize.Interval.parse/2 reads text as Localize.Interval.to_string/3 writes it at the given :format, so "Jun 16 – 20, 2026" at :medium parses, where only two dates written whole did.

  • A relative time uses its named form only for a whole offset: Localize.DateTime.Relative.to_string(0.9999, unit: :day) is "in 1 day", where it was "tomorrow".

  • Localize.Date.parse/2 without a format keeps its compiled patterns for each locale and calendar: "Jun 16, 2026" in en takes about 0.4 ms, where it took 5.

  • Localize.Interval.parse/2 keeps its compiled patterns too, so an en interval takes about 2 ms, where it took up to 80. They use up to about 300 KB of :persistent_term for each locale and calendar.

  • The Japanese calendar keeps all 237 eras, from 大化 (645) to 令和, where CLDR 49 has Meiji onwards only; the earlier eras take curated dates and CLDR 48.2's names. 白鳳 carries private_era: true, and four unattested eras unverified: true.

  • Skeleton matching ranks narrow and short widths nearer abbreviated than wide and breaks ties by TR35's field order, as CLDR's reference generator does: ja :yMdEEEEE renders "2024/7/6(土)", not "2024/7/6土".

Fixed

  • A cached locale whose bytes do not match the release's hash manifest is stale, as one of another data version is, so mix localize.download_locales and runtime downloads replace it where it was kept as current.

  • A stale cached locale that cannot be refreshed logs a warning naming mix localize.download_locales, once per locale, where it fell back to its parent with only a debug message.

  • Regional and script locales name subdivisions, inheriting from their parents: en-GB names :gbeng "England" where it named none. Names CLDR marks unconfirmed are left out.

  • Relative months know a month by its name, cldr_month/1, again: a Julian year reckoned from 25 March counts its March as two months, and 21 February to 25 March is "next month", where it was "in 2 months".

  • A fiscal calendar's date is written as the Gregorian date it is, through the calendar's cardinal_year/3: 10 August 2026 in a year begun in July is "Aug 10, 2026", where it was "Aug 10, 2027". It reads back through date_from_cardinal_date/3, and an interval across 1 January writes both years.

  • A locale's -u-mu-fahrenhe overrides the unit to Fahrenheit, where the override was dropped: BCP 47 caps a value at eight characters, so CLDR spells it fahrenhe, which named no unit.

  • A currency amount uses the locale's currency separators where CLDR gives them: fr-CH writes 1'234.56 CHF and de-AT € 1.234,56, where both used their plain-number separators.

  • Locale generation fails on a CLDR key its structs do not define, where struct/2 dropped it silently, which is how the currency separators went missing.

  • A missing locale falls back to its parents and then to :en rather than to the und root, which has no currency data and no localized names. Only an explicit request for :und loads the root, and the error of a locale that cannot be loaded names the locale requested rather than the last fallback tried.

  • A pre-validated Localize.Number.Format.Options struct takes the sign from each number it formats. The sign of the number it was validated with was reused, so options validated with 0 formatted -70 as "CHF 70.00".

  • Localize.validate_locale/1 returns an error for an unknown -u-tz- timezone id, a -t- numeric value that is not a 4, 6 or 8 digit date and a -t- keyword without a value, where it raised. A -u-vt- value of several codepoints is accepted, where it raised too.

  • Localize.Message.format_to_safe_list/3 returns a data-model error rather than raising WithClauseError: a duplicate declaration parses and then fails validation, which its with had no clause for.

  • RBNF plural-keyed substitutions select with the plural rules of the locale the rule set came from, since the forms they choose between are that locale's own words.

  • Localize.Locale.gettext_locale_id/2 caches its match per locale and known Gettext locales, so a repeated call takes about 0.4 µs where it took about 34 µs.

  • Localize.Currency.currency_strings/2 reads the index of currency-naming strings from the locale data, where the pipeline now generates it, so a call takes about 0.6 µs where it built the index each time for about 1.2 ms. A filtered call is still built, because a filter changes which strings name a single currency.

  • Localize.validate_locale/1 returns Localize.InvalidLocaleError for an unknown -u-tz- zone, a -t- date that is not 4, 6 or 8 digits and a -t- keyword without a value, where it raised. A multi-codepoint -u-vt- value is accepted.

  • A display name is that of the code it is keyed by, not of a retired alias: ak is "Akan", where it was "Twi", and fil is "Filipino" on every OTP release (#59).

  • Inflection data is stored under its data version, so an earlier version is never read for a current one, as a stale zh file was for zh-TW. mix localize.download_inflection --prune removes superseded versions.

  • A locale that :persistent_term has no room for returns a Localize.LiteralMemoryError naming the VM flag +MIscs, where loading it stopped the VM. All CLDR locales take about 814 MB of its 1 GB default; the performance guide's "Memory" section gives the sizes.

  • A skeleton asking a non-Gregorian date for a field its formats lack has it appended, after matching the calendar's formats and then the Gregorian's: a Hebrew :yMMMdw is "1 Tamuz 5786 (week: 39)", where it was an error. The Chinese and Dangi calendars match only their own.

  • A week date in every ISO 8601 form is the calendar's own week, where "2026W252" and "2026-W25" were ISO 8601's in every calendar. A week or day the calendar lacks is an error, where "2026-W53-1" was read in a 52-week year.

  • A year, or a year and quarter, written for a calendar other than Calendar.ISO reads as a map of those fields: "2023 AD" in a calendar of weeks is %{year: 2023}, where it was an error, and "Q2 2023 AD" is the year and quarter, not the quarter's first day.

  • A related Gregorian year reads as the calendar year that begins in it, however far from the reference date: the Islamic "AH 1699-12-28" is 16 June 1700, where it was an error. Of two years beginning in one Gregorian year, the nearer is read.

  • A cyclic year beside an era is that era's year of the name: Calendrical.LunarJapanese's "Genroku (1688–1704) geng-chen 4 29" is 1700, where it was 2000. A name the era has no year of is an error.

  • An inverted range read for a calendar whose dates are read in another names its two dates in the calendar asked for: Localize.Interval.parse/2 of "Feb 5, 2024 – Feb 1, 2024" for a calendar of weeks returned a Localize.DateRangeParseError holding the Gregorian dates the text was read as.

  • A year below 1 is read where its numbering spells its sign: ja writes a Japanese date before the first era with the jpanyear numbering, "大化マイナス688年3月15日", and it was read as no date, alone and in an interval.

  • A week without its year is a week of the reference date's week-based year: "Sun (week: 53)" read on 3 January 2021 is that day, where it was no date. Two such weeks read as an interval.

  • A pattern's '' is read inside and outside quoted text, as TR35 has it, so mt's "25 ġimgħa ta' 2026" reads back, where its "ta'" was read as "ta".

  • A weekday beside a week is that day of the week in every calendar, where "Tue, week 25 of 2026" was read as the week's first day.

  • An interval at a standard format is written in that format's numbering, as the date alone is: he's Hebrew medium interval is "א׳–ה׳ בתמוז ה׳תשפ״ו", where it used digits. It reads back, and an interval at a skeleton stays in digits.

  • A year a format names is never written as a number, nor a numbered year as a name, as TR35's adjustments require: en's Chinese medium interval is "Mo5 2 – 6, bing-wu", where the related year took the name's place. Between two numbered years the one asked for is written, "5/2/2026 – 5/6/2026".

  • An interval whose dates are in two sixty-year cycles of the Chinese or Dangi calendar writes both dates in full, "5/2/2026 – 5/2/2086" for en's yMd, as ICU does, where the interval's pattern wrote the two alike, "5/2/43 – 5/2/43". A format that writes no year takes one first, as it does for any two years.

  • G on a Chinese or Dangi date writes the number of its sixty-year cycle, "78" in 2026, as ICU does, where it returned Localize.ItemNotFoundError. Read beside the year's place it names one year: "77 60" began in 1983.

  • An interval written without a year across the new year ends in the year after: "Dec 28 – Jan 3" was refused as an inverted range of the reference date's year. The later date is a year on only where it would otherwise come before the earlier, by its calendar's own days, and never an earlier day of the same month.

  • A date without its year in a range takes the other date's year, or the next across the new year: "June 16 to August 20, 2031" is that summer, where it was five years earlier, and "Dec 28 – Jan 3, 2027" reads, where it was refused.

  • A zone keeps its metazone from 1970 on, as ICU has it: New York in 1965 is "GMT-05:00" and "New York Time", and Los Angeles in 1850 "GMT-07:52:58", not "Pacific Standard Time". Localize.DateTime.Timezone.metazone_for/2 is nil before 1970.

  • A pattern that writes a year's name beside its number keeps the name when a skeleton asks for another numbered year. The skeleton r wrote a Chinese year as "2026(2026)" in most locales, root's "r(U)" turned to "r(r)", and writes "2026(bing-wu)".

  • Localize.DateTime.parse/2 reads with a Localize.DateTime.SemanticSkeleton given as :format, where the skeleton was ignored. A skeleton whose minutes are optional reads "2 PM", which Localize.Time.parse/2 refused.

  • A date and time is written with the numbering its date format states, CLDR's numbers attribute, as the date alone is. he's Hebrew date and time was "1 בתמוז 5786, 14:30" beside the date's "א׳ בתמוז ה׳תשפ״ו", and zh's Chinese day "2" beside "初二".

  • An interval whose CLDR item names the year is read with the related year written in the name's place. en's Chinese "Mo5 2 – 6, 2026", beside the item "MMM d – d, U", was an error, as "2026 M05 2–6" was in 487 locales.

  • A date two of a locale's formats read is read by the one whose own separators it has. af's "10-7-2569 BE" is its GyMd's, "M-d-y G", the seventh of October, where its "d/M/y GGGGG" read it first, a hyphen for its slash, as the tenth of July.

  • An interval written without a year is read as dates of the reference date's year, as a date alone is. "Jun 16 – 20", which the month and day fields write, was read only as two partial dates with as: :map.

  • An interval reads with or without spaces about its dash, so en's "Jun 16–20, 2026" and de's "16. – 20.06.2026" read, where they were errors.

  • An interval in a year two eras share is read where one of its dates is written without its month. en's "May 1 – 5, 1 Reiwa" asked the calendar about the year 2019 alone, which is Heisei 31 until April, and was an error; each date now asks with the month and the day it takes from the other.

  • A numeric field takes the abbreviated width of a named one where the calendar has no numeric format, as CLDR's conformance data and ICU do: GyMd for a Chinese date in en is "Mo5 2, 2026", where it was "Fifth Month 2, 2026(bing-wu)".

  • An interval's values are written and read in the order the locale's fallback pattern states, as ICU does: kek writes 16 June 2026 to 20 August 2027 as "20/8/2027 – 16/6/2026", where the earlier date came first. A date written once beside two times stays before them.

  • The first year of a Japanese era is read as ja writes it. Its full, long and medium dates take CLDR's jpanyear numbering, whose year 1 is 元, so "令和元年5月1日" was an error.

  • A year of an era that began before a calendar changed how it numbers its years is read. Calendrical.Reform.Japan counts its years from 645 until 1872 and as the Gregorian calendar does from 1873, so every date from the reform to the end of Meiji in 1912, "Jun 16, 6 Meiji", was an error.

  • A range is cut at every place its separator occurs, where only the first was tried, so da's "2026-W25-2-2026-W27-1" and "October 5, 2026 to October 10, 2026" read. Text before the first date (fr-CH's "du {0} au {1}") and a later-date-first pattern read too.

  • An interval is read by the pattern whose own separators are in the text before one that reads it through a lenient separator. ha's yMd writes a year's difference with root's pattern, "26-06-16 – 27-08-20", which its own day-first pattern read as 26 June 2016 to 27 August 2020.

  • An interval date without an era takes the other date's era, where it took the reference date's: ja's "H5/06/16~5/06/20" is five days of 1993, where it ran to 2023. A narrow era name reads where it names one era, so "R 8/06/16 – 8/06/20" reads back.

  • A Hebrew date in Elul is read beside its era. The year of an era is settled by asking the calendar about the date, and it was asked about CLDR's number for the month, 13, in a common year of twelve months, so de's "07.13.5786 AM", af's "07 Elul 5786 AM" and every interval ending in Elul were errors.

  • A narrow month name reads where the format writes one and the calendar's narrow names are distinct: mn's "2026 VI" and "2026 оны VI/16 – VIII/20" read back. en's "J", shared by three months, is still no month.

  • A format with a variant is read as the formatter writes it, standard pattern first: en-CA's Chinese short date "5/2/2026" is the fifth month's second day, where the variant was tried first, and its yMd interval "6/16/2026–6/20/2026" reads back.

  • A skeleton's year is a number at every width and U a name at every width, so a y request never takes the year-name format: yMd for a Chinese date in en is "5/2/2026", where it was "5/2/bing-wu". yyyy and yy stay apart, so yyyy in en is "2024".

  • A 24-hour hour beside a day period reads only where the two agree, as TR35 checks, so ksh's "4:00:00 n.M. GMT+5:30", four in the afternoon, is no longer read as 04:00.

  • Localize.DateTime.Timezone.resolve/3 takes the reading whose zone is written so at the date: it's "Ora dell’Europa orientale (Kaliningrad)" is Kaliningrad in 2026, where it was Minsk. A name qualified by a city is that city's zone, so "Israel Time (Gaza)" is Gaza.

  • A country or city in parentheses is read as a zone's qualifier only after a name the locale writes, so text that is no name before it is no zone. A 24-hour pattern took a time's day period for part of its zone: en-GB read "4:00:00 pm (India)" as 04:00 in India.

  • A locale's GMT literal alone is read as GMT, as TR35's parsing reads "the absence of a numeric offset" in "localized or global formats": "غرينتش" in ar and ga's "MAG" are offset 0, where only "GMT", "UTC" and "UT" were.

  • Localize.DateTime.Timezone.parse_zone/2, parse_offset/2 and resolve/3 read a number with no sign after the GMT literal as east ("GMT 3", "UTC5:30"), as TR35 allows. A zone inside a date or time still needs its sign, since the number could be the year or the hour.

  • An offset's seconds are written by the localized GMT format and by Z to ZZZ: Los Angeles before standard time is "GMT-07:52:58", where it read back 58 seconds out. parse_offset/2 reads seconds after a one-digit hour and hours up to 23, so Juneau's "+15:02:19" reads.

  • The long localized GMT format (OOOO, ZZZZ, and a zone name's fallback to it) has a two-digit hour in cs, fi and vmw, as TR35 gives the long format always: "GMT+05:30" and "UTC+05.30", where the one-digit hour of those locales' hourFormat, the short format's, wrote "GMT+5:30" and "UTC+5.30".

  • resolve/3, parse_zone/2 and parse_offset/2 read an offset in any numbering system's digits, as TR35 has it: ar-EG's "غرينتش+٥:٣٠" and ne's "GMT+५:३०" read, where they were an unknown zone in the 67 locales with digits of their own.

  • Localize.DateTime.Timezone.parse_zone/2 and resolve/3 return an error for bytes that are not UTF-8, where they raised an ArgumentError.

  • A custom MF2 function receives its options under their names as strings, as Localize.Message.Function documents. It received an atom for any name an atom existed for, so the documented func_opts["format"] read nil.

  • A zone name qualified by a place reads as that place's zone even where its country has a primary zone: "Chile Time (Punta Arenas)" is Punta Arenas, where it was Santiago. A metazone name shaped like a region format reads as the name, so ko's "사모아 표준시" is American Samoa.

  • A standard or daylight name reads with the time its zone keeps at the date, as the formatter names it, where "Chile Summer Time (Punta Arenas)" read back an hour out. A standard name that is its metazone's only name ("Kyrgyzstan Time") follows the zone's clock.

  • The location format writes a country the locale has no name for by its code, as TR35 and ICU4C do: Havana's zone is "CU" in su and "ora de CU" in oc, where it was "Havana". The code reads back in a locale that writes it.

  • A location whose country's name has the shape of the fallback format is read as that country: fr-CA's "heure : Saint-Martin (France)" was read as France's zone, and ru's "Виргинские о-ва (Великобритания)" as the United Kingdom's.

  • Localize.Interval.parse/2 reads two-digit years with the era written once: nl's Buddhist "16-06-69 – 20-06-69 BE" reads, where its second year was the year 69. The reading nearest the reference date is taken.

  • The six zones whose data key is not their lowercase name, such as America/Port-au-Prince and Antarctica/McMurdo, take the city names their locale gives them, "بلانك-سابلون" in ar, where every locale used their own name.

  • A zone name qualified by a place is read where the place or the name holds the fallback format's own text: pt-AO's "Hora de Greenwich (Côte d’Ivoire (Costa do Marfim))" was an error, and ko's "중부유럽 하계 표준시(독일)", whose name has the shape of the standard region format, was read as standard time.

  • A date and time in a calendar other than Calendar.ISO takes the zone names of the instant it names, where its fields were read as ISO's: a Persian date in January 2026 in Asia/Almaty is "Kazakhstan Time", where it was "GMT+05:00". metazone_for/2 reads it in its calendar too.

  • A time reads only an hour its field allows, as TR35 and a strict ICU do, where "45:30 PM" was 21:30 and "13:30 AM" 01:30 in 618 locales. "13:30 PM" is an error too.

  • Localize.Time.parse/2 reads ISO 8601's times in every locale: "14:30" without seconds, which fi, da and 13 more did not read, and "T14", "T1430" and "T143045". z for Z and U+2212 in an offset read too.

  • Localize.DateTime.parse/2 reads every ISO 8601 date-time form: week dates and ordinal days before the T, times without seconds or minutes ("2026-W25-2T10:30"), and the basic form ("20260616T103000"). MessageFormat 2's date and time literals take the same forms.

  • Localize.Date.parse/2 reads the rest of ISO 8601's date forms: a week alone ("2026-W25"), as its Monday in ISO 8601's weeks, and basic-format week and ordinal dates ("2026W252", "2026166"). A year and month, or a year, reads as: :map in every locale.

  • Localize.Time.parse/2 reads an hour only in the range TR35 gives its field, h 1 to 12 and K 0 to 11: "0:30 AM" at h:mm a and "12:30 AM" at K:mm a are errors, where they were read as 00:30 and 12:30. H and k were held to their ranges already.

  • An interval writes only the fields its format names, as TR35's algorithm has it: :MMMd from 28 December to 3 January is "Dec 28 – Jan 3", and :yMMMHm on two June days at 10:00 is "Jun 2026, 10:00". A year, month or era the values differ in is written where the format asks for it.

  • Localize.Time.to_string/2 writes a Time or NaiveDateTime at a standard format with the locale's standard time pattern, where it took its skeleton's, which differs in 44 locales at :short: bg's short time is "10:30", not "10:30 ч.".

  • A date and time with no zone is written at :long and :full with Localize.Time.to_string/2's time, so it reads back in every locale: ja's full is "2024年4月3日水曜日 10:30:00", where the time was "10時30分00秒". fa, es and zh-Hant no longer leave "()" or "[]" for the missing zone.

  • A date interval compares its two dates in the fields its format writes, as TR35 does, so a week format writes both weeks (format: :yw is "week 25 of 2026 – week 30 of 2026", where only the first was written) and a quarter is written once for two dates within it ("Q2 2026", where "Q2 2026 – Q2 2026" was written).

  • Localize.Interval.parse/2 reads the year of a Chinese or Dangi interval as the related Gregorian year or as the calendar's own, whichever is nearer the reference year, so "11/8/2023 – 11/18/2023", which the formatter writes for two days of the Chinese year 4660, reads back, where it was the Chinese year 2023.

  • An ISO 8601 offset or Z written hard against a time is kept: Localize.DateTime.parse("11/22/2023 14:30:45+02:00", locale: :en) is a DateTime two hours ahead of UTC, where the offset was dropped and a NaiveDateTime came back.

  • Compiling Localize on Elixir 1.17 and 1.18 no longer logs "The on_load function for module Elixir.Localize.Nif returned: {:undef, …}" and an UndefinedFunctionError: Localize.Priv, which the NIF module's @on_load callback calls, is compiled and loaded first.

  • Localize.Interval.to_string/3 and to_parts/3 take dates with only some fields, in CLDR's interval format for them: two months are "Jun – Aug" and a year and month "Jun – Aug 2026", where they were errors. A date and time with no year keeps its date.

  • w and Y take a date without its day when every day it could be is in one week.

  • A specific time zone name takes standard or daylight time from TR35's stdOffset and dstOffset where a metazone period names them, as CLDR 49 does for Europe/Dublin and America/Winnipeg, rather than from the time zone database's flag: Dublin's summer is "Irish Standard Time" however std_offset is set.

  • Parsing no longer depends on map iteration order, which varies between VMs: standard formats are tried in Localize.DateTime.Format.standard_formats/0 order, and a place name two zones share resolves to the one Localize.Territory picks, else the alphabetically first.

  • Localize.Locale.parent/1 returns a parent that names its CLDR locale, where it cleared the CLDR locale id with the child's subtags, so a caller walking up the locales no longer validates each parent against every locale, about 20 ms a step.

  • Localize.Number.to_string/2 with an RBNF rule only an ancestor locale has, such as Hebrew and Roman numerals, takes a fraction of a millisecond where it took 40 to 50: the chain of locales a rule is looked up in is found once a locale, where each call revalidated every parent tag.

  • A date or time pattern is read by code points, so a combining mark after a quote is literal text: nnh's long and full dates ("lyɛ̌ʼ 4 na …"), whose caron follows a quoted "lyɛ", parse, where the quote never closed.

  • A numeric month written in an algorithmic numbering parses, as haw writes its short date's month in Roman numerals ("31/xii/24").

  • A comma that is the only separator between two date fields is no longer read as optional, so en-ZW's "May 2019" is May 2019, where its "dd MMM,y" read it as 20 May 2019. A comma beside a space may still be left out ("May 5 2026").

  • A Hebrew date written in Hebrew numerals parses, as he writes it ("כ״ב בטבת ה׳תשפ״ד" is 22 Tevet 5784): a hebr year is read by its letters' values, its thousands marked by a geresh, where none of he's Hebrew dates read back.

  • Localize.Time.parse/2, and the time of Localize.DateTime.parse/2, read the time formats of the :calendar given before the Gregorian calendar's, so de's Chinese "10 vorm." parses as the formatter writes it. A :calendar that is not a calendar module is a Localize.UnknownCalendarError, as it is for a date.

  • The localized GMT format writes its offset in the locale's digits, as TR35 and ICU do ("GMT-४" in ne, "غرينتش-٤" in ar-EG), or in those of -u-nu- or the formatter's :number_system. Localize.DateTime.Timezone.gmt_format/3 and non_location_format/3 take a :number_system option.

  • Localize.Territory.territory_from_locale/1 returns the region of a -u-rg- subdivision, so en-u-rg-gbsct (Scotland) is :GB where it was :gbsct, and such a locale's currency, unit preferences and time zone names are its region's.

  • The week fields Y, w, W, e and c honour a locale's -u-fw- and -u-rg-, which they ignored: Friday is e 5 in en-u-fw-mon. first_day_for_locale/1 and min_days_for_locale/1 follow TR35's algorithm in full, -u-ca-iso8601 and -u-sd- included.

  • A datetime interval splits a skeleton into date and time fields, as TR35 does: one day's :yMMMdHm is "Jun 15, 2026, 10:00 – 14:30", where it was an error, and :Hm writes the times alone. A skeleton of date fields formats as a date interval, "Jun 15 – 16, 2026".

  • A date or time its calendar lacks is a Localize.InvalidValueError in formatting, intervals, Localize.Calendar.localize/3 and relative time, by the calendar's valid_date?/3 and valid_time?/4. Some fields raised for one and others wrote it, "Feb 30, 2019" or hour 25 as "1:00 AM".

  • A calendar whose year begins in another month, such as a fiscal year beginning in July, names its months for the months they are, period 1 "Jul" and "7", where they were named by their place in the year, "Jan" and "1".

  • Localize starts inside an escript built with include_priv_for: [:localize], reading its data from a copy extracted into the user cache directory that rebuilds share; one built without it stops at startup saying so, where every process crashed on its first read. Closes #58.

  • A collation table file that cannot be read is logged as a warning, as a missing one already was, where any other read error stopped Localize with a CaseClauseError.

  • Localize.DateTime.Relative.to_string/2 counts the quarters, and the calendar weeks from the locale's first day for :mon to :sun, between two dates, where it formatted their difference in seconds as that many quarters or weekdays.

  • Localize.DateTime.Relative.to_string/2 counts a number of seconds beyond the range of a float, where it raised ArithmeticError.

  • Localize.Duration.new/2 counts years, months and days with the calendar's diff/3 and shift/6, giving the span Date.shift/2 adds to the earlier date: 14 January to 13 July is 5 months and 29 days, where it was 30. A date the calendar lacks is an error, where it raised.

  • Localize.Duration.new/2 measures two zoned date-times as two moments, as ECMA-262 Temporal does: 10:00 UTC to 18:00 in Karachi is 3 hours, where it was 8. A time paired with a naive date-time no longer raises, and no pairing gives a negative duration.

  • Localize.DateTime.Relative.to_string/2 returns an error for a DateTime built by hand without its offsets from UTC or its time zone, where it raised ArithmeticError.

  • u writes the calendar's extended year, its extended_year/3, where it wrote the date's year: 15 June 1 BC in the Julian calendar, whose year is -1, is "0", as TR35 defines it. A calendar without extended_year/3 is refused, as one without any other answer is.

  • A parsed range of dates is ascending or inverted by its calendar's days, where the dates' fields decided: in a calendar whose year turns after its first month, as a Julian year reckoned from 25 March does, 31 December to 1 January of the same year is no longer refused as inverted.

  • Localize.Substitution.substitute/2 substitutes into a template of any shape, where it raised on one it did not list, such as el's zone fallback format "[{1} ({0})]".

  • A zone in parsed text reads in the locale's own words and spelling, "heure d’été de l’Est nord-américain" or "غرينتش-4", and a standard or daylight name keeps its own offset, as ICU reads it. Of 52,560 zoned date-times formatted in every locale, all but 80 in nnh round-trip, where 14,099 did.

  • Localize.Time.parse/2 reads a time written in the digits of the locale's number system, as the date parser does, so bn "১০:০৫ AM" and fa "۱۰:۰۵" parse, and with them the date-times of 67 locales that write their own digits.

  • The date parser reads month, quarter, weekday and era names written in the locale's digits, as dz's abbreviated months ("༤" is April), ckb's short weekdays and ff-Adlm's Japanese eras are, where transliterating the input's digits left them unmatched.

  • Localize.Time.parse/2 tries the locale's standard formats first, then its available formats with those carrying a zone or a flexible day period last, where their order followed a map, so ms "11:59 PTG" is 23:59 rather than 11:59 in a zone "PTG".

  • A flexible day period reads as the period its hour falls in, so fr's "9:05 matin" is 09:05, not 21:05, and as AM or PM where the locale names none. A word the locale does not name is no longer read as the morning.

  • Localize.Interval.to_string/3 and to_parts/3 format with the formats of the endpoints' calendar — its interval patterns, date and time formats and date-time pattern — where they used the Gregorian calendar's, and return an error for endpoints in two calendars.

  • An interval across a change of era writes each date's era where its format has one: format: :GyMMMd is "Dec 31, 1 BC – Jan 1, 1 AD", and the Japanese calendar's formats, which write the era with the year, "Apr 30, 31 Heisei – May 1, 1 Reiwa" across the change within 2019.

  • An interval whose endpoints differ in no field it shows is the date at the requested standard format, as Localize.Date.to_string/2 and ECMA-402 write it: ko "2023. 4. 1.", where it was "2023/4/1". A date-time interval formatted whole honours :style.

  • Localize.DateTime.parse/2 returns the date-time where it raised for every calendar but Calendar.ISO, and reads a date-time pattern that puts the time first, as vi's "{0} {1}" does.

  • The date parser tries the locale's standard formats first and then its available formats most specific first, where their order varied between runs, and reads a yy year beside the era of a calendar numbering its years as they are in the century around the reference year ("01.04.66 BE" is 2566 BE).

  • Localize.DateTime.to_string/2 and Localize.Time.to_string/2 resolve formats in the value's calendar — its date-time pattern, available formats and append items — where they used the Gregorian calendar's, and Localize.DateTime.parse/2 reads the calendar's date-time pattern as well as the Gregorian one.

  • The date and interval parsers read a lunisolar date as the formatter and ICU4C write it: r, U, leap months ("Mo2bis", "闰二月") and zh's and ja's Han numerals. Where a locale writes both the calendar year and the related year, as ko does, the nearer reading is taken.

  • A date read without its year is completed with the reference date's year in the calendar it is read in, so "15 Nisan" in the Hebrew calendar is in 5786 where it was in the Hebrew year 2026, and a two-digit year is read in the century around it in every calendar.

  • Era names come from the CLDR calendar a calendar's info/0 names as :era_calendar_type, so Calendrical's lunisolar Japanese calendar names its eras (令和), where it returned an error. A year written without its era reads as one of the reference date's era, as ICU reads it.

  • The date parser reads a year of an era that begins before the calendar's first year from the calendar year it begins in, so "Hedar 15, 5495 AA" is the Ethiopic year -5, as Calendrical numbers it.

  • A lunisolar month written as a number is its traditional number, and a leap month is in CLDR's numeric leap pattern, as ICU4C writes them — the Chinese leap second month is "2bis" in en and "闰2" in zh where it was its place in the year. A partial lunisolar date writes its related and cyclic year.

  • The date parser reads a year written with its sign, as a calendar without a before era writes one below 1 — "Mar 15, -456 BE" in the Buddhist calendar — and never pivots a signed year.

  • The date and interval parsers read a year with its era in every calendar, as the formatter writes it — "Jun 1, 44 BC" is -0043-06-01 where it was 2044-06-01 and "1 BC" is year 0 — and no longer pivot a two-digit year its era qualifies.

  • A year before a Calendrical calendar's first era counts back from the era, as TR35 and Calendar.ISO count it — Calendrical.Gregorian year 0 is "1 BC" and Calendrical.Roc year -5 "6 B.R.O.C." — where it showed the year as it is, "0 BC".

  • A partial date shows its era and year of era as its days do — %{year: 2025, month: 1} with :GyMMM is "Jan 2025 AD" where the era was blank, and %{year: 0} is year "1" — and a Japanese one whose days span two eras returns an error naming the fields that settle it.

  • The short localized GMT format of a whole hour keeps the offset pattern up to its hour field, as ICU does, so he's "GMT-5" carries one left-to-right mark where it carried two.

  • A binary that is not UTF-8 is an error wherever Localize reads text — number and unit parsing, number and date/time patterns, territory names, collation and inflection — where it raised ArgumentError or UnicodeConversionError. Localize.Unit.parse(<<255>>) returns Localize.InvalidValueError.

  • A MessageFormat 2 date/time literal keeps the UTC offset it carries, where |2006-01-02T15:04:06-05:00| was read as a floating time and its offset dropped.

  • A fixed UTC offset carried under a UTC identifier, as a parsed ISO 8601 offset and Calendrical's fixed offsets are, names its zone in the localized GMT format ("GMT-5"), where it was named "UTC".

  • Localize.DateTime.Timezone.metazone_for/2 takes a map whose fields name no real date and time, returning the zone's current metazone, where it raised.

  • Localize.Locale.parent/1 gives a parent found by dropping a subtag its own CLDR locale rather than the child's, so ar-SA, de-AT, fr-CA, es-419 and zh-Hant-HK spell numbers out with their language's RBNF rules instead of failing. A validated und walks to root rather than to English.

  • Localize.DateTime.parse/2 and Localize.Time.parse/2 with as: :map give a fixed offset such as "GMT+5" the DateTime zone fields the struct form resolves, where the map kept time_zone: "GMT+5" with no offset; a named zone resolves too when the input gives its full date.

  • Localize.Time.parse/2 and Localize.DateTime.parse/2 read a time in the locale's -u-hc- hour cycle ("24:30" under h24) and resolve a flexible day period's hour by CLDR's day period rules. Japanese "夜中0:30" is 00:30, where it parsed as 12:30.

  • Localize.Date.parse/2, Localize.DateTime.parse/2 and Localize.Date.parse_range/2 parse a date with an era, whose patterns never compiled, and one whose weekday does not lead its pattern. A month abbreviation that is also a weekday name (es "mar") is no longer stripped.

  • Localize.Date.parse/2 resolves a month name to that month in the parsed year, so a Hebrew month name parses to its position in an ordinary or a leap year, and "Adar II" parses.

  • A semantic skeleton passed as :format for a partial date or time is used rather than replaced by a skeleton derived from the fields present.

  • prefer: :menu on Localize.Language.display_name/2 composes CLDR's menu="core" and menu="extension" halves where a locale ships no alt="menu" string of its own, so "ku" renders "Kurdish (Kurmanji)" rather than the unqualified "Kurdish". Affects 571 language entries across 446 locales.

  • Localize.DateTime keeps a width the matched format's own skeleton asks for, as Localize.Date did, so fr :yyMd renders "06/07/24" not "6/7/24". A zone keeps its width the same way, so el :yMdHmsv names the zone in full, as its Hmsv format does.

  • A skeleton's weekday matches the locale's weekday formats whether spelled E, e or c, and a year whichever of y, Y, u, U or r, taking the width asked for. en :EEEE renders "Saturday" where it gave "Sat", and ja :U "2024年".

  • j and C carry TR35's day-period width, three or four letters wide and five or six narrow, so en :jjjjj renders "2 p". An a beside an hour sets only the width of the locale's own day period, TR35 treating ha as h, so zh-Hant :ha keeps its "Bh時".

  • A skeleton no available format can build on starts from its first field and appends the rest, as CLDR's reference generator does, so :G renders "AD" and :QQQQ "3rd quarter" where both errored.

  • A locale passed to Localize.Inflection must be a well-formed identifier. One containing "../" reached files outside the inflection data directory, loaded them, and created a new atom for every spelling of the path.

  • Quoted text in a matched format survives width adjustment: dsb's jjm format "'zeg'. H:mm" rendered "eg. 14:30", its quoted z read as a zone field and deleted.

  • Skeleton resolution is covered by CLDR's four skeleton conformance files, 2,871 Gregorian cases, which had not been exercised. All agree with CLDR's reference generator but 116, each recorded with its TR35 reason: hour widths, J padding, C in Indian locales and en yyyy.

  • Full UCA conformance restored: all 210,155 pairs in both CLDR collation conformance files sort correctly, where 637 and 536 failed. FractionalUCA.txt and the UCD property files had been hand-vendored at Unicode 17 and are now copied by the pipeline.

  • Localize.Locale.LocaleDisplay.display_name/2 renders a locale in root (or und) as bare subtag codes per TR35's code fallback — display_name("nl-BE", locale: :root) is "nl (BE)", where it answered in English. root is now accepted wherever und is.

  • Month names come from the calendar's cldr_month/2, so Hebrew months are named correctly in ordinary and leap years ("Adar II" included) and a Chinese leap month takes the leap-month pattern ("Second Monthbis"), where they were looked up by month number.

  • The Localize.FormatError message for a selector without an annotation is rendered. Its example declaration was being formatted as a live placeholder, which failed, so the raw template was returned and a warning logged.

  • Public functions return {:error, exception} — usually Localize.InvalidValueError — for options that are not a keyword list and for arguments or option values of the wrong type, where they raised; predicates answer false.

  • Localize.Message.format/3 returns a Localize.FormatError where it raised when a placeholder, :string or a :string selector was given a value with no string form — a tuple, map, pid, function, a list that is not chardata, or a bitstring that is not whole bytes.

  • Localize.to_string/1,2 and Localize.List return Localize.InvalidValueError for an improper list and for a bitstring that is not whole bytes, where they raised FunctionClauseError.

  • Localize.Message.format/3 with backend: :nif falls back to the Elixir interpreter for bindings other than strings, 64-bit integers and floats, where some raised and others were misformatted. The NIF decodes JSON \b, \f and \u escapes and exponent-form numbers, which reached ICU as text.

  • Localize.Nif.mf2_format/3 returns {:error, exception} for arguments that are not a map or JSON object of strings, 64-bit integers and floats, and for a message or locale that is not a string, where it raised.

  • Localize.Message.format/3 and format_to_iolist/3 return an error for an improper binding list, where both backends raised, and the NIF backend skips a list element that is not a {name, value} pair, as the interpreter does.

  • Localize.Currency.current_currency_for_territory/1 returns CLDR's primary currency where several are current, where it depended on atom-creation order (LS gave LSL, ZW USD), and gives ML its XOF. The territory currency data carries each currency's CLDR position as :order.

  • Localize.Currency.currency_strings/2, and so currency parsing, leaves out a narrow symbol that several current currencies share, such as "kr" in en, where it went to whichever currency came first in atom-creation order. One current currency among historic ones still takes it.

  • Currency parsing takes the longest matching currency string by bytes rather than graphemes, so a plural spelled with as many graphemes as its singular (in te, mr, ml and 14 other locales) no longer matches as the singular and leaves the rest of the word in the remainder.

  • Localize.Number.System.number_system_names_for/1 and Localize.Number.Format.format_system_types_for/1 return systems in type order (default, native, traditional, finance), where they followed atom-creation order.

  • The module mix localize.unit.gen_conversions generates lists each unit once in known_units/0, where the simple base units appeared twice.

  • Localize.Territory.to_territory_code/2 matches a name as written before its normalised form and, where territories share it, prefers a country to a region, then the first code alphabetically. In scn the name South Africa and Southern Africa share gives :ZA.

  • Fuzzy currency matching takes the alphabetically first of equally close currency strings, where it took whichever came first in map order.

  • Functions taking a Localize.LanguageTag, including every :locale option, no longer raise on a hand-built struct with wrong-shaped fields: a nil list or map is empty, a string subtag is its atom, and anything else returns Localize.InvalidLocaleError.

  • Interpolating a parsed Localize.LanguageTag into a string gives its BCP 47 form, where it raised because the tag had no canonical id yet.

  • Localize.Territory.territory_from_locale/1 takes the territory of a language tag struct that has none from its likely subtags, so a parsed "fr" gives :FR where it gave the default locale's :US.

  • A locale that Localize holds data for now resolves to itself. resolve_cldr_locale/1 asked the language matcher even for an exact identity, so ar-EG, which formats in arab digits, resolved to ar, which uses latn.

  • root is accepted wherever und is. TR35 makes it a synonym for und, but CLDR's alias data does not carry it, so Localize.validate_locale("root") failed while Localize.Locale.cldr_locale_id_from("root") succeeded.

  • mix localize.download_locales rejects a name that is not a CLDR locale rather than turning it into an atom, and unit preference lookups no longer create atoms from the prefixes of a usage.

  • Locale downloads retry a CDN server error, timeout or dropped connection up to three times with exponential backoff, where one failure was final; a 404 still fails at once. :http_retries, :http_retry_delay and :http_ip_family configure the retries and the address family.

  • Skeleton formats honour the field widths asked for. TR35 matches a skeleton then adjusts its widths to the request; only the first step happened, so format: :MMMM rendered "Jul" in en where the literal pattern "MMMM" rendered "July".

  • A skeleton combining a date and a time adjusts both halves' field widths to the request, where a requested zone symbol took the matched format's — :MMMMdjmsO gave "GMT" not "GMT+0". Every Gregorian case in CLDR's datetime conformance fixture now passes, from 47 of 78.

  • Skeleton matching is deterministic. Where two formats sat the same distance from a skeleton the winner came from the formats map's iteration order, which depends on when the atom keys were created — so one skeleton could resolve differently between runs.

  • A matched format keeps a field width the locale states deliberately. TR35 leaves a pattern field alone where the matched availableFormats id already carries the requested width, so ru's yMd renders dd.MM.y rather than being narrowed to d.M.y.

  • Resolving a date-time skeleton no longer creates atoms. Localize.DateTime.Format.Match.split_fractional_seconds/1 interned the remainder left after stripping the fractional-second field; it now resolves against existing atoms only, with identical output.

  • A skeleton whose date and time halves no single available format covers joins them at the length TR35 derives from the requested date fields rather than always at :medium.

  • :date_format and :time_format resolve a skeleton through TR35 matching, where a missing skeleton errored. A skeleton date format joins through the locale's date-time pattern, so ja renders "2026年6月15日 14:30".

  • Localize.DateTime.to_string/2 and to_parts/2 keep both halves of a map holding some date and some time fields, joined through the locale's wrapper, where %{year: 2026, month: 6, hour: 14} rendered "6/2026". A partial value given a skeleton is formatted with it.

  • Partial times keep the locale's hour cycle, so %{hour: 14, minute: 30} is "14:30" in de rather than "2:30 PM". A -u-hc- override now applies to partial times and throughout Localize.DateTime, which ignored it.

  • Localize.Time, Localize.DateTime and Localize.Interval render a -u-hc-h11 or -u-hc-h24 cycle with the K or k hour ("0:30 AM", "24:30") and resolve j in an interval skeleton. Under -u-hc-, an explicit h or H keeps the cycle it names, as in ICU.

  • Time skeletons resolve day periods as ICU does: ha renders "h a" and hb "h b" where both gave a flexible day period, and an H or k skeleton drops a requested one. J drops it as TR35 requires and C ignores a -u-hc- override.

  • Localize.DateTime.to_string/2 applies a -u-hc- hour cycle to a skeleton that matches an available format exactly or takes append items, and renders an X or x zone field requested by a skeleton, which it dropped.

  • b and B judge noon and midnight at the precision their pattern shows, as ICU does, so "h B" renders 12:05 as "12 noon". VVV renders Etc/UTC as the unknown location, "Unknown Location" in en, rather than "UTC".

  • S has exactly as many digits as the field has letters, as TR35 specifies, where it stopped at six and at the value's precision, so ss.SSS on 45.9 seconds is "45.900". U without cyclic year names formats as y, and YYYYYY pads rather than rendering empty.

  • Timezone format symbols follow TR35: V is the BCP 47 short identifier ("usnyc"), VVV the exemplar city ("New York") and VVVV the generic location format, where all three returned the IANA name or a GMT offset. An alias like US/Eastern resolves to its canonical zone.

  • The generic location format names a country where TR35 requires one — a single-zone territory, or a zone in primaryZones — so VVVV renders Europe/Rome as "Italy Time". primaryZones is new supplemental data, read from metaZones.xml because the JSON omits it.

  • v and vvvv fall back to the generic location format before the localized GMT format, which TR35 specifies for the generic symbols only, so Asia/Kolkata is "India Time" rather than "GMT+05:30". The specific symbols z and zzzz still fall straight through to GMT.

  • ZZZZ spells a zero offset as "GMT+00:00", matching OOOO as TR35 requires of the localized GMT formats. A metazone with no daylight name also resolves a generic request to its standard name, so vvvv is "Greenwich Mean Time" for Etc/GMT and "India Standard Time" for Asia/Kolkata.

  • An unknown timezone offset renders the locale's gmtUnknownFormat — "GMT+?", translated where the locale translates it — instead of a hard-coded English "GMT".

  • The short localized GMT format drops the hour's leading zero when the offset has minutes, as TR35 specifies, so O for India is "GMT+5:30" rather than "GMT+05:30".

  • Localize.Duration.to_time_string/2 no longer raises on a field longer than two letters, such as "hhh:mm"; the field formats as U+FFFD, as in a date pattern.

  • Date intervals use the locale's interval pattern where CLDR ships a matching one, adjusted to the requested widths, rather than gluing two full endpoints — de at :medium renders "03.–05.05.2026". TR35's closest-match step was skipped.

  • An interval whose endpoints differ in no field it shows is one value, as TR35's interval algorithm has it, where fields: :year_and_month gave "Jul – Jul 2024". A selection of fields without a year writes none for dates of two years, so fields: :month_and_day across the new year is "Dec 30 – Jan 2".

  • Time intervals across noon take CLDR's day-period pattern, "10:00 AM – 2:00 PM" rather than "10:00 – 2:00 PM", and an :h interval within one hour formats a single time. A skeleton the interval table lacks, such as :hms, joins both times through the fallback pattern.

  • Same-day datetime intervals join the date, shown once, to CLDR's time interval pattern as TR35 specifies, so en :short is "7/6/24, 10:00 – 10:30 AM" rather than "7/6/24, 10:00 AM – 10:30 AM". Datetimes differing only in seconds format as an interval at :medium rather than as one value.

  • Localize.DateTime.Relative.to_string/2 and to_parts/2 return {:error, %Localize.InvalidValueError{}} for a value that is not a number, date, time or datetime, and for options that are not a keyword list, rather than raising FunctionClauseError.

  • Localize.DateTime.Relative.to_string/2 and to_parts/2 measure a value against a :relative_to of another type where the conversion is unambiguous, and return Localize.InvalidValueError otherwise. The baseline was silently replaced by the current time.

  • Localize.DateTime.Relative.to_string/2 and to_parts/2 format the number for the locale ("in 1,000 days", with ECMA-402 parts) and select the pattern by the displayed number's plural category. A float with :unit is a count of that unit, where it was read as seconds.

  • A display name with no entry for the requested preference and none for :standard falls back to a stable key rather than whichever the map yielded first, which depended on atom creation order in the VM.

  • Localize.Number.Rbnf.rule_names_for_locale/1 returns its names sorted. On OTP 26 and later their order followed the order the VM had created the atoms, so it varied between runs.

  • Localize.Number formats a non-default numbering system with the data CLDR gives the locale for it: fa-u-nu-latn is "1,000.5", not "1٬000٫5", and en-u-nu-arab "١٬٠٠٠٫٥", not "١,٠٠٠.٥".

  • The long currency format no longer raises in sd and ckb, whose arab has no unit patterns and now takes their latn ones, and writes a currency with no display name by its code, as TR35 specifies ("٣٫٠٠ USD" in ckb).

  • Number patterns give each currency sign width its TR35 meaning — ¤¤ the ISO code, ¤¤¤ the plural name, ¤¤¤¤¤ the narrow symbol — and take the currency's decimal places and CLDR's currency spacing, so ¤#,##0.00 gives "CHF 1,234.56" and "¥1,235".

  • Localize.Number.to_string/2 rounds significant digits and scientific mantissas half-even, or by :rounding_mode, where it rounded half-up. A mantissa shows the digits TR35 derives from its pattern, so "##0.##E0" renders 12345 as "12.3E3".

  • Localize.Number.to_string/2 and to_parts/2 format a pattern with quoted literal text, such as #,##0 'units', where the pattern lexer looped and exhausted memory. A pattern with a character no pattern syntax accepts, such as a trailing *, returns an error.

  • Localize.Number.to_string/2 keeps the whitespace a pattern ends with, as ICU does and as to_parts/2 already did. A :wrapper function receives a quoted pattern character tagged :literal, which it never received.

  • Localize.Number.resolve_currencies/2 and resolve_currency/2 match a currency name or code only as a whole word, so "100 US dolars" no longer resolves to the Argentine peso from the "ars" it ends with.

  • -0.0 keeps its sign, :max_fractional_digits applies to a scientific mantissa, and compact formatting keeps two significant digits below 1 and groups on ICU's MIN2 rule.

  • Localize.Number.to_string/2 no longer raises on a very small float with a large :max_fractional_digits — to_string(1.0e-308, max_fractional_digits: 400) raised ArithmeticError because reconstructing the value divided by a power of ten outside the double range.

  • Compact number formatting in Swahili was wrong by a factor of ten, and of a thousand for some ranges. The compact divisor counts zeros in the pattern, which included the negative subpattern — elfu 000;elfu -000 read as six zeros rather than three.

  • Plural selection now keeps a Decimal's fraction operands. pluralize/3 converted to a float first, and TR35 selects on the visible fraction digits and their value, so 1.2 was treated as "1.200" and chose other where Serbian, Croatian and Bosnian require few.

  • Localize.Unit.to_string/2 selects the plural form of a rounded number by the digits the formatter displays, so hr 0.0045 hours is "0,004 sata" rather than "0,004 sati".

  • Unit preferences read the -u-ms and -u-mu keywords, in TR35's order mu > ms > rg > (likely) region: en-u-rg-uszzzz-ms-metric gives celsius and en-US-u-rg-uszzzz-ms-uksystem imperial gallons.

  • A unit whose quantity CLDR ships no preferences for falls back to base units rather than erroring: preferred_units/2 on ampere gives [:ampere], on candela-per-cubic-foot ["candela-per-cubic-meter"]. Derived compound base units come back as strings, not atoms.

  • A unit usage supplied as a string resolves instead of silently falling back to :default. Every usage CLDR defines now has an interned atom, which Localize.Unit.Preference.known_usages/0 lists, so usage: "fluid" returns imperial units for en-GB where it returned cubic inches.

  • RBNF alternation branches are evaluated against the number rather than the remainder. A branch carrying a quotient substitution saw 0, so Russian's 201 spelled "нольсти" instead of "двести"; Ukrainian, Polish, Czech and others were affected the same way.

  • RBNF fraction digits spell with the current rule set, a fraction rule's bracketed integer part survives, plural selection uses the numerator, and the >>> preceding-rule chain is built in full.

  • RBNF optional rule text renders unconditionally where ICU would not have split the rule in two — its base value must be positive and an even multiple of its divisor — so Afrikaans spells the year 1100 "elf honderd nul".

  • RBNF rule selection implements ICU's rollback rule: a rule taking a remainder is passed over for the one below when the number divides exactly but the rule's own base value does not. Bulgarian's financial cardinals no longer spell a trailing "и нула".

  • Localize.Message.format/3 selects a plural or ordinal variant by the number as its :number, :integer, :offset or :percent function displays it, so Russian {2 :number minimumFractionDigits=1} is other. An :integer operand given as a Decimal formats rather than raising.

  • MessageFormat 2 literals keep all their code points, as TR35 requires, where the parser normalized them to NFC; keys still match a selector's value in NFC. Localize.Message.Print.to_string/2 quotes a literal that is not an MF2 name, so a canonical message parses back unchanged.