Shared plumbing for the extract/merge/check/report providers.
Centralizes the parts that would otherwise be duplicated across the four
providers: the common getopt option set, project source discovery + the
abstract-form walk, deduplication of extracted call sites into catalog
entries (merging #: references), .pot/.po directory resolution, and a
uniform format_error/1. Keeping this in one module makes the providers
thin wrappers and lets a single suite cover the walk/dedup logic to 100%.
Catalog layout
The .pot templates live in priv/gettext/<Domain>.pot; the translated
catalogs in priv/gettext/<Locale>/LC_MESSAGES/<Domain>.po. This mirrors
the runtime loader's default path (erli18n:default_po_path/3) so a
project's extracted templates and loaded catalogs share one tree.
Summary
Types
A deduplicated catalog entry: one logical {Domain, Context, Msgid} with
all the #: references that pointed at it (in first-seen source order).
kind/plural come from the first occurrence.
A #: source reference: a relative source path and a 1-based line.
Functions
The getopt option spec shared by the providers.
Collapse a domain's raw extracted entries into deduplicated catalog
entries, keyed by {Context, Msgid}, merging each duplicate's reference.
Build a rebar3_erli18n_po_meta:catalog() (.pot template) from a domain's
deduplicated entries: an empty header, every msgstr empty, references as
#: lines.
Walk every project app's src/ and extract all recognized call sites,
grouped and deduplicated by domain.
Render a shared provider error to a human-readable string.
Render a code:which/1 result as a printable string: the .beam path
verbatim when loaded, or the special atom (non_existing, preloaded,
cover_compiled) spelled out so the diagnostic line is unambiguous about
WHY the cross-package module is not a concrete path.
When the ERLI18N_DIAG_LOADPATH OS environment variable is set, log the
loaded erli18n_po path through the rebar3 logger at provider-run time, so
the cross-package load path can be captured from a real
rebar3 erli18n {extract,merge,check,report} run. A no-op (returns ok,
emits nothing) when the variable is unset, so it adds no output to ordinary
runs.
The .po path for {Domain, Locale}:
<pot_dir>/<Locale>/LC_MESSAGES/<Domain>.po.
The .pot template directory: <RootApp>/priv/gettext (or the
--pot-dir override). The first project app is treated as the root.
The loaded location of the erli18n_po runtime module, as code:which/1
sees it at the moment of the call — non_existing if the module is not on
the code path, preloaded/cover_compiled for those special cases, or the
absolute .beam path otherwise.
Types
-type dedup_entry() :: #{domain := atom(), kind := rebar3_erli18n_keywords:kind(), context := undefined | binary(), msgid := binary(), plural := undefined | binary(), references := [reference_ref()]}.
A deduplicated catalog entry: one logical {Domain, Context, Msgid} with
all the #: references that pointed at it (in first-seen source order).
kind/plural come from the first occurrence.
-type reference_ref() :: {file:filename(), pos_integer()}.
A #: source reference: a relative source path and a 1-based line.
Functions
The getopt option spec shared by the providers.
--domain restricts the operation to a single domain; --locale selects a
target locale (merge/report); --names-only switches check to the laxer
msgid-set comparison; --pot-dir overrides the default priv/gettext root.
-spec dedup_entries([rebar3_erli18n_extract_forms:extracted()]) -> [dedup_entry()].
Collapse a domain's raw extracted entries into deduplicated catalog
entries, keyed by {Context, Msgid}, merging each duplicate's reference.
References are kept in first-seen order with duplicates removed; the entry
list is returned sorted by {Context, Msgid} for deterministic output.
-spec entries_to_pot([dedup_entry()]) -> rebar3_erli18n_po_meta:catalog().
Build a rebar3_erli18n_po_meta:catalog() (.pot template) from a domain's
deduplicated entries: an empty header, every msgstr empty, references as
#: lines.
-spec extract_project(rebar3_erli18n_host:state()) -> {ok, #{atom() => [dedup_entry()]}} | {error, term()}.
Walk every project app's src/ and extract all recognized call sites,
grouped and deduplicated by domain.
Returns {ok, #{Domain => [dedup_entry()]}}, or the first
{error, Reason} an epp parse raised. Each domain's entry list is sorted
by {Context, Msgid} for deterministic, diff-stable output.
Render a shared provider error to a human-readable string.
-spec format_lib_path(non_existing | cover_compiled | preloaded | file:filename()) -> string().
Render a code:which/1 result as a printable string: the .beam path
verbatim when loaded, or the special atom (non_existing, preloaded,
cover_compiled) spelled out so the diagnostic line is unambiguous about
WHY the cross-package module is not a concrete path.
-spec maybe_log_runtime_lib_path() -> ok.
When the ERLI18N_DIAG_LOADPATH OS environment variable is set, log the
loaded erli18n_po path through the rebar3 logger at provider-run time, so
the cross-package load path can be captured from a real
rebar3 erli18n {extract,merge,check,report} run. A no-op (returns ok,
emits nothing) when the variable is unset, so it adds no output to ordinary
runs.
-spec po_path(rebar3_erli18n_host:state(), atom(), string()) -> file:filename().
The .po path for {Domain, Locale}:
<pot_dir>/<Locale>/LC_MESSAGES/<Domain>.po.
-spec pot_dir(rebar3_erli18n_host:state()) -> file:filename().
The .pot template directory: <RootApp>/priv/gettext (or the
--pot-dir override). The first project app is treated as the root.
-spec runtime_lib_path() -> non_existing | cover_compiled | preloaded | file:filename().
The loaded location of the erli18n_po runtime module, as code:which/1
sees it at the moment of the call — non_existing if the module is not on
the code path, preloaded/cover_compiled for those special cases, or the
absolute .beam path otherwise.
This is the structural proof of the plugin -> lib load path. Every provider
reaches erli18n_po:parse/1, erli18n_po:dump/1, and
erli18n_po:escape_string/1 across the published {deps, [erli18n]}
boundary. In a downstream consumer that surfaces the unpublished lib via
_checkouts/erli18n, this resolves under the consumer's
_build/<profile>/checkouts/erli18n/ebin/erli18n_po.beam, demonstrating
that the checkout (not a Hex fetch) backs the cross-package calls. See
apps/rebar3_erli18n/README.md ("Proven cross-package load path").