erli18n (erli18n v0.4.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.

Each family also has an interpolating f-suffix sibling that takes a trailing Bindings :: map() and substitutes named %{name} placeholders in the resolved translation: gettextf, ngettextf, pgettextf, npgettextf (plus the d/dc aliases). The plural members auto-bind count => N. Substitution is total and fail-soft — see erli18n_interp for the grammar (%{name}, %%/%%{name} escaping), value coercion, the lenient/strict missing-binding policy, the anti-DoS caps, and the bidi/RTL caveat.

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 persistent_term in the caller's own process (copy-free); only writes (load/reload) go through the writer 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 writer) does not empty the catalogs: they live in persistent_term, which is node-global and owned by the runtime, so they survive a writer crash with no heir machinery — on restart the writer only re-derives its observability view and lookups keep serving the loaded translations (see erli18n_server and erli18n_pt_store).
  • 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">>
8> erli18n:gettextf(my_domain, <<"Hello, %{name}">>, #{name => <<"Ada">>}).
<<"Bonjour, Ada">>
9> erli18n:ngettextf(my_domain, <<"%{count} file">>, <<"%{count} files">>, 3, #{}).
<<"3 fichiers">>

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

Map of %{name} interpolation bindings for the f-suffix family (gettextf, ngettextf, pgettextf, npgettextf and their d/dc aliases). Keys are atoms (the placeholder names) and values are coerced to UTF-8 text totally by erli18n_interp:format/2. The plural members auto-bind count => N (a caller-supplied count wins). See erli18n_interp for the full grammar, value coercion, and anti-DoS caps.

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

Canonicalizes one BCP-47 / POSIX locale tag to erli18n catalog-key shape (<<"pt-BR">><<"pt_BR">>, <<"iw">><<"he">>).

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 gettextf/4 (C macro name dcgettext, interpolating). The category (LC_MESSAGES) is always implicit and is not a parameter. Same semantics and fallback as gettextf/4.

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 ngettextf/6 (C macro name dcngettext, interpolating). The category (LC_MESSAGES) is always implicit and is not a parameter. Same semantics, fallback, and count => N auto-binding as ngettextf/6.

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 npgettextf/7 (C macro name dcnpgettext, interpolating). The category (LC_MESSAGES) is always implicit and is not a parameter. Same semantics, fallback, and count => N auto-binding as npgettextf/7.

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.

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

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 gettextf/3 (C macro name dgettext, interpolating). Same semantics and fallback.

Alias of gettextf/4 (C macro name dgettext, interpolating). 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 ngettextf/5 (C macro name dngettext, interpolating). Same semantics, fallback, and count => N auto-binding.

Alias of ngettextf/6 (C macro name dngettext, interpolating). Same semantics, fallback, and count => N auto-binding.

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 npgettextf/6 (C macro name dnpgettext, interpolating). Same semantics, fallback, and count => N auto-binding.

Alias of npgettextf/7 (C macro name dnpgettext, interpolating). Same semantics, fallback, and count => N auto-binding.

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

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

Alias of pgettextf/4 (C macro name dpgettext, interpolating). Same semantics and fallback.

Alias of pgettextf/5 (C macro name dpgettext, interpolating). 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).

Like gettext/1, then interpolates %{name} placeholders in the resolved translation using Bindings.

Like gettext/2 (explicit domain), then interpolates %{name} placeholders using Bindings. Maps the interpolating form of dgettext.

Main interpolating singular form: like gettext/3 (explicit domain and locale), then interpolates %{name} placeholders using Bindings.

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).

Chooses the best supported locale for a client preference list, always returning a usable locale.

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).

Like ngettext/3, then interpolates %{name} placeholders using Bindings with count => N auto-bound.

Like ngettext/4 (explicit domain), then interpolates %{name} placeholders using Bindings with count => N auto-bound (caller override wins).

Main interpolating plural form: like ngettext/5 (explicit domain and locale), then interpolates %{name} placeholders using Bindings with count => N auto-bound (caller override wins).

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).

Like npgettext/4, then interpolates %{name} placeholders using Bindings with count => N auto-bound (caller override wins).

