Skip to content

Commit 491afe8

Browse files
committed
Add Claude Code skill covering all subsystems, installable from the repo as a plugin marketplace
1 parent a876111 commit 491afe8

11 files changed

Lines changed: 1494 additions & 0 deletions

File tree

.claude-plugin/marketplace.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"name": "localize",
3+
"owner": {
4+
"name": "Kip Cole"
5+
},
6+
"description": "Skills for building localized Elixir applications with the Localize library",
7+
"plugins": [
8+
{
9+
"name": "localize",
10+
"source": "./",
11+
"description": "Write localized Elixir applications with Localize: numbers, currencies, dates/times, units, lists, collation and MessageFormat 2"
12+
}
13+
]
14+
}

.claude-plugin/plugin.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"name": "localize",
3+
"displayName": "Localize",
4+
"description": "Write localized Elixir applications with Localize: numbers, currencies, dates/times, units, lists, collation and MessageFormat 2",
5+
"author": {
6+
"name": "Kip Cole"
7+
},
8+
"homepage": "https://hexdocs.pm/localize",
9+
"repository": "https://github.com/elixir-localize/localize",
10+
"license": "Apache-2.0",
11+
"keywords": ["elixir", "localization", "i18n", "cldr", "formatting", "collation", "messageformat"]
12+
}

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
1010

1111
* `mix localize.update_cldr` orchestrates the CLDR data-update pipeline (copy sources → generate supplemental → compile gate → generate locales → test gate), with `--check` preflight, `--locales` subsetting, and each step isolated in a fresh VM. The full update process is documented in the consolidated CLDR Update Guide (`CLDR_UPDATE_INTEGRATION.md`).
1212

13+
* A Claude Code skill covering all major subsystems (numbers, dates/times, units, lists, collation, MessageFormat 2, locale validation and configuration), installable via `/plugin marketplace add elixir-localize/localize`. All 300+ skill examples are execution-verified against the library.
14+
1315
## [0.46.0] — July 6th, 2026
1416

1517
### Changed

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,17 @@ Localize consolidates the functionality of the `ex_cldr_*` library family into a
4848

4949
* **MessageFormat 2** — parse and evaluate ICU MessageFormat 2 message strings.
5050

