feat: migrate email templates to gettext (Phase 2 - Emails)

- Update UserNotifier to use t_email() helper for all email content
- Extract 4 email subjects and 4 email bodies to emails domain:
  * Update email instructions
  * Log in instructions
  * Confirmation instructions
  * Reset password instructions
- Replace string interpolation with Gettext bindings (%{email}, %{url})
- Auto-populate English translations (msgstr = msgid)
- All emails now ready for future internationalization

First module migration complete per design plan.
This commit is contained in:
Graham McIntire 2026-02-02 09:48:30 -06:00
parent cfb60cd186
commit 19b635c46f
No known key found for this signature in database
8 changed files with 179 additions and 77 deletions

View file

@ -1,7 +1,11 @@
import Config import Config
# Disable Honeybadger in development # Disable Honeybadger in development
config :honeybadger, environment_name: :dev, exclude_envs: [:dev] config :honeybadger,
environment_name: :dev,
exclude_envs: [:dev],
# Don't use custom filter in dev since Honeybadger is excluded
filter: Honeybadger.Filter.Default
# Do not include metadata nor timestamps in development logs # Do not include metadata nor timestamps in development logs
config :logger, :default_formatter, format: "[$level] $message\n" config :logger, :default_formatter, format: "[$level] $message\n"

View file

@ -5,7 +5,10 @@ defmodule Towerops.Accounts.UserNotifier do
Sends emails for password reset, email confirmation, and other user events. Sends emails for password reset, email confirmation, and other user events.
""" """
use Gettext, backend: ToweropsWeb.Gettext
import Swoosh.Email import Swoosh.Email
import ToweropsWeb.GettextHelpers
alias Towerops.Accounts.User alias Towerops.Accounts.User
alias Towerops.Mailer alias Towerops.Mailer
@ -38,16 +41,24 @@ defmodule Towerops.Accounts.UserNotifier do
Deliver instructions to update a user email. Deliver instructions to update a user email.
""" """
def deliver_update_email_instructions(user, url) do def deliver_update_email_instructions(user, url) do
deliver(user.email, "Update email instructions", """ subject = t_email("Update email instructions")
Hi #{user.email}, body =
t_email(
"""
Hi %{email},
You can change your email by visiting the URL below: You can change your email by visiting the URL below:
#{url} %{url}
If you didn't request this change, please ignore this. If you didn't request this change, please ignore this.
""") """,
email: user.email,
url: url
)
deliver(user.email, subject, body)
end end
@doc """ @doc """
@ -61,46 +72,70 @@ defmodule Towerops.Accounts.UserNotifier do
end end
defp deliver_magic_link_instructions(user, url) do defp deliver_magic_link_instructions(user, url) do
deliver(user.email, "Log in instructions", """ subject = t_email("Log in instructions")
Hi #{user.email}, body =
t_email(
"""
Hi %{email},
You can log into your account by visiting the URL below: You can log into your account by visiting the URL below:
#{url} %{url}
If you didn't request this email, please ignore this. If you didn't request this email, please ignore this.
""") """,
email: user.email,
url: url
)
deliver(user.email, subject, body)
end end
defp deliver_confirmation_instructions(user, url) do defp deliver_confirmation_instructions(user, url) do
deliver(user.email, "Confirmation instructions", """ subject = t_email("Confirmation instructions")
Hi #{user.email}, body =
t_email(
"""
Hi %{email},
You can confirm your account by visiting the URL below: You can confirm your account by visiting the URL below:
#{url} %{url}
If you didn't create an account with us, please ignore this. If you didn't create an account with us, please ignore this.
""") """,
email: user.email,
url: url
)
deliver(user.email, subject, body)
end end
@doc """ @doc """
Deliver instructions to reset a user password. Deliver instructions to reset a user password.
""" """
def deliver_reset_password_instructions(user, url) do def deliver_reset_password_instructions(user, url) do
deliver(user.email, "Reset password instructions", """ subject = t_email("Reset password instructions")
Hi #{user.email}, body =
t_email(
"""
Hi %{email},
You can reset your password by visiting the URL below: You can reset your password by visiting the URL below:
#{url} %{url}
If you didn't request this change, please ignore this. If you didn't request this change, please ignore this.
This link will expire in 1 hour. This link will expire in 1 hour.
""") """,
email: user.email,
url: url
)
deliver(user.email, subject, body)
end end
end end

View file

