Skip to content

Releases: elixir-localize/localize

Localize version 0.49.0

Choose a tag to compare

@kipcole9 kipcole9 released this 11 Jul 22:00

[0.49.0] — July 12th, 2026

Fixed

  • Change the namespaces on the leex lexers and yecc parsers to avoid module name clash with ex_cldr versions. 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

Choose a tag to compare

@kipcole9 kipcole9 released this 07 Jul 20:06

[0.48.0] — July 8th, 2026

Added

  • Localize.Unit.to_string/2 accepts humanize: true to render any unit in the unit people expect for its magnitude: bit-, byte-, hertz- and watt-based units scale through the humanize/2 prefix ladder (honouring system: :si | :iec) and all other units render through usage-based preferences. An explicit :usage option keeps precedence for preference-scaled units.

  • Localize.Unit.humanize/2 and humanize!/2 also 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

Choose a tag to compare

@kipcole9 kipcole9 released this 07 Jul 12:58

[0.47.0] — July 7th, 2026

Added

  • Localize.Unit.humanize/2 and humanize!/2 convert a byte- or bit-based unit to the prefix that best fits its magnitude, so 1_500_000 byte formats as "1.5MB" with format: :narrow. The :system option selects :si (powers of 1000, default) or :iec (powers of 1024) prefixes.

  • Localize.Number.to_string/2 accepts format: :short and format: :long as ex_cldr-compatible aliases, resolving to :decimal_short / :decimal_long, or to :currency_short / :currency_long when a :currency is specified.

  • mix localize.update_cldr orchestrates the CLDR data-update pipeline (copy sources → generate supplemental → compile gate → generate locales → test gate), with --check preflight, --locales subsetting, 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 DateTime renders "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: :mr renders "१५ मार्च, २०२१" (likewise bn, fa, my and others) matching ICU. Pattern-level numbers= and caller :number_system_overrides keep 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 with Localize.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/1 honours the -u-fw- extension per TR35: "en-u-fw-mon" returns 1 (Monday) instead of the territory default.

Localize version 0.46.0

Choose a tag to compare

@kipcole9 kipcole9 released this 05 Jul 21:07

