chore(deps): update dependency localize to v1 #33

Open
rosa wants to merge 1 commit from renovate/localize-1.x into main
Owner

This PR contains the following updates:

Package Type Update Change
localize (source) prod major ~> 0.50 → ~> 1.0

Release Notes

elixir-localize/localize (localize)

v1.3.0

Compare Source

Added
  • Localize.Date.parse/2, Localize.Time.parse/2 and Localize.DateTime.parse/2 parse a localized date, time or datetime string back to a value. They delegate to calendrical, which the application must add as a dependency.

  • Localize.DependencyRequiredError is returned when an operation needs a companion package the application does not depend on, naming the :package and the :operation. It is what the new parse/2 functions return when calendrical is absent.

Changed
  • Breaking. Localize.validate_territory/1 returns the canonical territory code, so validate_territory("AN") is now {:ok, :CW} where it was {:ok, :AN} — likewise SU to :RU, DD to :DE, and CS and YU both to :RS. This matches validate_locale/1, which already resolved them.
Fixed
  • Localize.Utils.Math.mod/2 and amod/2 carry overloaded contracts, so integer arguments type as integer results under dialyzer. Downstream calendar arithmetic no longer types as float-possible when it flows through these functions.

  • Localize.validate_territory/1 accepts territory codes that CLDR replaces rather than lists: "UK", and the alpha-3 and numeric forms "GBR" and "826", were all rejected and now resolve to :GB. This matters for anything mapping a ccTLD to a territory, since .uk is the domain while GB is the code.

  • Localize.Interval.to_string/3 and to_parts/3 no longer raise FunctionClauseError for en-CA with fields: :month_and_day, format: :short. CLDR publishes an alt="variant" interval pattern for en-CA, and it is now resolved as a single date's pattern is, honouring :prefer.

  • en-CA skeleton formats such as :yMd and :MEd render the locale's standard pattern, 2026-05-03, rather than the day-first variant 3/5/2026 they returned whatever :prefer asked for. prefer: :variant still selects the variant.

v1.2.0

Compare Source

Changed
  • Breaking. Localize.Number.Parser.scan/2 now treats a space as a grouping separator in locales that group with one, so runs of digits that it previously returned separately can come back as a single number. Under fr, "chambres 12 14 16" was ["chambres ", 12, " ", 14, " ", 16] and is now ["chambres ", 121416]; pass lenient: false to require exact grouping sizes and get the old result. This is what lets "1 234,5" be found as one number, which is the point of the change.

  • Breaking. Localize.Number.parse/2 and scan/2 validate that grouping separators sit in plausible positions, matching ICU, so input that was previously accepted can now fail. Under fr, parse("3 4") was {:ok, 34} and is now an error, because a single digit is not a group in any locale.

  • Localize.Number.parse/2 and Localize.Number.Parser.scan/2 take a :lenient option governing that validation. true, the default, requires each group to be at least two digits, which is ICU's lenient rule; false requires exactly the locale's grouping size — three for most locales, and the 3-then-2 of en-IN.

Fixed
  • Timezone display names use the exemplar city CLDR ships rather than one derived from the IANA identifier. The lookup matched %{city: _} where the zone data keys :exemplar_city, so it never fired: 48 zones in en and 190 in ja were named from the identifier instead, losing renames (America/Godthab is "Nuuk"), accents ("Córdoba"), and in non-Latin locales the script entirely.

  • Timezone display names resolve three-part identifiers such as America/Indiana/Knox, whose leaf CLDR keys by string one level deeper. They were split into two parts, so the lookup sought a city named "Indiana/Knox" and fell back to "Knox" rather than CLDR's "Knox, Indiana".

  • Localize.Number.Parser.scan/2 finds a grouped number written with an ordinary keyboard space in a locale that formats with U+202F. Under fr, scan("1 234,5") was [1, " ", 234.5] and is now [1234.5].

  • Localize.Number.parse/2 treats every character in [:Zs:] as a grouping space, per TR35's loose matching for lenient parsing. It previously accepted only U+0020 and the locale's own separator, so a fr number carrying U+00A0 or U+2009 — as copied out of formatted output — failed to parse.

  • Localize.Number.parse/2 ignores every character in [:Cf:], per the same passage's instruction to ignore format characters, "in particular ... any RLM, LRM or ALM used to control BIDI formatting". CLDR writes the minus sign of ar, he, fa and 16 other locales as a BIDI mark followed by the sign, so a negative number copied out of that text previously failed to parse.

  • Localize.Number.parse/2 applies CLDR's parseLenients character folds, which were generated into the locale data but never read. The minus, plus, comma and full-stop families now fold to their canonical form, so the 18 locales writing minusSign as U+2212 — fa, fi, sv and no among them — parse their own negative numbers back. Per TR35 the fold applies to the locale's own separators as well as the input, which is what lets de-CH accept both spellings of its apostrophe group separator.

