Migrating from ex_cldr to Localize

Copy Markdown View Source

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]
end

In 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.

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 coverage

Full options reference

OptionDefaultDescription
:default_localeDerived from LOCALIZE_DEFAULT_LOCALE env var → LANG env var → :enApplication-wide default locale.
:supported_localesnil (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_localesdeprecatedDeprecated and ignored. Use :supported_locales and mix localize.download_locales.
:locale_cache_dirApplication.app_dir(:localize, "priv/localize/locales")Directory where downloaded locale ETF files are cached.
:allow_runtime_locale_downloadfalseWhen 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_providerLocalize.Locale.Provider.PersistentTermModule implementing Localize.Locale.Provider for locale data loading.
:niffalseEnable the optional ICU4C NIF backend. Also settable via LOCALIZE_NIF=true.
:mf2_functions%{}Map of custom MF2 function modules (see Localize.Message.Function).
:cacertfileSystem defaultPath to a custom CA certificate file for HTTPS connections.
:https_proxynilHTTPS proxy URL. Also reads HTTPS_PROXY env var.

Default locale resolution

The default locale is resolved once on first access using this precedence chain:

  1. LOCALIZE_DEFAULT_LOCALE environment variable.

  2. config :localize, default_locale: :fr in your application config.

  3. The LANG environment variable (e.g., en_US.UTF-8), with the charset suffix stripped and POSIX underscores converted to BCP 47 hyphens.

  4. :en as 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
:de

All 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"}
  ]
end

Module mapping

ex_cldr moduleLocalize module
MyApp.Cldr.NumberLocalize.Number
MyApp.Cldr.DateTimeLocalize.DateTime
MyApp.Cldr.DateLocalize.Date
MyApp.Cldr.TimeLocalize.Time
MyApp.Cldr.UnitLocalize.Unit
MyApp.Cldr.ListLocalize.List
MyApp.Cldr.TerritoryLocalize.Territory
MyApp.Cldr.LanguageLocalize.Language
MyApp.Cldr.CurrencyLocalize.Currency
Cldr.LocaleDisplayLocalize.Locale.LocaleDisplay
Cldr.MessageLocalize.Message
Cldr.CollationLocalize.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.1

Lists

# 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_cldrLocalize
Territory.from_territory_code/3Territory.display_name/2
Territory.from_subdivision_code/3Territory.subdivision_name/2
Territory.to_unicode_flag/1Territory.unicode_flag/1
Territory.country_codes/0Territory.individual_territories/0
List.known_list_formats/0List.known_list_styles/0
List.list_formats_for/1List.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

Functionex_cldr optionLocalize 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
:en

The 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, true

Or: 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

Aspectex_cldrLocalize
Setupuse Cldr backend moduleNone required
Available localesPre-configured listAll 766 CLDR locales (constrainable via :supported_locales)
Locale data loadingCompile-time embeddingRuntime lazy loading + on-demand download
Locale argumentBackend module requiredNot needed — defaults to Localize.get_locale()
Default localePer-backend configProcess dictionary + app config + env vars
Error format{:error, {Module, string}}{:error, %Exception{}}
Dependencies11+ packagesSingle package
Polymorphic APINoneLocalize.to_string/2 via Localize.Chars protocol
Custom MF2 functionsNoneLocalize.Message.Function behaviour + :functions option
NIF supportNoneOptional (number, unit, plural, MF2, collation)