diff --git a/config/runtime.exs b/config/runtime.exs index 3fbb035..b1532fb 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -12,10 +12,8 @@ if System.get_env("PHX_SERVER") && System.get_env("RELEASE_NAME") do config :lokal, LokalWeb.Endpoint, server: true end -config :lokal, LokalWeb.ViewHelpers, shibao_mode: System.get_env("SHIBAO_MODE") == "true" - -# Set locale -Gettext.put_locale(System.get_env("LOCALE") || "en_US") +# Set default locale +config :gettext, :default_locale, System.get_env("LOCALE") || "en_US" maybe_ipv6 = if System.get_env("ECTO_IPV6") == "true", do: [:inet6], else: [] diff --git a/contributing.md b/contributing.md index f8cb007..7ffed2f 100644 --- a/contributing.md +++ b/contributing.md @@ -96,7 +96,7 @@ In `dev` mode, Lokal will listen for these environment variables at runtime. Defaults to `false`. - `POOL_SIZE`: Controls the pool size to use with PostgreSQL. Defaults to `10`. - `REGISTRATION`: Controls if user sign-up should be invite only or set to public. Set to `public` to enable public registration. Defaults to `invite`. -- `LOCALE`: Sets a custom locale. Defaults to `en_US`. +- `LOCALE`: Sets a custom default locale. Defaults to `en_US`. ## `MIX_ENV=test` diff --git a/lib/lokal/accounts.ex b/lib/lokal/accounts.ex index 60c2313..fdcbe12 100644 --- a/lib/lokal/accounts.ex +++ b/lib/lokal/accounts.ex @@ -269,6 +269,35 @@ defmodule Lokal.Accounts do end end + @doc """ + Returns an `%Changeset{}` for changing the user locale. + + ## Examples + + iex> change_user_locale(user) + %Changeset{data: %User{}} + + """ + @spec change_user_locale(User.t()) :: Changeset.t(User.t()) + def change_user_locale(%{locale: locale} = user), do: User.locale_changeset(user, locale) + + @doc """ + Updates the user locale. + + ## Examples + + iex> update_user_locale(user, "valid locale") + {:ok, %User{}} + + iex> update_user_password(user, "invalid locale") + {:error, %Changeset{}} + + """ + @spec update_user_locale(User.t(), locale :: String.t()) :: + {:ok, User.t()} | {:error, Changeset.t(User.t())} + def update_user_locale(user, locale), + do: user |> User.locale_changeset(locale) |> Repo.update() + @doc """ Deletes a user. must be performed by an admin or the same user! diff --git a/lib/lokal/accounts/user.ex b/lib/lokal/accounts/user.ex index 67646b0..074a456 100644 --- a/lib/lokal/accounts/user.ex +++ b/lib/lokal/accounts/user.ex @@ -18,6 +18,7 @@ defmodule Lokal.Accounts.User do field :hashed_password, :string field :confirmed_at, :naive_datetime field :role, Ecto.Enum, values: [:admin, :user], default: :user + field :locale, :string has_many :invites, Invite, on_delete: :delete_all @@ -32,6 +33,7 @@ defmodule Lokal.Accounts.User do confirmed_at: NaiveDateTime.t(), role: atom(), invites: [Invite.t()], + locale: String.t() | nil, inserted_at: NaiveDateTime.t(), updated_at: NaiveDateTime.t() } @@ -60,7 +62,7 @@ defmodule Lokal.Accounts.User do Changeset.t(t() | new_user()) def registration_changeset(user, attrs, opts \\ []) do user - |> cast(attrs, [:email, :password, :role]) + |> cast(attrs, [:email, :password, :role, :locale]) |> validate_email() |> validate_password(opts) end @@ -185,4 +187,14 @@ defmodule Lokal.Accounts.User do do: changeset, else: changeset |> add_error(:current_password, dgettext("errors", "is not valid")) end + + @doc """ + A changeset for changing the user's locale + """ + @spec locale_changeset(t() | Changeset.t(t()), locale :: String.t() | nil) :: Changeset.t(t()) + def locale_changeset(user_or_changeset, locale) do + user_or_changeset + |> cast(%{"locale" => locale}, [:locale]) + |> validate_required(:locale) + end end diff --git a/lib/lokal_web.ex b/lib/lokal_web.ex index abf7367..6a07712 100644 --- a/lib/lokal_web.ex +++ b/lib/lokal_web.ex @@ -47,6 +47,7 @@ defmodule LokalWeb do use Phoenix.LiveView, layout: {LokalWeb.LayoutView, "live.html"} + on_mount LokalWeb.InitAssigns unquote(view_helpers()) end end diff --git a/lib/lokal_web/controllers/user_settings_controller.ex b/lib/lokal_web/controllers/user_settings_controller.ex index 9c153a2..22171f8 100644 --- a/lib/lokal_web/controllers/user_settings_controller.ex +++ b/lib/lokal_web/controllers/user_settings_controller.ex @@ -10,10 +10,11 @@ defmodule LokalWeb.UserSettingsController do render(conn, "edit.html", page_title: gettext("Settings")) end - def update(conn, %{"action" => "update_email"} = params) do - %{"current_password" => password, "user" => user_params} = params - user = conn.assigns.current_user - + def update(%{assigns: %{current_user: user}} = conn, %{ + "action" => "update_email", + "current_password" => password, + "user" => user_params + }) do case Accounts.apply_user_email(user, password, user_params) do {:ok, applied_user} -> Accounts.deliver_update_email_instructions( @@ -33,14 +34,15 @@ defmodule LokalWeb.UserSettingsController do |> redirect(to: Routes.user_settings_path(conn, :edit)) {:error, changeset} -> - render(conn, "edit.html", email_changeset: changeset) + conn |> render("edit.html", email_changeset: changeset) end end - def update(conn, %{"action" => "update_password"} = params) do - %{"current_password" => password, "user" => user_params} = params - user = conn.assigns.current_user - + def update(%{assigns: %{current_user: user}} = conn, %{ + "action" => "update_password", + "current_password" => password, + "user" => user_params + }) do case Accounts.update_user_password(user, password, user_params) do {:ok, user} -> conn @@ -49,12 +51,27 @@ defmodule LokalWeb.UserSettingsController do |> UserAuth.log_in_user(user) {:error, changeset} -> - render(conn, "edit.html", password_changeset: changeset) + conn |> render("edit.html", password_changeset: changeset) end end - def confirm_email(conn, %{"token" => token}) do - case Accounts.update_user_email(conn.assigns.current_user, token) do + def update( + %{assigns: %{current_user: user}} = conn, + %{"action" => "update_locale", "user" => %{"locale" => locale}} + ) do + case Accounts.update_user_locale(user, locale) do + {:ok, _user} -> + conn + |> put_flash(:info, dgettext("prompts", "Language updated successfully.")) + |> redirect(to: Routes.user_settings_path(conn, :edit)) + + {:error, changeset} -> + conn |> render("edit.html", locale_changeset: changeset) + end + end + + def confirm_email(%{assigns: %{current_user: user}} = conn, %{"token" => token}) do + case Accounts.update_user_email(user, token) do :ok -> conn |> put_flash(:info, dgettext("prompts", "Email changed successfully.")) @@ -84,11 +101,10 @@ defmodule LokalWeb.UserSettingsController do end end - defp assign_email_and_password_changesets(conn, _opts) do - user = conn.assigns.current_user - + defp assign_email_and_password_changesets(%{assigns: %{current_user: user}} = conn, _opts) do conn |> assign(:email_changeset, Accounts.change_user_email(user)) |> assign(:password_changeset, Accounts.change_user_password(user)) + |> assign(:locale_changeset, Accounts.change_user_locale(user)) end end diff --git a/lib/lokal_web/live/init_assigns.ex b/lib/lokal_web/live/init_assigns.ex new file mode 100644 index 0000000..7fb4dbd --- /dev/null +++ b/lib/lokal_web/live/init_assigns.ex @@ -0,0 +1,19 @@ +defmodule LokalWeb.InitAssigns do + @moduledoc """ + Ensures common `assigns` are applied to all LiveViews attaching this hook. + """ + import Phoenix.LiveView + alias Lokal.Accounts + + def on_mount(:default, _params, %{"locale" => locale, "user_token" => user_token}, socket) do + Gettext.put_locale(locale) + + socket = + socket + |> assign_new(:current_user, fn -> Accounts.get_user_by_session_token(user_token) end) + + {:cont, socket} + end + + def on_mount(:default, _params, _session, socket), do: {:cont, socket} +end diff --git a/lib/lokal_web/live/invite_live/index.ex b/lib/lokal_web/live/invite_live/index.ex index 80aeb9a..e0a921b 100644 --- a/lib/lokal_web/live/invite_live/index.ex +++ b/lib/lokal_web/live/invite_live/index.ex @@ -10,9 +10,7 @@ defmodule LokalWeb.InviteLive.Index do alias Phoenix.LiveView.JS @impl true - def mount(_params, session, socket) do - %{assigns: %{current_user: current_user}} = socket = socket |> assign_defaults(session) - + def mount(_params, _session, %{assigns: %{current_user: current_user}} = socket) do socket = if current_user |> Map.get(:role) == :admin do socket |> display_invites() diff --git a/lib/lokal_web/live/live_helpers.ex b/lib/lokal_web/live/live_helpers.ex index a851eea..113911f 100644 --- a/lib/lokal_web/live/live_helpers.ex +++ b/lib/lokal_web/live/live_helpers.ex @@ -4,21 +4,8 @@ defmodule LokalWeb.LiveHelpers do """ import Phoenix.LiveView.Helpers - import Phoenix.LiveView - alias Lokal.Accounts alias Phoenix.LiveView.JS - def assign_defaults(socket, %{"user_token" => user_token} = _session) do - socket - |> assign_new(:current_user, fn -> - Accounts.get_user_by_session_token(user_token) - end) - end - - def assign_defaults(socket, _session) do - socket - end - @doc """ Renders a live component inside a modal. diff --git a/lib/lokal_web/live/page_live.ex b/lib/lokal_web/live/page_live.ex index b697d8d..b3499af 100644 --- a/lib/lokal_web/live/page_live.ex +++ b/lib/lokal_web/live/page_live.ex @@ -6,13 +6,8 @@ defmodule LokalWeb.PageLive do use LokalWeb, :live_view @impl true - def mount(_params, session, socket) do - socket = - socket - |> assign_defaults(session) - |> assign(page_title: gettext("Home"), query: "", results: %{}) - - {:ok, socket} + def mount(_params, _session, socket) do + {:ok, socket |> assign(page_title: gettext("Home"), query: "", results: %{})} end @impl true diff --git a/lib/lokal_web/router.ex b/lib/lokal_web/router.ex index 76e5e5c..4132179 100644 --- a/lib/lokal_web/router.ex +++ b/lib/lokal_web/router.ex @@ -11,6 +11,17 @@ defmodule LokalWeb.Router do plug :protect_from_forgery plug :put_secure_browser_headers plug :fetch_current_user + plug :put_user_locale, default: Application.get_env(:gettext, :default_locale, "en_US") + end + + defp put_user_locale(%{assigns: %{current_user: %{locale: locale}}} = conn, default: default) do + Gettext.put_locale(locale || default) + conn |> put_session(:locale, locale || default) + end + + defp put_user_locale(conn, default: default) do + Gettext.put_locale(default) + conn |> put_session(:locale, default) end pipeline :require_admin do diff --git a/lib/lokal_web/templates/user_registration/new.html.heex b/lib/lokal_web/templates/user_registration/new.html.heex index ddc9cb8..b113baa 100644 --- a/lib/lokal_web/templates/user_registration/new.html.heex +++ b/lib/lokal_web/templates/user_registration/new.html.heex @@ -30,6 +30,15 @@ <%= password_input(f, :password, required: true, class: "input input-primary col-span-2") %> <%= error_tag(f, :password, "col-span-3") %> + <%= label(f, :locale, gettext("Language"), class: "title text-lg text-primary-600") %> + <%= select( + f, + :locale, + [{gettext("English"), "en_US"}], + class: "input input-primary col-span-2" + ) %> + <%= error_tag(f, :locale) %> + <%= submit(dgettext("actions", "Register"), class: "mx-auto btn btn-primary col-span-3") %> <% end %> diff --git a/lib/lokal_web/templates/user_settings/edit.html.heex b/lib/lokal_web/templates/user_settings/edit.html.heex index c0f500d..f8b8d3b 100644 --- a/lib/lokal_web/templates/user_settings/edit.html.heex +++ b/lib/lokal_web/templates/user_settings/edit.html.heex @@ -1,17 +1,18 @@ -
+