Added
  • Localize.DateTime.Timezone.exemplar_city/3 returns the localized exemplar city for an IANA timezone identifier. derive: false returns an error rather than deriving a name from the identifier, distinguishing a name CLDR vouches for from one this library invented.

v1.1.1

Compare Source

Fixed
  • Localize.Unit.to_string/2 selects the plural category from the rendered digits rather than the input value, as TR35 defines the plural operands over the source number — "the visual appearance of the digits of the result". The float 1.0 renders as 1 and is now "1 hectare" rather than "1 hectares", and conversely 1 with fractional_digits: 2 renders as 1.00 and is now "1.00 hectares" rather than "1.00 hectare".

  • Localize.Unit.localize/2 now resolves preferred units whose name is more than one word. It converted the atoms from Localize.Unit.Preference.preferred_units/2 with Atom.to_string/1, yielding "cubic_inch" where the parser wants "cubic-inch", so 35 of CLDR's 90 preferred units failed with a Localize.ParseError — taking the default usage of speed, volume, area, energy and pressure with them.

Changed
  • The package now declares Unicode-3.0 alongside Apache-2.0, and LICENSE.md gains a "Unicode Data" section setting out which CLDR and UCD data is embedded, what it becomes, and reproducing the Unicode copyright and permission notice as that license requires. Modules emitted by mix localize.unit.gen_conversions carry the same dual-license SPDX header, so a consumer running reuse lint over the generated file no longer needs a .license sidecar.

v1.1.0

Compare Source

Fixed
  • The t:Localize.Currency.t/0 type now declares :iso_digits as non_neg_integer() | nil. Historic currencies (for example :BGN) have no ISO 4217 minor-unit definition and carry nil, which the module's own is_nil/1 guards already handle; the previous non_neg_integer() type caused false pattern_match warnings in downstream code that guards against nil.
Added
  • mix localize.unit.gen_conversions generates a dependency-free unit conversion module from CLDR data, for embedded targets where compiling Localize costs more than the project itself. There is no unit selection to make: Localize's own parser is inlined into the generated module, so it accepts every identifier Localize.Unit accepts — prefixed, powered and -per- compound alike — through to_base/2, resolve/1 and a compile-time ~u sigil.

v1.0.1

Compare Source

Fixed
  • Localize.LanguageTag.best_match/3 now prefers a territory's own language when candidates are otherwise equidistant, so sgs (Samogitian, spoken in Lithuania) matches lt rather than en-LT. The distance trie scores every candidate sharing the desired script and territory identically, which previously left the winner to depend on the order of the supported list.

  • Localize.ParseError reports an over-cap input as reason: :input_too_large with the byte counts in new :size and :limit fields, instead of interpolating them into a prose :reason string that its own type never allowed. The message, number and unit-identifier parsers all use it, and the oversized input is no longer retained on the exception.

v1.0.0

Compare Source

The first stable release. MF2 message validation is separated from parsing: Localize.Message.Parser.parse/1 checks syntax, Localize.Message.Validator.validate/1 checks the TR35 data-model rules, and everything that formats or serializes a message runs both. A message like .local $count = {$count :number} — a declaration that reads the variable it declares, which is a Duplicate Declaration error rather than a valid annotation of an external variable — is now rejected wherever it appears rather than only when formatted. The construct for annotating an external variable is .input {$count :number}.

Added
  • An ecosystem guide sets out the library's shape: a set of structured, locale-aware types, and the four operations — formatting, parsing, ordering and serialization — that apply both to them and to most of Elixir's own types. It tabulates which library provides each operation for each type.

  • Localize.Message.Validator.validate/1 is public, checking a parsed message against the TR35 data-model rules. Localize.Message.Parser.parse/1 remains syntax-only, so tooling that must accept invalid input — to_tokens/2 and the highlighters built on it — parses without validating, while everything that formats or serializes composes the two and reports syntax errors before data-model ones.