@ -35,6 +35,8 @@ defmodule ToweropsWeb.GettextHelpers do
gettext("can't be blank") gettext("can't be blank")
""" """
use Gettext, backend: ToweropsWeb.Gettext
@doc """ @doc """
Translates a string in the default domain. Translates a string in the default domain.
@ -42,9 +44,7 @@ defmodule ToweropsWeb.GettextHelpers do
""" """
defmacro t(msgid) do defmacro t(msgid) do
quote do quote do
require ToweropsWeb.Gettext dgettext("default", unquote(msgid))
ToweropsWeb.Gettext.dgettext("default", unquote(msgid))
end end
end end
@ -53,9 +53,7 @@ defmodule ToweropsWeb.GettextHelpers do
""" """
defmacro t(msgid, bindings) do defmacro t(msgid, bindings) do
quote do quote do
require ToweropsWeb.Gettext dgettext("default", unquote(msgid), unquote(bindings))
ToweropsWeb.Gettext.dgettext("default", unquote(msgid), unquote(bindings))
end end
end end
@ -66,9 +64,7 @@ defmodule ToweropsWeb.GettextHelpers do
""" """
defmacro t_auth(msgid) do defmacro t_auth(msgid) do
quote do quote do
require ToweropsWeb.Gettext dgettext("auth", unquote(msgid))
ToweropsWeb.Gettext.dgettext("auth", unquote(msgid))
end end
end end
@ -77,9 +73,7 @@ defmodule ToweropsWeb.GettextHelpers do
""" """
defmacro t_auth(msgid, bindings) do defmacro t_auth(msgid, bindings) do
quote do quote do
require ToweropsWeb.Gettext dgettext("auth", unquote(msgid), unquote(bindings))
ToweropsWeb.Gettext.dgettext("auth", unquote(msgid), unquote(bindings))
end end
end end
@ -90,9 +84,7 @@ defmodule ToweropsWeb.GettextHelpers do
""" """
defmacro t_equipment(msgid) do defmacro t_equipment(msgid) do
quote do quote do
require ToweropsWeb.Gettext dgettext("equipment", unquote(msgid))
ToweropsWeb.Gettext.dgettext("equipment", unquote(msgid))
end end
end end
@ -101,9 +93,7 @@ defmodule ToweropsWeb.GettextHelpers do
""" """
defmacro t_equipment(msgid, bindings) do defmacro t_equipment(msgid, bindings) do
quote do quote do
require ToweropsWeb.Gettext dgettext("equipment", unquote(msgid), unquote(bindings))
ToweropsWeb.Gettext.dgettext("equipment", unquote(msgid), unquote(bindings))
end end
end end
@ -114,9 +104,7 @@ defmodule ToweropsWeb.GettextHelpers do
""" """
defmacro t_admin(msgid) do defmacro t_admin(msgid) do
quote do quote do
require ToweropsWeb.Gettext dgettext("admin", unquote(msgid))
ToweropsWeb.Gettext.dgettext("admin", unquote(msgid))
end end
end end
@ -125,9 +113,7 @@ defmodule ToweropsWeb.GettextHelpers do
""" """
defmacro t_admin(msgid, bindings) do defmacro t_admin(msgid, bindings) do
quote do quote do
require ToweropsWeb.Gettext dgettext("admin", unquote(msgid), unquote(bindings))
ToweropsWeb.Gettext.dgettext("admin", unquote(msgid), unquote(bindings))
end end
end end
@ -138,9 +124,7 @@ defmodule ToweropsWeb.GettextHelpers do
""" """
defmacro t_email(msgid) do defmacro t_email(msgid) do
quote do quote do
require ToweropsWeb.Gettext dgettext("emails", unquote(msgid))
ToweropsWeb.Gettext.dgettext("emails", unquote(msgid))
end end
end end
@ -149,9 +133,7 @@ defmodule ToweropsWeb.GettextHelpers do
""" """
defmacro t_email(msgid, bindings) do defmacro t_email(msgid, bindings) do
quote do quote do
require ToweropsWeb.Gettext dgettext("emails", unquote(msgid), unquote(bindings))
ToweropsWeb.Gettext.dgettext("emails", unquote(msgid), unquote(bindings))
end end
end end
end end

View file