<%= gettext("Settings") %>


- <%= form_for @email_changeset, - Routes.user_settings_path(@conn, :update), - [ - class: - "flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center" - ], - fn f -> %> + <.form + let={f} + for={@email_changeset} + action={Routes.user_settings_path(@conn, :update)} + class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center" + >

<%= dgettext("actions", "Change email") %>

@@ -45,17 +46,16 @@ <%= submit(dgettext("actions", "Change email"), class: "mx-auto btn btn-primary col-span-3" ) %> - <% end %> +
- <%= form_for @password_changeset, - Routes.user_settings_path(@conn, :update), - [ - class: - "flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center" - ], - fn f -> %> + <.form + let={f} + for={@password_changeset} + action={Routes.user_settings_path(@conn, :update)} + class="flex flex-col space-y-4 sm:space-y-0 sm:grid sm:grid-cols-3 sm:gap-4 justify-center items-center" + >

<%= dgettext("actions", "Change password") %>

@@ -101,7 +101,40 @@ <%= submit(dgettext("actions", "Change password"), class: "mx-auto btn btn-primary col-span-3" ) %> - <% end %> + + +
+ + <.form + let={f} + for={@locale_changeset} + action={Routes.user_settings_path(@conn, :update)} + class="flex flex-col space-y-4 justify-center items-center" + > +

