erli18n (erli18n v0.1.0)

Copy Markdown View Source

Public façade (API) of the erli18n library — the single entry point for anyone using the library in an application (GNU gettext family).

What it is and what problem it solves

erli18n translates strings (msgids) using .po catalogs compatible with GNU gettext (Poedit, Crowdin, Transifex, Weblate, xgettext). This module mirrors the C gettext macro family, mapping each macro to an eponymous function:

  • Singular: gettext/1,2,3 / dgettext/2,3 (explicit domain) / dcgettext/3 (category — always LC_MESSAGES).
  • Plural: ngettext/3,4,5 / dngettext/4,5 / dcngettext/5.
  • Contextual singular (msgctxt): pgettext/2,3,4 / dpgettext/3,4 / dcpgettext/4.
  • Contextual plural: npgettext/4,5,6 / dnpgettext/5,6 / dcnpgettext/6.

You almost never need to touch erli18n_server directly: this façade is the documented door, and the load/observability functions here are thin passthroughs to the server.

Mental model

  • The current locale is PER-PROCESS state. setlocale/1 writes to the caller's process dictionary and which_locale/0 reads it back. This state is not inherited by processes created with spawn/1: each new process starts with which_locale() = undefined and falls back to default_locale/0. It is the BEAM equivalent of libc's uselocale(3) (thread-local).
  • The default domain and locale are application:env (node-global), not per-process state. They are the fallback when the caller has not opted into a per-process locale, and the starting point for the no-domain variants (gettext/1, ngettext/3, ...). See default_locale/0, set_default_locale/1, textdomain/0, textdomain/1.
  • The resolved locale of each lookup is which_locale/0 if set, otherwise default_locale/0 (internal function resolved_locale/0).
  • Reads are lock-free. The erli18n_server lookup_* functions read directly from protected ETS in the caller's own process; only writes (load/reload) go through the owning gen_server. There is no process bottleneck on the read path.
  • The category is always LC_MESSAGES. The d*/dc* variants exist solely for NAME parity with C gettext; the category is never a parameter.
  • Graceful degradation. On a miss (missing catalog, nonexistent entry) or an empty translation (msgstr ""), the lookup returns the original msgid (or msgid_plural in the plural form, according to N). A crash of erli18n_server (the worker/writer) does not empty the catalogs: the ETS table is held by a dedicated owner (erli18n_table_owner) that is its heir, so on restart it comes back INTACT via 'ETS-TRANSFER' and lookups keep serving the loaded translations — there is no loss of the caller's process nor of the translations (see erli18n_server and erli18n_table_owner).
  • Plural evaluation is total (anti-DoS). The Plural-Forms rule from the .po header is the source of truth at runtime. Evaluation clamps (libintl-style: out-of-range form → 0, zero divisor → 0) and large ASTs are rejected at load time. The error reaches the caller of this façade enveloped in {plural_compile_error, _} (part of erli18n_server:ensure_error()), with the internal detail coming from erli18n_plural:compile_error() — that is, an AST that is too large appears as {error, {plural_compile_error, {expr_too_complex, _, _}}} in the return of ensure_loaded/3,4 and reload/3,4. Thus a pathological rule neither brings down the request process nor stalls the hot path.

When you touch this module

  1. At app boot: application:ensure_all_started(erli18n) and load the catalogs with ensure_loaded/3,4 (use default_po_path/3 to assemble the conventional path).
  2. Per request: optionally setlocale/1 in the process serving the request.
  3. In UI code: call gettext/1, ngettext/3, pgettext/2, etc.

Quickstart

1> application:ensure_all_started(erli18n).
{ok, [erli18n]}
2> PoPath = erli18n:default_po_path(minha_app, my_domain, <<"fr">>).
"/.../minha_app/priv/locale/fr/LC_MESSAGES/my_domain.po"
3> {ok, N} = erli18n:ensure_loaded(my_domain, <<"fr">>, PoPath).
{ok, 12}
%% N = 12 (entries loaded). Re-running this step would return
%% {ok, already} (idempotent), not an integer — see `ensure_loaded/3`.
4> erli18n:setlocale(<<"fr">>).
ok
5> erli18n:gettext(my_domain, <<"Hello, world">>).
<<"Bonjour, monde">>
6> erli18n:ngettext(my_domain, <<"file">>, <<"files">>, 42).
<<"42 fichiers">>
7> erli18n:pgettext(my_domain, <<"month">>, <<"May">>).
<<"Mai">>

Key functions

Lookup rules (R1-R6)

This module's internal comments label the lookup behavior with R1-R6 (coming from BR-MIGRAR-001/002 and PSD-003). What each one means, for anyone who has never seen the project:

  • R1 — singular: gettext/dgettext/dcgettext family.
  • R2 — plural: ngettext/dngettext/dcngettext family, including the plural fallback (Msgid if N == 1, otherwise MsgidPlural).
  • R3 — contextual singular (msgctxt): pgettext/dpgettext/dcpgettext family.
  • R4 — contextual plural: npgettext/dnpgettext/dcnpgettext family.
  • R5 — locale resolution: uses which_locale/0 if the process set one via setlocale/1, otherwise default_locale/0 (see internal helper resolved_locale/0).
  • R6 — default domain resolution: the no-domain variants (gettext/1, ngettext/3, ...) use textdomain/0 as the domain.

