Skip to content

Localise version 0.45.0

Choose a tag to compare

@kipcole9 kipcole9 released this 05 Jul 11:46
· 327 commits to main since this release

[0.45.0] — July 5th, 2026

A quality release: the codebase now passes mix credo --strict with zero findings (enforced in CI), the full MessageFormat 2 working-group conformance suite runs against the formatter, and the documentation gains four new guides. A test-coverage push from 70% toward 90% surfaced — and this release fixes — more than twenty conformance and correctness bugs across date/time, number, unit, collation and language-tag handling.

Upgrading from 0.42 or earlier? Version 0.43.0 renamed the inventory functions from the available_*/known_* names to the uniform known_*/supported_*/*_for scheme and swapped two :format/:style option names. Every old name still works as a deprecated delegate until Localize 1.0 — see the 0.43.0 entry below for the full list.

Added

  • The complete MessageFormat 2 working-group conformance suite (16 test files including all nine functions/ suites) now runs against the formatter: 250 cases asserting formatted output and expected errors, with unimplemented features excluded and documented per case.

  • Four new guides: plural rules, list formatting, locale validation and matching, and display names. Every example is execution-verified.

  • Localize.Territory.territory_names_for/1 and territories_for/1 return the localized territory-name inventory for a locale, completing the *_for family across languages, scripts, subdivisions and territories.

  • CLDR metazone data joins the supplemental-data pipeline: Localize.DateTime.Timezone.metazone_for/2 returns the metazone for an IANA zone at a given instant, and zone_for_metazone/2 returns a metazone's representative zone per territory. The z/v format symbols now draw on the full 191-metazone CLDR mapping instead of a 20-zone builtin table.

  • A documentation depth pass adds around 150 execution-verified doctest examples and 40 ### Options sections across the number, unit, date/time, locale, territory, list, collation and message modules, and corrects stale claims (RBNF number-system conversion, locale defaults, the validate_locale/1 matching warning).

  • A test-coverage push adds around 1,700 tests across exceptions, collation, date/time formatting, MessageFormat 2 tooling, language-tag validity, number/unit internals and the runtime plumbing, raising runtime-library line coverage from 70% to 91%. The 90% threshold is now enforced in CI.

Changed

  • The MF2 :offset function follows the specification: it takes exactly one of the add or subtract options (non-negative integer) and adjusts the operand for both formatting and selection. The previous ICU-style offset= option, which only subtracted during selection, is replaced.

  • The MF2 numberingSystem function option accepts any valid CLDR numbering system, matching Intl/ICU: {$n :number numberingSystem=thai} renders Thai digits in any locale. Unknown system names still return an error; the :number_system option of Localize.Number.to_string/2 is unchanged.

  • Credo (strict) added to the CI lint stage with a committed .credo.exs. Policy: the Design.AliasUsage check is disabled (Localize module names such as List, Date and String shadow the stdlib when aliased; aliasing is applied opportunistically where safe), nesting depth stays at the default maximum of 2, and complexity/apply/arity exceptions are documented inline at each site.

  • Deeply nested functions across the library were refactored into multi-clause private helpers with pattern matching (~120 helpers). Behavior is unchanged; the full test suite, dialyzer and doc builds gate the refactor.

  • Logger configuration for this project's dev/test environments now renders the domain: :localize metadata attached to Localize log messages.

Fixed

  • The U (cyclic year) and r (related Gregorian year) format symbols work: U renders the localized sexagesimal name (丙午, "bing-wu") from the calendar's cyclic name sets and r the Gregorian year in which the calendar year begins, both probing the date's calendar module per the Calendrical protocol. Previously both rendered the raw year number.

  • Date/time skeleton matching compared candidate format fields against the wrong skeleton fields, making format selection nondeterministic across VM runs (format: :yMdHm could render "Jul" or "July" run to run). Selection is now deterministic, prefers the exact skeleton, and matches text-field widths by class (EEEE) per TR35.

  • The "alt" language code (Southern Altai) was unreachable: the data pipeline stored it as the atom :alt (a collision with the alt-variant style key), so Localize.Language.display_name("alt") returned an error. Fixed at data load; the pipeline fix lands with the next CLDR data regeneration.

  • -t- extension dates encode with zero padding (ja-t-it-m0-ungegn-20070315 round-trips exactly); previously single-digit months and days encoded with an embedded space, producing an invalid tag.

  • Localize.validate_locale("en-u-vt-00a4") no longer raises during canonical-id formatting, and Localize.LanguageTag.parse("zh-min-nan") no longer raises ArgumentError (the grammar dropped the extended-language tags for multi-subtag forms).

  • CLDR {1}…{0}… substitution patterns with two literals placed the substituted items in the wrong order.

  • Collation reorder: [..., :others] places unmentioned scripts at the :others position per TR35; previously they kept unremapped weights, producing orderings incomparable with the reordered scripts.

  • The collation max_variable option takes effect in the Elixir backend: :space narrows and :symbol/:currency widen the shifted range per the UCA. Previously the setting was ignored and shifting was fixed at the punctuation boundary.

  • Compound unit identifiers canonicalize in the TR35 unitQuantity order, so "kilowatt-hour" keeps its CLDR pattern and renders "2 kilowatt-hours" instead of "2 hour-kilowatt".

  • :currency_long_with_symbol renders both the symbol-formatted number and the currency name ("$123.00 US dollars"); both long currency formats now derive the currency from the locale when no :currency option is given.

  • Localize.Interval.to_string/3 returns an error for mixed endpoint kinds (e.g. a Date and a Time) instead of rendering malformed output, and equal Time endpoints honour the :time_format option in the single-time fallback.

  • The Q and q date-format symbols were swapped: Q now renders the format-context quarter and q the stand-alone quarter per TR35. Visible only in locales whose quarter names differ by context.

  • Doubled apostrophes inside quoted date-format text render a literal apostrophe ('o''clock' → "o'clock"); previously the escape was dropped.

  • Zone format symbols (Z, O, v, V, x, X) render empty for values without a time zone, matching z; previously they fabricated a UTC zone.

  • Localize.Duration.new/2 accepts NaiveDateTime pairs (previously raised) and rejects reversed Time pairs with an error instead of returning a negative duration.

  • RBNF plural substitutions ($(cardinal,…)$) select the plural category on the quotient of the rule divisor per ICU: Russian 2,000,000 spells out as "два миллиона", not "два миллионов".

  • Requesting a spellout ruleset a locale does not have returns Localize.UnknownRbnfRuleError — with its available: field now populated — instead of silently formatting digits.

  • The :spellout format resolves to spellout-numbering (ICU's default ruleset) deterministically; previously the choice among a locale's gendered rulesets varied across OTP releases (German 1.5 could spell "eine", "ein" or "eins" depending on the VM).

  • Numeric collation computes the value of non-ASCII digits from their Unicode block's zero digit: Arabic-Indic "٢" sorts before "١٠" and "٠" compares equal to "0"; previously digit values were computed modulo 10 of the codepoint, wrapping mid-block.

  • :fractional_digits applies to numbers with a zero integer part: Number.to_string(0.4, fractional_digits: 0) returns "0" (previously "0.4"), with float and Decimal inputs rounding identically across all rounding modes.

  • Localize.Utils.Map.extract_strings/2 no longer drops strings that precede a nested list or map, and underscore_keys(nil) returns nil instead of raising.

  • Number.to_range_string/3 with approximate: true formats the full range ("~3–5"); previously the range end was silently dropped.

  • Number.PluralRule.plural_type/2 returns a bare category atom on both backends; the NIF backend previously returned an {:ok, atom} tuple.

  • Converting a mixed unit to another mixed unit works (foot-and-inchyard-and-foot-and-inch); previously it always returned a :mixed_units error.

  • Unit plural categories for Decimal values honour the visible fraction (Decimal "1" renders "1 meter"), and denominator units keep their SI prefix and power ("per 100 kilometers", not "per 100 meter").

  • Localize.Utils.Math.power/2 with a fractional exponent between 0 and 1 returned the reciprocal (power(4, 0.5) gave 0.5); Decimal bases now always return Decimals.

  • Locale downloads run in a dedicated :localize httpc profile with the proxy set per request, so a configured proxy no longer leaks into later requests or into the host application's default httpc profile.

  • Display names of parsed (unvalidated) language tags with -t- extensions or multi-part -u- values no longer crash or mangle subtags, and multiple -u-dx- scripts render translated and list-separated.

  • Localize.Date.to_string/2 no longer overwrites a user-supplied :number_system_overrides option.

  • Small fixes: UnknownSubdivisionError carries the original input instead of nil; UnknownCurrencyError for a binary code always carries the binary (previously the atom when it happened to exist); MF2 match results deduplicate their bound-variable lists; Localize.Utils.Json.decode!/1 raises ArgumentError on trailing data instead of a bare MatchError.

  • Language tags with duplicate singleton subtags (en-u-ca-gregory-u-nu-thai) are rejected with a parse error per RFC 5646; previously the second occurrence was silently dropped.

  • Grandfathered language tags resolve to their preferred values from the CLDR alias data: i-klingon canonicalizes to tlh (displaying "Klingon"), zh-min-nan to nan, en-GB-oed to en-GB-oxendict, and the sign-language tags to their ISO codes. Previously irregular grandfathered tags parsed to an empty tag.

  • The MF2 Elixir backend reports the same errors as the NIF backend: an unbound variable used as a function option value returns a bind error, and an invalid :locale option returns a locale error; both were previously silently ignored.

  • Per-compound units without a direct CLDR pattern compose from the numerator and the per-pattern: foot-per-second renders "2 feet per second" instead of the raw identifier.

  • MF2 :percent selection multiplies the operand by 100 before plural-category selection, matching the formatted value per the specification.

  • An MF2 .match on an unbound variable returns an unresolved-variable error instead of silently selecting the fallback variant.

  • A duplicated boolean assertion in the message custom-function tests masked half of an intended check; general mechanical cleanups (map_join, empty-enum checks, number underscores, redundant with clauses) applied throughout.