does anybody know how to create a link in a page that redirects to the current page but changes a query param ?
My goal is to make a “switch language” link that redirects to the current page and adds a ?language=en or ?language=fr query param.
I know I can do it for a given page using the router helpers, but did not find how to make it more “generic”, like for example in Rails with link_to:
link_to "Switch to English", controller: controller, action: action, locale: :en
This way I could put the link in the header partial once for all.
Replying to myself, but for now on I ended with this (overly complicated) solution:
defmodule BoxxyWeb.PageView do
use BoxxyWeb, :view
def language_switcher(conn) do
# the current path
path = conn.request_path
# all the params
conn = Plug.Conn.fetch_query_params(conn)
query = conn.query_params
# the current language
current_language = Gettext.get_locale(BoxxyWeb.Gettext)
next_language =
case current_language do
"fr" ->
"en"
"en" ->
"fr"
_ ->
"fr"
end
# build the final path with new query
next_path = path <> "?" <> URI.encode_query(Map.put(query, "locale", next_language))
link(
gettext("Change language to %{locale}", locale: String.upcase(next_language)),
to: next_path
)
end
end
for now on with the current_path I could simplify the code to:
defmodule BoxxyWeb.PageView do
use BoxxyWeb, :view
import Phoenix.Controller, only: [current_path: 2]
alias Plug.Conn.Query
def language_switcher(conn) do
locale = next_locale()
link(
gettext("Change language to %{locale}", locale: String.upcase(locale)),
to: current_path_with_params(conn, %{"locale" => locale})
)
end
# my custom variant of current_path/2 that KEEPS the existing params
def current_path_with_params(conn, params) do
query_params = Enum.into(params, conn.query_params)
current_path(conn, %{}) <> "?" <> Query.encode(query_params)
end
defp next_locale do
case Gettext.get_locale(BoxxyWeb.Gettext) do
"fr" -> "en"
_ -> "fr"
end
end
end
@derek-zhou@trisolaran I was thinking about your remarks, I might move to use the cookie, but I don’t want to set it unless needed.
What would you think of an approach such as:
parse the Accept-Language header to detect the browser preference
allow the user to set the cookie to change the langage
But If I use this approach, what is the best way to set the cookie ? Do I keep the logic with the additional “locale” query param or is there some better (more idiomatic) option ?
What I did in a previous project of mine is adding a Plug called Locale that would parse each request, look for locale preferences and set them in the session. The plug would look into 3 places for locale preferences:
The request parameters (something like locale=fr in the query string)
The session
The Accept-Language header
Where 1) takes precedence over 2) which takes precedence over 3).
In this way, you can always overwrite the language preferences from the session and the headers by directing the user to a URL that specifies a new locale in the query string. The plug will then overwrite whatever locale preferences are currently specified in the session.
Conversely, an initial request without a locale in the query string or in the session will get the locale from the Accept-Language header. You can of course decide not to set the locale in the session in this case if you prefer.
I have a similar module, but without the accept-language support and the cookie set on query params.
Another option that was mentioned in the forum would be to use something like the Cldr.Plug.SetLocale — Cldr v2.6.0 but I did non figure out (yet) how to configure it correctly.
For those interested, here is a way to configure the plug form cldr, that works
Step 1: install ex_cldr in mix.exs
defp deps do
[
# ...
{:ex_cldr, "~> 2.25.0"}
]
end
Step 2: create a Cldr module
defmodule XXXWeb.Cldr do
use Cldr,
# name of the phoenix OTP app
otp_app: :xxx,
# name of the Gettext module: SUPER IMPORTANT to link them both !
gettext: XXXWeb.Gettext,
# optional sub-module: not mandatory but avoid a warning
providers: [],
# from the docs, should re-load the latest definitions when building in prod
force_locale_download: Mix.env() == :prod
end
Step 3: insert the plug in the pipeline
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug Cldr.Plug.SetLocale,
# Important: tell the plug to set the local for BOTH cldr and gettext
apps: [:cldr, :gettext],
# Important: the name of the Cldr module
# (no need to explicitly tell the gettext module, it will deduce it from the cldr module config earlier)
cldr: XXXWeb.Cldr,
# where to load the locale from: query param, then cookie, the header
from: [:query, :cookie, :accept_language],
# name of the query param to look for
param: "locale"
plug :fetch_live_flash
# ...
end
(Optional) Step 4: if using phx.gen.auth, update the UserAuth.renew_session/1 function
defp renew_session(conn) do
# load the locale, if any
preferred_locale = get_session(conn, "cldr_locale")
conn
|> configure_session(renew: true)
|> clear_session()
# and restore the cookie
|> put_session("cldr_locale", preferred_locale)
end
it also correctly loads my default locale from gettext, without having to set the default_locale option mentioned in the docs (which is good imho since we have a single source of truth this way)