Skip to main content

src/rebar3_erli18n_keywords.erl

-module(rebar3_erli18n_keywords).

-moduledoc """
Name-AND-arity keyword spec for the erli18n facade family.

Extraction keys every recognized call site by `{Name, Arity}` and reads the
literal-bearing argument slots from a data-driven table. Keying by arity is
mandatory: the `d`/`dc` families put `Domain` in the FIRST argument, which
shifts `Context`/`Msgid`/`MsgidPlural` one slot right relative to the bare
family, and the Phase 1 `f`-family APPENDS a trailing `Bindings` map that
the spec ignores for msgid extraction (the leading slots are identical to
the non-`f` sibling).

Each row is a `slots()` map giving 1-based argument indices:

- `domain` — index of a literal-atom Domain argument (d/dc families), or
  `from_macro` when the call carries no Domain slot (the bare family, which
  the extractor keys under the module's `?GETTEXT_DOMAIN`).
- `context` — index of a literal `msgctxt` argument (p/np families), absent
  otherwise.
- `msgid` — index of the literal `msgid` (always present).
- `plural` — index of the literal `msgid_plural` (n/np families), absent
  otherwise.

Slots are verified against the `erli18n` clause heads. A call whose
referenced slot is not a compile-time literal (string/charlist or, for
`domain`, a literal atom) is skipped by the extractor — never mis-keyed.
""".

-export([spec/0, lookup/2]).

-export_type([slots/0, kind/0]).

-doc """
The literal-bearing argument slots for one `{Name, Arity}` row.

`domain` is either a 1-based argument index (when the function carries a
Domain slot) or the atom `from_macro`, meaning "no Domain argument; the
extractor supplies the module's `?GETTEXT_DOMAIN`". `context`/`plural` are
present only for the families that carry them.
""".
-type slots() :: #{
    domain := pos_integer() | from_macro,
    msgid := pos_integer(),
    context => pos_integer(),
    plural => pos_integer(),
    kind := kind()
}.

-doc "Whether a recognized call yields a singular or a plural catalog entry.".
-type kind() :: singular | plural.

-doc """
Look up the slots for a `{Name, Arity}` call site.

Returns `{ok, slots()}` for a recognized facade function, or `error` for any
other call (which the extractor leaves untouched).
""".
-spec lookup(atom(), arity()) -> {ok, slots()} | error.
lookup(Name, Arity) ->
    maps:find({Name, Arity}, spec()).

