Curated library of 38 atomic skills, 7 personas, and 1 orchestrator for Elixir and Phoenix development. Organized by category: fundamentals, phoenix, database, testing, auth, infrastructure, quality, security, integrations, tooling, frameworks, personas, and orchestration. Covers core Elixir patterns, Phoenix LiveView, Ecto, OTP, Oban, testing, security, deployment, real-time, and modern tooling (Req, Swoosh, Cachex, Broadway, Ash).
91
91%
Does it follow best practices?
Impact
91%
1.37xAverage score across 56 eval scenarios
Advisory
Suggest reviewing before use
gettext/1, dgettext/2, or ngettext/3 in templates, LiveView, and controllersmix gettext.extract --merge to generate/update .pot and .po files.po files — confirm new msgid entries appear with empty msgstr valuesmsgstr values for each target localeGettext.put_locale/2dgettext/2 — dgettext("errors", "Not found") keeps error strings in a separate .po domain from default contentngettext/3 for anything countable — never build plurals by hand; plural rules vary by localegettext("Hello %{name}", name: name), never gettext("Hello #{name}"), which breaks extractionmix gettext.extract --merge after adding calls — keep .pot/.po files in sync before committingGettext.put_locale/2 from a plug (and on LiveView mount); never rely on the defaultput_locale supported locales; reject arbitrary values from params# mix.exs — add dependency
{:gettext, "~> 0.26"}
# lib/my_app_web/gettext.ex — define backend
defmodule MyAppWeb.Gettext do
use Gettext.Backend, otp_app: :my_app
endimport MyAppWeb.Gettext, then call gettext/1, dgettext/2, or ngettext/3.
defmodule MyAppWeb.HomeLive do
use MyAppWeb, :live_view
import MyAppWeb.Gettext
@impl true
def mount(_params, session, socket) do
locale = session["locale"] || "en"
Gettext.put_locale(MyAppWeb.Gettext, locale)
socket =
socket
|> assign(:greeting, gettext("Hello!"))
|> assign(:error, dgettext("errors", "Not found"))
# ngettext: pass count as integer arg and as a binding
|> assign(:summary,
ngettext("There is %{count} item", "There are %{count} items",
@item_count, count: @item_count))
{:ok, socket}
end
end<h1><%= gettext("Welcome to %{app_name}", app_name: "MyApp") %></h1>Locale files live under priv/gettext/<locale>/LC_MESSAGES/<domain>.po; the shared template is priv/gettext/<domain>.pot.
# priv/gettext/es/LC_MESSAGES/default.po
msgid "You have %{count} new message"
msgid_plural "You have %{count} new messages"
msgstr[0] "Tienes %{count} mensaje nuevo"
msgstr[1] "Tienes %{count} mensajes nuevos"# Extract new strings to .pot files
mix gettext.extract
# Merge .pot files into .po files
mix gettext.merge priv/gettext
# Extract and merge in one step
mix gettext.extract --mergeValidate: After extraction, open the relevant .po files and confirm new msgid entries appear with empty msgstr values. Fill in translations before deploying.
The recommended pattern is a plug that resolves locale from params → session → default, then calls Gettext.put_locale/2:
# lib/my_app_web/plugs/set_locale.ex
defmodule MyAppWeb.Plugs.SetLocale do
import Plug.Conn
@supported_locales ~w(en es fr de)
def init(opts), do: opts
def call(conn, _opts) do
locale =
get_locale_from_params(conn) ||
get_locale_from_session(conn) ||
"en"
Gettext.put_locale(MyAppWeb.Gettext, locale)
conn
end
defp get_locale_from_params(conn), do: validate(conn.params["locale"])
defp get_locale_from_session(conn), do: validate(get_session(conn, "locale"))
defp validate(locale) when locale in @supported_locales, do: locale
defp validate(_), do: nil
endRegister it in the router pipeline:
pipeline :browser do
# ...
plug MyAppWeb.Plugs.SetLocale
enddefmodule MyAppWeb.PageTest do
use MyAppWeb.ConnCase
test "renders translated welcome message", %{conn: conn} do
Gettext.put_locale(MyAppWeb.Gettext, "es")
conn = get(conn, ~p"/")
assert html_response(conn, 200) =~ "Bienvenido"
end
end| ❌ Don't | ✅ Do |
|---|---|
gettext("Hello #{name}") — interpolate before translating | gettext("Hello %{name}", name: name) — pass a binding so the msgid stays static |
Build plurals with if count == 1 | Use ngettext("%{count} item", "%{count} items", count, count: count) |
| Translate log lines and internal errors | Only wrap user-facing strings in gettext/* |
| Rely on a global default locale | Set it per request with Gettext.put_locale/2 in a plug and on LiveView mount |
Add gettext calls but skip extraction | Run mix gettext.extract --merge so .pot/.po files stay in sync |
put_locale any value from params | Validate against a supported-locale allowlist first |
| Dump every string into the default domain | Split with dgettext domains (e.g. "errors") |
| Predecessor | This Skill | Successor |
|---|---|---|
| phoenix-liveview-essentials | gettext-i18n | testing-essentials |
| apply-phoenix-controller-conventions | gettext-i18n | testing-essentials |
Companion skills:
security-essentials — validate and constrain locale input from user-controlled paramsphoenix-liveview-essentials — call Gettext.put_locale/2 on mount for translated LiveViews.tessl-plugin
evals
scenario-1
scenario-2
scenario-3
scenario-4
scenario-5
scenario-6
scenario-7
scenario-8
scenario-9
scenario-10
scenario-11
scenario-12
scenario-13
scenario-14
scenario-15
scenario-16
scenario-17
scenario-18
scenario-19
scenario-20
scenario-21
scenario-22
scenario-23
scenario-24
scenario-25
scenario-26
scenario-27
scenario-28
scenario-29
scenario-30
scenario-31
scenario-32
scenario-33
scenario-34
scenario-35
scenario-36
scenario-37
scenario-38
scenario-39
scenario-40
scenario-41
scenario-42
scenario-43
scenario-44
scenario-45
scenario-46
scenario-47
scenario-48
scenario-49
scenario-50
scenario-51
scenario-52
scenario-53
scenario-54
scenario-55
scenario-56
skills
frameworks
ash-framework
infrastructure
orchestration
elixir-skill-router
personas
phoenix
quality
security
security-essentials
tooling
mix-tasks-generators