+ <%= dgettext("actions", "Change Language") %> +

+ + <%= if @locale_changeset.action && not @locale_changeset.valid? do %> +
+

+ <%= dgettext("errors", "Oops, something went wrong! Please check the errors below.") %> +

+
+ <% end %> + + <%= hidden_input(f, :action, name: "action", value: "update_locale") %> + + <%= select(f, :locale, [{gettext("English"), "en_US"}, {"Spanish", "es"}], + class: "mx-2 my-1 min-w-md input input-primary" + ) %> + <%= error_tag(f, :locale) %> + + <%= submit(dgettext("actions", "Change language"), + class: "whitespace-nowrap mx-auto btn btn-primary", + data: [qa: dgettext("prompts", "Are you sure you want to change your language?")] + ) %> +
diff --git a/mix.exs b/mix.exs index 07ec05c..07eee6b 100644 --- a/mix.exs +++ b/mix.exs @@ -13,6 +13,7 @@ defmodule Lokal.MixProject do deps: deps(), dialyzer: [plt_add_apps: [:ex_unit]], consolidate_protocols: Mix.env() not in [:dev, :test], + preferred_cli_env: [test: :test], # ExDoc name: "Lokal", source_url: "https://gitea.bubbletea.dev/shibao/lokal", @@ -91,6 +92,7 @@ defmodule Lokal.MixProject do "dialyzer", "credo --strict", "format --check-formatted", + "ecto.drop --quiet", "ecto.create --quiet", "ecto.migrate --quiet", "test" diff --git a/priv/gettext/actions.pot b/priv/gettext/actions.pot index 6522d3a..dfa687d 100644 --- a/priv/gettext/actions.pot +++ b/priv/gettext/actions.pot @@ -11,8 +11,8 @@ msgid "" msgstr "" #, elixir-autogen, elixir-format -#: lib/lokal_web/templates/user_settings/edit.html.heex:16 -#: lib/lokal_web/templates/user_settings/edit.html.heex:45 +#: lib/lokal_web/templates/user_settings/edit.html.heex:17 +#: lib/lokal_web/templates/user_settings/edit.html.heex:46 msgid "Change email" msgstr "" @@ -23,7 +23,7 @@ msgid "Change password" msgstr "" #, elixir-autogen, elixir-format -#: lib/lokal_web/templates/user_settings/edit.html.heex:108 +#: lib/lokal_web/templates/user_settings/edit.html.heex:141 msgid "Delete User" msgstr "" @@ -92,3 +92,13 @@ msgstr "" #: lib/lokal_web/live/invite_live/form_component.html.heex:28 msgid "Save" msgstr "" + +#, elixir-autogen, elixir-format +#: lib/lokal_web/templates/user_settings/edit.html.heex:115 +msgid "Change Language" +msgstr "" + +#, elixir-autogen, elixir-format +#: lib/lokal_web/templates/user_settings/edit.html.heex:133 +msgid "Change language" +msgstr "" diff --git a/priv/gettext/default.pot b/priv/gettext/default.pot index ec559f0..49d8994 100644 --- a/priv/gettext/default.pot +++ b/priv/gettext/default.pot @@ -21,7 +21,7 @@ msgid "Confirm your account" msgstr "" #, elixir-autogen, elixir-format -#: lib/lokal_web/templates/user_settings/edit.html.heex:33 +#: lib/lokal_web/templates/user_settings/edit.html.heex:34 #: lib/lokal_web/templates/user_settings/edit.html.heex:89 msgid "Current password" msgstr "" @@ -80,7 +80,7 @@ msgstr "" #, elixir-autogen, elixir-format #: lib/lokal_web/controllers/user_settings_controller.ex:10 -#: lib/lokal_web/templates/user_settings/edit.html.heex:3 +#: lib/lokal_web/templates/user_settings/edit.html.heex:5 msgid "Settings" msgstr "" @@ -153,3 +153,8 @@ msgstr "" #: lib/lokal_web/live/invite_live/form_component.html.heex:24 msgid "Uses left" msgstr "" + +#, elixir-autogen, elixir-format +#: lib/lokal_web/templates/user_settings/edit.html.heex:128 +msgid "English" +msgstr "" diff --git a/priv/gettext/errors.pot b/priv/gettext/errors.pot index 31e03e7..9ea141b 100644 --- a/priv/gettext/errors.pot +++ b/priv/gettext/errors.pot @@ -94,7 +94,7 @@ msgid "must be equal to %{number}" msgstr "" #, elixir-autogen, elixir-format -#: lib/lokal_web/controllers/user_settings_controller.ex:67 +#: lib/lokal_web/controllers/user_settings_controller.ex:84 msgid "Email change link is invalid or it has expired." msgstr "" @@ -126,8 +126,9 @@ msgstr "" #, elixir-autogen, elixir-format #: lib/lokal_web/templates/user_registration/new.html.heex:16 #: lib/lokal_web/templates/user_reset_password/edit.html.heex:16 -#: lib/lokal_web/templates/user_settings/edit.html.heex:22 +#: lib/lokal_web/templates/user_settings/edit.html.heex:23 #: lib/lokal_web/templates/user_settings/edit.html.heex:66 +#: lib/lokal_web/templates/user_settings/edit.html.heex:121 msgid "Oops, something went wrong! Please check the errors below." msgstr "" @@ -149,7 +150,7 @@ msgid "Sorry, this invite was not found or expired" msgstr "" #, elixir-autogen, elixir-format -#: lib/lokal_web/controllers/user_settings_controller.ex:82 +#: lib/lokal_web/controllers/user_settings_controller.ex:99 msgid "Unable to delete user" msgstr "" @@ -175,22 +176,22 @@ msgid "You must confirm your account and log in to access this page." msgstr "" #, elixir-autogen, elixir-format -#: lib/lokal/accounts/user.ex:128 +#: lib/lokal/accounts/user.ex:130 msgid "did not change" msgstr "" #, elixir-autogen, elixir-format -#: lib/lokal/accounts/user.ex:149 +#: lib/lokal/accounts/user.ex:151 msgid "does not match password" msgstr "" #, elixir-autogen, elixir-format -#: lib/lokal/accounts/user.ex:186 +#: lib/lokal/accounts/user.ex:188 msgid "is not valid" msgstr "" #, elixir-autogen, elixir-format -#: lib/lokal/accounts/user.ex:82 +#: lib/lokal/accounts/user.ex:84 msgid "must have the @ sign and no spaces" msgstr "" diff --git a/priv/gettext/prompts.pot b/priv/gettext/prompts.pot index 3ab08a5..c7e8972 100644 --- a/priv/gettext/prompts.pot +++ b/priv/gettext/prompts.pot @@ -16,12 +16,12 @@ msgid "%{email} confirmed successfully." msgstr "" #, elixir-autogen, elixir-format -#: lib/lokal_web/controllers/user_settings_controller.ex:28 +#: lib/lokal_web/controllers/user_settings_controller.ex:29 msgid "A link to confirm your email change has been sent to the new address." msgstr "" #, elixir-autogen, elixir-format -#: lib/lokal_web/templates/user_settings/edit.html.heex:112 +#: lib/lokal_web/templates/user_settings/edit.html.heex:145 msgid "Are you sure you want to delete your account?" msgstr "" @@ -31,7 +31,7 @@ msgid "Are you sure you want to log out?" msgstr "" #, elixir-autogen, elixir-format -#: lib/lokal_web/controllers/user_settings_controller.ex:60 +#: lib/lokal_web/controllers/user_settings_controller.ex:77 msgid "Email changed successfully." msgstr "" @@ -56,7 +56,7 @@ msgid "Password reset successfully." msgstr "" #, elixir-autogen, elixir-format -#: lib/lokal_web/controllers/user_settings_controller.ex:47 +#: lib/lokal_web/controllers/user_settings_controller.ex:49 msgid "Password updated successfully." msgstr "" @@ -66,7 +66,7 @@ msgid "Please check your email to verify your account" msgstr "" #, elixir-autogen, elixir-format -#: lib/lokal_web/controllers/user_settings_controller.ex:78 +#: lib/lokal_web/controllers/user_settings_controller.ex:95 msgid "Your account has been deleted" msgstr "" @@ -126,3 +126,13 @@ msgstr "" #: lib/lokal_web/live/invite_live/form_component.html.heex:30 msgid "Saving..." msgstr "" + +#, elixir-autogen, elixir-format +#: lib/lokal_web/templates/user_settings/edit.html.heex:135 +msgid "Are you sure you want to change your language?" +msgstr "" + +#, elixir-autogen, elixir-format +#: lib/lokal_web/controllers/user_settings_controller.ex:65 +msgid "Language updated successfully." +msgstr "" diff --git a/priv/repo/migrations/20220505233700_add_locale_setting.exs b/priv/repo/migrations/20220505233700_add_locale_setting.exs new file mode 100644 index 0000000..2dd04e5 --- /dev/null +++ b/priv/repo/migrations/20220505233700_add_locale_setting.exs @@ -0,0 +1,9 @@ +defmodule Lokal.Repo.Migrations.AddLocaleSetting do + use Ecto.Migration + + def change do + alter table("users") do + add :locale, :string + end + end +end diff --git a/readme.md b/readme.md index 719802d..5f71063 100644 --- a/readme.md +++ b/readme.md @@ -41,7 +41,7 @@ You can use the following environment variables to configure Lokal in with `docker run -it shibaobun/lokal mix phx.gen.secret` and set for server to start. - `REGISTRATION`: Controls if user sign-up should be invite only or set to public. Set to `public` to enable public registration. Defaults to `invite`. -- `LOCALE`: Sets a custom locale. Defaults to `en_US`. +- `LOCALE`: Sets a custom default locale. Defaults to `en_US`. - `SMTP_HOST`: The url for your SMTP email provider. Must be set - `SMTP_PORT`: The port for your SMTP relay. Defaults to `587`. - `SMTP_USERNAME`: The username for your SMTP relay. Must be set!