@ -13,3 +13,43 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Language: en\n" "Language: en\n"
#: lib/towerops/accounts/user_notifier.ex:96
#, elixir-autogen, elixir-format
msgid "Confirmation instructions"
msgstr ""
#: lib/towerops/accounts/user_notifier.ex:47
#, elixir-autogen, elixir-format
msgid "Hi %{email},\n\nYou can change your email by visiting the URL below:\n\n%{url}\n\nIf you didn't request this change, please ignore this.\n"
msgstr ""
#: lib/towerops/accounts/user_notifier.ex:99
#, elixir-autogen, elixir-format
msgid "Hi %{email},\n\nYou can confirm your account by visiting the URL below:\n\n%{url}\n\nIf you didn't create an account with us, please ignore this.\n"
msgstr ""
#: lib/towerops/accounts/user_notifier.ex:78
#, elixir-autogen, elixir-format
msgid "Hi %{email},\n\nYou can log into your account by visiting the URL below:\n\n%{url}\n\nIf you didn't request this email, please ignore this.\n"
msgstr ""
#: lib/towerops/accounts/user_notifier.ex:123
#, elixir-autogen, elixir-format
msgid "Hi %{email},\n\nYou can reset your password by visiting the URL below:\n\n%{url}\n\nIf you didn't request this change, please ignore this.\n\nThis link will expire in 1 hour.\n"
msgstr ""
#: lib/towerops/accounts/user_notifier.ex:75
#, elixir-autogen, elixir-format
msgid "Log in instructions"
msgstr ""
#: lib/towerops/accounts/user_notifier.ex:120
#, elixir-autogen, elixir-format
msgid "Reset password instructions"
msgstr ""
#: lib/towerops/accounts/user_notifier.ex:44
#, elixir-autogen, elixir-format
msgid "Update email instructions"
msgstr ""

View file

@ -56,13 +56,13 @@ msgstr[1] ""
#: lib/towerops_web/components/core_components.ex:487 #: lib/towerops_web/components/core_components.ex:487
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Actions" msgid "Actions"
msgstr "" msgstr "Actions"
#: lib/towerops_web/components/layouts.ex:552 #: lib/towerops_web/components/layouts.ex:552
#: lib/towerops_web/components/layouts.ex:564 #: lib/towerops_web/components/layouts.ex:564
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Attempting to reconnect" msgid "Attempting to reconnect"
msgstr "" msgstr "Attempting to reconnect"
#: lib/towerops_web/helpers/time_helpers.ex:103 #: lib/towerops_web/helpers/time_helpers.ex:103
#: lib/towerops_web/helpers/time_helpers.ex:156 #: lib/towerops_web/helpers/time_helpers.ex:156
@ -70,19 +70,19 @@ msgstr ""
#: lib/towerops_web/helpers/time_helpers.ex:235 #: lib/towerops_web/helpers/time_helpers.ex:235
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Never" msgid "Never"
msgstr "" msgstr "Never"
#: lib/towerops_web/components/layouts.ex:559 #: lib/towerops_web/components/layouts.ex:559
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Something went wrong!" msgid "Something went wrong!"
msgstr "" msgstr "Something went wrong!"
#: lib/towerops_web/components/layouts.ex:547 #: lib/towerops_web/components/layouts.ex:547
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "We can't find the internet" msgid "We can't find the internet"
msgstr "" msgstr "We can't find the internet"
#: lib/towerops_web/components/core_components.ex:94 #: lib/towerops_web/components/core_components.ex:94
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "close" msgid "close"
msgstr "" msgstr "close"

View file

@ -10,3 +10,43 @@ msgid ""
msgstr "" msgstr ""
"Language: en\n" "Language: en\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: lib/towerops/accounts/user_notifier.ex:96
#, elixir-autogen, elixir-format
msgid "Confirmation instructions"
msgstr "Confirmation instructions"
#: lib/towerops/accounts/user_notifier.ex:47
#, elixir-autogen, elixir-format
msgid "Hi %{email},\n\nYou can change your email by visiting the URL below:\n\n%{url}\n\nIf you didn't request this change, please ignore this.\n"
msgstr "Hi %{email},\n\nYou can change your email by visiting the URL below:\n\n%{url}\n\nIf you didn't request this change, please ignore this.\n"
#: lib/towerops/accounts/user_notifier.ex:99
#, elixir-autogen, elixir-format
msgid "Hi %{email},\n\nYou can confirm your account by visiting the URL below:\n\n%{url}\n\nIf you didn't create an account with us, please ignore this.\n"
msgstr "Hi %{email},\n\nYou can confirm your account by visiting the URL below:\n\n%{url}\n\nIf you didn't create an account with us, please ignore this.\n"
#: lib/towerops/accounts/user_notifier.ex:78
#, elixir-autogen, elixir-format
msgid "Hi %{email},\n\nYou can log into your account by visiting the URL below:\n\n%{url}\n\nIf you didn't request this email, please ignore this.\n"
msgstr "Hi %{email},\n\nYou can log into your account by visiting the URL below:\n\n%{url}\n\nIf you didn't request this email, please ignore this.\n"
#: lib/towerops/accounts/user_notifier.ex:123
#, elixir-autogen, elixir-format
msgid "Hi %{email},\n\nYou can reset your password by visiting the URL below:\n\n%{url}\n\nIf you didn't request this change, please ignore this.\n\nThis link will expire in 1 hour.\n"
msgstr "Hi %{email},\n\nYou can reset your password by visiting the URL below:\n\n%{url}\n\nIf you didn't request this change, please ignore this.\n\nThis link will expire in 1 hour.\n"
#: lib/towerops/accounts/user_notifier.ex:75
#, elixir-autogen, elixir-format
msgid "Log in instructions"
msgstr "Log in instructions"
#: lib/towerops/accounts/user_notifier.ex:120
#, elixir-autogen, elixir-format
msgid "Reset password instructions"
msgstr "Reset password instructions"
#: lib/towerops/accounts/user_notifier.ex:44
#, elixir-autogen, elixir-format
msgid "Update email instructions"
msgstr "Update email instructions"

