PhoenixKit. Templates
(phoenix_kit_templates v0.1.1)
View Source
Renders a named message template for a recipient.
One renderer for every channel — email, push, Telegram, SMS, the in-app inbox — so localized, customizable message content is built the same way everywhere instead of once per channel.
The two layers
A template has a default, shipped by the package that sends the message, and an optional override, shipped by the host application.
The caller supplies the default already localized. That split is what keeps this package a leaf: translation belongs to the sending package's Gettext backend, and reaching for one here would mean depending on it.
PhoenixKit.Templates.render(
"new_login_alert",
%{subject: gettext("New login to your account"), text: gettext("Hi %{email}, …")},
%{"ip_address" => ip},
locale: "uk",
paths: [Application.app_dir(:my_app, "priv/phoenix_kit_templates")]
)On disk
A template's name is a DIRECTORY, never a filename. The files inside it are named for the part they supply, optionally carrying a locale:
priv/phoenix_kit_templates/ <- a root, passed as :paths
└── new_login_alert/ <- the template NAME (a directory)
├── subject.txt <- <part>.<ext>
├── subject.de.txt <- <part>.<locale>.<ext>
├── text.txt
├── text.de.txt
└── html.html| part | file | used by |
|---|---|---|
subject | subject[.locale].txt | email subject, push title |
text | text[.locale].txt | every channel |
html | html[.locale].html | email only, optional |
A directory rather than flat files because one template is up to three parts times however many locales a host translates — flat, they would interleave with every other template's files and you would be reading filename prefixes to tell them apart. Grouped, a template is one folder to copy, diff or delete.
This is also why name is the only identifier and is validated as
[a-z0-9][a-z0-9_-]*: it is a path segment, so it must be filesystem-safe.
It is slug-shaped by necessity, which is what makes a separate slug field a
second spelling of a constraint the path already enforces.
Resolution
Per part, independently, stopping at the first hit. For part subject and a
recipient locale of "de-AT":
1. subject.de-AT.txt host override · exact dialect
2. subject.de.txt host override · base language
3. subject.txt host override · locale-less
4. defaults[:subject] the caller's Gettext defaultRoots are tried in order, so an earlier root shadows a later one.
Independence matters. Given the tree above and a German recipient, a host that
wrote only text.txt still gets the package's translated German subject; the
override applies to the body alone. And subject.de.txt wins for a German
reader while an Italian one falls through to subject.txt.
Why the default is not a file
A shipped default expressed as one file per locale forks the same sentence seven ways and drifts. Expressed as a Gettext call it rides the extraction and translation pipeline the sending package already runs, and ships translated. Files are how a host overrides — a host writes one or two languages, not seven — and how chrome-bearing HTML gets authored. See the design doc.
Parts
subject, text and html, named for what they are rather than for email:
push uses subject-as-title plus text, Telegram and SMS use text alone, the
in-app inbox uses text. html is genuinely optional — core's own auth emails
ship text-only, so a template with no html is the normal case.
Summary
Types
Package-shipped content, already localized by the caller.
Rendered content, ready for a channel to deliver.
Types
@type defaults() :: %{ optional(PhoenixKit.Templates.Overrides.part()) => String.t() | nil }
Package-shipped content, already localized by the caller.
Rendered content, ready for a channel to deliver.
Functions
@spec missing_variables( String.t(), defaults(), PhoenixKit.Templates.Substitution.variables(), keyword() ) :: %{optional(PhoenixKit.Templates.Overrides.part()) => [String.t()]}
Placeholder names that render/4 would leave unbound, keyed by part.
Parts that would render cleanly are omitted, so an empty map means the render
is fully bound. Intended for a test or a preview screen — render/4 itself
never fails over a bad placeholder, because a message with one flawed line is
still better than a message that never arrives.
@spec render( String.t(), defaults(), PhoenixKit.Templates.Substitution.variables(), keyword() ) :: rendered()
Renders name into %{subject:, text:, html:}.
Options
:locale— the recipient's locale, dialect included ("en-GB").nilselects only locale-less overrides.:paths— host override roots, tried in order. Defaults to[], which means the caller's defaults are used verbatim.
Unbound {{placeholders}} survive into the output rather than blanking or
raising; see PhoenixKit.Templates.Substitution.