fix: translation of login page
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Simon 2026-03-13 14:11:54 +01:00
parent 086ecdcb1b
commit 99a8d64344
Signed by: simon
GPG key ID: 40E7A58C4AA1EDB2
18 changed files with 487 additions and 200 deletions

View file

@ -1,28 +1,42 @@
defmodule MvWeb.SignInLive do
@moduledoc """
Custom sign-in page with language selector and conditional Single Sign-On button.
Custom sign-in page with public header and hero layout (same as Join/Join Confirm).
- Renders a language selector (same pattern as LinkOidcAccountLive).
- Wraps the default AshAuthentication SignIn component in a container with
`data-oidc-configured` so that CSS can hide the SSO button when OIDC is not configured.
Uses Layouts.public_page (no sidebar, no app-layout hooks). Wraps the AshAuthentication
SignIn component in a hero section. Container has data-oidc-configured so CSS can hide
the SSO button when OIDC is not configured.
Keeps `use Phoenix.LiveView` (not MvWeb :live_view) so AshAuthentication's sign_in_route
live_session on_mount chain is not mixed with LiveHelpers hooks.
## Locale overrides
`MvWeb.AuthOverridesDE` is prepended to the overrides list when the locale is "de",
providing static German strings for components that do not use `_gettext` internally
(e.g. HorizontalRule renders its `:text` override directly).
"""
use Phoenix.LiveView
use Gettext, backend: MvWeb.Gettext
alias AshAuthentication.Phoenix.Components
alias Mv.Config
alias MvWeb.{AuthOverridesDE, Layouts}
@impl true
def mount(_params, session, socket) do
overrides =
session
|> Map.get("overrides", [AshAuthentication.Phoenix.Overrides.Default])
# Locale: same fallback as LiveUserAuth so config :default_locale (e.g. "en" in test) is respected
locale =
session["locale"] || Application.get_env(:mv, :default_locale, "de")
locale = session["locale"] || Application.get_env(:mv, :default_locale, "de")
# Set both backend-specific and global locale so Gettext.get_locale/0 and
# Gettext.get_locale/1 both return the correct value (important for the
# language-selector `selected` attribute in Layouts.public_page).
Gettext.put_locale(MvWeb.Gettext, locale)
Gettext.put_locale(locale)
# Prepend DE-specific overrides when locale is German so that components
# without _gettext support (e.g. HorizontalRule) still render in German.
base_overrides = Map.get(session, "overrides", [AshAuthentication.Phoenix.Overrides.Default])
locale_overrides = if locale == "de", do: [AuthOverridesDE], else: []
overrides = locale_overrides ++ base_overrides
socket =
socket
@ -36,10 +50,9 @@ defmodule MvWeb.SignInLive do
|> assign(:context, session["context"] || %{})
|> assign(:auth_routes_prefix, session["auth_routes_prefix"])
|> assign(:gettext_fn, session["gettext_fn"])
|> assign(:live_action, :sign_in)
|> assign_new(:live_action, fn -> :sign_in end)
|> assign(:oidc_configured, Config.oidc_configured?())
|> assign(:oidc_only, Config.oidc_only?())
|> assign(:root_class, "grid h-screen place-items-center bg-base-100")
|> assign(:sign_in_id, "sign-in")
|> assign(:locale, locale)
@ -54,50 +67,43 @@ defmodule MvWeb.SignInLive do
@impl true
def render(assigns) do
~H"""
<main
id="sign-in-page"
role="main"
class={@root_class}
data-oidc-configured={to_string(@oidc_configured)}
data-oidc-only={to_string(@oidc_only)}
data-locale={@locale}
>
<h1 class="sr-only">{dgettext("auth", "Sign in")}</h1>
<%!-- Language selector --%>
<nav
aria-label={dgettext("auth", "Language selection")}
class="absolute top-4 right-4 flex justify-end z-10"
>
<form method="post" action="/set_locale" class="text-sm">
<input type="hidden" name="_csrf_token" value={Plug.CSRFProtection.get_csrf_token()} />
<select
name="locale"
onchange="this.form.submit()"
class="select select-sm select-bordered bg-base-100"
aria-label={dgettext("auth", "Select language")}
>
<option value="de" selected={@locale == "de"}>Deutsch</option>
<option value="en" selected={@locale == "en"}>English</option>
</select>
</form>
</nav>
<.live_component
module={Components.SignIn}
otp_app={@otp_app}
live_action={@live_action}
path={@path}
auth_routes_prefix={@auth_routes_prefix}
resources={@resources}
reset_path={@reset_path}
register_path={@register_path}
id={@sign_in_id}
overrides={@overrides}
current_tenant={@current_tenant}
context={@context}
gettext_fn={@gettext_fn}
/>
</main>
<Layouts.public_page flash={@flash}>
<div class="max-w-4xl mx-auto">
<div
class="hero min-h-[60vh] bg-base-200 rounded-lg"
id="sign-in-page"
role="main"
data-oidc-configured={to_string(@oidc_configured)}
data-oidc-only={to_string(@oidc_only)}
data-locale={@locale}
>
<div class="hero-content flex-col items-start text-left">
<div class="w-full max-w-md">
<h1 class="text-xl font-semibold leading-8">
{if @live_action == :register,
do: dgettext("auth", "Register"),
else: dgettext("auth", "Sign in")}
</h1>
<.live_component
module={Components.SignIn}
otp_app={@otp_app}
live_action={@live_action}
path={@path}
auth_routes_prefix={@auth_routes_prefix}
resources={@resources}
reset_path={@reset_path}
register_path={@register_path}
id={@sign_in_id}
overrides={@overrides}
current_tenant={@current_tenant}
context={@context}
gettext_fn={@gettext_fn}
/>
</div>
</div>
</div>
</div>
</Layouts.public_page>
"""
end
end

