From 19b635c46f44b617c6d6f84dd016b60462cc0810 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Mon, 2 Feb 2026 09:48:30 -0600 Subject: [PATCH] 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. --- config/dev.exs | 6 +- lib/towerops/accounts/user_notifier.ex | 85 ++++++++++++++++++-------- lib/towerops_web/gettext_helpers.ex | 42 ++++--------- priv/gettext/emails.pot | 40 ++++++++++++ priv/gettext/en/LC_MESSAGES/default.po | 12 ++-- priv/gettext/en/LC_MESSAGES/emails.po | 40 ++++++++++++ priv/gettext/en/LC_MESSAGES/errors.po | 30 ++++----- priv/static/changelog.txt | 1 + 8 files changed, 179 insertions(+), 77 deletions(-) diff --git a/config/dev.exs b/config/dev.exs index 02074897..521cb535 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -1,7 +1,11 @@ import Config # 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 config :logger, :default_formatter, format: "[$level] $message\n" diff --git a/lib/towerops/accounts/user_notifier.ex b/lib/towerops/accounts/user_notifier.ex index 19dc2e06..af5098f1 100644 --- a/lib/towerops/accounts/user_notifier.ex +++ b/lib/towerops/accounts/user_notifier.ex @@ -5,7 +5,10 @@ defmodule Towerops.Accounts.UserNotifier do Sends emails for password reset, email confirmation, and other user events. """ + use Gettext, backend: ToweropsWeb.Gettext + import Swoosh.Email + import ToweropsWeb.GettextHelpers alias Towerops.Accounts.User alias Towerops.Mailer @@ -38,16 +41,24 @@ defmodule Towerops.Accounts.UserNotifier do Deliver instructions to update a user email. """ 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 @doc """ @@ -61,46 +72,70 @@ defmodule Towerops.Accounts.UserNotifier do end 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 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 @doc """ Deliver instructions to reset a user password. """ 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 diff --git a/lib/towerops_web/gettext_helpers.ex b/lib/towerops_web/gettext_helpers.ex index 8bf34c90..e44bc167 100644 --- a/lib/towerops_web/gettext_helpers.ex +++ b/lib/towerops_web/gettext_helpers.ex @@ -35,6 +35,8 @@ defmodule ToweropsWeb.GettextHelpers do gettext("can't be blank") """ + use Gettext, backend: ToweropsWeb.Gettext + @doc """ Translates a string in the default domain. @@ -42,9 +44,7 @@ defmodule ToweropsWeb.GettextHelpers do """ defmacro t(msgid) do quote do - require ToweropsWeb.Gettext - - ToweropsWeb.Gettext.dgettext("default", unquote(msgid)) + dgettext("default", unquote(msgid)) end end @@ -53,9 +53,7 @@ defmodule ToweropsWeb.GettextHelpers do """ defmacro t(msgid, bindings) do quote do - require ToweropsWeb.Gettext - - ToweropsWeb.Gettext.dgettext("default", unquote(msgid), unquote(bindings)) + dgettext("default", unquote(msgid), unquote(bindings)) end end @@ -66,9 +64,7 @@ defmodule ToweropsWeb.GettextHelpers do """ defmacro t_auth(msgid) do quote do - require ToweropsWeb.Gettext - - ToweropsWeb.Gettext.dgettext("auth", unquote(msgid)) + dgettext("auth", unquote(msgid)) end end @@ -77,9 +73,7 @@ defmodule ToweropsWeb.GettextHelpers do """ defmacro t_auth(msgid, bindings) do quote do - require ToweropsWeb.Gettext - - ToweropsWeb.Gettext.dgettext("auth", unquote(msgid), unquote(bindings)) + dgettext("auth", unquote(msgid), unquote(bindings)) end end @@ -90,9 +84,7 @@ defmodule ToweropsWeb.GettextHelpers do """ defmacro t_equipment(msgid) do quote do - require ToweropsWeb.Gettext - - ToweropsWeb.Gettext.dgettext("equipment", unquote(msgid)) + dgettext("equipment", unquote(msgid)) end end @@ -101,9 +93,7 @@ defmodule ToweropsWeb.GettextHelpers do """ defmacro t_equipment(msgid, bindings) do quote do - require ToweropsWeb.Gettext - - ToweropsWeb.Gettext.dgettext("equipment", unquote(msgid), unquote(bindings)) + dgettext("equipment", unquote(msgid), unquote(bindings)) end end @@ -114,9 +104,7 @@ defmodule ToweropsWeb.GettextHelpers do """ defmacro t_admin(msgid) do quote do - require ToweropsWeb.Gettext - - ToweropsWeb.Gettext.dgettext("admin", unquote(msgid)) + dgettext("admin", unquote(msgid)) end end @@ -125,9 +113,7 @@ defmodule ToweropsWeb.GettextHelpers do """ defmacro t_admin(msgid, bindings) do quote do - require ToweropsWeb.Gettext - - ToweropsWeb.Gettext.dgettext("admin", unquote(msgid), unquote(bindings)) + dgettext("admin", unquote(msgid), unquote(bindings)) end end @@ -138,9 +124,7 @@ defmodule ToweropsWeb.GettextHelpers do """ defmacro t_email(msgid) do quote do - require ToweropsWeb.Gettext - - ToweropsWeb.Gettext.dgettext("emails", unquote(msgid)) + dgettext("emails", unquote(msgid)) end end @@ -149,9 +133,7 @@ defmodule ToweropsWeb.GettextHelpers do """ defmacro t_email(msgid, bindings) do quote do - require ToweropsWeb.Gettext - - ToweropsWeb.Gettext.dgettext("emails", unquote(msgid), unquote(bindings)) + dgettext("emails", unquote(msgid), unquote(bindings)) end end end diff --git a/priv/gettext/emails.pot b/priv/gettext/emails.pot index a9563ba7..0b707541 100644 --- a/priv/gettext/emails.pot +++ b/priv/gettext/emails.pot @@ -13,3 +13,43 @@ msgid "" msgstr "" "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 "" diff --git a/priv/gettext/en/LC_MESSAGES/default.po b/priv/gettext/en/LC_MESSAGES/default.po index 0afc10c8..03588dd8 100644 --- a/priv/gettext/en/LC_MESSAGES/default.po +++ b/priv/gettext/en/LC_MESSAGES/default.po @@ -56,13 +56,13 @@ msgstr[1] "" #: lib/towerops_web/components/core_components.ex:487 #, elixir-autogen, elixir-format msgid "Actions" -msgstr "" +msgstr "Actions" #: lib/towerops_web/components/layouts.ex:552 #: lib/towerops_web/components/layouts.ex:564 #, elixir-autogen, elixir-format msgid "Attempting to reconnect" -msgstr "" +msgstr "Attempting to reconnect" #: lib/towerops_web/helpers/time_helpers.ex:103 #: lib/towerops_web/helpers/time_helpers.ex:156 @@ -70,19 +70,19 @@ msgstr "" #: lib/towerops_web/helpers/time_helpers.ex:235 #, elixir-autogen, elixir-format msgid "Never" -msgstr "" +msgstr "Never" #: lib/towerops_web/components/layouts.ex:559 #, elixir-autogen, elixir-format msgid "Something went wrong!" -msgstr "" +msgstr "Something went wrong!" #: lib/towerops_web/components/layouts.ex:547 #, elixir-autogen, elixir-format msgid "We can't find the internet" -msgstr "" +msgstr "We can't find the internet" #: lib/towerops_web/components/core_components.ex:94 #, elixir-autogen, elixir-format msgid "close" -msgstr "" +msgstr "close" diff --git a/priv/gettext/en/LC_MESSAGES/emails.po b/priv/gettext/en/LC_MESSAGES/emails.po index 0dde1207..74923aed 100644 --- a/priv/gettext/en/LC_MESSAGES/emails.po +++ b/priv/gettext/en/LC_MESSAGES/emails.po @@ -10,3 +10,43 @@ msgid "" msgstr "" "Language: en\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" diff --git a/priv/gettext/en/LC_MESSAGES/errors.po b/priv/gettext/en/LC_MESSAGES/errors.po index 844c4f5c..555854f6 100644 --- a/priv/gettext/en/LC_MESSAGES/errors.po +++ b/priv/gettext/en/LC_MESSAGES/errors.po @@ -12,42 +12,42 @@ msgstr "" ## From Ecto.Changeset.cast/4 msgid "can't be blank" -msgstr "" +msgstr "can't be blank" ## From Ecto.Changeset.unique_constraint/3 msgid "has already been taken" -msgstr "" +msgstr "has already been taken" ## From Ecto.Changeset.put_change/3 msgid "is invalid" -msgstr "" +msgstr "is invalid" ## From Ecto.Changeset.validate_acceptance/3 msgid "must be accepted" -msgstr "" +msgstr "must be accepted" ## From Ecto.Changeset.validate_format/3 msgid "has invalid format" -msgstr "" +msgstr "has invalid format" ## From Ecto.Changeset.validate_subset/3 msgid "has an invalid entry" -msgstr "" +msgstr "has an invalid entry" ## From Ecto.Changeset.validate_exclusion/3 msgid "is reserved" -msgstr "" +msgstr "is reserved" ## From Ecto.Changeset.validate_confirmation/3 msgid "does not match confirmation" -msgstr "" +msgstr "does not match confirmation" ## From Ecto.Changeset.no_assoc_constraint/3 msgid "is still associated with this entry" -msgstr "" +msgstr "is 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 msgid "should have %{count} item(s)" @@ -97,16 +97,16 @@ msgstr[1] "" ## From Ecto.Changeset.validate_number/3 msgid "must be less than %{number}" -msgstr "" +msgstr "must be less than %{number}" msgid "must be greater than %{number}" -msgstr "" +msgstr "must be greater than %{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}" -msgstr "" +msgstr "must be greater than or equal to %{number}" msgid "must be equal to %{number}" -msgstr "" +msgstr "must be equal to %{number}" diff --git a/priv/static/changelog.txt b/priv/static/changelog.txt index 81dd69e0..8c6dcf69 100644 --- a/priv/static/changelog.txt +++ b/priv/static/changelog.txt @@ -5,6 +5,7 @@ Devices Tested & Working 2026-02-02 * Experimental mikrotik api and backup +* Starting i18n 2026-02-01 * Firmware version fetching and notification for Mikrotik devices