Summary
vutuv currently ships in two languages: English (en) and German (de). This request adds a complete Spanish (es) translation. "Complete" means everything a member can see in Spanish: every UI string, every flash/toast message, all transactional emails, error pages, and the language picker — not just the page chrome.
Background: how internationalization works today
vutuv uses Gettext (VutuvWeb.Gettext, backed by priv/gettext) for almost all on-screen text, but emails are a separate mechanism. There are three distinct translation surfaces, and a complete language has to cover all three:
-
On-screen UI + flash messages → Gettext .po files.
Every gettext(...)/dgettext(...) call resolves against priv/gettext/<locale>/LC_MESSAGES/*.po. Today only en and de exist:
priv/gettext/default.pot — the extracted catalog, currently ~1012 messages.
priv/gettext/errors.pot — Ecto/changeset error catalog.
priv/gettext/{en,de}/LC_MESSAGES/{default.po,errors.po}.
Flash messages (put_flash/3) are ordinary gettext(...) strings, so they live in default.po and are covered by translating it. (96 put_flash call sites exist; the bulk are already gettext-wrapped — see the audit task below.)
-
Emails → per-locale .eex template files (NOT Gettext).
Vutuv.Notifications.Emailer (lib/vutuv/notifications/emailer.ex) picks the body template by the recipient's locale, building the filename as "<base>_<locale>.text" and rendering it via VutuvWeb.EmailText. The subject line is translated with Gettext.with_locale/3 in the recipient's locale. So each email type has one template file per language under lib/vutuv_web/templates/email/. Today every type has a _de and an _en variant; Spanish needs a matching _es file for each:
_signature_es.text.eex (shared signature partial)
email_creation_email_es.text.eex
login_email_es.text.eex (the login PIN mail — critical path)
registration_email_es.text.eex
registration_attempt_es.text.eex
verification_confirmation_es.text.eex
unread_messages_es.text.eex
user_deletion_email_es.text.eex
moderation_warning_es.text.eex
moderation_frozen_es.text.eex
moderation_review_es.text.eex
moderation_revised_es.text.eex
moderation_suspension_es.text.eex
moderation_deactivation_es.text.eex
moderation_admin_digest_es.text.eex / moderation_admin_urgent_es.text.eex (admin-facing moderation queue mails — in scope, must be translated)
_footer.text.eex is locale-neutral and shared; check whether it contains any language-specific text.
ad_booking_de.text.eex currently exists only in German — confirm whether it is user-facing before deciding to translate.
Ordering matters: Emailer.get_locale/1 falls back to "en" for any locale not in the configured :locales list. The moment es is added to config, the Emailer will look for *_es.text.eex and crash if the file is missing. So the config change and the email templates must ship in the same deploy.
-
Config → the master locale list.
VutuvWeb.Plug.Locale (request locale from the user's locale field or Accept-Language) and VutuvWeb.Live.LiveLocale (LiveView re-apply) both gate on config[:locales]:
config/config.exs → locales: ~w(en de)
config/prod.exs → locales: ~w(en de)
Scope — what "complete" covers
Explicitly out of scope (stay German only)
lib/vutuv_web/templates/page/datenschutzerklaerung.html.heex (privacy policy / Datenschutzerklärung)
lib/vutuv_web/templates/page/impressum.html.heex (imprint / Impressum)
These are hardcoded German legal pages and must remain German regardless of the member's chosen language.
Implementation checklist (where the work lives)
- Add the locale to config —
config/config.exs and config/prod.exs: locales: ~w(en de es). (Ship together with the email templates, per the crash note above.)
- Scaffold and translate the Gettext catalogs:
mix gettext.extract to refresh default.pot, then mix gettext.merge priv/gettext --locale es to create priv/gettext/es/LC_MESSAGES/{default.po,errors.po}.
- Translate every
msgstr. (See the "German UI vocabulary" conventions in the project memory for the kind of care German got; Spanish needs equivalent native-quality choices, not machine output.)
- Add the Spanish email templates — one
*_es.text.eex per type listed above, plus _signature_es.text.eex, under lib/vutuv_web/templates/email/.
- Add Spanish to the language picker —
lib/vutuv_web/templates/user/form_content.html.heex currently hardcodes [{gettext("English"), "en"}, {gettext("German"), "de"}]; add {gettext("Spanish"), "es"}.
- Add the endonym for the agent-doc language hint —
@language_names in lib/vutuv_web/agent_docs/agent_docs.ex (%{"en" => "English", "de" => "Deutsch"}): add "es" => "Español". Once es PO files exist, the public pages' ?lang=es agent format and the cross-language <link>/hint work automatically (AgentDocs.doc_locale/2 keys off Gettext.known_locales).
- No JS string work needed — the only dynamic user-facing JS text (the live username-availability verdict in
assets/js/app.js) comes from the server response (data.message), so it is already Gettext-translated server-side. Confirm this still holds.
Verification (do not rely on mix test alone)
Per CLAUDE.md: the Swoosh test adapter never renders a real email, so green tests do not prove the Spanish emails work. After implementing:
- Set a test user's
locale to es, walk the main flows in a browser, and trigger flashes (login, post, follow, errors) to confirm Spanish copy.
- Trigger and read the actual Spanish emails — especially the login PIN mail — via the dev
/sent_emails inbox, to confirm the _es templates render and the subjects are Spanish.
- Hit a public page as
…/<slug>.md?lang=es to confirm the agent-format Spanish path resolves.
Acceptance criteria
Summary
vutuv currently ships in two languages: English (
en) and German (de). This request adds a complete Spanish (es) translation. "Complete" means everything a member can see in Spanish: every UI string, every flash/toast message, all transactional emails, error pages, and the language picker — not just the page chrome.Background: how internationalization works today
vutuv uses Gettext (
VutuvWeb.Gettext, backed bypriv/gettext) for almost all on-screen text, but emails are a separate mechanism. There are three distinct translation surfaces, and a complete language has to cover all three:On-screen UI + flash messages → Gettext
.pofiles.Every
gettext(...)/dgettext(...)call resolves againstpriv/gettext/<locale>/LC_MESSAGES/*.po. Today onlyenanddeexist:priv/gettext/default.pot— the extracted catalog, currently ~1012 messages.priv/gettext/errors.pot— Ecto/changeset error catalog.priv/gettext/{en,de}/LC_MESSAGES/{default.po,errors.po}.Flash messages (
put_flash/3) are ordinarygettext(...)strings, so they live indefault.poand are covered by translating it. (96put_flashcall sites exist; the bulk are alreadygettext-wrapped — see the audit task below.)Emails → per-locale
.eextemplate files (NOT Gettext).Vutuv.Notifications.Emailer(lib/vutuv/notifications/emailer.ex) picks the body template by the recipient's locale, building the filename as"<base>_<locale>.text"and rendering it viaVutuvWeb.EmailText. The subject line is translated withGettext.with_locale/3in the recipient's locale. So each email type has one template file per language underlib/vutuv_web/templates/email/. Today every type has a_deand an_envariant; Spanish needs a matching_esfile for each:_signature_es.text.eex(shared signature partial)email_creation_email_es.text.eexlogin_email_es.text.eex(the login PIN mail — critical path)registration_email_es.text.eexregistration_attempt_es.text.eexverification_confirmation_es.text.eexunread_messages_es.text.eexuser_deletion_email_es.text.eexmoderation_warning_es.text.eexmoderation_frozen_es.text.eexmoderation_review_es.text.eexmoderation_revised_es.text.eexmoderation_suspension_es.text.eexmoderation_deactivation_es.text.eexmoderation_admin_digest_es.text.eex/moderation_admin_urgent_es.text.eex(admin-facing moderation queue mails — in scope, must be translated)_footer.text.eexis locale-neutral and shared; check whether it contains any language-specific text.ad_booking_de.text.eexcurrently exists only in German — confirm whether it is user-facing before deciding to translate.Ordering matters:
Emailer.get_locale/1falls back to"en"for any locale not in the configured:localeslist. The momentesis added to config, the Emailer will look for*_es.text.eexand crash if the file is missing. So the config change and the email templates must ship in the same deploy.Config → the master locale list.
VutuvWeb.Plug.Locale(request locale from the user'slocalefield orAccept-Language) andVutuvWeb.Live.LiveLocale(LiveView re-apply) both gate onconfig[:locales]:config/config.exs→locales: ~w(en de)config/prod.exs→locales: ~w(en de)Scope — what "complete" covers
default.po, ~1012 messages) translated to Spanish.errors.po) translated.default.po; audit that every user-facingput_flashisgettext-wrapped — fix any that are not as part of this work).moderation_admin_digest,moderation_admin_urgent).VutuvWeb.ErrorHTML/errors.po) render in Spanish.Explicitly out of scope (stay German only)
lib/vutuv_web/templates/page/datenschutzerklaerung.html.heex(privacy policy / Datenschutzerklärung)lib/vutuv_web/templates/page/impressum.html.heex(imprint / Impressum)These are hardcoded German legal pages and must remain German regardless of the member's chosen language.
Implementation checklist (where the work lives)
config/config.exsandconfig/prod.exs:locales: ~w(en de es). (Ship together with the email templates, per the crash note above.)mix gettext.extractto refreshdefault.pot, thenmix gettext.merge priv/gettext --locale esto createpriv/gettext/es/LC_MESSAGES/{default.po,errors.po}.msgstr. (See the "German UI vocabulary" conventions in the project memory for the kind of care German got; Spanish needs equivalent native-quality choices, not machine output.)*_es.text.eexper type listed above, plus_signature_es.text.eex, underlib/vutuv_web/templates/email/.lib/vutuv_web/templates/user/form_content.html.heexcurrently hardcodes[{gettext("English"), "en"}, {gettext("German"), "de"}]; add{gettext("Spanish"), "es"}.@language_namesinlib/vutuv_web/agent_docs/agent_docs.ex(%{"en" => "English", "de" => "Deutsch"}): add"es" => "Español". OnceesPO files exist, the public pages'?lang=esagent format and the cross-language<link>/hint work automatically (AgentDocs.doc_locale/2keys offGettext.known_locales).assets/js/app.js) comes from the server response (data.message), so it is already Gettext-translated server-side. Confirm this still holds.Verification (do not rely on
mix testalone)Per
CLAUDE.md: the Swoosh test adapter never renders a real email, so green tests do not prove the Spanish emails work. After implementing:localetoes, walk the main flows in a browser, and trigger flashes (login, post, follow, errors) to confirm Spanish copy./sent_emailsinbox, to confirm the_estemplates render and the subjects are Spanish.…/<slug>.md?lang=esto confirm the agent-format Spanish path resolves.Acceptance criteria