erli18n_app (erli18n v0.4.0)

Copy Markdown View Source

The erli18n library's application callback: the OTP entry point that brings the process tree up and down.

What it is and what problem it solves

This is the module pointed to by {mod, {erli18n_app, []}} in erli18n.app.src. When someone runs application:ensure_all_started(erli18n) (or the release boot), OTP calls start/2 here, and when the application stops, it calls stop/1. There is nothing erli18n-specific in it beyond delegating: it exists because the application_controller requires a callback module, not because there is any i18n logic to run at boot.

Mental model (for the maintainer)

Think of this module as the thinnest possible shell around the supervision tree, with ONE cleanup responsibility on shutdown. The load-bearing complexity lives one hop ahead:

  • start/2 calls erli18n_sup:start_link/0 and returns the supervisor's {ok, Pid} raw, unwrapped. That Pid becomes the pid of the application that the application_controller goes on to monitor.
  • The real topology lives in erli18n_sup: a one_for_one strategy with a single child — erli18n_server (worker/writer). The catalogs live in persistent_term (see erli18n_pt_store), which is owned by the runtime, not by any process, so a crash of the worker loses no catalog and the tree needs no table owner, no heir, and no child ordering. See erli18n_sup.
  • stop/1 has real work: it erases the catalogs. persistent_term is node-global and is NOT cleared when the application stops, so without an explicit erase a stop/start cycle would leave stale catalogs behind (and a fresh start would see them as already loaded). stop/1 calls erli18n_pt_store:erase_all/0 to remove every {erli18n_catalog, _, _} term. This module reads no application:get_env/2: the env defaults (emit_lookup_telemetry, memory_warning_threshold, memory_warning_rate_limit_seconds) are declared in erli18n.app.src and consumed by erli18n_telemetry — never here.
  • The ordered shutdown of the child (including the worker's terminate/2) is the supervision tree's job, triggered by the application_controller before it calls stop/1. By the time stop/1 runs the worker is already down, but the catalogs it installed still live in persistent_term — which is exactly why stop/1 must erase them here.

When a dev touches this module

Almost never directly. The normal path is:

  • Library consumer: runs application:ensure_all_started(erli18n) and moves on to erli18n:gettext/3, erli18n_server:ensure_loaded/3, etc. Does not import this module.
  • Maintainer: touches this only if the shape of the boot changes — for example, starting to read Args from {mod, {erli18n_app, Args}}, doing one-shot setup at start, or handling Type (normal vs takeover/failover in a distributed scenario). Today both arguments are ignored on purpose. If you are going to add a new top-level process, the place is erli18n_sup:init/1, not here — start/2 should stay a one-liner.

Quickstart

1> {ok, Started} = application:ensure_all_started(erli18n).
{ok,[erli18n]}
2> lists:member(erli18n, Started).
true
3> is_pid(whereis(erli18n_sup)).
true
4> is_pid(whereis(erli18n_server)).
true
5> application:stop(erli18n).
ok

The exact list in {ok, Started} depends on the environment. Since telemetry is declared in optional_applications in erli18n.app.src, if it is present but not yet started, ensure_all_started/1 brings it up too and includes it in the list (e.g. {ok,[telemetry,erli18n]}). The literal {ok,[erli18n]} above holds when telemetry is absent or already up; that is why the robust test is lists:member(erli18n, Started), not a comparison against the whole list.

See start/2 and stop/1 for the semantics of each callback.

Summary

Functions

Callback application:start/2: brings up the root supervision tree.

Callback application:stop/1: erases the loaded catalogs on shutdown.

Functions

start(Type, Args)

Callback application:start/2: brings up the root supervision tree.

Delegates raw to erli18n_sup:start_link/0 and returns the supervisor's {ok, Pid} without any wrapping. That Pid is the pid that the application_controller adopts as the pid of the erli18n application and goes on to monitor; when it dies, the application is considered terminated.

Parameters

  • Type — the start type that OTP passes (normal on a regular boot; {takeover, Node} / {failover, Node} in distributed applications). Ignored: the erli18n boot is identical in any mode, so there is no branching on Type.
  • Args — the term from {mod, {erli18n_app, Args}} in erli18n.app.src, today []. Ignored: no boot configuration is read here (the runtime defaults live in env and are read by erli18n_telemetry, not by this callback).

Return

  • {ok, Pid} — on success, passed through directly from erli18n_sup:start_link/0.
  • Any {error, Reason} coming from below propagates intact: this callback has no try/catch and no fallback. The normal case of a child failing at boot is an {error, {shutdown, Reason}} — the form that supervisor:start_link/3 returns (and which the Return section of erli18n_sup:start_link/0 already documents) when the erli18n_server child fails its own init/1. That is the term the maintainer will see and match on. An error here makes ensure_all_started/1 fail and the application does not come up — the correct OTP behavior, with no masking.

Example

1> {ok, Started} = application:ensure_all_started(erli18n).
{ok,[erli18n]}
2> lists:member(erli18n, Started).
true
3> SupPid = whereis(erli18n_sup), is_pid(SupPid).
true

The literal {ok,[erli18n]} on line 1 holds when telemetry (declared in optional_applications) is absent or already started; if it gets brought up now, it also appears in the list. Hence the lists:member/2 instead of comparing the whole list.

Sibling function: stop/1. Topology started: erli18n_sup:init/1.

stop(State)

Callback application:stop/1: erases the loaded catalogs on shutdown.

The application_controller calls stop/1 after having already torn down the supervision tree (terminating erli18n_server, which runs its own terminate/2). The worker is down by now, but the catalogs it installed still live in persistent_term, which is node-global and is NOT cleared when the application stops. So this callback calls erli18n_pt_store:erase_all/0 to remove every {erli18n_catalog, _, _} term — otherwise a stop/start cycle would leak stale catalogs (and a fresh start would treat them as already loaded). It returns ok.

Parameters

  • State — the value that start/2 would have returned as the second element of {ok, Pid, State}. Since start/2 returns the 2-tuple {ok, Pid} (without State), it is the application_controller that substitutes [] for the State when calling this callback: when start/2 uses the {ok, Pid} form instead of {ok, Pid, State}, the controller normalizes the missing state to []. That is why the argument here is []. Ignored: the cleanup target (persistent_term) is node-global, not carried in State.

Return

  • ok — always. erli18n_pt_store:erase_all/0 returns the count of erased catalogs (used only for observability/tests); this callback discards it and returns the OTP-mandated ok. It has no error path: erase_all/0 is total.

Example

1> application:ensure_all_started(erli18n).
{ok,[erli18n]}
2> application:stop(erli18n).
ok
3> whereis(erli18n_sup).
undefined

Sibling function: start/2. The shutdown of the child belongs to the supervision tree: see erli18n_sup.