Localize is a locale-aware formatting and data library for Elixir built on the Unicode CLDR repository. It provides the same domain coverage as the ex_cldr family of libraries but with a fundamentally different architecture.
This document describes the key design decisions and how they differ from ex_cldr.
No backend compile-time modules
ex_cldr
In ex_cldr, every application defines one or more backend modules using use Cldr:
defmodule MyApp.Cldr do
use Cldr,
locales: [:en, :fr, :de],
providers: [Cldr.Number, Cldr.DateTime, Cldr.Territory]
endThe use Cldr macro triggers a @before_compile hook that runs Cldr.Backend.Compiler.__before_compile__/1. This generates a large module containing:
A function clause per configured locale with embedded CLDR data (territory names, number formats, date patterns, etc.).
Delegation functions for every registered provider (Number, DateTime, Territory, Language, etc.).
Configuration accessors (
known_locale_names/0,default_locale/0, etc.).
The generated module is compiled into a single BEAM file whose size grows with the number of configured locales and providers. Changing the locale list or adding a provider triggers a full recompilation of the backend.
Localize
Localize has no backend concept. All public API functions live directly in domain modules (Localize.Number, Localize.Date, Localize.Territory, etc.) and are available immediately without any compile-time setup.
There is nothing to configure to start using the library. All 766 CLDR locales are available at runtime without pre-declaration.
Dynamic data loading at runtime
ex_cldr
Locale data is loaded at compile time by Cldr.Locale.Loader.get_locale/2 and embedded in the backend module via Macro.escape/1. The data becomes part of the compiled bytecode:
# Inside the backend compiler (simplified)
for locale_name <- known_locale_names(config) do
locale_data = Locale.Loader.get_locale(locale_name, config)
def locale_data(unquote(locale_name)), do: unquote(Macro.escape(locale_data))
endThis means:
Only pre-configured locales are available.
Adding a locale requires recompilation.
BEAM file sizes grow linearly with locale count.
Localize
Locale data is loaded lazily at runtime on first access and cached in :persistent_term for zero-copy concurrent reads. The default provider reads pre-built ETF files from an on-disk cache (priv/localize/locales/) and, if a locale is not cached, downloads it from the Localize release CDN:
# Localize.Locale.Provider.PersistentTerm (simplified)
def load(locale) do
locale_id = to_locale_id(locale)
case Localize.Locale.Provider.Cache.get(locale_id) do
{:ok, locale_data} ->
{:ok, locale_data}
{:error, _} ->
{:ok, binary} = Localize.Locale.Provider.download_locale(locale_id)
Localize.Locale.Provider.Cache.store(locale_id, binary)
{:ok, :erlang.binary_to_term(binary)}
end
endThe cache directory is configurable via Localize.Locale.Provider.locale_cache_dir/0, which reads the :locale_cache_dir application environment key, falling back to Path.join(:code.priv_dir(:localize), "localize/locales"). Cached files are tagged with the current Localize.version/0 so stale files are detected and re-downloaded on upgrade.
This means:
All CLDR locales are available without configuration.
Locale data is loaded only when needed, reducing startup time for applications that use few locales.
Memory is used only for locales actually accessed.
No recompilation when locale requirements change.
Applications can ship with a pre-warmed cache or rely on on-demand download at first use.
Supplemental data (plural rules, territory containment, currency codes, etc.) follows the same in-memory pattern — stored as ETF files in priv/localize/supplemental_data/, deserialized on first access, and cached in :persistent_term.
Configurable data providers
Localize defines a Localize.Locale.Provider behaviour that abstracts how locale data is stored and retrieved:
defmodule Localize.Locale.Provider do
@callback load(locale_id :: atom()) :: :ok | {:error, term()}
@callback store(locale_id :: atom(), data :: map()) :: :ok
@callback loaded?(locale_id :: atom()) :: boolean()
@callback get(locale_id :: atom(), keys :: list(), options :: Keyword.t()) ::
{:ok, term()} | {:error, term()}
endThe default provider is Localize.Locale.Provider.PersistentTerm, which reads ETF locale files from the on-disk cache, downloads them on demand when missing, and caches the decoded term in :persistent_term. Alternative providers could store data in ETS, a database, or a remote service.
ex_cldr has no equivalent abstraction — data access is hard-wired through the compiled backend module.
Consolidated library
ex_cldr
The ex_cldr ecosystem is distributed across many packages:
ex_cldr— core library, backend compiler, locale loading.ex_cldr_numbers— number formatting.ex_cldr_dates_times— date, time, datetime, interval formatting.ex_cldr_units— unit formatting and conversion.ex_cldr_lists— list formatting.ex_cldr_currencies— currency data and formatting.ex_cldr_territories— territory names and metadata.ex_cldr_languages— language display names.ex_cldr_locale_display— full locale display names.ex_cldr_messages— ICU/MF2 message formatting.ex_cldr_collation— Unicode collation.
Each package registers as a provider in the backend and generates its own module namespace (e.g., MyApp.Cldr.Number, MyApp.Cldr.Territory).
Localize
All functionality is merged into a single library with domain modules under the Localize namespace:
Localize.Number— number formatting.Localize.Date,Localize.Time,Localize.DateTime— date/time formatting.Localize.Interval— date/time interval formatting.Localize.Unit— unit formatting and conversion.Localize.List— list formatting.Localize.Currency— currency data and formatting.Localize.Territory— territory names, metadata, and relationships.Localize.Language— language display names.Localize.Locale.LocaleDisplay— full locale display names.Localize.Message— ICU MessageFormat 2.0 formatting.Localize.Collation— Unicode collation.Localize.Calendar— calendar localization.
A single dependency, a single version, and a single compilation unit.
Process locale management
ex_cldr
Locale state is per-backend. Each backend has its own current locale stored in the process dictionary:
Cldr.put_locale(MyApp.Cldr, "de")
Cldr.get_locale(MyApp.Cldr)Applications with multiple backends can have different current locales simultaneously.
Localize
There is a single process-wide locale stored in the process dictionary via Localize.put_locale/1 and Localize.get_locale/0. All formatting functions default to this locale.
The default locale is resolved once on first access using a precedence chain:
LOCALIZE_DEFAULT_LOCALEenvironment variable.Application.get_env(:localize, :default_locale).LANGenvironment variable (charset stripped).:enas final fallback.
Both the process locale and the default locale are Localize.LanguageTag structs, validated via Localize.validate_locale/1.
Error handling
ex_cldr
Errors are returned as two-element tuples containing an exception module and a message string:
{:error, {Cldr.UnknownTerritoryError, "The territory :ZZ is unknown"}}Localize
Errors are returned as exception structs with structured fields and localizable messages via Gettext:
{:error, %Localize.UnknownTerritoryError{territory: :ZZ}}Exception messages use Gettext.dpgettext/5 with domain "localize" and context strings, enabling translation of error messages themselves.
Compiled artifact caching
Number format metadata and datetime format tokens are parsed from CLDR pattern strings at runtime. To avoid repeated parsing, Localize caches compiled artifacts in :persistent_term:
Number formats —
Localize.Number.Format.Compileroutput (aMetastruct) cached by format string.DateTime formats —
Localize.DateTime.Format.Compileroutput (a token list) cached by format string.
These are effectively compile-once-use-forever within a VM lifetime, similar to ex_cldr's precompile_number_formats option but without requiring explicit configuration.
Plural rules
Both libraries generate per-locale plural rule functions at compile time from CLDR data. In Localize, the rule data is loaded from ETF at compile time for function generation, but the module attribute is deleted after generation so the raw data is not embedded in the BEAM file. Runtime access to plural rule data goes through SupplementalData with :persistent_term caching.
Optional NIF integration
Localize includes an optional NIF binding for ICU4C (Localize.Nif) that provides native-speed MessageFormat 2.0 parsing and formatting. The NIF is opt-in via the LOCALIZE_NIF=true environment variable and falls back to a pure Elixir implementation when unavailable.
ex_cldr does not include NIF support.
Collation
Localize includes a Unicode Collation Algorithm implementation (Localize.Collation) with a pre-built collation table stored in :persistent_term. This provides locale-aware string sorting without external dependencies.
ex_cldr_collation provides similar functionality as a separate package.