Skip to content

Commit 7066fae

Browse files
committed
Add usage_rules.md
1 parent 0565bb0 commit 7066fae

2 files changed

Lines changed: 129 additions & 1 deletion

File tree

mix.exs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,9 @@ defmodule Localize.MixProject do
5454
"README*",
5555
"CHANGELOG*",
5656
"LICENSE*",
57+
"usage-rules.md",
5758
"priv/localize/*.etf",
58-
"priv/localize/verison",
59+
"priv/localize/version",
5960
"priv/localize/localize_patch_version",
6061
"priv/localize/supplemental_data",
6162
"priv/localize/validity",

usage-rules.md

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
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

Comments
 (0)