GES233

GES233

Sharing a not-much-elegate way to implement i18n in Phoenix LiveView (can you help improve it?)

Hi everyone,

I’ve been working on implementing real-time i18n in my own Phoenix LiveView application, specifically on the registration page. The goal was to allow users to change the language of the form instantly.

After complete this feature, I had browse forum to see community’s solution and I found Routex - build powerful Phoenix routes: localize, customize, and innovate finally. It’s a pity to erase the experience so I wanna write my solution and have a discuss related how to make it better(the code seems to be a little bit ugly, frankly speaking).

Repo: GES233/EchoesUnderBlossoms: Some echoes should not be forgotten. (document seems to be a bit chunibyo cause I refactored it by Gemini several times.)

The Initial Goal & Problem

On my registration LiveView (HanaShirabeWeb.MemberLive.Registration), I have a language selector. When a user changes the language, the UI text (labels, buttons) should update instantly.

At the beginning, I write a function attampt to handle it simply, Gettext.put_locale(some_target_locale).

I forgot what the problem was, but the nav bar remained.

First attempt

To my shame, I knew absolutely nothing about Phoenix and LiveView before this.

I implement a cookie to store the locale state before(a plug called SetLocale), so I tried to use it to persist locale state at client side.

This is how it determine languages:

defp fetch_locale_from_sources(conn) do
  locale_from_user =
    if !is_nil(conn.assigns.current_scope),
      do: conn.assigns.current_scope.member.prefer_locale,
      else: nil

  [
    conn.params["locale"],
    locale_from_user,
    conn.req_cookies[@locale_cookie],
    get_req_header(conn, "accept-language") |> parse_accept_language()
  ]
end

But when I add /?locale=en/ja/... request to refresh the page, the language changes, but with full of rough(I don’t know hot to describe it in English accurately) and all data in form disappear.

I tried to convince myself that since the user required changing the language settings in form, the data wasn’t necessary to store.

I don’t know if I succeeded, but it seems like other data isn’t being saved.

But I’m actually quite against stuffing all sorts of data into the user-visible params, because many websites stuff links with all sorts of data that could potentially track users(such as Bilibili[1], Douyin, RedNote, etc.), and what’s even more disgusting is that many people spread these links everywhere with several parameters that have no meaning for sharing.

So I have HIGH requirements for the simplicity of website links.

I don’t remember how many AI programs I’ve tried(at least GPT/Grok/Gimini/Qwen/Deepseek), so I’ll just go straight to solution.

ColocatedHook + Controller + Delay Reflash

1. Phoenix LiveView → Client(ColocatedHook)

def handle_event(
      "locale_changed",
      %{
        "_target" => ["registration_form", "prefer_locale"],
        "registration_form" => %{"prefer_locale" => locale}
      },
      socket
    ) do
  Gettext.put_locale(HanaShirabeWeb.Gettext, locale)

  # The test code for this function only needs to account for cookie updates.
  socket = push_event(socket, "set_locale_cookie", %{locale: locale})

  {:noreply, socket}
  end

2. Client → Controller

in <script :type={Phoenix.LiveView.ColocatedHook} name=".LocaleFormInput">:

  export default {
    mounted() {
      const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content");

      this.handleEvent("set_locale_cookie", ({ locale }) => {
        fetch(`/set-locale/${locale}`, {
          method: "POST",
          headers: {
            "x-csrf-token": csrfToken
          }
        }).then(response => {
          if (response.ok) {
            this.pushEvent("locale_cookie_updated", {locale: locale});
        }}).catch(error => {
          console.error(`Failed to set locale cookie to '${locale}':`, error)
        });
      });
    }
  }

And in server side:

defmodule HanaShirabeWeb.LocaleController do
  @moduledoc """
  This is actually a plugin designed to allow the page language to be updated immediately
  when the language is changed in the registration form.
  """
  use HanaShirabeWeb, :controller

  # It needs to be determined that cookies have a lower priority than `?locale=(...)` and user settings.
  def update(conn, %{"locale" => locale}) do
    # I extract the inject-cookie-phase into a function
    conn |> HanaShirabeWeb.SetLocale.persist(locale) |> send_resp(204, "")
  end
end

with router: post "/set-locale/:locale", LocaleController, :update

3. Refresh

# Because of the properties of LiveView's underlying socket and the "global" action of changing the language
# this means it cannot be solved using Phoenix.LiveView
# so this very inelegant method has to be used.
# すみません
def handle_event("locale_cookie_updated", %{"locale" => locale}, socket) do
  # for that flash message with `Locale Updated!`
  Gettext.put_locale(locale)

  {:noreply,
   socket
   |> put_flash(:info, gettext("Locale updated!"))
   |> redirect(to: ~p"/sign_up", replace: true)}