Fixed
  • Duplicate Declaration is now caught wherever a message is used, not only when it is formatted. .local $count = {$count :number} reads the variable it declares, so it was rejected by format/3 but serialized happily by canonical_message/2 and accepted at compile time as a Gettext msgid. Use .input {$count :number} to annotate an external variable.

  • Three further TR35 data-model rules are now enforced: Missing Selector Annotation (a selector that does not resolve, directly or through a chain of locals, to a declaration carrying a function), Variant Key Mismatch (a variant whose key count differs from the selector count), and Missing Fallback Variant (no variant with only * keys). None was detected previously — the first two were documented as enforced during match evaluation and were not, and all three could be formatted or serialized without error.

  • Localize.Message.canonical_message/2 no longer serializes a message that violates the data-model rules; it previously round-tripped every invalid form, including a .local declaration reading the variable it declares. The compile-time Gettext interpolator validates too, so a mis-authored msgid fails the build rather than surfacing at runtime.

  • The MF2 working-group data-model-errors.json conformance cases now assert the specific error reason rather than merely that some error occurred. The WG cases pass no bindings, so an incidental unbound-variable error satisfied the old assertion — which is how three unimplemented rules stayed green.

  • Localize.validate_territory/1 now accepts integer M49 codes as its typespec and documentation always promised — validate_territory(1) returns {:ok, :"001"}. An integer previously raised FunctionClauseError because the validator's guard omitted is_integer/1, even though its normalization already zero-padded integers. Numeric codes may also be given as strings ("001"); note that country-level M49 numbers such as 840 are not in CLDR's territory validity set, which uses the alpha-2 code ("US") there.

v0.50.0

Compare Source

Added
  • Localize.LanguageTag.remove_likely_subtags/2 and !/2 accept favor: :script | :region to select the TR35 removal variant (zh-Hant-TW minimizes to "zh-Hant" or "zh-TW"), verified against the full CLDR likely-subtags test data.

  • The MessageFormat working group's :test:function, :test:format and :test:select registry functions are implemented, so the WG conformance suite's selection-mechanics and fallback cases now run.

  • The MF2 numeric functions accept the signDisplay option (auto, always, exceptZero, negative, never), with the plus sign taken from the locale's number symbols.

  • An MF2 pattern expression that re-annotates a declared variable operates on the declaration's original value, and numeric functions inherit unset options from a numeric declaration — .local $x = {41 :integer signDisplay=always} rendered with {$x :offset add=1} produces "+42".

  • The locale download base URL can be overridden with config :localize, locale_base_url: "..." for deployments mirroring the locale files; downloads are verified against the bundled hash manifest regardless of source.

Changed
  • An unannotated MF2 placeholder with a numeric operand formats with the locale-aware :number default ({$pi} renders "3.142" in en and "3,142" in fr), matching the MF2 implicit formatting rules; previously the value was stringified verbatim.

  • MessageFormat 2 follows the specification's error semantics: unknown functions are an error (Localize.FormatError reason :unknown_function) instead of formatting the operand, and data-model validation rejects duplicate declarations, duplicate option names, and duplicate variants (NFC-normalized keys) with dedicated error reasons.

  • MessageFormat 2 function validation is strict per TR35: the select option must be a literal set directly on the selector expression, digit-size options must be non-negative integers, string operands of numeric functions must match the number-literal production, and :currency, :unit, :date, :time and :datetime are rejected as selectors.

  • Localize.Number.Format.Options is documented again: validate_options/2 is the supported way to pre-resolve formatting options for hot loops (as the Performance guide describes), so the module is part of the public API.

  • The TR35 conformance guide documents the MessageFormat 2 working-group suite exclusions precisely: unknown-function fallback, duplicate declaration/option validation, select and digit-size option validation, u:dir/u:id expression options, and the :isolate bidi strategy deviation.

Deprecated
  • Localize.Currency.currencies_for_locale/2,3, currencies_for_locale!/2,3, currency_strings/2,3 and currency_strings!/2,3 take the include/exclude filters as :only and :except options instead of positional arguments. The positional forms still work but are deprecated and will be removed by Localize 1.0 and no later than December 2026.
Fixed
  • Calendar- and provider-module probes in date/datetime formatting (era year, day of era, ISO week and day of year, day of week, CLDR calendar type) and Localize.Locale.Provider.allow_download?/1 ensure the module is loaded before function_exported?/3, so a cold module no longer silently takes the gregorian or default fallback branch.
Security
  • Localize.Locale.Provider.locale_file_name/1 rejects locale identifiers that do not have the shape of a CLDR locale id, as defense in depth for the cache-path and download-URL interpolations.

v0.49.0

Compare Source

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.

v0.48.0

Compare Source

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.

v0.47.0

Compare Source

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.

v0.46.0

Compare Source

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.

v0.45.0

Compare Source

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 (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), 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-inch → yard-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.