[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

Choose a tag to compare

@kipcole9 kipcole9 released this 05 Jul 11:46

[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-varia...

Read more

Localize version 0.44.0

Choose a tag to compare

@kipcole9 kipcole9 released this 04 Jul 03:39

[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 B format symbol selects CLDR day periods ("in the morning", "mittags", "noon", "midnight") from the day-period rules now in the supplemental data pipeline, and b renders noon/midnight at the exact points. Languages without rules fall back to AM/PM.

  • Localize.LanguageTag.U.parse/1 and parse!/1 parse a standalone BCP 47 -u- extension string (with or without the u- singleton) into a validated Localize.LanguageTag.U struct, using the same canonicalization as Localize.validate_locale/1. Localize.LanguageTag.U.encode/1 is 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 by mix localize.generate_locale_hashes at release time), before any decode or cache write. A mismatch or missing entry fails the download with Localize.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, es and fr in addition to en, and the likely-subtags suite asserts the CLDR FAIL rows.

Fixed

  • Localize.Validity.U.encode/2 emits the preferred BCP 47 spelling instead of a deprecated alias (:islamic_civil encodes as "islamic-civil", not "islamicc"), and decode/2 accepts struct-field atom keys and the canonical hyphenated calendar spellings ("islamic-civil").

  • Week fields honour the locale's week configuration: w and Y compute 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), and W computes week-of-month accordingly. Previously all three were ISO-only.

  • Multi-replacement territory aliases follow TR35 Annex C: hy-SU canonicalizes to hy-AM via the likely territory, uk-SU to uk-UA, sr-YU to sr-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 the und mapping.

  • Locales whose language has no CLDR match resolve to root (und) data instead of the alphabetically nearest wrong language — tlh no longer formats with Afar data.

  • Compact formats no longer raise on a negative :fractional_digits option.

Changed

  • mix test --cover now 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

Choose a tag to compare

@kipcole9 kipcole9 released this 05 Jul 11:46

[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/1Language.languages_for/1.

  • Language.known_languages/1Language.language_names_for/1.

  • Script.available_scripts/1Script.scripts_for/1.

  • Script.known_scripts/1Script.script_names_for/1.

  • Subdivision.available_subdivisions/1Subdivision.subdivisions_for/1.

  • Subdivision.known_subdivisions/1Subdivision.subdivision_names_for/1.

  • Territory.available_styles/0Territory.known_styles/0.

Two option names are also swapped for consistency; the deprecated keys remain accepted until 1.0:

  • The :style option of Localize.Duration.to_string/2 and of Localize.quote/2 is deprecated in favour of :format.

  • The :format option of Localize.Unit.display_name/2 is deprecated in favour of :style.

Changed

  • Language.display_name/2 and Script.display_name/2 return {:error, %Localize.InvalidValueError{}} for an invalid :style or :fallback option instead of raising; the ! variants raise as before.

  • Localize.quote/2 returns an error for an unknown format instead of silently returning the input string unquoted.

  • The backend: :nif paths of Number.to_string/2, Number.PluralRule.plural_type/2, Unit.to_string/2 and Message.format/3 now validate the locale through Localize.validate_locale/1 and 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/0 returns all CLDR territory codes.

  • Localize.validate_currency/1 delegates to Localize.Currency.validate_currency/1 so 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!/3 and Unit.display_name!/2.

Localize version 0.42.0

Choose a tag to compare

@kipcole9 kipcole9 released this 05 Jul 11:47

[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: 1234 renders "1.2K" (previously "1K"). Pass :fractional_digits or :max_fractional_digits to override.

  • The json_polyfill dependency 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 :json module 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.ISO dates: 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 :usage option to Localize.Unit.Preference.preferred_units/2 no longer creates atoms from user input; it falls back to :default preferences per TR35.

Added

  • The hex package now includes the NIF sources (c_src/), so LOCALIZE_NIF=true builds from the published package.

Localize version 0.41.3

Choose a tag to compare

@kipcole9 kipcole9 released this 01 Jul 21:56

[0.41.3] — July 1st, 2026

Changed

  • Localize.LanguageTag's canonical_locale_id field — and therefore Localize.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 off cldr_locale_id.

  • Localize.Language.display_name/2 now 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 equivalent Localize.LanguageTag return the same result.

  • Localize.Language.display_name/2 and Localize.Locale.LocaleDisplay.display_name/2 both now validate their input through Localize.validate_locale/1 and drive the display off canonical_locale_id, so a string and an equivalent (maximized) Localize.LanguageTag render identically and no likely subtags leak into the output (a validated "en" tag is "English", not "English (United States)").

Fixed

  • Localize.Language.display_name/2 and Localize.Locale.LocaleDisplay.display_name/2 now break the two-subtag candidate tie toward the earlier subtag per TR35, trying lang-script before lang-region.

  • Localize.validate_locale/1 now 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

Choose a tag to compare

@kipcole9 kipcole9 released this 01 Jul 21:56

[0.41.2] — July 1st, 2026

Bug Fixes

  • Plural rules now resolve likely subtags before selecting a rule set. Localize.Number.PluralRule.Cardinal and Ordinal (pluralize/3, plural_rules_for/1, plural_rule/3) and Localize.Number.PluralRule.plural_type/2 validate the locale through Localize.validate_locale/1 instead of a bare parse, so a string and an equivalent Localize.LanguageTag select the same category and region-specific rules apply — e.g. plural_rule(0, "pt-PT") is now :other (Portugal) rather than :one (the base pt/Brazil rule). This also corrects plural selection in message formatting.

  • Localize.Territory.contains?/2 now validates both arguments, so string and atom territories are equivalent (contains?("EU", "DK") matches contains?(:EU, :DK)); it returns false for 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/1 now canonicalises its input through Localize.validate_locale/1, so a string, atom, and equivalent Localize.LanguageTag yield identical collation options and legacy aliases resolve correctly ("iw" behaves as "he", "pt_BR" as "pt-BR").