I have a plug locale which redirects all requests like /registration/new to urls like /de/registration/new, /fr/registration/new, /ru/registration/new according to browser's accept-language header or by users choice.
So I need my forms to be localized and their action should lead to localized paths too because the mail should be sent also translated and also I need to save user's language for later use.
I've put pow_routes() and pow_extension_routes() into the scope in the router.ex file
scope "/:locale" do
pow_routes()
pow_extension_routes()
end
I've overriden all templates for registration, session, email_confirmation and reset_password and mailer view templates
mix pow.phoenix.gen.templates
mix pow.extension.phoenix.gen.templates --extension PowResetPassword --extension PowEmailConfirmation
mix pow.extension.phoenix.mailer.gen.templates --extension PowResetPassword --extension PowEmailConfirmation
And in the registration form template new.html.eex I changed the @action to "/"<>@locale<>@action to make form to be sent to localized path:
<%= form_for @changeset, "/"<>@locale<>@action, [as: :user], fn f -> %>
And made custom mailer backend according to Readme file and modified jp_web.ex file too.
And because I need all messages to be translated too I needed to create JpWeb.Pow.Messages file and add all messages from Pow and from Extensions messages files.
And of course I added lines to config file:
# Configure Pow authentication
config :jp, :pow,
user: Jp.Users.User,
repo: Jp.Repo,
extensions: [PowResetPassword, PowEmailConfirmation],
controller_callbacks: Pow.Extension.Phoenix.ControllerCallbacks,
web_module: JpWeb,
mailer_backend: JpWeb.PowMailer,
web_mailer_module: JpWeb,
messages_backend: JpWeb.Pow.Messages
And after trying to submit the form I got error:
** (exit) an exception was raised:
** (UndefinedFunctionError) function JpWeb.Pow.Messages.PowEmailConfirmation.Phoenix.Messages.email_confirmation_required/1 is undefined (module JpWeb.Pow.Messages.PowEmailConfirmation.Phoenix.Messages is not available)
Though the user was created and logger outputs the email into the console.
So what else do I need to do to get rid of the error?
Oh thanks, looks like a bug! I'll work on this, and push a fix for it ASAP.
It's excellent that you are using Pow with a multilingual website. One of the businesses I was working on was going to use multilingual Pow setup but unfortunately didn't happen. So I never got around to test this properly in such setup. Excited to hear how it all works out for you!
BTW, can you just post the JpWeb.Pow.Messages module? I just need to see the headers, what use modules you have?
This is how your setup should look like:
defmodule JpWeb.Pow.Messages do
use Pow.Phoenix.Messages
use Pow.Extension.Phoenix.Messages,
extensions: [ResetPassword, PowEmailConfirmation]
import JpWeb.Gettext
# ...
end
Also, I would also suggest that you change "/"<>@locale<>@action to generated URI instead: Routes.pow_registration_path(conn, :create, @locale).
You can look at the code is the repo:
https://github.com/igormisha/jp/invitations
Checkout to the pow_auth branch.
Oh, I see the issue, which originates from my code examples. You should change:
use Pow.Extension.Phoenix.Messages,
extensions: [ResetPassword, EmailConfirmation]
To:
use Pow.Extension.Phoenix.Messages,
extensions: [PowResetPassword, PowEmailConfirmation]
I鈥檒l update docs, and maybe also set up warnings when non-existing extensions are used.
Yep, the error is gone, Thanks!
Replaced
"/" <> @locale <> @action
with what you suggested with minor changes:
Routes.locale_pow_registration_path(@conn, :create, @locale)
Hi,
I'm also trying to implement multi language with a default language (when no preference).
Example urls:
/registration/new # for default
/fr/registration/new
/ru/registration/new
Since the website is already live and I don't want to change default urls with a locale. So, I have to go with a default language option however I can't find a way to manage it with pow and your recommendations above.
Here's my router.ex
scope "/", MyAppWeb do
pipe_through :browser
get "/", PageController, :index
get "/w/:query", GlossaryController, :index
get "/games/:game", GameController, :index
get "/contribute", ContributeController, :index
end
scope "/:locale" do # also tried with `_` optional version: `/:_locale`
pipe_through :browser
pow_routes()
pow_extension_routes()
end
scope "/:locale", MyAppWeb do
pipe_through :browser
get "/", PageController, :index
get "/w/:query", GlossaryController, :index
get "/games/:game", GameController, :index
get "/contribute", ContributeController, :index
end
# also tried with all localized versions.
scope "/fr", MyAppWeb do
pipe_through :browser
get "/games/:game", GameController, :index
get "/contribute", ContributeController, :index
end
scope "/ru", MyAppWeb do
pipe_through :browser
get "/games/:game", GameController, :index
get "/contribute", ContributeController, :index
end
My Localization Plug:
defmodule MyAppWeb.Plugs.Locale do
import Plug.Conn
@supported_locales Gettext.known_locales(MyAppWeb.Gettext)
def init(_options), do: nil
def call(%Plug.Conn{params: %{"locale" => locale}} = conn, _options)
when locale in @supported_locales do
MyAppWeb.Gettext |> Gettext.put_locale(locale)
conn
|> assign(:locale, locale)
|> put_session("locale", locale)
|> put_resp_cookie("locale", locale, max_age: 365 * 24 * 60 * 60)
end
def call(conn, _options), do: conn
defp fetch_locale_from(conn) do
(conn.assigns[:locale] || conn.params['locale'] || conn.cookies["locale"])
|> check_locale
end
defp check_locale(locale) when locale in @supported_locales, do: locale
defp check_locale(_), do: nil
end
lib/myapp_web/pow_routes.ex # thanks to #106's dev
defmodule MyAppWeb.Pow.Routes do
use Pow.Phoenix.Routes
@impl true
def path_for(conn, plug, verb, vars, query_params) do
vars = [Map.get(conn.params, :locale, "en")] ++ vars
url = Pow.Phoenix.Routes.url_for(conn, plug, verb, vars, query_params)
IO.inspect(url)
url
end
end
and added it in config as well:
config :myapp, :pow,
user: MyApp.Users.User,
repo: MyApp.Repo,
extensions: [PowResetPassword, PowEmailConfirmation],
controller_callbacks: Pow.Extension.Phoenix.ControllerCallbacks,
mailer_backend: MyAppWeb.PowMailer,
web_mailer_module: MyAppWeb,
routes_backend: MyAppWeb.Pow.Routes
I don't know where's this Routes.locale_pow_registration_path(@conn, :create, @locale) coming from but my problem is not yet there. Just in case I'm sharing registration/new.html.eex
<h1>Register</h1>
<%= form_for @changeset, Routes.pow_registration_path(@conn, :create, @conn.assigns[:locale]), [as: :user], fn f -> %>
<%= if @changeset.action do %>
<div class="alert alert-danger">
<p>Oops, something went wrong! Please check the errors below.</p>
</div>
<% end %>
<%= label f, Pow.Ecto.Schema.user_id_field(@changeset) %>
<%= text_input f, Pow.Ecto.Schema.user_id_field(@changeset) %>
<%= error_tag f, Pow.Ecto.Schema.user_id_field(@changeset) %>
<%= label f, :password %>
<%= password_input f, :password %>
<%= error_tag f, :password %>
<%= label f, :confirm_password %>
<%= password_input f, :confirm_password %>
<%= error_tag f, :confirm_password %>
<div>
<%= submit "Register" %>
</div>
<% end %>
<span><%= link "Sign in", to: Routes.pow_session_path(@conn, :new, @conn.assigns[:locale]) %></span>
I'm having issue with call backend part. Here's my stack trace:
[info] GET /en/registration/new
[debug] Processing with Pow.Phoenix.RegistrationController.new/2
Parameters: %{"locale" => "en"}
Pipelines: [:browser]
"http://localhost:4000/en/registration" # it actually create a url correctly.
[info] Sent 500 in 22ms
[error] #PID<0.998.0> running MyAppWeb.Endpoint (connection #PID<0.965.0>, stream id 2) terminated
Server: localhost:4000 (http)
Request: GET /en/registration/new
** (exit) an exception was raised:
** (Protocol.UndefinedError) protocol Phoenix.Param not implemented for [] of type List. This protocol is implemented for the following type(s): Any, Map, Integer, BitString, Atom
(phoenix 1.4.12) lib/phoenix/param.ex:121: Phoenix.Param.Any.to_param/1
(MyApp 0.1.0) MyAppWeb.Router.Helpers.pow_session_path/4
(pow 1.0.16) lib/pow/phoenix/templates/registration_template.ex:28: Pow.Phoenix.RegistrationTemplate.render/2
(MyApp 0.1.0) lib/MyApp_web/templates/layout/app.html.eex:28: MyAppWeb.LayoutView."app.html"/1
(phoenix 1.4.12) lib/phoenix/view.ex:410: Phoenix.View.render_to_iodata/3
(phoenix 1.4.12) lib/phoenix/controller.ex:729: Phoenix.Controller.__put_render__/5
(phoenix 1.4.12) lib/phoenix/controller.ex:746: Phoenix.Controller.instrument_render_and_send/4
(pow 1.0.16) lib/pow/phoenix/controllers/registration_controller.ex:1: Pow.Phoenix.RegistrationController.action/2
(pow 1.0.16) lib/pow/phoenix/controllers/registration_controller.ex:1: Pow.Phoenix.RegistrationController.phoenix_controller_pipeline/2
(phoenix 1.4.12) lib/phoenix/router.ex:288: Phoenix.Router.__call__/2
(MyApp 0.1.0) lib/MyApp_web/endpoint.ex:1: MyAppWeb.Endpoint.plug_builder_call/2
(MyApp 0.1.0) lib/plug/debugger.ex:122: MyAppWeb.Endpoint."call (overridable 3)"/2
(MyApp 0.1.0) lib/MyApp_web/endpoint.ex:1: MyAppWeb.Endpoint.call/2
(phoenix 1.4.12) lib/phoenix/endpoint/cowboy2_handler.ex:42: Phoenix.Endpoint.Cowboy2Handler.init/4
(cowboy 2.7.0) myapp/deps/cowboy/src/cowboy_handler.erl:41: :cowboy_handler.execute/2
(cowboy 2.7.0) myapp/deps/cowboy/src/cowboy_stream_h.erl:320: :cowboy_stream_h.execute/3
(cowboy 2.7.0) myapp/deps/cowboy/src/cowboy_stream_h.erl:302: :cowboy_stream_h.request_process/3
(stdlib 3.11.2) proc_lib.erl:249: :proc_lib.init_p_do_apply/3
owboy_stream_h.request_process/3
(stdlib 3.11.2) proc_lib.erl:249: :proc_lib.init_p_do_apply/3
Thank you!
Okay I've forgotten to add web_module to the config. After added, it worked! However @conn.assigns[:locale] is sometimes nil I had to write if like this: <span><%= link "Register", to: Routes.pow_registration_path(@conn, :new, (if @conn.assigns[:locale], do: @conn.assigns[:locale], else: "fr")) %></span>
any idea @danschultzer ?
Thank you!
I just learnt that assings being reseted on every request which means I have to set the value every time. Modifying my plug.
You can make the plug much simpler since you can expect that locale is only defined in the path, and just set up fallback to default locale:
defmodule MyAppWeb.Plugs.Locale do
import Plug.Conn
@supported_locales Gettext.known_locales(MyAppWeb.Gettext)
@default_locale "en"
def init(opts), do: opts
def call(%Plug.Conn{params: %{"locale" => locale}} = conn, _opts)
when locale in @supported_locales, do: set_locale(conn, locale)
def call(conn, _opts), do: set_locale(conn, @default_locale)
defp set_locale(conn, locale) do
Gettext.put_locale(MyAppWeb.Gettext, locale)
assign(conn, :locale, locale)
end
end
But I highly recommend CLDR. That library has two plugs that can help you. As an example, you could put this in your endpoint:
plug Cldr.Plug.SetLocale,
apps: [:cldr, :gettext],
from: [:path, :cookie, :accept_language],
param: "locale",
gettext: MyAppWeb.Gettext,
cldr: MyAppWeb.Cldr
This will first pull the locale from path, then cookie and last accept language. And then it'll set the locale for your Gettext and Cldr modules. I always use this library for any platforms where I need i18n 馃槃
Oh really? I saw CLDR from other issue, and I checked it out but it looks very complicated for a beginner and didn't had time to explore it. If the settings are just that simple then it's really cool! I'll try it! Thank you very much @danschultzer !
Yeah, it's intimidating since i18n can be very complicated. It's a really useful library to handle localization with little effort. The maintainer @kipcole9 is also very helpful.
I think it's underutilized in the Elixir community. It would be great if someone wrote a tutorial or made a walkthrough video for it to show the benefits of CLDR.
@danschultzer so even with fallback I can't make this request: http://localhost:4000/session/new while http://localhost:4000/en/session/new works.
no route found for GET /session/new (MyAppWeb.Router)
So with CLDR is that possible?
You'll need to ensure the routes without locale param also exists:
scope "/:locale" do
pipe_through :browser
pow_routes()
pow_extension_routes()
end
scope "/" do
pipe_through :browser
pow_routes()
pow_extension_routes()
end
Also you got an issue in your callback routes. You should call path_for/4 instead of url_for/4, and you should also override the url_for/4 method. There will likely be redirect errors otherwise.
If you use CLDR, you can do this:
defmodule MyAppWeb.Pow.Routes do
use Pow.Phoenix.Routes
@impl true
def path_for(conn, plug, verb, vars, query_params) do
vars = [MyAppWeb.Cldr.get_locale()] ++ vars
Pow.Phoenix.Routes.path_for(conn, plug, verb, vars, query_params)
end
@impl true
def url_for(conn, plug, verb, vars, query_params) do
vars = [MyAppWeb.Cldr.get_locale()] ++ vars
Pow.Phoenix.Routes.url_for(conn, plug, verb, vars, query_params)
end
end
WOW! I did that part like this
scope "/", MyAppWeb do
pipe_through :browser
pow_routes()
pow_extension_routes()
end
before but I was getting some weird errors but this time is okay; I shouldn't have bind to my Module. Okay. I got it!
Now all is good!
Thanks a lot!
Most helpful comment
You can make the plug much simpler since you can expect that locale is only defined in the path, and just set up fallback to default locale:
But I highly recommend CLDR. That library has two plugs that can help you. As an example, you could put this in your endpoint:
This will first pull the locale from path, then cookie and last accept language. And then it'll set the locale for your Gettext and Cldr modules. I always use this library for any platforms where I need i18n 馃槃