GES233

GES233

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 ↩︎

Showing Posts 1 to 4

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:

BartOtten

BartOtten

To extend @LostKobrakai’s advice since you discovered Routex:

Routex gets the locale information from a configurable variety of sources. This allows navigation with the proper locale every time. When fully loading a page using navigate, the whole page is re-rendered so all is good by default. When using patch you might need some cheats.

 <%!-- Because we use `patch`, we need a workaround to force LV to refresh this element
        entirely as it contains Gettext translated strings in template (so their changes are not tracked) --%>

<%= Gettext.with_locale(@runtime.language, fn -> %>
  <h1>Localization</h1>
  <p>
    Routex' Localization extensions are highly customizable. This example page does not even scratch
    the surface of what is possible. Here are a few notable features making them unique:
  </p>
<% end %>

Or to force update links on navigation when using ‘patch’:

  # @url in the navigate links forces them to update when switching region
  # using '<.link patch={~p"/my/url"}`. It is adviced to use 'navigate' instead
  # of `patch` to simply full refresh the page when switching regions."
 <li><.link navigate={@url && ~p"/localize"} class="btn btn-ghost">Localize</.link></li>
 <li><.link navigate={@url && ~p"/verified"} class="btn btn-ghost">Verified Routes</.link></li>
 <li><.link navigate={@url && ~p"/cloak/products"} class="btn btn-ghost">Cloak</.link></li>
— All posts loaded —

Where Next? Top

Trending in Questions Top

stjefim
Hello! Suppose you are building workflow (order / task / payment) processing system with the following requirements: Each workflow con...
New
jonnycharles
I’m in search of an Elixir library that offers PDF generation capabilities similar to Ruby’s Prawn. While there have been discussions abo...
New
spammy
I’m looking to build a personal workflow to quickly deploy web applications written in elixir/phoenix, for local consumption (ie not on t...
New
dli
Before I dive in myself, did anyone successfully sprinkle Hologram into their existing LiveView app? Looking for hints regarding: Addi...
New
roeland
Kia ora, We have been using elixir-google-api to connect to Google Drive. However, with the updates to Tesla due to CVEs this is now bro...
New
bottlenecked
Hi all, I wanted to ask how the community is dealing with post-release steps. Today we have Ecto migrations, which make sure that the db...
New
rahultumpala
Hello, I have an Elixir backend that implements a custom protocol over TCP. I want to load test the backend and assess the performance o...
New

Other Trending Topics Top

JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
mcass19
ExRatatui lets you cook up rich terminal UIs in Elixir, powered by Rust’s ratatui via Rustler NIFs. Build interactive terminal applicatio...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge &amp; Solve. They are GUI (Emerge) and State management (S...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New
ausimian
Emily is an Elixir library that runs Nx computations on Apple’s MLX. Install it as the default Nx backend and Nx, defn, Axon, Nx.Serving,...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews