Localise version 0.45.0
[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/1andterritories_for/1return the localized territory-name inventory for a locale, completing the*_forfamily across languages, scripts, subdivisions and territories. -
CLDR metazone data joins the supplemental-data pipeline:
Localize.DateTime.Timezone.metazone_for/2returns the metazone for an IANA zone at a given instant, andzone_for_metazone/2returns a metazone's representative zone per territory. Thez/vformat 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
### Optionssections across the number, unit, date/time, locale, territory, list, collation and message modules, and corrects stale claims (RBNF number-system conversion, locale defaults, thevalidate_locale/1matching 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
:offsetfunction follows the specification: it takes exactly one of theaddorsubtractoptions (non-negative integer) and adjusts the operand for both formatting and selection. The previous ICU-styleoffset=option, which only subtracted during selection, is replaced. -
The MF2
numberingSystemfunction 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_systemoption ofLocalize.Number.to_string/2is unchanged. -
Credo (strict) added to the CI lint stage with a committed
.credo.exs. Policy: theDesign.AliasUsagecheck is disabled (Localize module names such asList,DateandStringshadow 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: :localizemetadata attached to Localize log messages.
Fixed
-
The
U(cyclic year) andr(related Gregorian year) format symbols work:Urenders the localized sexagesimal name (丙午, "bing-wu") from the calendar's cyclic name sets andrthe 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: :yMdHmcould render "Jul" or "July" run to run). Selection is now deterministic, prefers the exact skeleton, and matches text-field widths by class (E≡EEE) 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), soLocalize.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-20070315round-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, andLocalize.LanguageTag.parse("zh-min-nan")no longer raisesArgumentError(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:othersposition per TR35; previously they kept unremapped weights, producing orderings incomparable with the reordered scripts. -
The collation
max_variableoption takes effect in the Elixir backend::spacenarrows and:symbol/:currencywiden 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_symbolrenders 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:currencyoption is given. -
Localize.Interval.to_string/3returns an error for mixed endpoint kinds (e.g. aDateand aTime) instead of rendering malformed output, and equalTimeendpoints honour the:time_formatoption in the single-time fallback. -
The
Qandqdate-format symbols were swapped:Qnow renders the format-context quarter andqthe 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, matchingz; previously they fabricated a UTC zone. -
Localize.Duration.new/2acceptsNaiveDateTimepairs (previously raised) and rejects reversedTimepairs 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 itsavailable:field now populated — instead of silently formatting digits. -
The
:spelloutformat resolves tospellout-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_digitsapplies 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/2no longer drops strings that precede a nested list or map, andunderscore_keys(nil)returns nil instead of raising. -
Number.to_range_string/3withapproximate: trueformats the full range ("~3–5"); previously the range end was silently dropped. -
Number.PluralRule.plural_type/2returns 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-inch→yard-and-foot-and-inch); previously it always returned a:mixed_unitserror. -
Unit plural categories for
Decimalvalues 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/2with 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
:localizehttpc 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/2no longer overwrites a user-supplied:number_system_overridesoption. -
Small fixes:
UnknownSubdivisionErrorcarries the original input instead of nil;UnknownCurrencyErrorfor 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!/1raisesArgumentErroron trailing data instead of a bareMatchError. -
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-klingoncanonicalizes totlh(displaying "Klingon"),zh-min-nantonan,en-GB-oedtoen-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
:localeoption 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-secondrenders "2 feet per second" instead of the raw identifier. -
MF2
:percentselection multiplies the operand by 100 before plural-category selection, matching the formatted value per the specification. -
An MF2
.matchon 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.