Releases: elixir-localize/localize
Release list
Localize version 0.49.0
[0.49.0] — July 12th, 2026
Fixed
-
Change the namespaces on the
leexlexers andyeccparsers to avoid module name clash withex_cldrversions. Thanks to @jerryluk for the PR. Closes #40. -
The coverage ignore list follows the renamed lexer/parser modules, restoring measured coverage above the 90% CI threshold; a guard test now fails when an ignore-list entry names a module that no longer exists.
Localize version 0.48.0
[0.48.0] — July 8th, 2026
Added
-
Localize.Unit.to_string/2acceptshumanize: trueto render any unit in the unit people expect for its magnitude: bit-, byte-, hertz- and watt-based units scale through thehumanize/2prefix ladder (honouringsystem: :si | :iec) and all other units render through usage-based preferences. An explicit:usageoption keeps precedence for preference-scaled units. -
Localize.Unit.humanize/2andhumanize!/2also scale hertz- and watt-based units through the SI prefix ladder ("2.5 gigahertz", "3.2 gigawatts") — like bits and bytes, these are locale-invariant quantities that CLDR's unit preferences do not cover. IEC prefixes remain restricted to bit- and byte-based units.
Localize version 0.47.0
[0.47.0] — July 7th, 2026
Added
-
Localize.Unit.humanize/2andhumanize!/2convert a byte- or bit-based unit to the prefix that best fits its magnitude, so1_500_000 byteformats as "1.5MB" withformat: :narrow. The:systemoption selects:si(powers of 1000, default) or:iec(powers of 1024) prefixes. -
Localize.Number.to_string/2acceptsformat: :shortandformat: :longas ex_cldr-compatible aliases, resolving to:decimal_short/:decimal_long, or to:currency_short/:currency_longwhen a:currencyis specified. -
mix localize.update_cldrorchestrates the CLDR data-update pipeline (copy sources → generate supplemental → compile gate → generate locales → test gate), with--checkpreflight,--localessubsetting, and each step isolated in a fresh VM. The full update process is documented in the consolidated CLDR Update Guide (CLDR_UPDATE_INTEGRATION.md). -
A Claude Code skill covering all major subsystems (numbers, dates/times, units, lists, collation, MessageFormat 2, locale validation and configuration), installable via
/plugin marketplace add elixir-localize/localize. All 300+ skill examples are execution-verified against the library.
Fixed
-
Timezone formatting consults zone-level names before metazone names per TR35, so a UTC
DateTimerenders "UTC" / "Coordinated Universal Time" instead of "GMT" / "Greenwich Mean Time", and Europe/London in summer renders "British Summer Time" instead of "GMT+01:00". Thanks to @gmile for the PR. Closes #37. -
Date and time formatting applies the locale's default numbering system to all numeric fields, so
locale: :mrrenders "१५ मार्च, २०२१" (likewise bn, fa, my and others) matching ICU. Pattern-levelnumbers=and caller:number_system_overrideskeep precedence. Thanks to @gmile for the PR. -
A
-u-nu-locale extension overrides the default numbering system in date and time formatting per TR35:"mr-u-nu-latn"renders latn digits and"en-u-nu-deva"renders deva digits, including when the locale is set withLocalize.put_locale/1. -
Territory-specific collation tailorings apply from locale data: sorting with
locale: "fr-CA"now uses French Canadian backwards accent comparison. Previously the lookup used only the bare language, silently dropping fr-CA's[backwards 2]override. -
Localize.Calendar.first_day_for_locale/1honours the-u-fw-extension per TR35:"en-u-fw-mon"returns 1 (Monday) instead of the territory default.
Localize version 0.46.0
[0.46.0] — July 6th, 2026
Changed
-
Full compile time drops from ~66s to ~3.5s: the RBNF lexer grammar no longer explodes the generated DFA (its generated Erlang shrinks from 891KB to 41KB), and the language/subdivision validity checks compile to a single map lookup instead of thousands of guard clauses. Token streams and validation results are unchanged.
-
The collation tailoring table loads at runtime on first use instead of being embedded at compile time, shrinking the library's largest beam from 1.7MB to 44KB.
-
Every module that embeds ETF data at compile time now declares the file with
@external_resource, so a CLDR data regeneration correctly recompiles the timezone, time-preference, validity and script-mapping modules.
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-varia...
Localize version 0.44.0
[0.44.0] — July 4th, 2026
This release closes the TR35 conformance gaps in the date/time formatting layer and fixes locale-fidelity bugs surfaced by newly-wired conformance data.
Added
-
Flexible day periods: the
Bformat symbol selects CLDR day periods ("in the morning", "mittags", "noon", "midnight") from the day-period rules now in the supplemental data pipeline, andbrenders noon/midnight at the exact points. Languages without rules fall back to AM/PM. -
Localize.LanguageTag.U.parse/1andparse!/1parse a standalone BCP 47-u-extension string (with or without theu-singleton) into a validatedLocalize.LanguageTag.Ustruct, using the same canonicalization asLocalize.validate_locale/1.Localize.LanguageTag.U.encode/1is now a documented public function. -
Downloaded locale files are verified against a SHA-256 hash manifest bundled with the package (
priv/localize/locale_hashes.etf, generated bymix localize.generate_locale_hashesat release time), before any decode or cache write. A mismatch or missing entry fails the download withLocalize.LocaleIntegrityError; when no manifest is present a one-time warning is logged and verification is skipped. -
The rbnf conformance suite now runs the vendored ICU reference data for
de,esandfrin addition toen, and the likely-subtags suite asserts the CLDR FAIL rows.
Fixed
-
Localize.Validity.U.encode/2emits the preferred BCP 47 spelling instead of a deprecated alias (:islamic_civilencodes as "islamic-civil", not "islamicc"), anddecode/2accepts struct-field atom keys and the canonical hyphenated calendar spellings ("islamic-civil"). -
Week fields honour the locale's week configuration:
wandYcompute week-of-year from the locale's first day and minimum days (2023-01-01 is week 1 in en, week 52 of 2022 in de), andWcomputes week-of-month accordingly. Previously all three were ISO-only. -
Multi-replacement territory aliases follow TR35 Annex C:
hy-SUcanonicalizes tohy-AMvia the likely territory,uk-SUtouk-UA,sr-YUtosr-RS; previously the first entry (RU) was always taken. -
RBNF decimal-format substitutions (
=#,##0=) format with the rule's locale instead of the process locale, so fr digits-ordinal renders "1 141e" and de "1.141." rather than the English "1,141". -
Likely-subtags no longer fabricates mappings for languages without one: private-use languages (
qaa) pass through unmaximized per the CLDR test data instead of inheriting theundmapping. -
Locales whose language has no CLDR match resolve to root (
und) data instead of the alphabetically nearest wrong language —tlhno longer formats with Afar data. -
Compact formats no longer raise on a negative
:fractional_digitsoption.
Changed
mix test --covernow excludes build-time tooling (the data-generation pipeline, mix tasks, compile-time parser generators) from coverage measurement so the number reflects the runtime library.
Localize version 0.43.0
[0.43.0] — July 4th, 2026
This release settles the public API naming and error conventions ahead of 1.0. Every renamed function keeps its old name as a deprecated delegate that will be removed by Localize 1.0 and no later than December 2026.
Deprecated
The naming rule is now uniform: known_* is the locale-independent CLDR universe, supported_* reflects configuration, and *_for is data localized into a display locale. The renamed functions are:
-
Language.available_languages/1→Language.languages_for/1. -
Language.known_languages/1→Language.language_names_for/1. -
Script.available_scripts/1→Script.scripts_for/1. -
Script.known_scripts/1→Script.script_names_for/1. -
Subdivision.available_subdivisions/1→Subdivision.subdivisions_for/1. -
Subdivision.known_subdivisions/1→Subdivision.subdivision_names_for/1. -
Territory.available_styles/0→Territory.known_styles/0.
Two option names are also swapped for consistency; the deprecated keys remain accepted until 1.0:
-
The
:styleoption ofLocalize.Duration.to_string/2and ofLocalize.quote/2is deprecated in favour of:format. -
The
:formatoption ofLocalize.Unit.display_name/2is deprecated in favour of:style.
Changed
-
Language.display_name/2andScript.display_name/2return{:error, %Localize.InvalidValueError{}}for an invalid:styleor:fallbackoption instead of raising; the!variants raise as before. -
Localize.quote/2returns an error for an unknown format instead of silently returning the input string unquoted. -
The
backend: :nifpaths ofNumber.to_string/2,Number.PluralRule.plural_type/2,Unit.to_string/2andMessage.format/3now validate the locale throughLocalize.validate_locale/1and pass ICU the canonical BCP 47 string, so both backends resolve aliases and-u-extensions identically. -
Internal implementation modules (
Localize.Utils.*, collation internals, data plumbing) are no longer part of the documented API. The hex package metadata links now point at hexdocs.
Added
-
Localize.Territory.known_territories/0returns all CLDR territory codes. -
Localize.validate_currency/1delegates toLocalize.Currency.validate_currency/1so all validators are available on the top module. -
Bang variants:
Number.to_at_least_string!/2,Number.to_at_most_string!/2,Number.to_approximately_string!/2,Calendar.display_name!/3andUnit.display_name!/2.
Localize version 0.42.0
[0.42.0] — July 4th, 2026
Changed
-
Compact decimal/currency formats (
:decimal_short,:decimal_long,:currency_short) now apply the ECMA-402/ICU default precision of at most two significant digits on the mantissa:1234renders "1.2K" (previously "1K"). Pass:fractional_digitsor:max_fractional_digitsto override. -
The
json_polyfilldependency is no longer declared conditionally at publish time. On OTP 26, add{:json_polyfill, "~> 0.2 or ~> 1.0"}to your own dependencies; the supervisor raises at application start with instructions if the:jsonmodule is missing.
Fixed
-
Compact-format plural selection now uses the mantissa as displayed, per TR35, fixing grammatically impossible output such as es "1 millones" for 1,050,000. Exact-match
count="1"compact patterns (fr 1000 → "mille") are now selected, matching ICU. -
Plural rules for compact values
{mantissa, exponent}now compute the n/i/v/w/f/t operands after shifting by the exponent per the TR35 operand table, and Decimals with positive exponents (1e6) no longer report phantom visible fraction digits. -
BCE years render era-relative for
Calendar.ISOdates: ISO year -1 formats as "2 BC", year 0 as "1 BC" (previously "-1 BC"/"0 BC"). -
Significant-digit rounding no longer forces a trailing fraction digit when the rounded value is integral: 1234.567 at 3 significant digits renders "1,230" (previously "1,230.0"), and minimum significant digits now pad trailing zeros ("1.00" at min 3).
-
A corrupt or truncated locale cache file is treated as a cache miss/stale instead of raising, and cache writes are atomic (write-temp-then-rename) so a crash mid-write can no longer tear a cache file.
-
Locale downloads no longer follow HTTP redirects — the CDN URL is fully known, so a redirect is always refused.
-
An unknown
:usageoption toLocalize.Unit.Preference.preferred_units/2no longer creates atoms from user input; it falls back to:defaultpreferences per TR35.
Added
- The hex package now includes the NIF sources (
c_src/), soLOCALIZE_NIF=truebuilds from the published package.
Localize version 0.41.3
[0.41.3] — July 1st, 2026
Changed
-
Localize.LanguageTag'scanonical_locale_idfield — and thereforeLocalize.LanguageTag.to_string/1— now holds the canonical-syntax form of the requested locale (aliases resolved and subtags ordered, but neither maximized nor minimized), so"en"stays"en"and"en-US"stays"en-US"instead of collapsing to the minimal identity form. Locale identity and data lookup are unaffected; they key offcldr_locale_id. -
Localize.Language.display_name/2now follows the TR35 display-name algorithm, which canonicalizes rather than adds likely subtags: a bare language keeps its own name ("en"→"English","es"→"Spanish"), while an explicitly supplied region or script still resolves to the region-specific CLDR name ("en-GB"→"British English","pt-BR"→"Brazilian Portuguese"). A string and an equivalentLocalize.LanguageTagreturn the same result. -
Localize.Language.display_name/2andLocalize.Locale.LocaleDisplay.display_name/2both now validate their input throughLocalize.validate_locale/1and drive the display offcanonical_locale_id, so a string and an equivalent (maximized)Localize.LanguageTagrender identically and no likely subtags leak into the output (a validated"en"tag is"English", not"English (United States)").
Fixed
-
Localize.Language.display_name/2andLocalize.Locale.LocaleDisplay.display_name/2now break the two-subtag candidate tie toward the earlier subtag per TR35, tryinglang-scriptbeforelang-region. -
Localize.validate_locale/1now accepts a-u-rg-region override that uses a subdivision id (e.g."en-u-rg-gbeng"). The subdivision value was being suffixed with the"zzzz"region filler and rejected as an unknown locale.
Localize version 0.41.2
[0.41.2] — July 1st, 2026
Bug Fixes
-
Plural rules now resolve likely subtags before selecting a rule set.
Localize.Number.PluralRule.CardinalandOrdinal(pluralize/3,plural_rules_for/1,plural_rule/3) andLocalize.Number.PluralRule.plural_type/2validate the locale throughLocalize.validate_locale/1instead of a bare parse, so a string and an equivalentLocalize.LanguageTagselect the same category and region-specific rules apply — e.g.plural_rule(0, "pt-PT")is now:other(Portugal) rather than:one(the basept/Brazil rule). This also corrects plural selection in message formatting. -
Localize.Territory.contains?/2now validates both arguments, so string and atom territories are equivalent (contains?("EU", "DK")matchescontains?(:EU, :DK)); it returnsfalsefor an invalid territory instead of silently treating a string as unmatched. It now also accepts territory strings, matching its sibling relationship functions. -
Localize.Collation.Options.from_locale/1now canonicalises its input throughLocalize.validate_locale/1, so a string, atom, and equivalentLocalize.LanguageTagyield identical collation options and legacy aliases resolve correctly ("iw"behaves as"he","pt_BR"as"pt-BR").