|
| 1 | +# Localize usage rules |
| 2 | + |
| 3 | +Rules for LLM coding agents using `Localize` as a dependency. These are not exhaustive — see the HexDocs guides for full reference. |
| 4 | + |
| 5 | +## Core conventions |
| 6 | + |
| 7 | +* All public formatting and validation functions return `{:ok, result}` or `{:error, exception}`. Pattern match with `case`/`with`, never `try/rescue`. The exception is a struct (e.g. `%Localize.UnknownLocaleError{}`), not a string. |
| 8 | + |
| 9 | +* Bang variants (`to_string!/2`, `validate_locale!/1`, etc.) exist for the rare cases where raising is preferred. |
| 10 | + |
| 11 | +* Never construct locale identifiers by hand and pass them around as raw strings. Call `Localize.validate_locale/1` once and pass the resulting `%Localize.LanguageTag{}` struct, or use a validated atom (`:en`, `:"en-AU"`). |
| 12 | + |
| 13 | +* 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 `-`. |
| 14 | + |
| 15 | +* There is no compile-time backend module. Do not write `use Localize` or `defmodule MyApp.Localize`. All 766 CLDR locales are available immediately. |
| 16 | + |
| 17 | +## Module map |
| 18 | + |
| 19 | +| Task | Use | |
| 20 | +|---|---| |
| 21 | +| Numbers, decimals, percentages, currencies | `Localize.Number.to_string/2` | |
| 22 | +| Dates | `Localize.Date.to_string/2` | |
| 23 | +| Times | `Localize.Time.to_string/2` | |
| 24 | +| Datetimes | `Localize.DateTime.to_string/2` | |
| 25 | +| Date/time intervals | `Localize.Interval.to_string/3` | |
| 26 | +| Relative time ("in 3 days") | `Localize.DateTime.Relative.to_string/2` | |
| 27 | +| Units of measure | `Localize.Unit.new/2` + `Localize.Unit.to_string/2` | |
| 28 | +| Lists ("a, b, and c") | `Localize.List.to_string/2` | |
| 29 | +| Territory display names, flags | `Localize.Territory.display_name/2`, `Localize.Territory.unicode_flag/1` | |
| 30 | +| Language display names | `Localize.Language.display_name/2` | |
| 31 | +| Locale display names | `Localize.Locale.LocaleDisplay.display_name/2` | |
| 32 | +| Currency metadata | `Localize.Currency.*` | |
| 33 | +| String sorting / collation | `Localize.Collation.sort/2`, `Localize.Collation.compare/3` | |
| 34 | +| MessageFormat 2 / pluralization | `Localize.Message.format/3` | |
| 35 | +| Calendar names (months, days, eras) | `Localize.Calendar.display_name/3` | |
| 36 | + |
| 37 | +## Locale management |
| 38 | + |
| 39 | +* 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 function then defaults its `:locale` option to `Localize.get_locale()`. |
| 40 | + |
| 41 | +* For temporary locale changes inside a function, use `Localize.with_locale/2` rather than save/put/restore. |
| 42 | + |
| 43 | +* The application-wide default locale is resolved (in order) from: `LOCALIZE_DEFAULT_LOCALE` env var → `config :localize, default_locale: ...` → `LANG` env var → `:en`. Set this at deploy time, not in code. |
| 44 | + |
| 45 | +## Common idioms |
| 46 | + |
| 47 | +```elixir |
| 48 | +# Numbers |
| 49 | +{:ok, "1,234.5"} = Localize.Number.to_string(1234.5) |
| 50 | +{:ok, "1.234,5"} = Localize.Number.to_string(1234.5, locale: :de) |
| 51 | +{:ok, "$1,234.56"} = Localize.Number.to_string(1234.56, currency: :USD) |
| 52 | +{:ok, "56%"} = Localize.Number.to_string(0.56, format: :percent) |
| 53 | + |
| 54 | +# Dates and times |
| 55 | +{:ok, "Jul 10, 2025"} = Localize.Date.to_string(~D[2025-07-10]) |
| 56 | +{:ok, "10. Juli 2025"} = Localize.Date.to_string(~D[2025-07-10], locale: :de, format: :long) |
| 57 | + |
| 58 | +# Units |
| 59 | +{:ok, unit} = Localize.Unit.new(42, "kilometer") |
| 60 | +{:ok, "42 km"} = Localize.Unit.to_string(unit, format: :short) |
| 61 | + |
| 62 | +# Lists |
| 63 | +{:ok, "apple, banana, and cherry"} = |
| 64 | + Localize.List.to_string(["apple", "banana", "cherry"]) |
| 65 | + |
| 66 | +# Collation |
| 67 | +["apple", "banana", "Cherry"] = |
| 68 | + Localize.Collation.sort(["banana", "apple", "Cherry"]) |
| 69 | + |
| 70 | +# Pluralization via MessageFormat 2 |
| 71 | +Localize.Message.format( |
| 72 | + ~S".input {$count :integer}\n.match $count\n one {{{$count} item}}\n * {{{$count} items}}", |
| 73 | + %{"count" => 3} |
| 74 | +) |
| 75 | +#=> {:ok, "3 items"} |
| 76 | + |
| 77 | +# Validation |
| 78 | +{:ok, %Localize.LanguageTag{cldr_locale_id: :en}} = Localize.validate_locale("en-US") |
| 79 | +``` |
| 80 | + |
| 81 | +## Performance rules |
| 82 | + |
| 83 | +* For hot loops formatting many numbers with the same locale and format, pre-validate options once via `Localize.Number.Format.Options.validate_options/2` and pass the resulting struct. This skips per-call locale resolution and currency lookup. Speedup is ~3x for plain decimals and ~50x for currency formatting. |
| 84 | + |
| 85 | +* Set the process locale once with `Localize.put_locale/1` rather than passing `:locale` as an option on every call. |
| 86 | + |
| 87 | +* `Localize.validate_locale/1` is cached in ETS — first call ~50µs, subsequent calls ~1µs. Pre-validate locales you know you will need at app startup. |
| 88 | + |
| 89 | +## Configuration |
| 90 | + |
| 91 | +```elixir |
| 92 | +# config/config.exs — minimum useful config |
| 93 | +config :localize, |
| 94 | + default_locale: :en, |
| 95 | + supported_locales: [:en, :fr, :de, :ja, "es-*"], |
| 96 | + preload_locales: [:en, :fr, :de] |
| 97 | +``` |
| 98 | + |
| 99 | +* `:supported_locales` constrains `validate_locale/1` to your declared subset. If unset (the default), all 766 CLDR locales are valid. Wildcard strings (`"es-*"`) expand to every matching locale. |
| 100 | + |
| 101 | +* `:preload_locales` loads locale data eagerly at app start instead of lazily on first use. Anything in `:preload_locales` is automatically considered supported. |
| 102 | + |
| 103 | +## Things not to do |
| 104 | + |
| 105 | +* Do not write `Localize.Number.to_string!(value)` and discard the locale — at least pass `locale: Localize.get_locale()` if you have not set the process locale, or accept the application default. |
| 106 | + |
| 107 | +* Do not pattern match on the old ex_cldr error shape `{:error, {Module, "string"}}`. Localize returns `{:error, %Exception{}}` — match on the struct or call `Exception.message/1`. |
| 108 | + |
| 109 | +* Do not call `String.to_atom/1` on user-supplied locale strings. Use `Localize.validate_locale/1`, which canonicalizes and validates without atom-table pollution. |
| 110 | + |
| 111 | +* Do not assume `Localize.Territory.territory_codes/0` returns a list of territories — it returns a *map* of ISO 3166 code mappings. Use `Localize.Territory.individual_territories/0` for the sorted list of leaf territory atoms. |
| 112 | + |
| 113 | +* Do not reach for `Cldr.*` modules or backend modules. Localize replaces the entire `ex_cldr_*` family with a single dependency and no compile-time configuration. |
| 114 | + |
| 115 | +* Do not use `Localize.Number.to_string` for currency parsing or `Localize.Number.parse` for formatting — they are separate functions in the same module. |
| 116 | + |
| 117 | +## Companion packages |
| 118 | + |
| 119 | +When the user needs functionality outside Localize's core CLDR scope: |
| 120 | + |
| 121 | +* `localize_person_names` — TR35 Part 8 person name formatting. |
| 122 | + |
| 123 | +* `localize_phonenumber` — phone number parsing and formatting. |
| 124 | + |
| 125 | +* `localize_address` — postal address formatting. |
| 126 | + |
| 127 | +* `intl` — higher-level ergonomic API modeled on the JavaScript `Intl` object. |
0 commit comments