51+
## Claude Code skill
52+
53+
Localize ships a [Claude Code](https://claude.com/claude-code) skill that teaches Claude the library's APIs and localization-first patterns — every example execution-verified against the library. With the skill installed, Claude writes plural-correct, currency-correct, collation-correct Elixir by default, even for single-locale applications.
54+
55+
```
56+
/plugin marketplace add elixir-localize/localize
57+
/plugin install localize@localize
58+
```
59+
60+
The skill source lives in [skills/localize](https://github.com/elixir-localize/localize/blob/main/skills/localize/SKILL.md).
61+
5162
## Supported Elixir and OTP versions
5263

5364
Localize requires **Elixir 1.17+** and **Erlang/OTP 26+**.

skills/localize/SKILL.md

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
---
2+
name: localize
3+
description: Write localized Elixir applications with the Localize library (hex package "localize") — formatting numbers, currencies, dates, times, durations, units of measure, lists, collation/sorting, and MessageFormat 2 messages. Use this skill whenever an Elixir app renders numbers, money, dates, times, quantities, or sorted text for people to read — even if the app targets a single locale — and for any task mentioning localize, ex_cldr migration, CLDR, locale validation, language tags, plural rules, or translatable message strings. Also use it when reviewing Elixir code that formats values by hand (interpolation, Calendar.strftime, :io_lib.format, Enum.sort on user-visible strings) since Localize is usually the better replacement.
4+
license: Apache-2.0
5+
---
6+
7+
# Writing localized Elixir applications with Localize
8+
9+
Localize (hex: `localize`) formats numbers, currencies, dates/times, units, lists and messages using Unicode CLDR data, and sorts text with the Unicode Collation Algorithm. It is the successor to the `ex_cldr` family: one package, no compile-time backend modules, data loaded at runtime.
10+
11+
## Why use it even for a single-locale app
12+
13+
Hand-formatting is where localization bugs are born, and most of them bite in English too:
14+
15+
* `"#{count} files"` is wrong for `count == 1`. `Localize.Message.format/3` or a cardinal plural rule gets "1 file / 2 files" right for free.
16+
* `"$#{amount}"` breaks on 1000000 (no grouping), negative amounts, and non-USD currencies. `Localize.Number.to_string(amount, format: :currency)` renders "$1,234.56" correctly today and "1.234,56 €" the day the app grows a second locale.
17+
* `Calendar.strftime/3` hardcodes one ordering and one language of month names. `Localize.Date.to_string/2` gives the locale's convention.
18+
* `Enum.sort/1` on user-visible strings puts "Zebra" before "apple" and mishandles accents. `Localize.Collation.sort/2` sorts the way people expect.
19+
20+
So when writing or reviewing Elixir that renders values for humans, reach for the Localize call rather than string interpolation, `strftime`, or raw `Enum.sort` — the single-locale output is equally correct and the app is localization-ready without a rewrite.
21+
22+
## Core conventions (apply everywhere)
23+
24+
* **Results are tagged tuples**: `{:ok, formatted}` or `{:error, %SomeError{}}` where the error is an exception struct with a readable `Exception.message/1`. Every formatter has a `!` variant that returns the bare value or raises.
25+
* **Every formatting function takes a `:locale` option** defaulting to the process locale `Localize.get_locale()` (itself defaulting to the app config, then `:en`). Set it per-process with `Localize.put_locale/1`, per-call with `locale: "fr"`, or per-block with `Localize.with_locale/2`.
26+
* **Validate untrusted locale input** with `Localize.validate_locale/1` — it parses, canonicalizes (aliases like `iw``he`), resolves likely subtags, applies `-u-` extensions, and caches. Pass the resulting `LanguageTag` around rather than re-validating strings.
27+
* **Zero config is valid config.** With no configuration, `:en` works out of the box; other locales download on first use (integrity-verified). Configure `supported_locales` and `default_locale` when the app knows its audience.
28+
29+
```elixir
30+
# config/config.exs — typical production setup
31+
config :localize,
32+
default_locale: :en,
33+
supported_locales: [:en, :de, :fr, :ja],
34+
otp_app: :my_app
35+
```
36+
37+
## Quick reference by subsystem
38+
39+
Each row shows the everyday call; the reference file has the full option set, more examples, and the edge cases. Read the reference file when the task goes beyond the row.
40+
41+
### Numbers, currencies, plurals — [references/numbers.md](references/numbers.md)
42+
43+
```elixir
44+
Localize.Number.to_string(1234.5) # {:ok, "1,234.5"}
45+
Localize.Number.to_string(1234.5, format: :currency) # {:ok, "$1,234.50"} (currency from locale)
46+
Localize.Number.to_string(1234.5, format: :currency, currency: :EUR, locale: :de)
47+
# {:ok, "1.234,50 €"} (no-break space before €)
48+
Localize.Number.to_string(0.456, format: :percent) # {:ok, "46%"}
49+
Localize.Number.to_string(1_234_000, format: :decimal_short) # {:ok, "1.2M"}
50+
Localize.Number.to_string(42, format: :ordinal) # {:ok, "42nd"}
51+
Localize.Number.PluralRule.plural_type(1, locale: :en) # :one (drive "file" vs "files")
52+
Localize.Number.parse("1,234.56") # parse localized input back
53+
```
54+
55+
Read the reference for: rounding and digit options, significant digits via patterns, spellout/RBNF, ranges ("3–5"), scanning numbers out of text, `Decimal` inputs, custom format patterns.
56+
57+
### Dates, times, intervals, durations — [references/dates-times.md](references/dates-times.md)
58+
59+
```elixir
60+
Localize.Date.to_string(~D[2026-07-06]) # {:ok, "Jul 6, 2026"}
61+
Localize.Date.to_string(~D[2026-07-06], format: :full) # {:ok, "Monday, July 6, 2026"}
62+
Localize.DateTime.to_string(dt, format: :yMMMdHm) # skeleton — locale picks the layout
63+
Localize.Interval.to_string(~D[2026-07-06], ~D[2026-07-09]) # {:ok, "Jul 6 – 9, 2026"} (thin spaces around the dash)
64+
Localize.DateTime.Relative.to_string(-3600) # {:ok, "1 hour ago"}
65+
Localize.Duration.to_string(duration) # {:ok, "11 months and 30 days"}
66+
```
67+
68+
Prefer style atoms (`:short`/`:medium`/`:long`/`:full`) or skeletons (`:yMd`, `:yMMMEd`, `:Hm`) over literal pattern strings — skeletons let each locale arrange fields its own way. Read the reference for: pattern symbols, time zones, week/quarter fields, non-Gregorian calendars, `to_time_string` clock-style durations.
69+
70+
### Units of measure — [references/units.md](references/units.md)
71+
72+
```elixir
73+
unit = Localize.Unit.new!(3.5, "kilometer")
74+
Localize.Unit.to_string(unit) # {:ok, "3.5 kilometers"}
75+
Localize.Unit.to_string(unit, format: :short) # {:ok, "3.5 km"}
76+
Localize.Unit.convert(unit, "mile") # {:ok, mile-unit}
77+
Localize.Unit.to_string(Localize.Unit.new!(5, "curr-usd-per-100-kilometer"))
78+
# {:ok, "$5.00 per 100 kilometers"}
79+
```
80+
81+
Compound identifiers compose (`kilowatt-hour`, `meter-per-second`, `foot-and-inch`); usage-based preferences (`usage: "person-height"`) pick regional units automatically. Read the reference for: conversion, mixed units, arithmetic, custom units, grammatical case/gender in inflected languages.
82+
83+
### Lists — inline, no reference needed
84+
85+
```elixir
86+
Localize.List.to_string(["a", "b", "c"]) # {:ok, "a, b, and c"}
87+
Localize.List.to_string(["a", "b"], list_style: :or) # {:ok, "a or b"}
88+
```
89+
90+
Never `Enum.join(items, ", ")` user-facing lists — conjunction rules differ by locale and by list length.
91+
92+
### Collation (sorting and comparing text) — [references/collation.md](references/collation.md)
93+
94+
```elixir
95+
Localize.Collation.sort(["banana", "Apple", "cherry"]) # ["Apple", "banana", "cherry"]
96+
Localize.Collation.compare("résumé", "resume", strength: :primary) # :eq — accent-insensitive
97+
Localize.Collation.sort(["file10", "file2"], numeric: true) # ["file2", "file10"]
98+
Localize.Collation.sort_key("text") # store for DB-side ordering
99+
```
100+
101+
Read the reference for: strength levels (accent/case-insensitive matching), `ignore_punctuation`, script reordering, locale collation types (`zh-u-co-pinyin`), search-style matching.
102+
103+
### MessageFormat 2 (translatable message strings) — [references/message-format.md](references/message-format.md)
104+
105+
```elixir
106+
Localize.Message.format("{{Hello {$name}!}}", %{"name" => "World"})
107+
# {:ok, "Hello World!"}
108+
109+
Localize.Message.format(
110+
".input {$count :number}\n.match $count\n1 {{You have one file}}\n* {{You have {$count} files}}",
111+
%{"count" => 3}
112+
)
113+
# {:ok, "You have 3 files"}
114+
```
115+
116+
MF2 is the right shape for any user-visible sentence containing a value: the plural/gender/select logic lives in the (translatable) message, not in Elixir `if`s. Read the reference for: built-in functions (`:number`, `:datetime`, `:currency`, `:unit`), markup, custom functions, Gettext integration, and when plain Gettext is the better fit.
117+
118+
### Locale validation, language tags, configuration — [references/locales-and-config.md](references/locales-and-config.md)
119+
120+
```elixir
121+
{:ok, locale} = Localize.validate_locale("pt-br") # canonical "pt-BR", ready to pass around
122+
Localize.put_locale(locale) # set for this process
123+
Localize.Territory.display_name(:NZ, locale: :fr) # {:ok, "Nouvelle-Zélande"}
124+
Localize.Currency.currency_from_locale(locale) # territory-appropriate default currency
125+
```
126+
127+
`-u-` extension keys on a locale (e.g. `"en-u-ca-buddhist-nu-thai-hc-h23"`) automatically drive calendar, digits, hour cycle, collation and more through every formatter. Read the reference for: the `LanguageTag` struct, supported/known vocabularies, display names, runtime data download and supervision, per-locale defaults derived from territory data.
128+
129+
## Patterns for application code
130+
131+
**Set the locale once per request, then format without ceremony.** In Phoenix, a plug resolves the user's locale (session, `Accept-Language`, URL) through `validate_locale/1` and calls `Localize.put_locale/1`; every downstream `to_string` call then does the right thing with no `:locale` plumbing.
132+
133+
**Prefer the non-bang variants at boundaries, bang variants internally.** User-supplied locales, currencies, or units can be invalid — handle `{:error, e}` (its `Exception.message/1` is presentable). For app-internal constants (`Localize.Unit.new!(1, "meter")`), the bang variant is cleaner and a raise means a programming error.
134+
135+
**Keep values as values until the last moment.** Store money as `Decimal` + currency code, dates as `Date`/`DateTime`, quantities as `Localize.Unit` — format at the presentation edge. Never parse formatted output back except with `Localize.Number.parse/2`.
136+
137+
**Plural-sensitive text belongs in messages, not conditionals.** Replace `if count == 1, do: "file", else: "files"` with an MF2 message or `PluralRule.pluralize/3` — English has 2 plural categories but Arabic has 6, and the conditional version silently caps the app at English.
138+
139+
## Common mistakes to catch in review
140+
141+
* String interpolation of numbers, money, dates, or lists into user-visible text.
142+
* `Calendar.strftime/3` or `Timex.format` for user-facing dates (fine for logs and machine formats).
143+
* `Enum.sort/1` / `Enum.sort_by(&String.downcase/1)` on human-visible lists.
144+
* Hardcoded `"$"`, `"%"`, thousands separators, or month/day names.
145+
* `String.to_atom(user_locale)` — use `validate_locale/1`, which is safe on untrusted input.
146+
* Concatenating sentence fragments around a value ("You have " <> n <> " new") — word order varies; use MF2.
147+
* Treating `validate_locale` output as a string — it returns a `%Localize.LanguageTag{}`; pass the struct.

0 commit comments

Comments
 (0)