Hi everyone! ![]()
I’d like to introduce erli18n — a GNU gettext – style internationalization library for Erlang/OTP, written in pure Erlang. It’s been on Hex for a few releases (now v0.5.0), and the latest one added a companion rebar3 plugin for the catalog workflow, so it felt like the right moment to introduce it here.
Upfront and honest: this started as a learning project — a deep dive into OTP (gen_server + supervision, persistent_term, the .po format and CLDR plural rules, PropEr property/fuzz testing, telemetry, native EEP-59 docs, and the whole Hex release pipeline). I worked hard to make it correct and thoroughly tested (100% coverage, plus a parity suite that checks output byte-for-byte against GNU msgfmt), but I’d still frame it as a serious learning project rather than battle-hardened infra — and that’s exactly why I’m here: I want blunt, critical feedback from people who’ve written a lot of Erlang.
The library — the full gettext macro family as plain Erlang functions:
application:ensure_all_started(erli18n).
{ok, _} = erli18n_server:ensure_loaded(my_domain, <<"pt_BR">>,
<<"priv/locale/pt_BR/LC_MESSAGES/my_domain.po">>).
<<"Olá, mundo">> = erli18n:gettext(my_domain, <<"Hello, world">>, <<"pt_BR">>).
%% ngettext picks the correct plural form for N (real CLDR rules)
<<"arquivos">> = erli18n:ngettext(my_domain, <<"file">>, <<"files">>, 42, <<"pt_BR">>).
%% pgettext for context
<<"Maio">> = erli18n:pgettext(my_domain, <<"month">>, <<"May">>, <<"pt_BR">>).
%% named %{var} interpolation via the f-suffix family — note: (domain, msgid, locale, bindings)
<<"Olá, Ana">> = erli18n:gettextf(my_domain, <<"Hello, %{name}">>, <<"pt_BR">>,
#{name => <<"Ana">>}).
What I focused on:
Drop-in .po/.pot— loads what translators already produce in Poedit, Weblate, Crowdin, orxgettext; output is checked byte-for-byte against GNUmsgfmtas a ground-truth oracle.
Real CLDR pluralization — an actual Plural-Formsevaluator with CLDR rules inlined, not a naiven == 1split.
Named %{var}interpolation — thef-suffix family (gettextf/ngettextf/pgettextf/ …) layered on top of the positional gettext API.
Locale negotiation + fallback — Accept-Languageparsing (BCP-47, q-values) and an opt-in RFC 4647 fallback chain (pt_BR→pt), both kept off the exact-hit hot path.
Copy-free reads — catalogs live in persistent_term, one term per{domain, locale}, so a lookup returns a pointer into the literal area instead of copying the term onto the caller’s heap. (Migrated from ETS in 0.4.0 after forum feedback — lock-free isn’t copy-free; the one tradeoff, a literal-area GC per reload, is documented up front.)
Optional telemetry— catalog spans, lookup misses, plural divergence, memory warnings;telemetryis an optional dependency.
Heavily tested — Common Test + PropEr + fuzzing, 100% coverage, and the msgfmtparity oracle.
New in 0.5.0 — rebar3_erli18n, a build-time catalog-tooling plugin ({plugins, [rebar3_erli18n]}, build-only — it never ships in your release):
rebar3 erli18n extract # scan source for gettext call sites -> .pot templates
rebar3 erli18n merge # .pot -> .po, preserving msgstr, fuzzy-matching, obsolete
rebar3 erli18n check # CI gate: fail if catalogs drift from source
rebar3 erli18n report # translated / fuzzy / missing, per {domain, locale}
It’s the xgettext + msgmerge half of the gettext workflow, native to rebar3: extract message ids straight from your source, merge them into catalogs without clobbering existing translations, and gate freshness in CI. (One honest caveat, same as gettext: only compile-time literal msgids are statically discoverable — a runtime-computed id still translates, it just can’t be auto-extracted.)
(Pure Erlang, OTP 27+, Apache-2.0. It’s a normal Hex dependency, so it’s callable from Elixir too — but its natural home is an Erlang / rebar3 project.)
Why I’m posting: I’d genuinely love feedback — on the API design, the OTP patterns, the persistent_term tradeoffs, the plugin’s extractor, anything that makes a seasoned BEAM dev wince. And if you have an Erlang project that wants gettext-style i18n without routing through Elixir’s build, please try it and tell me where it breaks.
Links:
erli18n: https://hex.pm/packages/erli18n ·
rebar3_erli18n: https://hex.pm/packages/rebar3_erli18n
Docs: https://hexdocs.pm/erli18n · https://hexdocs.pm/rebar3_erli18n
GitHub:
If you find it useful — or even just interesting to read — a
on GitHub helps me gauge whether it’s worth continuing. Thanks for reading, and any feedback, however harsh, is hugely appreciated! ![]()