View file

@ -33,91 +33,97 @@ defmodule MvWeb.JoinLive do
@impl true
def render(assigns) do
~H"""
<Layouts.app flash={@flash} current_user={@current_user}>
<div class="max-w-xl mx-auto mt-8 space-y-6">
<.header>
{gettext("Become a member")}
</.header>
<Layouts.public_page flash={@flash}>
<div class="max-w-4xl mx-auto">
<div class="hero min-h-[60vh] bg-base-200 rounded-lg">
<div class="hero-content flex-col items-start text-left">
<div class="max-w-xl w-full space-y-6">
<.header>
{gettext("Become a member")}
</.header>
<p class="text-base-content/80">
{gettext("Please enter your details for the membership application here.")}
</p>
<p class="text-base-content/80">
{gettext("Please enter your details for the membership application here.")}
</p>
<%= if @submitted do %>
<div data-testid="join-success-message" class="alert alert-success">
<p class="font-medium">
{gettext(
"We have saved your details. To complete your request, please click the link we sent to your email."
)}
</p>
</div>
<% else %>
<.form
for={@form}
id="join-form"
phx-submit="submit"
class="space-y-4"
>
<%= if @rate_limit_error do %>
<div class="alert alert-error">
{@rate_limit_error}
</div>
<% end %>
<%= if @submitted do %>
<div data-testid="join-success-message" class="alert alert-success">
<p class="font-medium">
{gettext(
"We have saved your details. To complete your request, please click the link we sent to your email."
)}
</p>
</div>
<% else %>
<.form
for={@form}
id="join-form"
phx-submit="submit"
class="space-y-4"
>
<%= if @rate_limit_error do %>
<div class="alert alert-error">
{@rate_limit_error}
</div>
<% end %>
<%= for field <- @join_fields do %>
<div>
<label for={"join-field-#{field.id}"} class="label">
<span class="label-text">{field.label}{if field.required, do: " *"}</span>
</label>
<input
type={input_type(field.id)}
name={field.id}
id={"join-field-#{field.id}"}
value={@form.params[field.id]}
required={field.required}
class="input input-bordered w-full"
/>
</div>
<% end %>
<%= for field <- @join_fields do %>
<div>
<label for={"join-field-#{field.id}"} class="label">
<span class="label-text">{field.label}{if field.required, do: " *"}</span>
</label>
<input
type={input_type(field.id)}
name={field.id}
id={"join-field-#{field.id}"}
value={@form.params[field.id]}
required={field.required}
class="input input-bordered w-full"
/>
</div>
<% end %>
<%!--
<%!--
Honeypot (best practice): legit field name "website", type="text", no inline CSS,
hidden via class in app.css (off-screen + 1px). tabindex=-1, autocomplete=off,
aria-hidden so screen readers skip. If filled silent failure (same success UI).
--%>
<div class="join-form-helper" aria-hidden="true">
<label for="join-website" class="sr-only">{gettext("Website")}</label>
<input
type="text"
name={@honeypot_field}
id="join-website"
tabindex="-1"
autocomplete="off"
class="join-form-helper-input"
/>
<div class="join-form-helper" aria-hidden="true">
<label for="join-website" class="sr-only">{gettext("Website")}</label>
<input
type="text"
name={@honeypot_field}
id="join-website"
tabindex="-1"
autocomplete="off"
class="join-form-helper-input"
/>
</div>
<p class="text-sm text-base-content/70">
{gettext(
"By submitting your application you will receive an email with a confirmation link. Once you have confirmed your email address, your application will be reviewed."
)}
</p>
<p class="text-xs text-base-content/60">
{gettext(
"Your details are only used to process your membership application and to contact you. To prevent abuse we also process technical data (e.g. IP address) only as necessary."
)}
</p>
<div>
<button type="submit" class="btn btn-primary">
{gettext("Submit request")}
</button>
</div>
</.form>
<% end %>
</div>
<p class="text-sm text-base-content/70">
{gettext(
"By submitting your application you will receive an email with a confirmation link. Once you have confirmed your email address, your application will be reviewed."
)}
</p>
<p class="text-xs text-base-content/60">
{gettext(
"Your details are only used to process your membership application and to contact you. To prevent abuse we also process technical data (e.g. IP address) only as necessary."
)}
</p>
<div>
<button type="submit" class="btn btn-primary">
{gettext("Submit request")}
</button>
</div>
</.form>
<% end %>
</div>
</div>
</div>
</Layouts.app>
</Layouts.public_page>
"""
end