end

Display

There’s no gif demostration because of its size.

Besides sharing this rather bumpy experience to vent, I’m also curious if there’s a more elegant way to do it?

Also, I know that the audience of my Repo project is not very relevant with here, but if you want to discuss it, you can do so here.


  1. 添加去除跳转时网址参数(?大概)功能的 · Issue #263 · the1812/Bilibili-Evolved ↩︎

Most Liked

derek-zhou

derek-zhou

Changing locale will not reactively change content, So most people will just push_navigate/2 and embed the local in url parameter. But then as you discovered, everything in the socket assigns are lost.

If for some reason you want to keep all the socket assigns, but update the content with new translation, you can do this:

  • keep locale in the socket assigns and pass it down, in addition to Gettext.put_locale/1
  • Write your own gettext wrappers with the passed down locale as the additional function parameter. You don’t need to do anything with the passed down locale, prefix it with _ in your wrapper

Then the changed locale will trigger re rendering. It is a lot of work though.

LostKobrakai

LostKobrakai

There’s really two things here. There’s updating the session (cookie) and there’s updating LV for a new gettext locale.

For the former, there’s simply no good solution when using cookies. Cookies can only be updated on http requests, so there’s no way around making one of those. With a server side session implementation you could do the write on the server without additional requests.

For the latter the problem is that gettext was implemented way before LV and/or change tracking became a thing. So gettext uses the process dictionary to store the current locale for gettext functions to pick it up. Changes in the process dictionary completely go around assigns and the change tracking of LV however. If you want the LV to rerender when the locale changes – without navigation being involved – you’d need to put the locale in assigns and make all places depending on the locale use Gettext.with_locale explicitly.

BartOtten

BartOtten

Version 1.3 was released yesterday. Now with Igniter install support and a few other Quality of Life improvements.

Also a new demo site is coming up in a few days.

Would love to receive your feedback :slight_smile:

Where Next?

Popular in Questions Top

sergio
In Ruby, I can go: User.find_by(email: "foobar@email.com").update(email: "hello@email.com") How can I do something similar in Elixir? ...
New
lessless
I believe there are people here who are dealing with CSV files import on the daily basis, and since Excel is a really popular tool there ...
New
gshaw
What is the idiomatic way of matching for not nil in Elixir? E.g., First way: defp halt_if_not_signed_in(conn, signed_in_account) when...
New
minhajuddin
I have seen a lot of code which picks the first element from a list using Enum.at(0) instead of List.first. Is there a reason why people ...
New
fireproofsocks
Forgive me if this is obvious, but how does one delete a database record WITHOUT selecting it first? Ecto.Repo — Ecto v3.14.0 has exampl...
New
ashish173
I am using Ecto timestamps with postgres, I can see the timestamps() use the :naive_dateime but for my use case I wanted to store the ti...
New
chensan
I have a User schema with a :from_id field set to type :string: defmodule TweetBot.Repo.Migrations.CreateUsers do use Ecto.Migration ...
New
PeterCarter
There are pre-rolled solutions for other frameworks that do work. However, Phoenix does not seem to have these. Have people had good expe...
New
dokuzbir
I want to highlight html closing tags when i click a html tag. That works in .html files but doesnt work for html.eex templates. How can...
New
vonH
In asking this question I am more interested about the expressiveness of the language itself and less concerned about the availability of...
New

Other popular topics Top

aadeshere1
I have a another noob question about loop. Since elixir is immutable, while loop is not directly possible. total = 10 while total != 0 ...
New
vertexbuffer
Hello, can anybody help here..? I have a list of players and I what to delete an element, but every for loop the list is reverting to ori...
New
AstonJ
Posting this to see if we can make things easier for people to get into Neovim. If you use Neovim and have a favourite distro please let ...
New
jononomo
I am trying to figure out how Mix knows whether the environment is test, dev, or prod – where is this set? Thanks.
New
pmjoe
I have a relationship of love and hate with Elixir. Lots of things are just absolutely right, but there are some things that are kind of ...
New
Fl4m3Ph03n1x
About me? ( if you have nothing better to do than reading about some random guy in the internet :stuck_out_tongue: ) Hello all, this is ...
New
AngeloChecked
What learn first? Rust or Elixir Hi Elixir community! I’m here because i want learn a new language. I’m a junior developer and mainly i ...
New
jay1
Why is it that the mnesia database isn’t the most preferred database for use in Elixir/Phoenix?
New
jononomo
For some reason my phoenix channels are working for me in my local dev environment, but as soon as I deploy via Docker, I get a 403 error...
New
vonH
In asking this question I am more interested about the expressiveness of the language itself and less concerned about the availability of...
New

Latest on Elixir Forum

We're in Beta

About us Mission Statement