v0.44.0

Compare Source

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.

v0.41.3

Compare Source

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.

v0.41.2

Compare Source

Fixed
  • 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").

v0.41.1

Compare Source

Fixed
  • Localize.Language.display_name/2 now normalises its input through Localize.validate_locale/1, so a string and an equivalent Localize.LanguageTag return the same name ("en-GB" and LanguageTag.new!("en-GB") both give "British English") and region-specific codes resolve with fallback ("pt-BR" → "Brazilian Portuguese", "ar-SA" → "Arabic" instead of raising). A malformed locale now returns Localize.InvalidLocaleError rather than Localize.UnknownLanguageError. Thanks to @​DVSLabs for the report. Closes #​36.

Configuration

📅 Schedule: (UTC)

  • Branch creation
    • At any time (no schedule defined)
  • Automerge
    • At any time (no schedule defined)

🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.

♻ Rebasing: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 Ignore: Close this PR and you won't be reminded about this update again.


  • If you want to rebase/retry this PR, check this box

This PR has been generated by Mend Renovate.

This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [localize](https://hex.pm/packages/localize) ([source](https://github.com/elixir-localize/localize)) | prod | major | `~> 0.50` → `~> 1.0` | --- ### Release Notes <details> <summary>elixir-localize/localize (localize)</summary> ### [`v1.3.0`](https://github.com/elixir-localize/localize/blob/HEAD/CHANGELOG.md#130--September-21st-2026) [Compare Source](https://github.com/elixir-localize/localize/compare/v1.2.0...v1.3.0) ##### Added - `Localize.Date.parse/2`, `Localize.Time.parse/2` and `Localize.DateTime.parse/2` parse a localized date, time or datetime string back to a value. They delegate to `calendrical`, which the application must add as a dependency. - `Localize.DependencyRequiredError` is returned when an operation needs a companion package the application does not depend on, naming the `:package` and the `:operation`. It is what the new `parse/2` functions return when `calendrical` is absent. ##### Changed - **Breaking.** `Localize.validate_territory/1` returns the canonical territory code, so `validate_territory("AN")` is now `{:ok, :CW}` where it was `{:ok, :AN}` — likewise `SU` to `:RU`, `DD` to `:DE`, and `CS` and `YU` both to `:RS`. This matches `validate_locale/1`, which already resolved them. ##### Fixed - `Localize.Utils.Math.mod/2` and `amod/2` carry overloaded contracts, so integer arguments type as integer results under dialyzer. Downstream calendar arithmetic no longer types as float-possible when it flows through these functions. - `Localize.validate_territory/1` accepts territory codes that CLDR replaces rather than lists: `"UK"`, and the alpha-3 and numeric forms `"GBR"` and `"826"`, were all rejected and now resolve to `:GB`. This matters for anything mapping a ccTLD to a territory, since `.uk` is the domain while `GB` is the code. - `Localize.Interval.to_string/3` and `to_parts/3` no longer raise `FunctionClauseError` for `en-CA` with `fields: :month_and_day, format: :short`. CLDR publishes an `alt="variant"` interval pattern for en-CA, and it is now resolved as a single date's pattern is, honouring `:prefer`. - `en-CA` skeleton formats such as `:yMd` and `:MEd` render the locale's standard pattern, `2026-05-03`, rather than the day-first variant `3/5/2026` they returned whatever `:prefer` asked for. `prefer: :variant` still selects the variant. ### [`v1.2.0`](https://github.com/elixir-localize/localize/blob/HEAD/CHANGELOG.md#120--August-16th-2026) [Compare Source](https://github.com/elixir-localize/localize/compare/v1.1.1...v1.2.0) ##### Changed - **Breaking.** `Localize.Number.Parser.scan/2` now treats a space as a grouping separator in locales that group with one, so runs of digits that it previously returned separately can come back as a single number. Under `fr`, `"chambres 12 14 16"` was `["chambres ", 12, " ", 14, " ", 16]` and is now `["chambres ", 121416]`; pass `lenient: false` to require exact grouping sizes and get the old result. This is what lets `"1 234,5"` be found as one number, which is the point of the change. - **Breaking.** `Localize.Number.parse/2` and `scan/2` validate that grouping separators sit in plausible positions, matching ICU, so input that was previously accepted can now fail. Under `fr`, `parse("3 4")` was `{:ok, 34}` and is now an error, because a single digit is not a group in any locale. - `Localize.Number.parse/2` and `Localize.Number.Parser.scan/2` take a `:lenient` option governing that validation. `true`, the default, requires each group to be at least two digits, which is ICU's lenient rule; `false` requires exactly the locale's grouping size — three for most locales, and the 3-then-2 of `en-IN`. ##### Fixed - Timezone display names use the exemplar city CLDR ships rather than one derived from the IANA identifier. The lookup matched `%{city: _}` where the zone data keys `:exemplar_city`, so it never fired: 48 zones in `en` and 190 in `ja` were named from the identifier instead, losing renames (`America/Godthab` is `"Nuuk"`), accents (`"Córdoba"`), and in non-Latin locales the script entirely. - Timezone display names resolve three-part identifiers such as `America/Indiana/Knox`, whose leaf CLDR keys by string one level deeper. They were split into two parts, so the lookup sought a city named `"Indiana/Knox"` and fell back to `"Knox"` rather than CLDR's `"Knox, Indiana"`. - `Localize.Number.Parser.scan/2` finds a grouped number written with an ordinary keyboard space in a locale that formats with U+202F. Under `fr`, `scan("1 234,5")` was `[1, " ", 234.5]` and is now `[1234.5]`. - `Localize.Number.parse/2` treats every character in `[:Zs:]` as a grouping space, per TR35's loose matching for lenient parsing. It previously accepted only U+0020 and the locale's own separator, so a `fr` number carrying U+00A0 or U+2009 — as copied out of formatted output — failed to parse. - `Localize.Number.parse/2` ignores every character in `[:Cf:]`, per the same passage's instruction to ignore format characters, "in particular ... any RLM, LRM or ALM used to control BIDI formatting". CLDR writes the minus sign of `ar`, `he`, `fa` and 16 other locales as a BIDI mark followed by the sign, so a negative number copied out of that text previously failed to parse. - `Localize.Number.parse/2` applies CLDR's `parseLenients` character folds, which were generated into the locale data but never read. The minus, plus, comma and full-stop families now fold to their canonical form, so the 18 locales writing `minusSign` as U+2212 — `fa`, `fi`, `sv` and `no` among them — parse their own negative numbers back. Per TR35 the fold applies to the locale's own separators as well as the input, which is what lets `de-CH` accept both spellings of its apostrophe group separator. ##### Added - `Localize.DateTime.Timezone.exemplar_city/3` returns the localized exemplar city for an IANA timezone identifier. `derive: false` returns an error rather than deriving a name from the identifier, distinguishing a name CLDR vouches for from one this library invented. ### [`v1.1.1`](https://github.com/elixir-localize/localize/blob/HEAD/CHANGELOG.md#111--August-16th-2026) [Compare Source](https://github.com/elixir-localize/localize/compare/v1.1.0...v1.1.1) ##### Fixed - `Localize.Unit.to_string/2` selects the plural category from the rendered digits rather than the input value, as TR35 defines the plural operands over the source number — "the visual appearance of the digits of the result". The float `1.0` renders as `1` and is now `"1 hectare"` rather than `"1 hectares"`, and conversely `1` with `fractional_digits: 2` renders as `1.00` and is now `"1.00 hectares"` rather than `"1.00 hectare"`. - `Localize.Unit.localize/2` now resolves preferred units whose name is more than one word. It converted the atoms from `Localize.Unit.Preference.preferred_units/2` with `Atom.to_string/1`, yielding `"cubic_inch"` where the parser wants `"cubic-inch"`, so 35 of CLDR's 90 preferred units failed with a `Localize.ParseError` — taking the default usage of speed, volume, area, energy and pressure with them. ##### Changed - The package now declares `Unicode-3.0` alongside `Apache-2.0`, and `LICENSE.md` gains a "Unicode Data" section setting out which CLDR and UCD data is embedded, what it becomes, and reproducing the Unicode copyright and permission notice as that license requires. Modules emitted by `mix localize.unit.gen_conversions` carry the same dual-license SPDX header, so a consumer running `reuse lint` over the generated file no longer needs a `.license` sidecar. ### [`v1.1.0`](https://github.com/elixir-localize/localize/blob/HEAD/CHANGELOG.md#110--August-11th-2026) [Compare Source](https://github.com/elixir-localize/localize/compare/v1.0.1...v1.1.0) ##### Fixed - The `t:Localize.Currency.t/0` type now declares `:iso_digits` as `non_neg_integer() | nil`. Historic currencies (for example `:BGN`) have no ISO 4217 minor-unit definition and carry `nil`, which the module's own `is_nil/1` guards already handle; the previous `non_neg_integer()` type caused false `pattern_match` warnings in downstream code that guards against `nil`. ##### Added - `mix localize.unit.gen_conversions` generates a dependency-free unit conversion module from CLDR data, for embedded targets where compiling Localize costs more than the project itself. There is no unit selection to make: Localize's own parser is inlined into the generated module, so it accepts every identifier `Localize.Unit` accepts — prefixed, powered and `-per-` compound alike — through `to_base/2`, `resolve/1` and a compile-time `~u` sigil. ### [`v1.0.1`](https://github.com/elixir-localize/localize/blob/HEAD/CHANGELOG.md#101--August-2nd-2026) [Compare Source](https://github.com/elixir-localize/localize/compare/v1.0.0...v1.0.1) ##### Fixed - `Localize.LanguageTag.best_match/3` now prefers a territory's own language when candidates are otherwise equidistant, so `sgs` (Samogitian, spoken in Lithuania) matches `lt` rather than `en-LT`. The distance trie scores every candidate sharing the desired script and territory identically, which previously left the winner to depend on the order of the supported list. - `Localize.ParseError` reports an over-cap input as `reason: :input_too_large` with the byte counts in new `:size` and `:limit` fields, instead of interpolating them into a prose `:reason` string that its own type never allowed. The message, number and unit-identifier parsers all use it, and the oversized input is no longer retained on the exception. ### [`v1.0.0`](https://github.com/elixir-localize/localize/blob/HEAD/CHANGELOG.md#100--July-31st-2026) [Compare Source](https://github.com/elixir-localize/localize/compare/v0.50.0...v1.0.0) The first stable release. MF2 message validation is separated from parsing: `Localize.Message.Parser.parse/1` checks syntax, `Localize.Message.Validator.validate/1` checks the TR35 data-model rules, and everything that formats or serializes a message runs both. A message like `.local $count = {$count :number}` — a declaration that reads the variable it declares, which is a Duplicate Declaration error rather than a valid annotation of an external variable — is now rejected wherever it appears rather than only when formatted. The construct for annotating an external variable is `.input {$count :number}`. ##### Added - An [ecosystem guide](https://hexdocs.pm/localize/ecosystem.html) sets out the library's shape: a set of structured, locale-aware types, and the four operations — formatting, parsing, ordering and serialization — that apply both to them and to most of Elixir's own types. It tabulates which library provides each operation for each type. - `Localize.Message.Validator.validate/1` is public, checking a parsed message against the TR35 data-model rules. `Localize.Message.Parser.parse/1` remains syntax-only, so tooling that must accept invalid input — `to_tokens/2` and the highlighters built on it — parses without validating, while everything that formats or serializes composes the two and reports syntax errors before data-model ones. ##### Fixed - **Duplicate Declaration** is now caught wherever a message is used, not only when it is formatted. `.local $count = {$count :number}` reads the variable it declares, so it was rejected by `format/3` but serialized happily by `canonical_message/2` and accepted at compile time as a Gettext msgid. Use `.input {$count :number}` to annotate an external variable. - Three further TR35 data-model rules are now enforced: **Missing Selector Annotation** (a selector that does not resolve, directly or through a chain of locals, to a declaration carrying a function), **Variant Key Mismatch** (a variant whose key count differs from the selector count), and **Missing Fallback Variant** (no variant with only `*` keys). None was detected previously — the first two were documented as enforced during match evaluation and were not, and all three could be formatted or serialized without error. - `Localize.Message.canonical_message/2` no longer serializes a message that violates the data-model rules; it previously round-tripped every invalid form, including a `.local` declaration reading the variable it declares. The compile-time Gettext interpolator validates too, so a mis-authored msgid fails the build rather than surfacing at runtime. - The MF2 working-group `data-model-errors.json` conformance cases now assert the specific error reason rather than merely that some error occurred. The WG cases pass no bindings, so an incidental unbound-variable error satisfied the old assertion — which is how three unimplemented rules stayed green. - `Localize.validate_territory/1` now accepts integer M49 codes as its typespec and documentation always promised — `validate_territory(1)` returns `{:ok, :"001"}`. An integer previously raised `FunctionClauseError` because the validator's guard omitted `is_integer/1`, even though its normalization already zero-padded integers. Numeric codes may also be given as strings (`"001"`); note that country-level M49 numbers such as `840` are not in CLDR's territory validity set, which uses the alpha-2 code (`"US"`) there. ### [`v0.50.0`](https://github.com/elixir-localize/localize/blob/HEAD/CHANGELOG.md#0500--July-15th-2026) [Compare Source](https://github.com/elixir-localize/localize/compare/v0.49.0...v0.50.0) ##### Added - `Localize.LanguageTag.remove_likely_subtags/2` and `!/2` accept `favor: :script | :region` to select the TR35 removal variant (`zh-Hant-TW` minimizes to "zh-Hant" or "zh-TW"), verified against the full CLDR likely-subtags test data. - The MessageFormat working group's `:test:function`, `:test:format` and `:test:select` registry functions are implemented, so the WG conformance suite's selection-mechanics and fallback cases now run. - The MF2 numeric functions accept the `signDisplay` option (`auto`, `always`, `exceptZero`, `negative`, `never`), with the plus sign taken from the locale's number symbols. - An MF2 pattern expression that re-annotates a declared variable operates on the declaration's original value, and numeric functions inherit unset options from a numeric declaration — `.local $x = {41 :integer signDisplay=always}` rendered with `{$x :offset add=1}` produces "+42". - The locale download base URL can be overridden with `config :localize, locale_base_url: "..."` for deployments mirroring the locale files; downloads are verified against the bundled hash manifest regardless of source. ##### Changed - An unannotated MF2 placeholder with a numeric operand formats with the locale-aware `:number` default (`{$pi}` renders "3.142" in en and "3,142" in fr), matching the MF2 implicit formatting rules; previously the value was stringified verbatim. - MessageFormat 2 follows the specification's error semantics: unknown functions are an error (`Localize.FormatError` reason `:unknown_function`) instead of formatting the operand, and data-model validation rejects duplicate declarations, duplicate option names, and duplicate variants (NFC-normalized keys) with dedicated error reasons. - MessageFormat 2 function validation is strict per TR35: the `select` option must be a literal set directly on the selector expression, digit-size options must be non-negative integers, string operands of numeric functions must match the `number-literal` production, and `:currency`, `:unit`, `:date`, `:time` and `:datetime` are rejected as selectors. - `Localize.Number.Format.Options` is documented again: `validate_options/2` is the supported way to pre-resolve formatting options for hot loops (as the Performance guide describes), so the module is part of the public API. - The TR35 conformance guide documents the MessageFormat 2 working-group suite exclusions precisely: unknown-function fallback, duplicate declaration/option validation, select and digit-size option validation, `u:dir`/`u:id` expression options, and the `:isolate` bidi strategy deviation. ##### Deprecated - `Localize.Currency.currencies_for_locale/2,3`, `currencies_for_locale!/2,3`, `currency_strings/2,3` and `currency_strings!/2,3` take the include/exclude filters as `:only` and `:except` options instead of positional arguments. The positional forms still work but are deprecated and will be removed by Localize 1.0 and no later than December 2026. ##### Fixed - Calendar- and provider-module probes in date/datetime formatting (era year, day of era, ISO week and day of year, day of week, CLDR calendar type) and `Localize.Locale.Provider.allow_download?/1` ensure the module is loaded before `function_exported?/3`, so a cold module no longer silently takes the gregorian or default fallback branch. ##### Security - `Localize.Locale.Provider.locale_file_name/1` rejects locale identifiers that do not have the shape of a CLDR locale id, as defense in depth for the cache-path and download-URL interpolations. ### [`v0.49.0`](https://github.com/elixir-localize/localize/blob/HEAD/CHANGELOG.md#0490--July-12th-2026) [Compare Source](https://github.com/elixir-localize/localize/compare/v0.48.0...v0.49.0) ##### Fixed - Change the namespaces on the `leex` lexers and `yecc` parsers to avoid module name clash with `ex_cldr` versions. Thanks to [@&#8203;jerryluk](https://github.com/jerryluk) for the PR. Closes [#&#8203;40](https://github.com/elixir-localize/localize/issues/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. ### [`v0.48.0`](https://github.com/elixir-localize/localize/blob/HEAD/CHANGELOG.md#0480--July-8th-2026) [Compare Source](https://github.com/elixir-localize/localize/compare/v0.47.0...v0.48.0) ##### 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. ### [`v0.47.0`](https://github.com/elixir-localize/localize/blob/HEAD/CHANGELOG.md#0470--July-7th-2026) [Compare Source](https://github.com/elixir-localize/localize/compare/v0.46.0...v0.47.0) ##### 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 [@&#8203;gmile](https://github.com/gmile) for the PR. Closes [#&#8203;37](https://github.com/elixir-localize/localize/issues/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 [@&#8203;gmile](https://github.com/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. ### [`v0.46.0`](https://github.com/elixir-localize/localize/blob/HEAD/CHANGELOG.md#0460--July-6th-2026) [Compare Source](https://github.com/elixir-localize/localize/compare/v0.45.0...v0.46.0) ##### 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. ### [`v0.45.0`](https://github.com/elixir-localize/localize/blob/HEAD/CHANGELOG.md#0450--July-5th-2026) [Compare Source](https://github.com/elixir-localize/localize/compare/v0.44.0...v0.45.0) 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 (`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), 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-inch` → `yard-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. ### [`v0.44.0`](https://github.com/elixir-localize/localize/blob/HEAD/CHANGELOG.md#0440--July-4th-2026) [Compare Source](https://github.com/elixir-localize/localize/compare/v0.41.3...v0.44.0) 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 (`=#,##&#8203;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. ### [`v0.41.3`](https://github.com/elixir-localize/localize/blob/HEAD/CHANGELOG.md#0413--July-1st-2026) [Compare Source](https://github.com/elixir-localize/localize/compare/v0.41.2...v0.41.3) ##### 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. ### [`v0.41.2`](https://github.com/elixir-localize/localize/blob/HEAD/CHANGELOG.md#0412--July-1st-2026) [Compare Source](https://github.com/elixir-localize/localize/compare/v0.41.1...v0.41.2) ##### Fixed - 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"`). ### [`v0.41.1`](https://github.com/elixir-localize/localize/blob/HEAD/CHANGELOG.md#0411--June-30th-2026) [Compare Source](https://github.com/elixir-localize/localize/compare/v0.41.0...v0.41.1) ##### Fixed - `Localize.Language.display_name/2` now normalises its input through `Localize.validate_locale/1`, so a string and an equivalent `Localize.LanguageTag` return the same name (`"en-GB"` and `LanguageTag.new!("en-GB")` both give `"British English"`) and region-specific codes resolve with fallback (`"pt-BR"` → `"Brazilian Portuguese"`, `"ar-SA"` → `"Arabic"` instead of raising). A malformed locale now returns `Localize.InvalidLocaleError` rather than `Localize.UnknownLanguageError`. Thanks to [@&#8203;DVSLabs](https://github.com/DVSLabs) for the report. Closes [#&#8203;36](https://github.com/elixir-localize/localize/issues/36). </details> --- ### Configuration 📅 **Schedule**: (UTC) - Branch creation - At any time (no schedule defined) - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4yODguMCIsInVwZGF0ZWRJblZlciI6IjQzLjI4OC4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJyZW5vdmF0ZSJdfQ==-->
chore(deps): update dependency localize to v1
Some checks failed
renovate/artifacts Artifact file update failure
ci/woodpecker/push/test Pipeline failed
ci/woodpecker/pr/test Pipeline failed
bcd4cf4bb9
Author
Owner

⚠️ Artifact update problem

Renovate failed to update an artifact related to this branch. You probably do not want to merge this PR as-is.

♻ Renovate will retry this branch, including artifacts, only when one of the following happens:

  • any of the package files in this branch needs updating, or
  • the branch becomes conflicted, or
  • you click the rebase/retry checkbox if found above, or
  • you rename this PR's title to start with "rebase!" to trigger it manually

The artifact failure details are included below:

File name: mix.lock
Command failed: install-tool elixir v1.20.2

### ⚠️ Artifact update problem Renovate failed to update an artifact related to this branch. You probably do not want to merge this PR as-is. ♻ Renovate will retry this branch, including artifacts, only when one of the following happens: - any of the package files in this branch needs updating, or - the branch becomes conflicted, or - you click the rebase/retry checkbox if found above, or - you rename this PR's title to start with "rebase!" to trigger it manually The artifact failure details are included below: ##### File name: mix.lock ``` Command failed: install-tool elixir v1.20.2 ```
Some checks failed
renovate/artifacts Artifact file update failure
ci/woodpecker/push/test Pipeline failed
ci/woodpecker/pr/test Pipeline failed
This pull request can be merged automatically.
This branch is out-of-date with the base branch
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin renovate/localize-1.x:renovate/localize-1.x
git switch renovate/localize-1.x

Merge

Merge the changes and update on Forgejo.

Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.

git switch main
git merge --no-ff renovate/localize-1.x
git switch renovate/localize-1.x
git rebase main
git switch main
git merge --ff-only renovate/localize-1.x
git switch renovate/localize-1.x
git rebase main
git switch main
git merge --no-ff renovate/localize-1.x
git switch main
git merge --squash renovate/localize-1.x
git switch main
git merge --ff-only renovate/localize-1.x
git switch main
git merge renovate/localize-1.x
git push origin main
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
rosa/spacetraders_client!33
No description provided.