# LocaleSlug

Locale-aware and script-aware URL slugs for Elixir. **Zero dependencies.**

```elixir
LocaleSlug.slugify("Größe Fußball", locale: "de")   #=> "groesse-fussball"
LocaleSlug.slugify("Töö õun", locale: "et")         #=> "too-oun"
LocaleSlug.slugify("Цветокоррекция")                #=> "tsvetokorrektsiya"
LocaleSlug.slugify("Καλημέρα")                      #=> "kalimera"
LocaleSlug.slugify("日本語 入門", script: :native)    #=> "日本語-入門"
```

## Why

Slug generation is locale-dependent, and no other Elixir package treats it that way.
`ö` must become `oe` in German and `o` in Estonian. Measured, by running each package:

| input | `slugify` 1.3.1 | `slugger` 0.3.0 | **LocaleSlug** |
|---|---|---|---|
| `Größe Fußball` | `grosse-fussball` | `groesse-fussball` ✓ | `groesse-fussball` ✓ *(`locale: "de"`)* |
| `Töö õun` (Estonian) | `too-oun` ✓ | `toeoe-oun` ✗ | `too-oun` ✓ *(`locale: "et"`)* |
| `Łódź` | — | — | `lodz` |
| `Καλημέρα` | — | — | `kalimera` |

`slugger` applies German rules to Estonian; `slugify` the reverse. Neither can do better,
because neither accepts a locale — and the two maintained transliterators (`any_ascii`,
`unidecode`) expose arity-1 functions, so they are *structurally* incapable of it. The
only locale-aware machinery in Elixir is an ICU C NIF, which needs a toolchain and ICU
headers on every host that compiles it.

## Install

```elixir
def deps do
  [{:locale_slug, "~> 0.1"}]
end
```

## Two axes

**`:locale`** picks locale preferences (German `ö→oe`) and, for languages with their own
official romanization, the schema — `uk` selects Ukraine's KMU 55:2010 rather than
treating Ukrainian as Russian with edits.

**`:script`** picks the output alphabet:

```elixir
LocaleSlug.slugify("Цветокоррекция", script: :latin)   #=> "tsvetokorrektsiya"
LocaleSlug.slugify("Цветокоррекция", script: :native)  #=> "цветокоррекция"
```

Native-script URLs are legal ([RFC 3987](https://www.rfc-editor.org/rfc/rfc3987)),
render natively in browsers, and are what Russian and Japanese sites routinely use.
`:native` is not "return unchanged" — it still normalises, lowercases locale-correctly,
and strips every RFC 3986 reserved character.

## Options

| option | default | |
|---|---|---|
| `:locale` | `nil` | BCP 47 tag: `"de"`, `"et-EE"`, `:et` |
| `:script` | `:latin` | `:latin` or `:native` |
| `:separator` | `"-"` | |
| `:max_length` | `nil` | in output characters; never severs a mapping |
| `:schema` | per-locale | e.g. `:kmu_2010`, or `%{cyrillic: :kmu_2010}` |
| `:fallback` | `:native` | `:empty` for ASCII-or-nothing |
| `:strict` | `false` | raise on malformed **options** only |

⚠️ **`:latin` is not an unconditional ASCII guarantee.** Under the default
`fallback: :native`, a script with no romanizer survives in its own script rather than
disappearing. Pass `fallback: :empty` when you need ASCII or nothing.

## Contextual rules

The romanization standards this implements are not character maps. ISO 843 Type 2 is a
*transcription*:

```elixir
LocaleSlug.slugify("μπύρα")   #=> "byra"    # μπ is /b/ at a word edge
LocaleSlug.slugify("λάμπα")   #=> "lampa"   # ...and /mp/ inside one
LocaleSlug.slugify("Ежик")    #=> "yezhik"  # BGN/PCGN: е is "ye" word-initially
LocaleSlug.slugify("небо")    #=> "nebo"    # ...and "e" after a consonant
```

## Locale input

Language tags only, case-insensitive, `_` or `-`:

```elixir
LocaleSlug.slugify(text, locale: "et")      # all equivalent
LocaleSlug.slugify(text, locale: "ET")
LocaleSlug.slugify(text, locale: "et_EE")
LocaleSlug.slugify(text, locale: :et)
```

`resolve/1` tells you what happened, so `slugify/2` never has to log:

```elixir
LocaleSlug.resolve("de")        #=> {:ok, %LocaleSlug.Resolver{}}
LocaleSlug.resolve("xx")        #=> {:missing, "xx", %Resolver{}}  — still slugs fine
LocaleSlug.resolve("GB")        #=> {:invalid, :country_only}
LocaleSlug.resolve("Estonian")  #=> {:invalid, :not_a_language_tag}
```

Only codes that are **not real language subtags** are rejected. `uk` is Ukrainian, `no`
is Norwegian, `ar` is Arabic, `ca` is Catalan — none of them are treated as country
codes, even though each doubles as one.

## Trust, and what `verified` means

Every table declares where it came from and how well it is checked:

```elixir
LocaleSlug.info("de")
#=> %{status: "provisional", source: "Duden / Rat für deutsche Rechtschreibung ...", ...}
```

* `verified` — **a human who reads the language signed off.** A machine may not grant it.
* `provisional` — sourced from a published standard, no native-speaker sign-off yet.
* `draft` — proposed, reviewed by nothing.

Prefer `verified` tables for slugs you persist. Everything in 0.1.0 is `provisional`:
the tables cite real standards, but no native speaker has reviewed them yet. **Native
speakers wanted** — see below.

## Contributing a locale

Tables are YAML, not Elixir, so you do not need to write Elixir to fix one:

```yaml
locale: de
status: provisional
source: "Duden / Rat für deutsche Rechtschreibung — umlaut transcription"
mappings:
  "ä": "ae"
  "ö": "oe"
examples:            # these become tests
  - in:  "Größe Fußball"
    out: "groesse-fussball"
```

Add an `in`/`out` pair to `examples:` and the suite proves it. A PR that changes a
mapping must change or add an example.

## Limits

* **Turkish `ı`/`i` collapse.** `ılık` and `ilik` both slug to `ilik`. Six Turkish
  letters fold onto bare ASCII, and that fold *is* the Turkish convention — inventing a
  spelling to avoid it would produce slugs no Turkish speaker recognises. Use
  `script: :native` for fidelity; use your schema for uniqueness.
* **Slugs are not identifiers.** Any lossy romanization merges words, and so does plain
  English (`Café`/`Cafe`, `Blue Shirt`/`Blue shirt`). Uniqueness belongs to your schema.
* **Cyrillic with no locale is romanized Russian-style.** Nothing in a string declares
  its language and we do not guess from character frequency, so a Ukrainian shop must
  pass `locale: "uk"`.
* **Kanji is out of scope for `:latin`.** The reading is a property of the word, not the
  character (生物 is *seibutsu* or *namamono*), and it is an open research problem.
  `script: :native` handles Japanese correctly today.

## License

MIT. Romanization tables derive in part from Unicode CLDR/ICU data under the Unicode
License v3 — see [NOTICE](NOTICE).