Summary

Types

msgctxt context (undefined | binary()) that disambiguates homographs (e.g. the same msgid "May" as a month vs. a verb). undefined means "no context".

Translation domain (atom()). Partitions the msgid space — each pair (Domain, Locale) is an independent .po catalog. The default domain comes from textdomain/0.

BCP-47 locale as a binary (binary()), e.g. <<"en">>, <<"pt_BR">>. It is the catalog key and the basis of plural-form selection.

Translation source key (binary()): the text in the source language, exactly as extracted by xgettext. It is also the fallback value when there is no translation.

Plural form of the source key (binary()): the .po msgid_plural. Used as the fallback when N /= 1 and there is no translation.

Translated text returned to the caller (binary()). On a miss or an empty translation, it is the msgid itself (or msgid_plural), never undefined.

Functions

Alias of gettext/3 (C macro name dcgettext). The C category (LC_MESSAGES) is always implicit in erli18n and is therefore not a parameter — there is no LC_NUMERIC, LC_TIME, etc. Same semantics and fallback as gettext/3.

Alias of ngettext/5 (C macro name dcngettext). The category (LC_MESSAGES) is always implicit and is not a parameter. Same semantics and fallback as ngettext/5.

Alias of npgettext/6 (C macro name dcnpgettext). The category (LC_MESSAGES) is always implicit and is not a parameter. Same semantics and fallback as npgettext/6.

Alias of pgettext/4 (C macro name dcpgettext). The category (LC_MESSAGES) is always implicit and is not a parameter. Same semantics and fallback as pgettext/4.

Returns the application's default locale (env erli18n.default_locale, default <<"en">>).

Resolves the conventional .po path for the application App.

Alias of gettext/2 (C macro name dgettext). Same semantics and fallback; it exists solely for name parity with C gettext.

Alias of gettext/3 (C macro name dgettext). Same semantics and fallback.

Alias of ngettext/4 (C macro name dngettext). Same semantics and fallback.

Alias of ngettext/5 (C macro name dngettext). Same semantics and fallback.

Alias of npgettext/5 (C macro name dnpgettext). Same semantics and fallback.

Alias of npgettext/6 (C macro name dnpgettext). Same semantics and fallback.

Alias of pgettext/3 (C macro name dpgettext). Same semantics and fallback.

Alias of pgettext/4 (C macro name dpgettext). Same semantics and fallback.

Loads the .po catalog at PoPath for the pair (Domain, Locale), idempotently. It is the typical boot call: load each catalog once.

Same as ensure_loaded/3, with Opts controlling the load — use this form when the .po is untrusted input (multi-tenant) and you want explicit anti-DoS limits.

Translates Msgid in the default domain and the resolved locale — the most common form in UI code.

Translates Msgid in an explicit domain Domain, in the resolved locale. Maps the C macro dgettext(domain, msgid).

Main form of the singular family: translates Msgid in the domain Domain and the locale Locale, both explicit (no context/msgctxt). All other singular arities resolve domain/locale and delegate here. Maps the C macro dcgettext(domain, msgid, LC_MESSAGES).

Lists the currently loaded catalogs as {Domain, Locale, NumEntries} tuples, one per domain/locale pair. Returns an empty list if nothing is loaded.

Returns a map with memory-usage information for the loaded catalogs. Useful for observability and operational diagnostics (growth alerts, dashboards).

Translates the correct plural form for N in the default domain and the resolved locale.

Same as ngettext/3, but with the domain Domain explicit; the locale is the resolved one (which_locale/0 or default_locale/0). Maps the C macro dngettext(domain, msgid, msgid_plural, n).

Main form of the plural family: translates the correct plural form for N in the domain Domain and the locale Locale, both explicit. The other plural arities delegate here. Maps the C macro dcngettext(domain, msgid, msgid_plural, n, LC_MESSAGES).

Translates the correct plural form for N, disambiguated by Context (msgctxt), in the default domain and the resolved locale. Combines context and plural.

Same as npgettext/4, but with the domain Domain explicit; the locale is the resolved one. Maps the C macro dnpgettext(domain, msgctxt, msgid, msgid_plural, n).

Main form of the contextual plural family: translates the correct plural form for N, disambiguated by Context (msgctxt), in the domain Domain and the locale Locale, both explicit. The other contextual plural arities delegate here. Maps the C macro dcnpgettext(domain, msgctxt, msgid, msgid_plural, n, LC_MESSAGES).

Translates Msgid disambiguated by Context (msgctxt) in the default domain and the resolved locale. Use it when the same msgid needs different translations per role (e.g. "May" as a month vs. a verb).

Same as pgettext/2, but with the domain Domain explicit; the locale is the resolved one. Maps the C macro dpgettext(domain, msgctxt, msgid).