Like npgettext/5 (explicit domain), then interpolates %{name} placeholders using Bindings with count => N auto-bound (caller override wins). Maps the interpolating form of dnpgettext.

Main interpolating contextual plural form: like npgettext/6 (explicit domain and locale), then interpolates %{name} placeholders using Bindings with count => N auto-bound (caller override wins).

Parses an HTTP Accept-Language header into a priority-ordered [{LanguageRange, Q}] list (Q an integer in milli-units, 0..1000).

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).

Like pgettext/2, then interpolates %{name} placeholders in the resolved translation using Bindings.

Like pgettext/3 (explicit domain), then interpolates %{name} placeholders using Bindings. Maps the interpolating form of dpgettext.

Main interpolating contextual singular form: like pgettext/4 (explicit domain and locale), then interpolates %{name} placeholders using Bindings.

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 application's locale-fallback mode (env erli18n.locale_fallback) 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 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

bindings()

-type bindings() :: erli18n_interp:bindings().

Map of %{name} interpolation bindings for the f-suffix family (gettextf, ngettextf, pgettextf, npgettextf and their d/dc aliases). Keys are atoms (the placeholder names) and values are coerced to UTF-8 text totally by erli18n_interp:format/2. The plural members auto-bind count => N (a caller-supplied count wins). See erli18n_interp for the full grammar, value coercion, and anti-DoS caps.

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

canonicalize_locale(Tag)

-spec canonicalize_locale(binary()) -> binary().

Canonicalizes one BCP-47 / POSIX locale tag to erli18n catalog-key shape (<<"pt-BR">><<"pt_BR">>, <<"iw">><<"he">>).

Total and idempotent over binary content; see erli18n_negotiate:canonicalize/1 for the full algorithm, the legacy-alias table, and the documented non-goals (the script⇄region inference zh_Hanszh_CN is out of scope).

1> erli18n:canonicalize_locale(<<"PT_br.UTF-8">>).
<<"pt_BR">>

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">>

dcgettextf(Domain, Msgid, Locale, Bindings)

-spec dcgettextf(domain(), msgid(), locale(), bindings()) -> translation().

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

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">>

dcngettextf(Domain, Msgid, MsgidPlural, N, Locale, Bindings)

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

Alias of ngettextf/6 (C macro name dcngettext, interpolating). The category (LC_MESSAGES) is always implicit and is not a parameter. Same semantics, fallback, and count => N auto-binding as ngettextf/6.

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">>

dcnpgettextf(Domain, Context, Msgid, MsgidPlural, N, Locale, Bindings)

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

Alias of npgettextf/7 (C macro name dcnpgettext, interpolating). The category (LC_MESSAGES) is always implicit and is not a parameter. Same semantics, fallback, and count => N auto-binding as npgettextf/7.

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">>

dcpgettextf(Domain, Context, Msgid, Locale, Bindings)

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

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

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">>

dgettextf(Domain, Msgid, Bindings)

-spec dgettextf(domain(), msgid(), bindings()) -> translation().

Alias of gettextf/3 (C macro name dgettext, interpolating). Same semantics and fallback.

dgettextf(Domain, Msgid, Locale, Bindings)

-spec dgettextf(domain(), msgid(), locale(), bindings()) -> translation().

Alias of gettextf/4 (C macro name dgettext, interpolating). Same semantics and fallback.

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">>

dngettextf(Domain, Msgid, MsgidPlural, N, Bindings)

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

Alias of ngettextf/5 (C macro name dngettext, interpolating). Same semantics, fallback, and count => N auto-binding.

dngettextf(Domain, Msgid, MsgidPlural, N, Locale, Bindings)

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

Alias of ngettextf/6 (C macro name dngettext, interpolating). Same semantics, fallback, and count => N auto-binding.

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">>

dnpgettextf(Domain, Context, Msgid, MsgidPlural, N, Bindings)

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

Alias of npgettextf/6 (C macro name dnpgettext, interpolating). Same semantics, fallback, and count => N auto-binding.

dnpgettextf(Domain, Context, Msgid, MsgidPlural, N, Locale, Bindings)

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

