Getting a link to the current page

Hello all,

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.

Thanks in advance for any idea :smile:

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

If you have a better solution, please tell me !

1 Like

Can’t give you a good code sample right now but consider doing this with a Plug. You want to have the language accessible everywhere anyway, right?

yes exactly, I want it in the header once for all

I think you can use current_path/2 function from Phoenix.Controller to get the path easily.

Docs: Phoenix.Controller — Phoenix v1.6.6

1 Like

Query string is ugly, considering that every page needs the locale. It is better to use a cookie for static views and local storage for live views.

I also support cookie, but the query string seemed like a good idea in some scenarios : quick toggling using a button/select, links in emails etc.

You can have a hybrid approach: store the language in the session (cookie) but use a parameter in the query string to set it.

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 ?

Hi @Aurel,

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:

  1. The request parameters (something like locale=fr in the query string)
  2. The session
  3. 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.

This is the code: smoodle/locale.ex at master · maxmarcon/smoodle · GitHub
The code is quite old and is using an old phoenix version, but I think you should be able to get the idea.

1 Like

Thanks a lot for the tips !

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.

I will keep you posted of my findings :smiley: :

EDIT there is also this function that looks very interesting Cldr.AcceptLanguage — Cldr v2.6.0

2 Likes

Oh yeah that module looks great and just like what you need :slight_smile: :+1:

For those interested, here is a way to configure the plug form cldr, that works :smile:

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

(cookie name comes from here)

1 Like

to test my code I used basic tags in a template:

  <p><%= Gettext.get_locale(BoxxyWeb.Gettext) %></p>
  <p><%= BoxxyWeb.Cldr.get_locale() |> inspect() %></p>

and it prints as follow when passed ?locale=en (my default locale is fr):

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)