Main form of the contextual singular family: translates Msgid disambiguated by Context (msgctxt) in the domain Domain and the locale Locale, both explicit. The other contextual singular arities delegate here. Maps the C macro dcpgettext(domain, msgctxt, msgid, LC_MESSAGES).

Reloads (atomically) the (Domain, Locale) catalog from PoPath, even if it is already loaded — it is the path to apply changes to the .po without restarting the app (unlike ensure_loaded/3, which is a no-op if already present).

Same as reload/3, with Opts (the same fields as ensure_loaded/4: include_fuzzy, max_bytes, max_entries, timeout). Keeps the reload atomic and the previous catalog intact in case of error.

Sets the application's default locale (env erli18n.default_locale) to Locale (binary) and returns ok.

Sets the current locale of the calling process to Locale (binary) and returns ok.

Returns the application's default domain (env erli18n.default_domain, default ?GETTEXT_DOMAIN = default).

Sets the application's default domain (env erli18n.default_domain) to Domain (atom) and returns ok. Equivalent to C gettext's textdomain(3) function.

Removes the (Domain, Locale) catalog from memory and returns ok.

Lists the keys (entries) of the (Domain, Locale) catalog, useful for introspection and tests (e.g. asserting that a .po loaded the expected entry).

Returns the current locale of the calling process, or undefined if none has been set.

Types

context()

-type context() :: erli18n_server:context().

msgctxt context (undefined | binary()) that disambiguates homographs (e.g. the same msgid "May" as a month vs. a verb). undefined means "no context".

domain()

-type domain() :: erli18n_server:domain().

Translation domain (atom()). Partitions the msgid space — each pair (Domain, Locale) is an independent .po catalog. The default domain comes from textdomain/0.

locale()

-type locale() :: erli18n_server:locale().

BCP-47 locale as a binary (binary()), e.g. <<"en">>, <<"pt_BR">>. It is the catalog key and the basis of plural-form selection.

msgid()

-type msgid() :: erli18n_server:msgid().

Translation source key (binary()): the text in the source language, exactly as extracted by xgettext. It is also the fallback value when there is no translation.

msgid_plural()

-type msgid_plural() :: binary().

Plural form of the source key (binary()): the .po msgid_plural. Used as the fallback when N /= 1 and there is no translation.

translation()

-type translation() :: erli18n_server:translation().

Translated text returned to the caller (binary()). On a miss or an empty translation, it is the msgid itself (or msgid_plural), never undefined.

Functions

dcgettext(Domain, Msgid, Locale)

-spec dcgettext(domain(), msgid(), locale()) -> translation().

Alias of gettext/3 (C macro name dcgettext). The C category (LC_MESSAGES) is always implicit in erli18n and is therefore not a parameter — there is no LC_NUMERIC, LC_TIME, etc. Same semantics and fallback as gettext/3.

1> erli18n:dcgettext(my_domain, <<"Hello">>, <<"de">>).
<<"Hallo">>

dcngettext(Domain, Msgid, MsgidPlural, N, Locale)

-spec dcngettext(domain(), msgid(), msgid_plural(), integer(), locale()) -> translation().

Alias of ngettext/5 (C macro name dcngettext). The category (LC_MESSAGES) is always implicit and is not a parameter. Same semantics and fallback as ngettext/5.

1> erli18n:dcngettext(my_domain, <<"file">>, <<"files">>, 2, <<"fr">>).
<<"2 fichiers">>

dcnpgettext(Domain, Context, Msgid, MsgidPlural, N, Locale)

-spec dcnpgettext(domain(), context(), msgid(), msgid_plural(), integer(), locale()) -> translation().

Alias of npgettext/6 (C macro name dcnpgettext). The category (LC_MESSAGES) is always implicit and is not a parameter. Same semantics and fallback as npgettext/6.

1> erli18n:dcnpgettext(my_domain, <<"email">>, <<"message">>, <<"messages">>, 3, <<"de">>).
<<"3 Nachrichten">>

dcpgettext(Domain, Context, Msgid, Locale)

-spec dcpgettext(domain(), context(), msgid(), locale()) -> translation().

Alias of pgettext/4 (C macro name dcpgettext). The category (LC_MESSAGES) is always implicit and is not a parameter. Same semantics and fallback as pgettext/4.

1> erli18n:dcpgettext(my_domain, <<"month">>, <<"May">>, <<"de">>).
<<"Mai">>

default_locale()

-spec default_locale() -> locale().

Returns the application's default locale (env erli18n.default_locale, default <<"en">>).

It is the locale fallback used by every lookup when the calling process has not set a locale via setlocale/1 (that is, when which_locale/0 is undefined).

1> erli18n:default_locale().
<<"en">>
2> erli18n:set_default_locale(<<"pt_BR">>).
ok
3> erli18n:default_locale().
<<"pt_BR">>

