0.2.0 — 2026-08-10
Cyrillic beyond Russian. 0.1.0 shipped a Cyrillic layer that only really covered Russian, and named three romanization schemas it did not contain — so the two ways of asking for Ukrainian, Bulgarian or Serbian both failed, one loudly and one silently.
⚠️ Slug output changes
Cyrillic slugs from 0.1.0 are not all reproducible in 0.2.0. If you persist slugs,
re-generate or pin the old values before upgrading. Nothing outside Cyrillic moves;
German, Estonian, Greek and script: :native are byte-identical.
0.1.0 gave two different wrong answers depending on :fallback — the default let the
unromanized letter through, :empty dropped it:
| input | 0.1.0 (default) | 0.1.0 (fallback: :empty) | 0.2.0 |
|---|---|---|---|
Київ | kiіv | ki-v | kiyiv |
Ґанок | ґanok | anok | ganok |
Ђорђе | ђorђe | or-e | djordje |
Київ (locale: "uk") | киів | "" | kyiv |
България (locale: "bg") | българия | "" | bulgaria |
Шаркаўшчына | sharkaushchyna | sharkaushchyna | sharkawshchyna |
Fixed
locale: "uk","bg"and"sr"produced an empty slug for any Cyrillic input. The resolver mapped them tokmu_2010/bg_2009/sr_ascii, none of which shipped a table.Tables.script/1returnednil, the matcher found no rules, and every Cyrillic grapheme was stripped — so passing the correct locale for Ukrainian reproduced the exact bug this package exists to fix. Under the defaultfallback: :nativeit surfaced as unromanized passthrough ("киів") instead, which looked like it had worked.- The default Cyrillic schema deleted every letter Russian does not have. BGN/PCGN
is a family of per-language systems, and only its Russian member shipped;
і ї є ґ ј љ њ ћ џ ђ ѕhad no rule at all. A name written only in those letters slugged to"". - An unknown
:schemain the map form bypassed validation, soschema: %{cyrillic: :kmu_2010}slipped paststrict: true. The bare-atom form already raised.
Added
- Ukrainian — KMU 55:2010 (
schema: :kmu_2010, selected bylocale: "uk"). Ukraine's own standard, including its word-position splits (є→ye/ie) and theзг→zghcluster.Київ→kyiv,Запоріжжя→zaporizhzhia. - Bulgarian — the 2009 Transliteration Act (
schema: :bg_2009, vialocale: "bg"), including Чл. 5(2)'s word-final-ия→-iaand Чл. 6's named exception for the country itself,България→Bulgariarather than theBalgariatheъ→arule would otherwise give. - Serbian — Gaj's alphabet, ASCII-folded (
schema: :sr_ascii, vialocale: "sr"). The fold is lossy and every collision is declared in the table'sdeviations:. - The default Cyrillic schema now answers for the whole script, taking letters
Russian lacks from the BGN/PCGN system that defines them. It stays Russian-primary
on shared letters, so
Київwith no locale iskiyiv— name the locale forkyiv. - Belarusian
ўnow follows the standard the table cites: BGN/PCGN Belarusian 1979 romanizes itw, soШаркаўшчынаissharkawshchyna. It previously fell through touby Unicode decomposition, which no cited source supported. LocaleSlug.info/1gained:script.
Changed
A script now contributes two L1 layers: the schema you selected, and the script default beneath it as a backstop. A national standard is not a complete script table — KMU 55:2010 has no
ъ ы э ё— so without this, selecting one would delete the letters that language happens not to use.An unknown
:schemanow raisesArgumentError, with or without:strict— as does a schema given under the wrong script, like%{greek: :kmu_2010}. Previously the map form skipped validation entirely and the atom form only complained understrict: true, so a typo such as:kmu_201silently romanized Ukrainian with the Russian-primary default and looked entirely plausible doing it. That is the same silent-wrong-output failure as the bug above, one option away.The split is deliberate: a
:localemay legitimately arrive from data, so a miss stays soft and:strictopts into raising. A:schemanames a table rather than describing input, so an id with no table is a typo either way. The raise is never triggered by the text being slugified — only by a value the operator controls.:schemanow accepts a string anywhere it accepted an atom, so a value from config or an env var needs noString.to_atom/1on external input. Passing a string previously matched no clause at all and died withCaseClauseError. Every other malformed shape — a tuple, a list, a string map key, a value with noString.Chars— now raises a readableArgumentErrornaming the actual problem instead of surfacing asCaseClauseErrororProtocol.UndefinedError.
Internal
examples:inpriv/*.yamlare now executed as tests. They never were: the generator parsed them and dropped them before codegen, while the README and AGENTS.md both promised "they become tests". That unenforced contract is why the Cyrillic gap shipped. A table with no examples now fails the suite.- The "every letter is accounted for" invariant swept only
U+0430..U+044F, and so passed while all ofU+0450..U+045Fvanished. It now covers the extended block. - A test now proves every schema id the resolver can name actually ships a table.
Nothing compared those two lists before, which is exactly how
uk → kmu_2010came to point at a table that did not exist. - 189 tests, up from 106.
0.1.0 — 2026-08-09
First release.
- Locale-aware slugs: German
ö→oe, Estonianö→o— the distinction no other Elixir package makes. - Script romanization from cited standards: Cyrillic (BGN/PCGN) and Greek
(ISO 843 Type 2), including their contextual rules — Greek
μπisbat a word edge andmpinside one; Russianеisyeword-initially and after a vowel or sign. script: :nativefor Unicode/IRI slugs, correct for Japanese and Chinese today.- Letters with no canonical decomposition (
ø æ ł ı đ ß ð þ) no longer vanish, and non-ASCII digits fold rather than being deleted. :max_lengthnever severs a mapping's output —щ→shchis kept whole or dropped.- Zero runtime dependencies.