persistent_term storage layer for erli18n translation catalogs.
Each loaded {Domain, Locale} catalog is stored as ONE persistent term:
key {erli18n_catalog, Domain, Locale}, value a single map holding
{singular, Context, Msgid} => Translation{plural, Context, Msgid, Index} => Translation'$header' => header_state()
Reads are copy-free (persistent_term:get/2 does not copy the term onto the
caller's heap) and lock-free from the caller process. The read semantics mirror
the previous ETS reads byte-for-byte: a missing catalog or a missing key both
yield undefined, and the plural read evaluates the compiled rule (or the
C/Germanic fallback) exactly as before.
Read-path rule (load-bearing)
Call persistent_term:get/2 FRESH on every lookup and let the returned map be
transient. NEVER cache the catalog map in a long-lived process's state or
process dictionary: a process holding the old term is forced into a major
(fullsweep) garbage collection when the catalog is reloaded, and would serve a
stale catalog. (erlang.org/doc/apps/erts/persistent_term.html, Best Practices.)
Reload cost
put_map/3 (install/reload) and unload/2 (persistent_term:erase/1) of the
catalog map defer a node-wide literal-area cleanup: every process still
referencing the old map runs a major GC, and all processes are made runnable to
scan their heaps. It is paid once per (re)load — acceptable for erli18n's
load-once-at-boot workload, but it is a real cost that ETS did not have.
Summary
Functions
List every loaded catalog as {Domain, Locale, Map}. Filters the node's
persistent terms by the erli18n_catalog namespace, so it is O(total
persistent terms on the node) — an observability call, never the hot path.
Build the per-catalog value map from parsed .po entries and a header state,
WITHOUT touching persistent_term (pure). Used to construct the map off the
measured/serialized write path before a single put_map/3.
Number of data (non-header) keys in a catalog map.
The data (non-header) keys of a catalog map.
Erase every {erli18n_catalog, _, _} persistent term on the node and return
how many were removed. Called on application stop, since persistent terms are
node-global and are NOT cleared when the application stops.
Return the whole catalog map for {Domain, Locale}, or undefined if not loaded.
Plural-aware lookup. Mirrors erli18n_server:lookup_plural_form/5: read the
header, select the form index via the compiled rule (or the C/Germanic
fallback), then read the per-index plural key. Miss => undefined.
Singular lookup. Mirrors erli18n_server:lookup_singular/4 (miss => undefined).
Total number of stored keys in a catalog map (data keys plus the header).
Build the catalog map and install it in one step (the install/load commit).
Header lookup. Mirrors erli18n_server:lookup_header/2 (miss => undefined).
Merge entries into the existing catalog map, creating it if absent and
preserving the header if present. Used by the low-level insert API
(erli18n_server:insert_singular/5 etc.): an empty merge is a no-op (no
catalog is created, no put). A negative or non-integer plural form index is a
loud function_clause crash, matching the historical insert contract.
Install a pre-built catalog map as the single persistent term for {Domain, Locale}.
Reload a catalog: whole-term replacement. Identical to load/4 because
persistent_term:put/2 overwrites the term at the key, so there are no stale
entries to prune (a concurrent reader sees either the entire old or the entire
new catalog, never a half-applied one).
Approximate storage size of a catalog map in bytes (term word size * word).
Unload a catalog: erase the persistent term. Idempotent (ok whether present or not).
Types
-type catalog_map() :: #{stored_key() => translation() | header_state()}.
-type context() :: undefined | binary().
-type data_key() :: {singular, context(), msgid()} | {plural, context(), msgid(), non_neg_integer()}.
-type domain() :: atom().
-type header_state() :: erli18n_server:header_state().
-type locale() :: binary().
-type msgid() :: binary().
-type stored_key() :: data_key() | '$header'.
-type translation() :: binary().
Functions
-spec all() -> [{domain(), locale(), catalog_map()}].
List every loaded catalog as {Domain, Locale, Map}. Filters the node's
persistent terms by the erli18n_catalog namespace, so it is O(total
persistent terms on the node) — an observability call, never the hot path.
-spec build_map([erli18n_po:entry()], header_state()) -> catalog_map().
Build the per-catalog value map from parsed .po entries and a header state,
WITHOUT touching persistent_term (pure). Used to construct the map off the
measured/serialized write path before a single put_map/3.
-spec data_count(catalog_map()) -> non_neg_integer().
Number of data (non-header) keys in a catalog map.
-spec data_keys(catalog_map()) -> [data_key()].
The data (non-header) keys of a catalog map.
-spec erase_all() -> non_neg_integer().
Erase every {erli18n_catalog, _, _} persistent term on the node and return
how many were removed. Called on application stop, since persistent terms are
node-global and are NOT cleared when the application stops.
-spec get_map(domain(), locale()) -> catalog_map() | undefined.
Return the whole catalog map for {Domain, Locale}, or undefined if not loaded.
-spec get_plural_form(domain(), locale(), context(), msgid(), integer()) -> {ok, translation()} | undefined.
Plural-aware lookup. Mirrors erli18n_server:lookup_plural_form/5: read the
header, select the form index via the compiled rule (or the C/Germanic
fallback), then read the per-index plural key. Miss => undefined.
-spec get_singular(domain(), locale(), context(), msgid()) -> {ok, translation()} | undefined.
Singular lookup. Mirrors erli18n_server:lookup_singular/4 (miss => undefined).
-spec key_count(catalog_map()) -> non_neg_integer().
Total number of stored keys in a catalog map (data keys plus the header).
-spec load(domain(), locale(), [erli18n_po:entry()], header_state()) -> ok.
Build the catalog map and install it in one step (the install/load commit).
-spec lookup_header(domain(), locale()) -> {ok, header_state()} | undefined.
Header lookup. Mirrors erli18n_server:lookup_header/2 (miss => undefined).
-spec merge_entries(domain(), locale(), [erli18n_po:entry()]) -> ok.
Merge entries into the existing catalog map, creating it if absent and
preserving the header if present. Used by the low-level insert API
(erli18n_server:insert_singular/5 etc.): an empty merge is a no-op (no
catalog is created, no put). A negative or non-integer plural form index is a
loud function_clause crash, matching the historical insert contract.
-spec put_map(domain(), locale(), catalog_map()) -> ok.
Install a pre-built catalog map as the single persistent term for {Domain, Locale}.
-spec reload(domain(), locale(), [erli18n_po:entry()], header_state()) -> ok.
Reload a catalog: whole-term replacement. Identical to load/4 because
persistent_term:put/2 overwrites the term at the key, so there are no stale
entries to prune (a concurrent reader sees either the entire old or the entire
new catalog, never a half-applied one).
-spec storage_bytes(catalog_map()) -> non_neg_integer().
Approximate storage size of a catalog map in bytes (term word size * word).
Unload a catalog: erase the persistent term. Idempotent (ok whether present or not).