Alias of npgettextf/7 (C macro name dnpgettext, interpolating). Same semantics, fallback, and count => N auto-binding.

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">>

dpgettextf(Domain, Context, Msgid, Bindings)

-spec dpgettextf(domain(), context(), msgid(), bindings()) -> translation().

Alias of pgettextf/4 (C macro name dpgettext, interpolating). Same semantics and fallback.

dpgettextf(Domain, Context, Msgid, Locale, Bindings)

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

Alias of pgettextf/5 (C macro name dpgettext, interpolating). Same semantics and fallback.

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 catalog install; 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.

gettextf(Msgid, Bindings)

-spec gettextf(msgid(), bindings()) -> translation().

Like gettext/1, then interpolates %{name} placeholders in the resolved translation using Bindings.

Resolves the translation in the default domain (textdomain/0) and the resolved locale (which_locale/0 or default_locale/0) exactly as gettext/1, then applies erli18n_interp:format/2 with Bindings. On a miss the resolution falls back to Msgid, and interpolation still runs over that fallback. Interpolation is total and fail-soft: an unbound placeholder is left literal and nothing crashes.

1> erli18n:setlocale(<<"fr">>).
ok
2> erli18n:gettextf(<<"Hello, %{name}!">>, #{name => <<"Ada">>}).
<<"Bonjour, Ada!">>

Crashes (function_clause) if Msgid is not a binary or Bindings is not a map. See gettextf/3 (domain) and gettextf/4 (locale).

gettextf(Domain, Msgid, Bindings)

-spec gettextf(domain(), msgid(), bindings()) -> translation().

Like gettext/2 (explicit domain), then interpolates %{name} placeholders using Bindings. Maps the interpolating form of dgettext.

1> erli18n:gettextf(my_domain, <<"Hello, %{name}!">>, #{name => <<"Ada">>}).
<<"Bonjour, Ada!">>

Same resolution and fallback as gettext/2. See gettextf/4 (explicit locale) and dgettextf/3 (alias).

gettextf(Domain, Msgid, Locale, Bindings)

-spec gettextf(domain(), msgid(), locale(), bindings()) -> translation().

Main interpolating singular form: like gettext/3 (explicit domain and locale), then interpolates %{name} placeholders using Bindings.

Resolves via gettext/3 (ignoring the per-process locale) and applies erli18n_interp:format/2. On a miss the resolution falls back to Msgid, over which interpolation still runs.

1> erli18n:gettextf(my_domain, <<"Hello, %{name}!">>, <<"fr">>,
1>                  #{name => <<"Ada">>}).
<<"Bonjour, Ada!">>

Crashes (function_clause) if any argument violates the type. See the aliases dgettextf/4 / dcgettextf/4.

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 — approximate bytes consumed by the catalog storage (already converted from VM words to bytes; multiplied by erlang:system_info(wordsize)). The field name is historical (storage is now persistent_term, not ETS); it is kept for backwards compatibility with the 0.3.0 return shape.
  • 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.

negotiate(Preferred, Available)

-spec negotiate([locale()] | [{locale(), erli18n_negotiate:qvalue()}], [locale()]) -> {ok, locale()}.

Chooses the best supported locale for a client preference list, always returning a usable locale.

Preferred is an ordered preference list — either [locale()] or the [{locale(), 0..1000}] output of parse_accept_language/1 — where position encodes priority. Available is the list of locales the caller supports (typically the Locale field of loaded_catalogs/0). Each preference is canonicalized and resolved through its BCP-47 fallback chain against Available; the first hit wins and is returned in its original Available casing. When nothing matches, returns {ok, default_locale()} so the result is always safe to feed to setlocale/1.

This helper is pure and independent of the locale_fallback env: it is the negotiation primitive for request middleware, distinct from the catalog lookup-time fallback chain (which the four lookup families apply on a miss).

1> erli18n:negotiate([<<"pt-BR">>], [<<"pt">>, <<"en">>]).
{ok,<<"pt">>}
2> erli18n:negotiate(
..    erli18n:parse_accept_language(<<"fr-CH, fr;q=0.9, en;q=0.5">>),
..    [<<"en">>, <<"de">>]).
{ok,<<"en">>}

See parse_accept_language/1, canonicalize_locale/1, and the lower-level erli18n_negotiate:negotiate/2 (which returns error instead of the default on no match).

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.

ngettextf(Msgid, MsgidPlural, N, Bindings)

-spec ngettextf(msgid(), msgid_plural(), integer(), bindings()) -> translation().

Like ngettext/3, then interpolates %{name} placeholders using Bindings with count => N auto-bound.

Resolves the correct plural form for N in the default domain and the resolved locale exactly as ngettext/3, then applies erli18n_interp:format/2 over the resolved string. count => N is merged into Bindings automatically (a caller-supplied count wins). On a miss the resolution falls back to Msgid (when N == 1) or MsgidPlural, over which interpolation still runs.

1> erli18n:setlocale(<<"en">>).
ok
2> erli18n:ngettextf(<<"%{count} file">>, <<"%{count} files">>, 3, #{}).
<<"3 files">>

Crashes (function_clause) if Msgid/MsgidPlural are not binaries, N is not an integer, or Bindings is not a map. See ngettextf/5 (domain) and ngettextf/6 (locale).

ngettextf(Domain, Msgid, MsgidPlural, N, Bindings)

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

Like ngettext/4 (explicit domain), then interpolates %{name} placeholders using Bindings with count => N auto-bound (caller override wins).

1> erli18n:ngettextf(my_domain, <<"%{count} file">>, <<"%{count} files">>,
1>                   42, #{}).
<<"42 fichiers">>

Same plural-form selection and fallback as ngettext/4. See ngettextf/6 (locale) and dngettextf/5 (alias).

ngettextf(Domain, Msgid, MsgidPlural, N, Locale, Bindings)

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

Main interpolating plural form: like ngettext/5 (explicit domain and locale), then interpolates %{name} placeholders using Bindings with count => N auto-bound (caller override wins).

Resolves via ngettext/5 (ignoring the per-process locale) and applies erli18n_interp:format/2 over the resolved string. On a miss the resolution falls back to Msgid (when N == 1) or MsgidPlural, over which interpolation still runs.

1> erli18n:ngettextf(my_domain, <<"%{count} file">>, <<"%{count} files">>,
1>                   5, <<"en">>, #{}).
<<"5 files">>

Crashes (function_clause) if any argument violates the type. See the aliases dngettextf/6 / dcngettextf/6.

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.

npgettextf(Context, Msgid, MsgidPlural, N, Bindings)

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

Like npgettext/4, then interpolates %{name} placeholders using Bindings with count => N auto-bound (caller override wins).

Resolves the contextual plural form for N in the default domain and the resolved locale exactly as npgettext/4, then applies erli18n_interp:format/2. On a miss the resolution falls back to Msgid (when N == 1) or MsgidPlural, over which interpolation still runs.

1> erli18n:npgettextf(<<"inbox">>, <<"%{count} message">>,
1>                    <<"%{count} messages">>, 3, #{}).
<<"3 messages">>

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

npgettextf(Domain, Context, Msgid, MsgidPlural, N, Bindings)

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

Like npgettext/5 (explicit domain), then interpolates %{name} placeholders using Bindings with count => N auto-bound (caller override wins). Maps the interpolating form of dnpgettext.

Same contextual plural-form selection and fallback as npgettext/5. See npgettextf/7 (locale) and dnpgettextf/6 (alias).

npgettextf(Domain, Context, Msgid, MsgidPlural, N, Locale, Bindings)

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

Main interpolating contextual plural form: like npgettext/6 (explicit domain and locale), then interpolates %{name} placeholders using Bindings with count => N auto-bound (caller override wins).

Resolves via npgettext/6 (ignoring the per-process locale) and applies erli18n_interp:format/2. On a miss the resolution falls back to Msgid (when N == 1) or MsgidPlural, over which interpolation still runs.

1> erli18n:npgettextf(my_domain, <<"inbox">>, <<"%{count} message">>,
1>                    <<"%{count} messages">>, 5, <<"de">>, #{}).
<<"5 Nachrichten">>

Crashes (function_clause) if any argument violates the type. See the aliases dnpgettextf/7 / dcnpgettextf/7.

parse_accept_language(Header)

-spec parse_accept_language(binary()) ->
                               [{erli18n_negotiate:language_range(), erli18n_negotiate:qvalue()}].

Parses an HTTP Accept-Language header into a priority-ordered [{LanguageRange, Q}] list (Q an integer in milli-units, 0..1000).

Total and fail-soft: malformed elements are skipped and a hostile or empty header yields [] — it never raises. Bounded against header/element DoS. The output is sorted by descending quality (stable on ties) and is drop-in compatible with cowboy_req:parse_header(<<"accept-language">>, Req). Feed it straight into negotiate/2.

1> erli18n:parse_accept_language(<<"da, en-gb;q=0.8, en;q=0.7">>).
[{<<"da">>,1000},{<<"en-gb">>,800},{<<"en">>,700}]

Delegates to erli18n_negotiate:parse_accept_language/1.

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.

pgettextf(Context, Msgid, Bindings)

-spec pgettextf(context(), msgid(), bindings()) -> translation().

Like pgettext/2, then interpolates %{name} placeholders in the resolved translation using Bindings.

Resolves the contextual singular in the default domain and the resolved locale exactly as pgettext/2, then applies erli18n_interp:format/2. On a miss the resolution falls back to Msgid (it never leaks a translation from a different context), over which interpolation still runs.

1> erli18n:pgettextf(<<"menu">>, <<"Open %{file}">>, #{file => <<"a.txt">>}).
<<"Abrir a.txt">>

Crashes (function_clause) if Context/Msgid are not binaries (or Context undefined) or Bindings is not a map. See pgettextf/4 (domain) and pgettextf/5 (locale).

pgettextf(Domain, Context, Msgid, Bindings)

-spec pgettextf(domain(), context(), msgid(), bindings()) -> translation().

Like pgettext/3 (explicit domain), then interpolates %{name} placeholders using Bindings. Maps the interpolating form of dpgettext.

Same contextual resolution and fallback as pgettext/3. See pgettextf/5 (locale) and dpgettextf/4 (alias).

pgettextf(Domain, Context, Msgid, Locale, Bindings)

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

Main interpolating contextual singular form: like pgettext/4 (explicit domain and locale), then interpolates %{name} placeholders using Bindings.

Resolves via pgettext/4 (ignoring the per-process locale) and applies erli18n_interp:format/2. On a miss the resolution falls back to Msgid, over which interpolation still runs.

1> erli18n:pgettextf(my_domain, <<"menu">>, <<"Open %{file}">>, <<"pt_BR">>,
1>                   #{file => <<"a.txt">>}).
<<"Abrir a.txt">>

Crashes (function_clause) if any argument violates the type. See the aliases dpgettextf/5 / dcpgettextf/5.

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 the live catalog, and the install is a single atomic persistent_term overwrite (whole-catalog replacement), so concurrent lookups never see an empty or half-applied 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).

set_locale_fallback/1

-spec set_locale_fallback(off | base_language | {explicit, #{locale() => [locale()]}}) -> ok.

Sets the application's locale-fallback mode (env erli18n.locale_fallback) and returns ok.

Modes:

  • off (default) — exact-match only; behavior is identical to 0.2.0 and the lookup hot path reads nothing extra.
  • base_language — on an exact miss, try the canonicalization-aware BCP-47 fallback chain (pt_BRptdefault_locale/0) before falling back to the msgid.
  • {explicit, Map}Map :: #{locale() => [locale()]}; for a listed locale the chain is the (canonicalized) override list, else it falls through to base_language. An override layer, not an allowlist.

The fallback chain runs only on a lookup MISS and only when this mode is not off, so enabling it never slows an exact hit. An invalid stored value is treated as off (fail-soft) at lookup time rather than crashing a translation.

1> erli18n:set_locale_fallback(base_language).
ok

See negotiate/2 (request-time negotiation) and erli18n_negotiate.

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 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).