eagle-head

eagle-head

Erli18n - GNU gettext-compatible i18n for Erlang/OTP (a learning project - feedback very welcome)

Hi everyone! :waving_hand:

I just published my first Hex package, erli18n — a GNU gettext–compatible internationalization library for Erlang/OTP, written in pure Erlang (and callable from Elixir too, since it’s a normal Hex dep: :erli18n.gettext(...)).

I want to be upfront about one thing: the main goal of this project is learning. I built it to dig deep into Erlang/OTP — gen_server + supervision, ETS ownership/heir patterns, the .po format and CLDR plural rules, property-based testing with PropEr, telemetry, native EEP-59 docs, and the whole Hex release pipeline. So please read it as a 0.1.0 learning project rather than battle-tested production infra — though I worked hard to make it correct and thoroughly tested.

What it does — the full GNU gettext C-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 returns the correct plural FORM for N (you format the number yourself)

<<"arquivo">> = erli18n:ngettext(my_domain, <<"file">>, <<"files">>, 1, <<"pt_BR">>).

<<"arquivos">> = erli18n:ngettext(my_domain, <<"file">>, <<"files">>, 42, <<"pt_BR">>).

%% pgettext for context; npgettext for context + plural

<<"Maio">> = erli18n:pgettext(my_domain, <<"month">>, <<"May">>, <<"pt_BR">>).

A few things I focused on:

  • :package: Drop-in .po / .pot — loads the files translators already produce in Poedit, Crowdin, Weblate, or xgettext.

  • :globe_showing_europe_africa: Real CLDR pluralization — an actual Plural-Forms evaluator, CLDR rules inlined for 49 locales.

  • :high_voltage: Lock-free lookups — reads run straight from ETS in the calling process; only writes go through a gen_server, so there’s no bottleneck on the hot path.

  • :bar_chart: Optional telemetry — 7 events (catalog spans, lookup misses, plural divergence, memory warnings); telemetry is an optional dependency.

  • :white_check_mark: Heavily tested — Common Test + PropEr + fuzzing, plus a parity suite that checks output byte-for-byte against GNU msgfmt as a ground-truth oracle.

(Pure Erlang, OTP 27+, Apache-2.0.)

Why I’m posting here:

  1. I’d genuinely love feedback — on the API design, the OTP patterns, anything that makes a seasoned BEAM dev wince. Since I’m here to learn, blunt and critical opinions are exactly what I’m after.

  2. 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:

If you find it useful or even just interesting to read, a :star: on GitHub would mean a lot and helps me gauge whether it’s worth continuing. Thanks for reading — any feedback, however harsh, is hugely appreciated! :folded_hands:

Most Liked

LostKobrakai

LostKobrakai

ETS might be lock free access, but still does a copy per access. For rarely changing values like translations :persistent_term can remove the term copying on reads by requiring a global garbage collection on writes. Unless you’re doing heavy runtime modifications to translations that tradeoffs is probably worthwhile.

Last Post!

eagle-head

eagle-head

erli18n 0.8.0 is out on Hex. This one is library-only — the rebar3_erli18n plugin is unchanged at 0.2.0.

The new piece is an optional erlydtl template bridge, erli18n_erlydtl. It lets an erlydtl template translate its {% trans %} / {% blocktrans %} tags through erli18n’s gettext core — contexts, CLDR plurals, per-request locale — while erlydtl keeps ownership of {{ var }} interpolation and auto-escaping. erli18n_erlydtl:translation_fun/1 returns a render-time fun bound to a gettext domain that you pass in erlydtl’s render/2 options; the pure decode/2 maps {% trans %}gettext, contextpgettext, a counted {% blocktrans %}ngettext, and context + count → npgettext.

To be straight about the Elixir angle: erlydtl is Erlang’s Django-style template engine, and most Elixir apps reach for EEx/HEEx with Gettext instead — so this is niche here. It matters if you actually render erlydtl templates: a mixed Erlang/Elixir app, or a ported Django project. erli18n has no Elixir dependency; from Elixir you would build the fun with :erli18n_erlydtl.translation_fun(:web) and hand it to :erlydtl’s render options. If you know Gettext, the tags map onto the same four calls Gettext exposes (gettext / pgettext / ngettext / npgettext), and the locale composes with the per-process :erli18n.setlocale(locale) you would set in a Plug — the same spot as Gettext.put_locale/1.

The integration is inverted: erli18n_erlydtl references zero erlydtl functions (erlydtl calls into the fun), so it is not an optional_applications entry and the published package still builds on kernel + stdlib alone — erlydtl is a test-only dependency you pull in only if you use it. A runnable example is in examples/erli18n_erlydtl_demo.

Where Next?

Popular in Announcing Top

jakub-zawislak
Hi everyone, I’m coming from the Symfony (PHP) framework. I like Phoenix, but it has a one thing that was build much better in the Symfo...
New
Crowdhailer
Raxx is an alternative to Plug and is inspired by projects such as Rack(Ruby) and Ring(Clojure). 1.0-rc.1 is now available. To use it re...
New
mbuhot
Leverage Open Api 3.0 (Swagger) to document, test, validate and explore your Plug and Phoenix APIs. Generate and serve a JSON Open API ...
New
tmbb
I’ve been working on two packages (not on hex.pm yet) to build admin interfaces for phoenix apps: bureaucrat - which contains a bunch ...
New
tmbb
PhoenixWS - Websockets over Phoenix Channels Source code on Github here: GitHub - tmbb/phoenix_ws: Websockets implemented over Phoenix Ch...
New
Qqwy
TypeCheck: Fast and flexible runtime type-checking for your Elixir projects. Core ideas Type- and function specifications are const...
336 14703 100
New
type1fool
WebAuthnLiveComponent WebAuthnComponents See this post about renaming the package. Passwordless authentication for Phoenix LiveView app...
New

Other popular topics Top

Qqwy
Update: How to use the Blogs &amp; Podcasts section You can post links to your blog posts or podcasts either in one of the Official Blog...
3271 130286 1222
New
gshaw
What is the idiomatic way of matching for not nil in Elixir? E.g., First way: defp halt_if_not_signed_in(conn, signed_in_account) when...
New
bsollish-terakeet
Credo is smart enough to check for (something like) this: assert length(the_list) == 0 with this response: Checking if an enum is empt...
New
WestKeys
Currently suffering from paralysis by [HTTP client] analysis. This is rather unusual in Elixirland as there tends to be consensus on the ...
New
jason.o
In the code below, if the create action is not set to accept “extra_key” as an input, it errors out with a message shown above. Is there ...
New
AstonJ
Posting this to see if we can make things easier for people to get into Neovim. If you use Neovim and have a favourite distro please let ...
New

We're in Beta

About us Mission Statement