How to handle i18n in channels?

Hey there,

I’m thinking about localize my messages send via channels (and push notifications on sns) and the question is how to use gettext for channel properly? I would like to use it in user_socket level when registering the connection.

2 Likes

You could have the client provide the locale, or if you are storing the locale for the user, you can grab it from storage based on the user id. I’ll assume the former:

in your layout:

<script>
  window.userLocale = "<%= assigns[:locale] || "en" %>
  window.userToken = "<%= assigns[:user_token] %>
</script>

In your app js

let socket = new Socket("/socket", {params: {locale: window.userLocale, 
                                             token: window.userToken})

In your UserSocket

def connect(socket, %{"token" => token, "locale" => locale}) do
  case Phoenix.Token.verify(socket, "user socket", token, max_age: 1209600) do
      {:ok, user_id} ->
        {:ok, socket |> assign(:user, user_id) |> assign(:locale, locale)}
      {:error, reason} ->
        :error
   end
end

in your channels:

def handle_in("foo", _params, socket) do
  {:reply, %{message: gettext(socket.assigns.locale, "foo"}, socket}
end
5 Likes

I (along with the rest of our team :slight_smile: have noticed that l10n is very much a roll-your-own-with-the-pieces-available thing with Phoenix apps currently. We’re also using channels in addition to the more “mundane” parts of Phoenix (and generally loving it …), but that was one thing that seemed to not come “out of the box” that was surprising … is there any plan on the horizon to “bake in” some l10n/i18n support? (I’ve looked through the github issues …)

There are obviously issues around persistence, but that could be provided by the application (if it indeed persists this information at all) …

Or is i18n considered too much of a “developers vary dramatically in opinion” type thing to consider adding it to the framework?

I just shudder slightly when I think of how often this exact type of code will be duplicated by Phoenix applications :wink:

3 Likes

Thanks both if you for replies. @chrismccord, I was thinking about Gettext.get_locale/1, but it seemed to not work in channels case. The simplest solution made my day, thanks a lot!

Btw, why using get_locale didn’t work in user_socket?

1 Like

Gettext.get_locale checks for the locale in the current (OTP) process. If it doesn’t exist, then it just returns the default (e.g. english, or whatever the default is otherwise set to in configuration) … AFAIK, UserSocket is its own OTP process, separate from the HTTP request that was used to fetch the HTML/JS content. So when it is created, if put_locale is not called, it won’t have one and just return the default.

From the docs:

Gettext stores the locale per-process (in the process dictionary) and per
Gettext module. This means that Gettext.put_locale/2 must be called in every
new process in order to have the right locale available in that process. Pay
attention to this behaviour, since not setting the locale with
Gettext.put_locale/2 will not result in any errors when Gettext.get_locale/1
is called; the default locale will be returned instead.

And since channels are shared by / independent from individual websockets, they also can’t get the locale directly with get_locale and need to check with the socket (after all … different users with different sockets may want different translations …)

… anyways, that’s my understanding of the cogs and wheels … @chrismccord will know far, far more authoritatively :slight_smile:

2 Likes

Yup, this is correct!

1 Like

Thanks again, @aseigo - perfect explanation :slight_smile:

1 Like

Somehow it looks like gettext/2 for locale and msgid in that order doesn’t work for me.

1 Like