Crashes with error({invalid_config, _}) if the value configured in the env is not a binary — the boundary is narrowed so a misconfiguration becomes a descriptive crash here, rather than a silent surprise downstream (e.g. an atom leaking into a gettext/3 that only accepts a binary). See set_default_locale/1 and which_locale/0.

default_po_path(App, Domain, Locale)

-spec default_po_path(atom(), domain(), locale()) -> file:filename().

Resolves the conventional .po path for the application App.

  • App — name of the OTP application whose priv/ contains the catalogs.
  • Domain — domain (becomes the file name <Domain>.po).
  • Locale — locale (becomes the directory <Locale>).

Assembles <PrivDir>/locale/<Locale>/LC_MESSAGES/<Domain>.po, where PrivDir is the priv/ directory of App (via code:priv_dir/1). A convenience to feed ensure_loaded/3,4 and reload/3,4 without assembling the path manually.

1> erli18n:default_po_path(minha_app, my_domain, <<"fr">>).
"/.../minha_app/priv/locale/fr/LC_MESSAGES/my_domain.po"

Delegates to erli18n_server:default_po_path/3.

dgettext(Domain, Msgid)

-spec dgettext(domain(), msgid()) -> translation().

Alias of gettext/2 (C macro name dgettext). Same semantics and fallback; it exists solely for name parity with C gettext.

1> erli18n:dgettext(my_domain, <<"Hello">>) =:= erli18n:gettext(my_domain, <<"Hello">>).
true

dgettext(Domain, Msgid, Locale)

-spec dgettext(domain(), msgid(), locale()) -> translation().

Alias of gettext/3 (C macro name dgettext). Same semantics and fallback.

1> erli18n:dgettext(my_domain, <<"Hello">>, <<"de">>).
<<"Hallo">>

dngettext(Domain, Msgid, MsgidPlural, N)

-spec dngettext(domain(), msgid(), msgid_plural(), integer()) -> translation().

Alias of ngettext/4 (C macro name dngettext). Same semantics and fallback.

1> erli18n:dngettext(my_domain, <<"file">>, <<"files">>, 2).
<<"2 fichiers">>

dngettext(Domain, Msgid, MsgidPlural, N, Locale)

-spec dngettext(domain(), msgid(), msgid_plural(), integer(), locale()) -> translation().

Alias of ngettext/5 (C macro name dngettext). Same semantics and fallback.

1> erli18n:dngettext(my_domain, <<"file">>, <<"files">>, 2, <<"fr">>).
<<"2 fichiers">>

dnpgettext(Domain, Context, Msgid, MsgidPlural, N)

-spec dnpgettext(domain(), context(), msgid(), msgid_plural(), integer()) -> translation().

Alias of npgettext/5 (C macro name dnpgettext). Same semantics and fallback.

1> erli18n:dnpgettext(my_domain, <<"email">>, <<"message">>, <<"messages">>, 3).
<<"3 messages">>

dnpgettext(Domain, Context, Msgid, MsgidPlural, N, Locale)

-spec dnpgettext(domain(), context(), msgid(), msgid_plural(), integer(), locale()) -> translation().

Alias of npgettext/6 (C macro name dnpgettext). Same semantics and fallback.

1> erli18n:dnpgettext(my_domain, <<"email">>, <<"message">>, <<"messages">>, 3, <<"de">>).
<<"3 Nachrichten">>

dpgettext(Domain, Context, Msgid)

-spec dpgettext(domain(), context(), msgid()) -> translation().

Alias of pgettext/3 (C macro name dpgettext). Same semantics and fallback.

1> erli18n:dpgettext(my_domain, <<"month">>, <<"May">>).
<<"Mai">>

dpgettext(Domain, Context, Msgid, Locale)

-spec dpgettext(domain(), context(), msgid(), locale()) -> translation().

Alias of pgettext/4 (C macro name dpgettext). Same semantics and fallback.

1> erli18n:dpgettext(my_domain, <<"month">>, <<"May">>, <<"de">>).
<<"Mai">>

ensure_loaded(Domain, Locale, PoPath)

-spec ensure_loaded(domain(), locale(), file:filename()) -> erli18n_server:ensure_result().

Loads the .po catalog at PoPath for the pair (Domain, Locale), idempotently. It is the typical boot call: load each catalog once.

  • Domain — catalog domain.
  • Locale — catalog locale.
  • PoPath — path to the .po (use default_po_path/3 for the conventional layout).

Idempotence: if the pair is already loaded, it returns {ok, already} without re-reading from disk — to force a re-read, use reload/3. Loading is atomic: parse, plural-rule compilation, and CLDR validation run before any ETS insertion; on error the state stays intact.

Return: {ok, NumEntries} on the first load, {ok, already} if already present, or {error, Reason} on a file/parse/compile failure (e.g. {error, {file_error, enoent}}, {error, {plural_compile_error, _}}).

1> erli18n:ensure_loaded(my_domain, <<"fr">>, "priv/locale/fr/LC_MESSAGES/my_domain.po").
{ok, 12}
2> erli18n:ensure_loaded(my_domain, <<"fr">>, "priv/locale/fr/LC_MESSAGES/my_domain.po").
{ok, already}