View file

@ -12,42 +12,42 @@ msgstr ""
## From Ecto.Changeset.cast/4 ## From Ecto.Changeset.cast/4
msgid "can't be blank" msgid "can't be blank"
msgstr "" msgstr "can't be blank"
## From Ecto.Changeset.unique_constraint/3 ## From Ecto.Changeset.unique_constraint/3
msgid "has already been taken" msgid "has already been taken"
msgstr "" msgstr "has already been taken"
## From Ecto.Changeset.put_change/3 ## From Ecto.Changeset.put_change/3
msgid "is invalid" msgid "is invalid"
msgstr "" msgstr "is invalid"
## From Ecto.Changeset.validate_acceptance/3 ## From Ecto.Changeset.validate_acceptance/3
msgid "must be accepted" msgid "must be accepted"
msgstr "" msgstr "must be accepted"
## From Ecto.Changeset.validate_format/3 ## From Ecto.Changeset.validate_format/3
msgid "has invalid format" msgid "has invalid format"
msgstr "" msgstr "has invalid format"
## From Ecto.Changeset.validate_subset/3 ## From Ecto.Changeset.validate_subset/3
msgid "has an invalid entry" msgid "has an invalid entry"
msgstr "" msgstr "has an invalid entry"
## From Ecto.Changeset.validate_exclusion/3 ## From Ecto.Changeset.validate_exclusion/3
msgid "is reserved" msgid "is reserved"
msgstr "" msgstr "is reserved"
## From Ecto.Changeset.validate_confirmation/3 ## From Ecto.Changeset.validate_confirmation/3
msgid "does not match confirmation" msgid "does not match confirmation"
msgstr "" msgstr "does not match confirmation"
## From Ecto.Changeset.no_assoc_constraint/3 ## From Ecto.Changeset.no_assoc_constraint/3
msgid "is still associated with this entry" msgid "is still associated with this entry"
msgstr "" msgstr "is still associated with this entry"
msgid "are still associated with this entry" msgid "are still associated with this entry"
msgstr "" msgstr "are still associated with this entry"
## From Ecto.Changeset.validate_length/3 ## From Ecto.Changeset.validate_length/3
msgid "should have %{count} item(s)" msgid "should have %{count} item(s)"
@ -97,16 +97,16 @@ msgstr[1] ""
## From Ecto.Changeset.validate_number/3 ## From Ecto.Changeset.validate_number/3
msgid "must be less than %{number}" msgid "must be less than %{number}"
msgstr "" msgstr "must be less than %{number}"
msgid "must be greater than %{number}" msgid "must be greater than %{number}"
msgstr "" msgstr "must be greater than %{number}"
msgid "must be less than or equal to %{number}" msgid "must be less than or equal to %{number}"
msgstr "" msgstr "must be less than or equal to %{number}"
msgid "must be greater than or equal to %{number}" msgid "must be greater than or equal to %{number}"
msgstr "" msgstr "must be greater than or equal to %{number}"
msgid "must be equal to %{number}" msgid "must be equal to %{number}"
msgstr "" msgstr "must be equal to %{number}"

View file

@ -5,6 +5,7 @@ Devices Tested & Working
2026-02-02 2026-02-02
* Experimental mikrotik api and backup * Experimental mikrotik api and backup
* Starting i18n
2026-02-01 2026-02-01
* Firmware version fetching and notification for Mikrotik devices * Firmware version fetching and notification for Mikrotik devices