Search documentation of Localize

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

### 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 coverage

### Full 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:

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

## Module 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.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_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/1`, 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
    :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

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)

← Previous Page Supervision
Next Page → Performance Comparison: Localize vs ex_cldr