-doc """
The full `{Name, Arity} => slots()` table for the erli18n facade.

Covers the `gettext`/`ngettext`/`pgettext`/`npgettext` families, their
`d`/`dc` variants, and the Phase 1 interpolating `f`-family — roughly fifty
arities in all.

The table is a single literal map, so the compiler builds it once and every
call returns the same shared constant. `lookup/2` is therefore a single
`maps:find` over a constant — no per-call construction or merge.
""".
-spec spec() -> #{{atom(), arity()} => slots()}.
spec() ->
    #{
        %% =========================
        %% Non-`f` families
        %% =========================

        %% gettext/dgettext/dcgettext — singular.
        {gettext, 1} => #{domain => from_macro, msgid => 1, kind => singular},
        {gettext, 2} => #{domain => 1, msgid => 2, kind => singular},
        {gettext, 3} => #{domain => 1, msgid => 2, kind => singular},
        {dgettext, 2} => #{domain => 1, msgid => 2, kind => singular},
        {dgettext, 3} => #{domain => 1, msgid => 2, kind => singular},
        {dcgettext, 3} => #{domain => 1, msgid => 2, kind => singular},

        %% ngettext/dngettext/dcngettext — plural.
        {ngettext, 3} => #{domain => from_macro, msgid => 1, plural => 2, kind => plural},
        {ngettext, 4} => #{domain => 1, msgid => 2, plural => 3, kind => plural},
        {ngettext, 5} => #{domain => 1, msgid => 2, plural => 3, kind => plural},
        {dngettext, 4} => #{domain => 1, msgid => 2, plural => 3, kind => plural},
        {dngettext, 5} => #{domain => 1, msgid => 2, plural => 3, kind => plural},
        {dcngettext, 5} => #{domain => 1, msgid => 2, plural => 3, kind => plural},

        %% pgettext/dpgettext/dcpgettext — contextual singular.
        {pgettext, 2} => #{domain => from_macro, context => 1, msgid => 2, kind => singular},
        {pgettext, 3} => #{domain => 1, context => 2, msgid => 3, kind => singular},
        {pgettext, 4} => #{domain => 1, context => 2, msgid => 3, kind => singular},
        {dpgettext, 3} => #{domain => 1, context => 2, msgid => 3, kind => singular},
        {dpgettext, 4} => #{domain => 1, context => 2, msgid => 3, kind => singular},
        {dcpgettext, 4} => #{domain => 1, context => 2, msgid => 3, kind => singular},

        %% npgettext/dnpgettext/dcnpgettext — contextual plural.
        {npgettext, 4} =>
            #{domain => from_macro, context => 1, msgid => 2, plural => 3, kind => plural},
        {npgettext, 5} => #{domain => 1, context => 2, msgid => 3, plural => 4, kind => plural},
        {npgettext, 6} => #{domain => 1, context => 2, msgid => 3, plural => 4, kind => plural},
        {dnpgettext, 5} => #{domain => 1, context => 2, msgid => 3, plural => 4, kind => plural},
        {dnpgettext, 6} => #{domain => 1, context => 2, msgid => 3, plural => 4, kind => plural},
        {dcnpgettext, 6} => #{domain => 1, context => 2, msgid => 3, plural => 4, kind => plural},

        %% =========================
        %% Phase 1 interpolating `f`-family
        %% =========================
        %%
        %% Every member appends a trailing `Bindings` map. The leading slots
        %% are byte-identical to the non-`f` sibling, so the spec reuses the
        %% same indices and simply ignores the trailing argument.

        %% gettextf/dgettextf/dcgettextf.
        {gettextf, 2} => #{domain => from_macro, msgid => 1, kind => singular},
        {gettextf, 3} => #{domain => 1, msgid => 2, kind => singular},
        {gettextf, 4} => #{domain => 1, msgid => 2, kind => singular},
        {dgettextf, 3} => #{domain => 1, msgid => 2, kind => singular},
        {dgettextf, 4} => #{domain => 1, msgid => 2, kind => singular},
        {dcgettextf, 4} => #{domain => 1, msgid => 2, kind => singular},

        %% ngettextf/dngettextf/dcngettextf.
        {ngettextf, 4} => #{domain => from_macro, msgid => 1, plural => 2, kind => plural},
        {ngettextf, 5} => #{domain => 1, msgid => 2, plural => 3, kind => plural},
        {ngettextf, 6} => #{domain => 1, msgid => 2, plural => 3, kind => plural},
        {dngettextf, 5} => #{domain => 1, msgid => 2, plural => 3, kind => plural},
        {dngettextf, 6} => #{domain => 1, msgid => 2, plural => 3, kind => plural},
        {dcngettextf, 6} => #{domain => 1, msgid => 2, plural => 3, kind => plural},

        %% pgettextf/dpgettextf/dcpgettextf.
        {pgettextf, 3} => #{domain => from_macro, context => 1, msgid => 2, kind => singular},
        {pgettextf, 4} => #{domain => 1, context => 2, msgid => 3, kind => singular},
        {pgettextf, 5} => #{domain => 1, context => 2, msgid => 3, kind => singular},
        {dpgettextf, 4} => #{domain => 1, context => 2, msgid => 3, kind => singular},
        {dpgettextf, 5} => #{domain => 1, context => 2, msgid => 3, kind => singular},
        {dcpgettextf, 5} => #{domain => 1, context => 2, msgid => 3, kind => singular},

        %% npgettextf/dnpgettextf/dcnpgettextf.
        {npgettextf, 5} =>
            #{domain => from_macro, context => 1, msgid => 2, plural => 3, kind => plural},
        {npgettextf, 6} => #{domain => 1, context => 2, msgid => 3, plural => 4, kind => plural},
        {npgettextf, 7} => #{domain => 1, context => 2, msgid => 3, plural => 4, kind => plural},
        {dnpgettextf, 6} => #{domain => 1, context => 2, msgid => 3, plural => 4, kind => plural},
        {dnpgettextf, 7} => #{domain => 1, context => 2, msgid => 3, plural => 4, kind => plural},
        {dcnpgettextf, 7} => #{domain => 1, context => 2, msgid => 3, plural => 4, kind => plural}
    }.