This guide is for developers currently using one or more ex_cldr_* libraries who want to migrate to Localize.
No compile-time configuration
The most significant change is that Localize requires no compile-time backend module. In ex_cldr you define a backend:
# ex_cldr — remove this entirely
defmodule MyApp.Cldr do
use Cldr,
locales: [:en, :fr, :de, :ja],
default_locale: :en,
providers: [Cldr.Number, Cldr.DateTime, Cldr.Unit, Cldr.List, Cldr.Territory]
endIn Localize there is no equivalent. Delete your backend module. All 766 CLDR locales are available at runtime without pre-declaration, and all formatting modules are ready to use immediately.
Configuration
Localize requires no compile-time configuration. All options are set in your application config and take effect at runtime.
Recommended migration config
Most ex_cldr projects configure a fixed set of locales in the backend module. The Localize equivalent is :supported_locales (constrains validation) plus mix localize.download_locales (pre-populates the cache at build time):
# config/config.exs
config :localize,
default_locale: :en,
supported_locales: [:en, :fr, :de, :ja]# At build time (Dockerfile, CI, or local)
mix localize.download_locales
In ex_cldr, locales were declared inside use Cldr, locales: [...] and embedded at compile time. In Localize, :supported_locales is an application environment key (no recompilation needed), and locale data is downloaded once at build time and loaded lazily into :persistent_term on first access.
Using Gettext locales
If your application uses Gettext, you can derive :supported_locales from your Gettext backend. Since the Gettext module must be compiled first, use config/runtime.exs:
# config/runtime.exs
config :localize,
supported_locales: Gettext.known_locales(MyApp.Gettext)POSIX-style locale names returned by Gettext (e.g. "pt_BR", "zh_Hans") are automatically normalised to BCP 47 and resolved to their CLDR canonical form via likely-subtag resolution. For example, "pt_BR" resolves to :pt (CLDR treats bare pt as Brazilian Portuguese) and "zh_Hans" resolves to :zh. No manual mapping is needed.
Only exact matches (distance score 0 in the CLDR matching algorithm) are accepted for :supported_locales — this ensures that misspelled or unrecognised locale names are caught at startup rather than silently mapping to a distant locale. Entries that cannot be resolved log a warning with domain: :localize and are skipped.
Coverage-level keywords (:modern, :moderate, :basic) are also accepted and expand to all CLDR locales at or above that level:
config :localize,
supported_locales: [:modern] # ~104 locales with modern CLDR coverageFull options reference
| Option | Default | Description |
|---|---|---|
:default_locale | Derived from LOCALIZE_DEFAULT_LOCALE env var → LANG env var → :en | Application-wide default locale. |
:supported_locales | nil (all 766 CLDR locales) | List of locale atoms, wildcard strings (e.g. "en-*"), coverage-level keywords (:modern, :moderate, :basic), or Gettext-style strings (e.g. "pt_BR"). POSIX underscores are normalised and entries are resolved via likely-subtag resolution — only exact matches (score 0) are accepted. Invalid entries log a warning and are skipped. When set, validate_locale/1 resolves against this list instead of all CLDR locales. |
:preload_locales | deprecated | Deprecated and ignored. Use :supported_locales and mix localize.download_locales. |
:locale_cache_dir | Application.app_dir(:localize, "priv/localize/locales") | Directory where downloaded locale ETF files are cached. |
:allow_runtime_locale_download | false | When true, locales not in the cache are downloaded from the CDN on first access. Default false — use mix localize.download_locales to pre-populate at build time. |
:locale_provider | Localize.Locale.Provider.PersistentTerm | Module implementing Localize.Locale.Provider for locale data loading. |
:nif | false | Enable the optional ICU4C NIF backend. Also settable via LOCALIZE_NIF=true. |
:mf2_functions | %{} | Map of custom MF2 function modules (see Localize.Message.Function). |
:cacertfile | System default | Path to a custom CA certificate file for HTTPS connections. |
:https_proxy | nil | HTTPS proxy URL. Also reads HTTPS_PROXY env var. |
Default locale resolution
The default locale is resolved once on first access using this precedence chain:
LOCALIZE_DEFAULT_LOCALEenvironment variable.config :localize, default_locale: :frin your application config.The
LANGenvironment variable (e.g.,en_US.UTF-8), with the charset suffix stripped and POSIX underscores converted to BCP 47 hyphens.:enas a final fallback.
The resolved locale is validated and cached as a Localize.LanguageTag struct in :persistent_term.
If any source provides an invalid locale, a warning is logged with domain: :localize metadata and the next source is tried.
Process locale
Set the locale for the current process:
iex> {:ok, _} = Localize.put_locale(:de)
iex> Localize.get_locale().cldr_locale_id
:deAll formatting functions default their :locale option to Localize.get_locale(). In a Phoenix application you would typically call Localize.put_locale/1 in a plug early in your pipeline.
Use Localize.with_locale/2 for temporary locale changes:
iex> Localize.with_locale(:ja, fn ->
...> Localize.Number.to_string(1234)
...> end)
{:ok, "1,234"}Pre-populating the locale cache
Run mix localize.download_locales at build time to download locale data for all configured :supported_locales. Locale data is then loaded lazily into :persistent_term on first access — no runtime downloads needed.
Dependency changes
Replace all ex_cldr_* dependencies with a single dependency:
# mix.exs
defp deps do
[
# Remove all of these:
# {:ex_cldr, "~> 2.0"},
# {:ex_cldr_numbers, "~> 2.0"},
# {:ex_cldr_dates_times, "~> 2.0"},
# {:ex_cldr_units, "~> 2.0"},
# {:ex_cldr_lists, "~> 2.0"},
# {:ex_cldr_currencies, "~> 2.0"},
# {:ex_cldr_territories, "~> 2.0"},
# {:ex_cldr_languages, "~> 2.0"},
# {:ex_cldr_locale_display, "~> 2.0"},
# {:ex_cldr_messages, "~> 2.0"},
# {:ex_cldr_collation, "~> 2.0"},
# Add this:
{:localize, "~> 0.1"}
]
endModule mapping
| ex_cldr module | Localize module |
|---|---|
MyApp.Cldr.Number | Localize.Number |
MyApp.Cldr.DateTime | Localize.DateTime |
MyApp.Cldr.Date | Localize.Date |
MyApp.Cldr.Time | Localize.Time |
MyApp.Cldr.Unit | Localize.Unit |
MyApp.Cldr.List | Localize.List |
MyApp.Cldr.Territory | Localize.Territory |
MyApp.Cldr.Language | Localize.Language |
MyApp.Cldr.Currency | Localize.Currency |
Cldr.LocaleDisplay | Localize.Locale.LocaleDisplay |
Cldr.Message | Localize.Message |
Cldr.Collation | Localize.Collation |
Cldr (core) | Localize |
API differences
No backend argument
In ex_cldr, most functions require a backend module as an argument. In Localize, remove it:
# ex_cldr
Cldr.Territory.display_name(:GB, backend: MyApp.Cldr)
Cldr.Territory.from_territory_code(:GB, MyApp.Cldr, locale: "pt")
# Localize
iex> Localize.Territory.display_name(:GB)
{:ok, "United Kingdom"}
iex> Localize.Territory.display_name(:GB, locale: :pt)
{:ok, "Reino Unido"}Error tuple format
ex_cldr returns {:error, {ExceptionModule, message}}. Localize returns {:error, %ExceptionStruct{}}:
# ex_cldr
{:error, {Cldr.UnknownTerritoryError, "The territory :ZZ is unknown"}}
# Localize
{:error, %Localize.UnknownTerritoryError{territory: :ZZ}}Update any case or with clauses that pattern match on the two-element error tuple.
Locale option defaults
All formatting functions default their :locale option to Localize.get_locale() (which returns a LanguageTag). You no longer need to pass :locale if you have set the process locale.
Formatting examples
Numbers
# ex_cldr
MyApp.Cldr.Number.to_string(1234.5)
MyApp.Cldr.Number.to_string(0.56, format: :percent)
MyApp.Cldr.Number.to_string(100, currency: :USD)
# Localize
iex> Localize.Number.to_string(1234.5)
{:ok, "1,234.5"}
iex> Localize.Number.to_string(0.56, format: :percent)
{:ok, "56%"}
iex> Localize.Number.to_string(100, currency: :USD)
{:ok, "$100.00"}Dates
# ex_cldr
MyApp.Cldr.Date.to_string(~D[2025-07-10])
MyApp.Cldr.Date.to_string(~D[2025-07-10], format: :full, locale: "fr")
# Localize
iex> Localize.Date.to_string(~D[2025-07-10])
{:ok, "Jul 10, 2025"}
iex> Localize.Date.to_string(~D[2025-07-10], format: :full, locale: :fr)
{:ok, "jeudi 10 juillet 2025"}Times
# ex_cldr
MyApp.Cldr.Time.to_string(~T[14:30:00])
# Localize
iex> Localize.Time.to_string(~T[14:30:00])
{:ok, "2:30:00\u202FPM"}
iex> Localize.Time.to_string(~T[14:30:00], format: :short)
{:ok, "2:30\u202FPM"}DateTimes
# ex_cldr
MyApp.Cldr.DateTime.to_string(~N[2025-07-10 14:30:00])
# Localize
iex> Localize.DateTime.to_string(~N[2025-07-10 14:30:00])
{:ok, "Jul 10, 2025, 2:30:00\u202FPM"}
iex> Localize.DateTime.to_string(~N[2025-07-10 14:30:00], format: :short)
{:ok, "7/10/25, 2:30\u202FPM"}Units
# ex_cldr
MyApp.Cldr.Unit.new!(100, :meter)
MyApp.Cldr.Unit.to_string(unit)
MyApp.Cldr.Unit.convert!(unit, :kilometer)
# Localize
iex> {:ok, unit} = Localize.Unit.new(100, "meter")
iex> Localize.Unit.to_string(unit)
{:ok, "100 meters"}
iex> Localize.Unit.to_string(unit, format: :short)
{:ok, "100 m"}
iex> {:ok, converted} = Localize.Unit.convert(unit, "kilometer")
iex> converted.value
0.1Lists
# ex_cldr
MyApp.Cldr.List.to_string(["a", "b", "c"])
# Localize
iex> Localize.List.to_string(["a", "b", "c"])
{:ok, "a, b, and c"}
iex> Localize.List.to_string(["a", "b", "c"], locale: :fr)
{:ok, "a, b et c"}Territories
# ex_cldr
Cldr.Territory.from_territory_code(:GB, MyApp.Cldr)
Cldr.Territory.from_territory_code(:GB, MyApp.Cldr, locale: "pt")
Cldr.Territory.parent(:FR)
Cldr.Territory.children(:EU)
Cldr.Territory.info(:US)
# Localize
iex> Localize.Territory.display_name(:GB)
{:ok, "United Kingdom"}
iex> Localize.Territory.display_name(:GB, locale: :pt)
{:ok, "Reino Unido"}
iex> Localize.Territory.parent(:FR)
{:ok, [:"155", :EU, :EZ, :UN]}
iex> Localize.Territory.children(:EU)
{:ok, [:AT, :BE, :CY, ...]}
iex> Localize.Territory.info(:US)
{:ok, %{gdp: 24660000000000, population: 341963000, ...}}Languages
# ex_cldr
MyApp.Cldr.Language.to_string("de")
MyApp.Cldr.Language.to_string("en", locale: "de")
# Localize (renamed from to_string to display_name)
iex> Localize.Language.display_name("de")
{:ok, "German"}
iex> Localize.Language.display_name("en", locale: :de)
{:ok, "Englisch"}
iex> Localize.Language.display_name("en-GB", style: :short)
{:ok, "UK English"}Calendar
# ex_cldr
MyApp.Cldr.Calendar.localize(~D[2024-01-15], :month, type: :stand_alone)
# Localize (option :type renamed to :context)
iex> Localize.Calendar.localize(~D[2024-01-15], :month, context: :stand_alone)
"January"
# New unified display_name API
iex> Localize.Calendar.display_name(:month, 1)
{:ok, "January"}
iex> Localize.Calendar.display_name(:calendar, :gregorian)
{:ok, "Gregorian Calendar"}
iex> Localize.Calendar.display_name(:date_time_field, :year)
{:ok, "year"}Text formatting
# ex_cldr
Cldr.quote("Hello", MyApp.Cldr)
Cldr.ellipsis("And so on", MyApp.Cldr)
# Localize
iex> Localize.quote("Hello")
{:ok, "\u201CHello\u201D"}
iex> Localize.ellipsis("And so on")
{:ok, "And so on\u2026"}Messages (ICU MessageFormat 2)
# ex_cldr
Cldr.Message.format("You have {count} items", %{"count" => 3}, MyApp.Cldr)
# Localize
iex> Localize.Message.format(
...> "{{You have {$count} items}}",
...> %{"count" => 3}
...> )
{:ok, "You have 3 items"}Note that Localize uses the MF2 (MessageFormat 2) syntax which differs from ICU MessageFormat 1. See the MF2 specification for syntax details.
Function renaming
Some functions have been renamed for clarity:
| ex_cldr | Localize |
|---|---|
Territory.from_territory_code/3 | Territory.display_name/2 |
Territory.from_subdivision_code/3 | Territory.subdivision_name/2 |
Territory.to_unicode_flag/1 | Territory.unicode_flag/1 |
Territory.country_codes/0 | Territory.individual_territories/0 |
List.known_list_formats/0 | List.known_list_styles/0 |
List.list_formats_for/1 | List.list_styles_for/1 |
Note that Localize.Territory.individual_territories/0 returns a sorted list of leaf territory code atoms (actual territories, excluding macro-regions such as :"001" or :"150"). This is distinct from Localize.Territory.territory_codes/0, which returns a map of ISO 3166 Alpha-2 codes to their Alpha-3 and numeric equivalents for all territories.
Option renaming
| Function | ex_cldr option | Localize option |
|---|---|---|
Localize.List.to_string/2, Localize.List.intersperse/2 | :format | :list_style |
The :format option on Localize.List.to_string/2 and Localize.List.intersperse/2 has been renamed to :list_style. This frees up the :format keyword to be passed through to per-element formatters when the list contains values like dates or numbers — for example, Localize.List.to_string([~D[2025-07-10], ~D[2025-08-15]], locale: :en, format: :long) now produces "July 10, 2025 and August 15, 2025" because :format is forwarded to Localize.Date.to_string/2 for each element while the list join uses the default :standard style. Migration is mechanical:
# ex_cldr
Cldr.List.to_string(["a", "b", "c"], MyApp.Cldr, format: :unit_narrow)
# Localize
iex> Localize.List.to_string(["a", "b", "c"], locale: :en, list_style: :unit_narrow)
{:ok, "a b c"}The companion helpers Localize.List.known_list_styles/0 and Localize.List.list_styles_for/1 (renamed from known_list_formats/0 and list_formats_for/1) return the available :list_style values.
Locale validation
# ex_cldr
Cldr.validate_locale("en", MyApp.Cldr)
# Localize
iex> {:ok, tag} = Localize.validate_locale("en")
iex> tag.cldr_locale_id
:enThe returned LanguageTag struct can be passed directly to any function that accepts a locale.
Gettext integration
Use Localize.Locale.gettext_locale_id/2 to find the best-matching Gettext locale for a CLDR locale:
iex> Localize.Locale.gettext_locale_id(:en, MyApp.Gettext)
{:ok, "en"}Optional NIF
Localize includes an optional NIF binding for ICU4C. When enabled, specific functions can use the NIF for formatting by passing backend: :nif. The default backend is always :elixir — no NIF is required. Functions that support the NIF include Localize.Number.to_string/2, Localize.Unit.to_string/2, Localize.Number.PluralRule.plural_type/2, Localize.Message.format/3, and Localize.Collation.compare/3.
Enable by setting:
config :localize, :nif, trueOr: export LOCALIZE_NIF=true at compile time. When the NIF is not available, Localize falls back to pure Elixir automatically. Check availability with Localize.Nif.available?/0.
Collation
# ex_cldr
Cldr.Collation.sort(["banana", "apple", "cherry"], MyApp.Cldr, locale: "en")
# Localize
iex> Localize.Collation.sort(["banana", "apple", "cherry"])
["apple", "banana", "cherry"]The collation table is loaded into :persistent_term on first use. No compile-time configuration is needed.
Polymorphic formatting
Localize provides Localize.to_string/2 and Localize.to_string!/2, which format any supported value type through the Localize.Chars protocol. This replaces the need to dispatch to the correct module by hand:
# Format any value — the protocol picks the right formatter
iex> Localize.to_string(1234.5, locale: :de)
{:ok, "1.234,5"}
iex> Localize.to_string(~D[2025-07-10], locale: :en)
{:ok, "Jul 10, 2025"}
iex> {:ok, unit} = Localize.Unit.new(42, "kilometer")
iex> Localize.to_string(unit, format: :short, locale: :en)
{:ok, "42 km"}Built-in implementations cover Integer, Float, Decimal, Date, Time, DateTime, NaiveDateTime, Range, BitString, List, Localize.Unit, Localize.Duration, Localize.LanguageTag, and Localize.Currency. Add implementations for your own types with defimpl Localize.Chars, for: MyApp.Money.
Summary of key differences
| Aspect | ex_cldr | Localize |
|---|---|---|
| Setup | use Cldr backend module | None required |
| Available locales | Pre-configured list | All 766 CLDR locales (constrainable via :supported_locales) |
| Locale data loading | Compile-time embedding | Runtime lazy loading + on-demand download |
| Locale argument | Backend module required | Not needed — defaults to Localize.get_locale() |
| Default locale | Per-backend config | Process dictionary + app config + env vars |
| Error format | {:error, {Module, string}} | {:error, %Exception{}} |
| Dependencies | 11+ packages | Single package |
| Polymorphic API | None | Localize.to_string/2 via Localize.Chars protocol |
| Custom MF2 functions | None | Localize.Message.Function behaviour + :functions option |
| NIF support | None | Optional (number, unit, plural, MF2, collation) |