Delegates to erli18n_server:ensure_loaded/3. See ensure_loaded/4 (with Opts and anti-DoS limits), reload/3, and unload/2.

ensure_loaded(Domain, Locale, PoPath, Opts)

Same as ensure_loaded/3, with Opts controlling the load — use this form when the .po is untrusted input (multi-tenant) and you want explicit anti-DoS limits.

  • Opts — map with the supported fields:
    • include_fuzzy — include entries marked as fuzzy.
    • max_bytes — rejects the file (via filelib:file_size/1, without reading the bytes) if it exceeds the limit.
    • max_entries — rejects the catalog (after the parse) if it has more entries than the limit.
    • timeout — commit timeout.

Exceeded bounds return {error, {input_too_large, _, _}} (byte limit) or {error, {too_many_entries, _, _}} (entry limit). The other semantics (idempotence, atomicity) are the same as ensure_loaded/3.

1> erli18n:ensure_loaded(my_domain, <<"fr">>, "fr.po", #{max_bytes => 1048576}).
{ok, 12}
2> erli18n:ensure_loaded(my_domain, <<"fr">>, "huge.po", #{max_bytes => 1024}).
{error, {input_too_large, _, _}}

Delegates to erli18n_server:ensure_loaded/4. See reload/4.

gettext(Msgid)

-spec gettext(msgid()) -> translation().

Translates Msgid in the default domain and the resolved locale — the most common form in UI code.

  • Msgid — key in the source language, exactly as xgettext extracted it.

The domain is textdomain/0 and the locale is the resolved one: which_locale/0 if the process set one via setlocale/1, otherwise default_locale/0. On a miss (catalog not loaded or nonexistent entry) or an empty translation (msgstr ""), it returns Msgid itself.

1> erli18n:setlocale(<<"pt_BR">>).
ok
2> erli18n:gettext(<<"Hello">>).
<<"Olá">>
3> erli18n:gettext(<<"No registered translation">>).
<<"No registered translation">>

Crashes (function_clause) if Msgid is not a binary. For an explicit domain use gettext/2; for an explicit locale, gettext/3.

gettext(Domain, Msgid)

-spec gettext(domain(), msgid()) -> translation().

Translates Msgid in an explicit domain Domain, in the resolved locale. Maps the C macro dgettext(domain, msgid).

  • Domain — domain/catalog to look in (not textdomain/0).
  • Msgid — key in the source language.

The locale is the resolved one (which_locale/0 or default_locale/0). On a miss or an empty translation, it returns Msgid.

1> erli18n:setlocale(<<"pt_BR">>).
ok
2> erli18n:gettext(errors, <<"Not found">>).
<<"Não encontrado">>

Crashes (function_clause) if Domain is not an atom or Msgid is not a binary. See gettext/3 (explicit locale) and dgettext/2 (alias).

gettext(Domain, Msgid, Locale)

-spec gettext(domain(), msgid(), locale()) -> translation().

Main form of the singular family: translates Msgid in the domain Domain and the locale Locale, both explicit (no context/msgctxt). All other singular arities resolve domain/locale and delegate here. Maps the C macro dcgettext(domain, msgid, LC_MESSAGES).

  • Domain — domain/catalog to look in.
  • Msgid — key in the source language.
  • Locale — exact locale (does not go through which_locale/0).

Return semantics (R1): if erli18n_server:lookup_singular/4 returns {ok, T} with T non-empty, it returns T; otherwise — miss OR an empty translation (msgstr "" is "untranslated"; the empty-binary guard is defence-in-depth) — it returns Msgid. On that miss path, it emits the telemetry event [erli18n, lookup, miss] when observability is enabled (see internal function emit_lookup_miss/5).

1> erli18n:gettext(my_domain, <<"Save">>, <<"de">>).
<<"Speichern">>
2> erli18n:gettext(my_domain, <<"Save">>, <<"nonexistent_locale">>).
<<"Save">>

Crashes (function_clause) if any argument violates the type. See pgettext/4 (contextual variant) and the aliases dgettext/3 / dcgettext/3.

loaded_catalogs()

-spec loaded_catalogs() -> [{domain(), locale(), non_neg_integer()}].

Lists the currently loaded catalogs as {Domain, Locale, NumEntries} tuples, one per domain/locale pair. Returns an empty list if nothing is loaded.

1> erli18n:loaded_catalogs().
[{my_domain, <<"fr">>, 12}, {my_domain, <<"de">>, 11}]

Delegates to erli18n_server:loaded_catalogs/0. See which_keys/2 (keys of a specific catalog) and memory_info/0.

memory_info()

-spec memory_info() -> map().

Returns a map with memory-usage information for the loaded catalogs. Useful for observability and operational diagnostics (growth alerts, dashboards).

The return has three fixed keys:

  • ets_bytes — bytes consumed by the ETS table (already converted from VM words to bytes; multiplied by erlang:system_info(wordsize)).
  • num_catalogs — number of loaded (Domain, Locale) catalogs.
  • num_keys — total number of entries (keys) across all catalogs.
1> erli18n:memory_info().
#{ets_bytes => 24576, num_catalogs => 3, num_keys => 130}

This façade's -spec weakens to map(), but the server guarantees exactly these three keys (erli18n_server:memory_info/0). See loaded_catalogs/0.

ngettext(Msgid, MsgidPlural, N)

-spec ngettext(msgid(), msgid_plural(), integer()) -> translation().

Translates the correct plural form for N in the default domain and the resolved locale.

  • Msgid — singular form (msgid), also the fallback when N == 1.
  • MsgidPlural — plural form (msgid_plural), the fallback when N /= 1.
  • N — count. Arbitrary integer: negatives and bignums are accepted (evaluation is bignum-clean).

The plural form is chosen by the Plural-Forms rule from the loaded .po header (not by the count of forms in the code). Evaluation is total: it clamps à la libintl (out-of-range form → 0, zero divisor → 0) and never brings down the caller; pathological rules are already rejected at load time. Fallback (R2): on a miss or an empty translation, it returns Msgid if N == 1, otherwise MsgidPlural.

1> erli18n:setlocale(<<"en">>).
ok
2> erli18n:ngettext(<<"file">>, <<"files">>, 1).
<<"file">>
3> erli18n:ngettext(<<"file">>, <<"files">>, 3).
<<"files">>

Crashes (function_clause) if Msgid/MsgidPlural are not binaries or N is not an integer. See ngettext/4 (domain) and ngettext/5 (locale).

ngettext(Domain, Msgid, MsgidPlural, N)

-spec ngettext(domain(), msgid(), msgid_plural(), integer()) -> translation().

Same as ngettext/3, but with the domain Domain explicit; the locale is the resolved one (which_locale/0 or default_locale/0). Maps the C macro dngettext(domain, msgid, msgid_plural, n).

1> erli18n:ngettext(my_domain, <<"file">>, <<"files">>, 42).
<<"42 fichiers">>

Same plural-form selection, fallback (R2), and crash modes as ngettext/3. See ngettext/5 for an explicit locale and dngettext/4 (alias).

ngettext(Domain, Msgid, MsgidPlural, N, Locale)

-spec ngettext(domain(), msgid(), msgid_plural(), integer(), locale()) -> translation().

Main form of the plural family: translates the correct plural form for N in the domain Domain and the locale Locale, both explicit. The other plural arities delegate here. Maps the C macro dcngettext(domain, msgid, msgid_plural, n, LC_MESSAGES).

  • Domain — domain/catalog to look in.
  • Msgid / MsgidPlural — singular/plural forms and the fallbacks.
  • N — count (arbitrary integer, incl. negatives and bignums).
  • Locale — exact locale (does not go through which_locale/0).

Calls erli18n_server:lookup_plural_form/5, which chooses the form by the Plural-Forms rule from the loaded header (total evaluation/clamp — see the moduledoc). Fallback (R2): on a miss or an empty translation, it returns Msgid if N == 1, otherwise MsgidPlural. On the miss path it emits [erli18n, lookup, miss] when observability is enabled.

1> erli18n:ngettext(my_domain, <<"1 file">>, <<"%d files">>, 1, <<"en">>).
<<"1 file">>
2> erli18n:ngettext(my_domain, <<"1 file">>, <<"%d files">>, 5, <<"en">>).
<<"%d files">>

Crashes (function_clause) if any argument violates the type. See npgettext/6 (contextual plural) and the aliases dngettext/5 / dcngettext/5.

npgettext(Context, Msgid, MsgidPlural, N)

-spec npgettext(context(), msgid(), msgid_plural(), integer()) -> translation().

Translates the correct plural form for N, disambiguated by Context (msgctxt), in the default domain and the resolved locale. Combines context and plural.

  • Contextmsgctxt (binary) or undefined.
  • Msgid / MsgidPlural — singular/plural forms and the fallbacks.
  • N — count (arbitrary integer).

The plural form comes from the Plural-Forms rule of the loaded header (total evaluation/clamp). Fallback: on a miss or an empty translation, it applies R2 to the msgid/msgid_plural pair (Msgid if N == 1, otherwise MsgidPlural).

1> erli18n:npgettext(<<"email">>, <<"message">>, <<"messages">>, 1).
<<"mensagem">>
2> erli18n:npgettext(<<"email">>, <<"message">>, <<"messages">>, 5).
<<"mensagens">>

Crashes (function_clause) if any argument violates the type. See npgettext/5 (domain) and npgettext/6 (locale).

npgettext(Domain, Context, Msgid, MsgidPlural, N)

-spec npgettext(domain(), context(), msgid(), msgid_plural(), integer()) -> translation().

Same as npgettext/4, but with the domain Domain explicit; the locale is the resolved one. Maps the C macro dnpgettext(domain, msgctxt, msgid, msgid_plural, n).

1> erli18n:npgettext(my_domain, <<"email">>, <<"message">>, <<"messages">>, 3).
<<"3 messages">>

Same plural-form selection, fallback, and crashes as npgettext/4. See npgettext/6 (explicit locale) and dnpgettext/5 (alias).

npgettext(Domain, Context, Msgid, MsgidPlural, N, Locale)

-spec npgettext(domain(), context(), msgid(), msgid_plural(), integer(), locale()) -> translation().

Main form of the contextual plural family: translates the correct plural form for N, disambiguated by Context (msgctxt), in the domain Domain and the locale Locale, both explicit. The other contextual plural arities delegate here. Maps the C macro dcnpgettext(domain, msgctxt, msgid, msgid_plural, n, LC_MESSAGES).

  • Domain — domain/catalog.
  • Contextmsgctxt (binary) or undefined.
  • Msgid / MsgidPlural — singular/plural forms and the fallbacks.
  • N — count (arbitrary integer).
  • Locale — exact locale.

Calls erli18n_server:lookup_plural_form/5 WITH the Context; the form comes from the Plural-Forms rule of the loaded header (total evaluation/clamp). Fallback: on a miss or an empty translation, it applies R2 to the msgid/msgid_plural pair. On the miss path it emits [erli18n, lookup, miss] (with the context in the metadata) when observability is enabled.

1> erli18n:npgettext(my_domain, <<"email">>, <<"message">>, <<"messages">>, 1, <<"de">>).
<<"Nachricht">>
2> erli18n:npgettext(my_domain, <<"email">>, <<"message">>, <<"messages">>, 5, <<"de">>).
<<"Nachrichten">>

Crashes (function_clause) if any argument violates the type. See the aliases dnpgettext/6 / dcnpgettext/6.

pgettext(Context, Msgid)

-spec pgettext(context(), msgid()) -> translation().

Translates Msgid disambiguated by Context (msgctxt) in the default domain and the resolved locale. Use it when the same msgid needs different translations per role (e.g. "May" as a month vs. a verb).

  • Contextmsgctxt binary, or undefined for "no context" (then equivalent to gettext/1).
  • Msgid — key in the source language.

On a miss or an empty translation, it returns Msgid. Important: there is no retry with another context — an entry from a different Context never leaks; the miss falls straight back to Msgid.

1> erli18n:pgettext(<<"month">>, <<"May">>).
<<"Maio">>
2> erli18n:pgettext(<<"verb">>, <<"May">>).
<<"Pode">>

Crashes (function_clause) if Context is not undefined/a binary or Msgid is not a binary. See pgettext/3 (domain) and pgettext/4 (locale).

pgettext(Domain, Context, Msgid)

-spec pgettext(domain(), context(), msgid()) -> translation().

Same as pgettext/2, but with the domain Domain explicit; the locale is the resolved one. Maps the C macro dpgettext(domain, msgctxt, msgid).

1> erli18n:pgettext(my_domain, <<"month">>, <<"May">>).
<<"Mai">>

Same fallback (no context retry) and crashes as pgettext/2. See pgettext/4 (explicit locale) and dpgettext/3 (alias).

pgettext(Domain, Context, Msgid, Locale)

-spec pgettext(domain(), context(), msgid(), locale()) -> translation().

Main form of the contextual singular family: translates Msgid disambiguated by Context (msgctxt) in the domain Domain and the locale Locale, both explicit. The other contextual singular arities delegate here. Maps the C macro dcpgettext(domain, msgctxt, msgid, LC_MESSAGES).

  • Domain — domain/catalog.
  • Contextmsgctxt (binary) or undefined.
  • Msgid — key in the source language.
  • Locale — exact locale.

Calls erli18n_server:lookup_singular/4 WITH the Context. Fallback (R3): on a miss or an empty translation, it returns Msgid — deliberately without a retry with Context = undefined, so as not to leak the translation of another context. On the miss path it emits [erli18n, lookup, miss] (with the context in the metadata) when observability is enabled.

1> erli18n:pgettext(my_domain, <<"menu">>, <<"Open">>, <<"de">>).
<<"Öffnen">>
2> erli18n:pgettext(my_domain, <<"nonexistent_context">>, <<"Open">>, <<"de">>).
<<"Open">>

Crashes (function_clause) if any argument violates the type. See npgettext/6 (contextual plural) and the aliases dpgettext/4 / dcpgettext/4.

reload(Domain, Locale, PoPath)

Reloads (atomically) the (Domain, Locale) catalog from PoPath, even if it is already loaded — it is the path to apply changes to the .po without restarting the app (unlike ensure_loaded/3, which is a no-op if already present).

  • Domain / Locale — catalog pair to reload.
  • PoPath — path to the .po (always re-read from disk).

The operation is atomic and without an empty window: the fallible pipeline (read/parse/compile) runs without touching ETS and the swap is atomic (insert-before-prune), so concurrent lookups never see the empty catalog. On error, the previous catalog stays intact.

Return: {ok, NumEntries} on success or {error, Reason} on failure.

1> erli18n:reload(my_domain, <<"fr">>, "priv/locale/fr/LC_MESSAGES/my_domain.po").
{ok, 15}

Delegates to erli18n_server:reload/3. See reload/4 (with Opts) and ensure_loaded/3.

reload(Domain, Locale, PoPath, Opts)

Same as reload/3, with Opts (the same fields as ensure_loaded/4: include_fuzzy, max_bytes, max_entries, timeout). Keeps the reload atomic and the previous catalog intact in case of error.

1> erli18n:reload(my_domain, <<"fr">>, "fr.po", #{max_entries => 5000}).
{ok, 15}

Delegates to erli18n_server:reload/4. See reload/3 and ensure_loaded/4.

set_default_locale(Locale)

-spec set_default_locale(locale()) -> ok.

Sets the application's default locale (env erli18n.default_locale) to Locale (binary) and returns ok.

  • Locale — new node-global default.

Affects all processes that rely on the locale fallback (those that have not called setlocale/1). It is application:env state, not per-process. Typically called once at app boot.

1> erli18n:set_default_locale(<<"fr">>).
ok
2> erli18n:default_locale().
<<"fr">>

Crashes (function_clause) if Locale is not a binary. See default_locale/0 (read) and setlocale/1 (per-process override).

setlocale(Locale)

-spec setlocale(locale()) -> ok.

Sets the current locale of the calling process to Locale (binary) and returns ok.

  • Locale — locale to use in this process's subsequent lookups that depend on the resolved locale (gettext/1, ngettext/3, ...).

Scope: the current process only. The value lives in the process dictionary and is not inherited by child processes — each request process must call setlocale/1 on its own. Typically called once at the start of handling each request.

1> erli18n:setlocale(<<"de">>).
ok
2> erli18n:which_locale().
<<"de">>

Crashes (function_clause) if Locale is not a binary. See which_locale/0 (read) and set_default_locale/1 (app-global default).

textdomain()

-spec textdomain() -> domain().

Returns the application's default domain (env erli18n.default_domain, default ?GETTEXT_DOMAIN = default).

It is the domain used by the variants without an explicit domain — gettext/1, ngettext/3, pgettext/2, npgettext/4.

1> erli18n:textdomain().
default
2> erli18n:textdomain(my_domain).
ok
3> erli18n:textdomain().
my_domain

Crashes with error({invalid_config, _}) if the value configured in the env is not an atom (same narrowing strategy as default_locale/0). See textdomain/1 (write).

textdomain(Domain)

-spec textdomain(domain()) -> ok.

Sets the application's default domain (env erli18n.default_domain) to Domain (atom) and returns ok. Equivalent to C gettext's textdomain(3) function.

  • Domain — new node-global default domain.

Affects all subsequent calls to the variants without an explicit domain. It is application:env state, not per-process.

1> erli18n:textdomain(errors).
ok
2> erli18n:textdomain().
errors

Crashes (function_clause) if Domain is not an atom. See textdomain/0 (read).

unload(Domain, Locale)

-spec unload(domain(), locale()) -> ok.

Removes the (Domain, Locale) catalog from memory and returns ok.

  • Domain / Locale — catalog pair to unload.

After the unload, lookups for that pair fall back again (returning the msgid/msgid_plural). It is idempotent: unloading a nonexistent catalog also returns ok.

1> erli18n:unload(my_domain, <<"fr">>).
ok
2> erli18n:unload(my_domain, <<"never_loaded">>).
ok

Delegates to erli18n_server:unload/2. See ensure_loaded/3 and loaded_catalogs/0.

which_keys(Domain, Locale)

-spec which_keys(domain(), locale()) -> [{singular, context(), msgid()} | {plural, context(), msgid()}].

Lists the keys (entries) of the (Domain, Locale) catalog, useful for introspection and tests (e.g. asserting that a .po loaded the expected entry).

  • Domain / Locale — catalog pair to inspect.

Each key is {singular, Context, Msgid} or {plural, Context, Msgid}, where Context is undefined when there is no msgctxt. Returns an empty list if the catalog is not loaded.

1> erli18n:which_keys(my_domain, <<"fr">>).
[{singular, undefined, <<"Hello">>},
 {plural, undefined, <<"file">>},
 {singular, <<"month">>, <<"May">>}]

Delegates to erli18n_server:which_keys/2. See loaded_catalogs/0.

which_locale()

-spec which_locale() -> locale() | undefined.

Returns the current locale of the calling process, or undefined if none has been set.

The value is what setlocale/1 wrote to this process's dictionary. Since the state is per-process and is not inherited across spawn/1, a freshly created process reads undefined here (and, on the lookup path, falls back to default_locale/0).

1> erli18n:which_locale().
undefined
2> erli18n:setlocale(<<"pt_BR">>).
ok
3> erli18n:which_locale().
<<"pt_BR">>

Crashes with error({invalid_process_locale, _}) if the private dictionary key ('$erli18n_locale') has been overwritten by a third party with a non-binary value — setlocale/1 only writes binaries, so any other shape is a contract violation and must fail visibly. See setlocale/1 (write) and default_locale/0 (fallback).