Rules for LLM coding agents using Localize as a dependency. These are not exhaustive — see the HexDocs guides for full reference.
-
All public formatting and validation functions return
{:ok, result}or{:error, exception}. Pattern match withcase/with, nevertry/rescue. The exception is a struct (e.g.%Localize.UnknownLocaleError{}), not a string. -
Bang variants (
to_string!/2,validate_locale!/1, etc.) exist for the rare cases where raising is preferred on error. -
Never construct locale identifiers by hand and pass them around as raw strings. Call
Localize.validate_locale/1once and pass the resulting%Localize.LanguageTag{}struct, or use a validated atom (:en,:"en-AU"). -
Locale identifiers are atoms in canonical form:
:en,:"en-AU",:"zh-Hant". Strings ("en-AU") are also accepted by every public function and validated on the way in. Do not mix_(POSIX) and-(BCP 47) — Localize accepts both but normalizes to-. -
There is no compile-time backend module. Do not write
use Localizeordefmodule MyApp.Localize. All 766 CLDR locales are available immediately.
| Task | Use |
|---|---|
| Format any supported value (polymorphic) | Localize.to_string/2 (delegates via Localize.Chars protocol) |
| Numbers, decimals, percentages, currencies | Localize.Number.to_string/2 |
| Dates | Localize.Date.to_string/2 |
| Times | Localize.Time.to_string/2 |
| Datetimes | Localize.DateTime.to_string/2 |
| Date/time intervals | Localize.Interval.to_string/3 |
| Relative time ("in 3 days") | Localize.DateTime.Relative.to_string/2 |
| Units of measure | Localize.Unit.new/2 + Localize.Unit.to_string/2 |
| Lists ("a, b, and c") | Localize.List.to_string/2 |
| Territory display names, flags | Localize.Territory.display_name/2, Localize.Territory.unicode_flag/1 |
| Language display names | Localize.Language.display_name/2 |
| Locale display names | Localize.Locale.LocaleDisplay.display_name/2 |
| Currency metadata | Localize.Currency.* |
| String sorting / collation | Localize.Collation.sort/2, Localize.Collation.compare/3 |
| MessageFormat 2 / pluralization | Localize.Message.format/3 |
| Custom MF2 functions | Localize.Message.Function behaviour + :functions option or config :localize, :mf2_functions |
| Calendar names (months, days, eras) | Localize.Calendar.display_name/3 |
-
The current locale is per-process. Set it once at the start of a request (e.g. in a Plug) with
Localize.put_locale(:de). Every formatting and parsing function then defaults its:localeoption toLocalize.get_locale(). -
For temporary locale changes inside a function, use
Localize.with_locale/2rather than save/put/restore. -
The application-wide default locale is resolved (in order) from:
LOCALIZE_DEFAULT_LOCALEenv var →config :localize, default_locale: ...→LANGenv var →:en. Set this at deploy time, not in code.
# Numbers
{:ok, "1,234.5"} = Localize.Number.to_string(1234.5)
{:ok, "1.234,5"} = Localize.Number.to_string(1234.5, locale: :de)
{:ok, "$1,234.56"} = Localize.Number.to_string(1234.56, currency: :USD)
{:ok, "56%"} = Localize.Number.to_string(0.56, format: :percent)
# Dates and times
{:ok, "Jul 10, 2025"} = Localize.Date.to_string(~D[2025-07-10])
{:ok, "10. Juli 2025"} = Localize.Date.to_string(~D[2025-07-10], locale: :de, format: :long)
# Units
{:ok, unit} = Localize.Unit.new(42, "kilometer")
{:ok, "42 km"} = Localize.Unit.to_string(unit, format: :short)
# Lists
{:ok, "apple, banana, and cherry"} =
Localize.List.to_string(["apple", "banana", "cherry"])
# Collation
["apple", "banana", "Cherry"] =
Localize.Collation.sort(["banana", "apple", "Cherry"])
# Pluralization via MessageFormat 2
Localize.Message.format(
~S".input {$count :integer}\n.match $count\n one {{{$count} item}}\n * {{{$count} items}}",
%{"count" => 3}
)
#=> {:ok, "3 items"}
# Validation
{:ok, %Localize.LanguageTag{cldr_locale_id: :en}} = Localize.validate_locale("en-US")
# Polymorphic formatting via Localize.Chars
# Same call site, different value types — dispatch is by protocol.
{:ok, "1.234,5"} = Localize.to_string(1234.5, locale: :de)
{:ok, "Jul 10, 2025"} = Localize.to_string(~D[2025-07-10], locale: :en)
{:ok, "42 km"} = Localize.to_string(unit, format: :short, locale: :en)
"1.234,5" = Localize.to_string!(1234.5, locale: :de) # bang variantLocalize.to_string/{1,2} and Localize.to_string!/{1,2} delegate to the Localize.Chars protocol, which has built-in implementations for Integer, Float, Decimal, Date, Time, DateTime, NaiveDateTime, Range, BitString, List, Localize.Unit, Localize.Duration, Localize.LanguageTag, and Localize.Currency. Unknown types raise Protocol.UndefinedError. Use the polymorphic entry point when the value's type isn't known until runtime; use the dedicated module functions (Localize.Number.to_string/2, etc.) when the type is known and you want unambiguous error messages.
-
For MF2 messages embedded in Elixir, use the
~Msigil (uppercase). It validates the message at compile time with precise line/column errors and canonicalises the string sogettextkeys stay stable regardless of developer whitespace. Do not use the lowercase~m— interpolation via#{…}breaks MF2's grammar. -
Enable the formatter plugin in
.formatter.exsto auto-canonicalise~Msigils and.mf2files onmix format:[plugins: [Localize.Message.Formatter.Plugin]]
The plugin is idempotent —
mix format --check-formattedis safe to run in CI.
-
For hot loops formatting many numbers with the same locale and format, pre-validate options once via
Localize.Number.Format.Options.validate_options/2and pass the resulting struct. This skips per-call locale resolution and currency lookup. Speedup is ~3x for plain decimals and ~50x for currency formatting. -
Set the process locale once with
Localize.put_locale/1rather than passing:localeas an option on every call. -
Localize.validate_locale/1is cached in ETS — first call ~50µs, subsequent calls ~1µs. Pre-validate locales you know you will need at app startup.
# config/config.exs — minimum useful config
config :localize,
default_locale: :en,
supported_locales: [:en, :fr, :de, :ja, "es-*"]-
:supported_localesconstrainsvalidate_locale/1to your declared subset. If unset (the default), all 766 CLDR locales are valid. Wildcard strings ("es-*") expand to every matching locale. -
Runtime locale downloading is off by default. Set
allow_runtime_locale_download: trueto enable on-demand downloading from the Localize CDN. Otherwise pre-populate the cache at build time withmix localize.download_locales(downloads the configured:supported_locales) ormix localize.download_locales en fr defor specific locales. Locale data is loaded lazily into:persistent_termon first access.
-
Do not write
Localize.Number.to_string!(value)and discard the locale — at least passlocale: Localize.get_locale()if you have not set the process locale, or accept the application default. -
Do not pattern match on the old ex_cldr error shape
{:error, {Module, "string"}}. Localize returns{:error, %Exception{}}— match on the struct or callException.message/1. -
Do not call
String.to_atom/1on user-supplied locale strings. UseLocalize.validate_locale/1, which canonicalizes and validates without atom-table pollution. -
Do not assume
Localize.Territory.territory_codes/1returns a list of territories — it returns a map of ISO 3166 code mappings. UseLocalize.Territory.individual_territories/0for the sorted list of leaf territory atoms. -
Do not reach for
Cldr.*modules or backend modules. Localize replaces the entireex_cldr_*family with a single dependency and no compile-time configuration.
When the user needs functionality outside Localize's core CLDR scope:
-
localize_person_names— TR35 Part 8 person name formatting. -
localize_phonenumber— phone number parsing and formatting. -
localize_address— postal address formatting. -
intl— higher-level ergonomic API modeled on the JavaScriptIntlobject.