PhoenixKit. Templates. Overrides
(phoenix_kit_templates v0.2.2)
View Source
Locates a host application's override file for one part of one template.
A host customizes a message by dropping a file into its own repo, where it is version-controlled and reviewable, instead of editing a database row through an admin UI. Nothing here writes; an override is only ever read.
Layout
name is a DIRECTORY, never a filename. Files inside it are named for the
part they supply, optionally carrying a locale:
<root>/<name>/<part>.<locale>.<ext> text.en-GB.txt
<root>/<name>/<part>.<ext> text.txtWorked:
priv/phoenix_kit_templates/ <- <root>
└── new_login_alert/ <- <name>
├── subject.txt
├── subject.de.txt
└── text.txtsubject, text and layout are .txt; html is .html; markdown is
.md. A root is typically Application.app_dir(:my_app, "priv/phoenix_kit_templates"), but this module
takes roots as an argument and reads no configuration of its own — it must
not know which application is using it.
layout is the one part with no locale: it names a layout group, which is
chosen per message rather than per language, so only layout.txt is ever
read and layout.<locale>.txt is ignored.
Lookup runs most- to least-specific and stops at the first file that exists:
text.en-GB.txt → text.en.txt → text.txtso a host that only cares about one language writes text.txt and is done,
while one that translates its overrides gets dialect precision. Roots are
tried in order, so an earlier root shadows a later one.
Each part is looked up separately: in the tree above, a German reader gets
subject.de.txt and text.txt, and an Italian reader gets subject.txt and
text.txt. A part with no file at all resolves to nil, and the caller falls
back to its own default.
locate/4 is read/4 that also returns the path of the file it read —
{path, content} — for callers that need to say where content came from.
read/4 is built on it, so the two always agree.
Reserved names
A name may start with one underscore (_layout). Such names are
reserved for shared parts that the caller assembles around a message — the
first is core's email layout, _layout. This package knows nothing about
layouts and wraps nothing: it finds _layout/html.html (and
html.<locale>.html) exactly as it finds any other name. The underscore is
meant to keep such a directory from colliding with a template name: by
convention ordinary names should not start with one, but this module does not
enforce that.
Runtime, not compile time
Overrides live in the host application, which is compiled separately from
this package — there is no point in this package's compilation at which they
could be read. So they are read at runtime and cached in :persistent_term,
including the absence of a file, since a missing override is the common case
and would otherwise cost a File.stat on every send. Files cannot change
without a deploy; reset_cache/0 exists for tests and dev reloads.
Path safety
name and locale are matched against strict patterns before they are ever
joined onto a root. A name is [a-z0-9][a-z0-9_-]* with an optional single
leading underscore; dots and slashes never match. They are literals at every
current call site, but this module turns a name into a filesystem read, and
that is not a boundary to leave to the caller's good behaviour —
../../../etc/passwd resolves to no override rather than to a file.
Summary
Types
Functions
Like read/4, but also says which file the content came from:
{path, content}, or nil when there is no override.
Validation, candidate order (locale → base language → locale-less, roots in
order) and the cache are exactly read/4's — read/4 is implemented on top
of this function, so the two can never disagree about which file wins. The
path is the one that was read (a root joined with the name and file), as a
preview screen would show it. An empty file is still found: whether empty
means absent is the caller's decision.
@spec parts() :: [part()]
The parts an override file can supply.
The contents of the best-matching override file, or nil when there is none.
locale may be nil, which skips straight to the locale-less candidate, as
does a locale that is not a well-formed tag. roots must be a list — a bare
string is a caller bug, and silently finding no overrides in it would hide
that.
@spec reset_cache([Path.t()] | :all) :: :ok
Drops cached override lookups.
Only tests and dev reloads need this: a deploy starts a fresh VM.
Pass a list of roots to clear only the entries that consulted them. That
scoping is what lets an async test clear its own tmp_dir without erasing a
concurrently-running one's cache — and it is the honest shape anyway, since a
dev reload usually means one application's files changed, not all of them.