How to force reevaluation of EEx tags in live view (for live updating gettext)

Hi all!

In our current project, we want to be able to live update all translated strings on the page (using gettext) as soon as the user selects a new language.
The event is handled using handle_event which calls Gettext.put_locale/1 as well as assigns the locale to the socket (assign(socket, locale: locale)).

Unfortunately, the translated strings on the page do not update for regular <%= gettext "foo" %> tags in EEx live templates.
We currently know of two workarounds that we can apply in our application to get translated text to update immedately:

  1. Wrap all content in the template in Gettext.with_locale(fn -> ... end). This has the major drawback that it prevents us from using live_component anywhere within that function call, as Live View doesn’t manage the elements anymore.

  2. Access assigns within tags that have translated text, e.g. <%= assigns && gettext "foo" %> or <%= gettext "foo", locale: @locale %>. Unfortunately, this approach seems cumbersume, repetitive and (in the second variant) requires us to pass @locale down to all views/components.

Thinking about there workarounds, I seemed to me as if gettext itself is working perfectly fine, but liveview doesn’t give it a chance to update if used in the simple form without any access to assigns.

After digging in the liveview source code, I found that tags are analyzed to find out whether they need to be re-rendered for changed assigns.
Trying to replicate the “any access to assigns will cause a rerender” situation, I tried to create a macro like this:

defmacro trans(msgid) do
  quote do
    var!(assigns) && gettext(unquote(msgid))
  end
end

and use it as <%= trans("foo") %>, but that still doesn’t update when it should.
I suspect that the macro is not expanded when the EEx template is compiled for live view and the access to assigns is thus not recognized.

What I found to be working well, but which requires source code modification in phoenix_live_view, is adding defp classify_taint(:gettext, _), do: :always to https://github.com/phoenixframework/phoenix_live_view/blob/900b151809ec4064f58e763c45c3906a5ff57ec2/lib/phoenix_live_view/engine.ex#L899

I’d like to hear some opinions of what would be a good solution to the problem we’re currently facing. I do understand that not all users “need” live updating translations on their page and always re-evaluating tags containing gettext calls incurs a performance penalty when compared to evaluating them just once.
Would it make sense to allow configuring custom “signatures” in config.exs to cater to such problems where live view can not correctly infer which tags need recomputation?

2 Likes

This is likely the way to go if you like to have proper change tracking. Needing to pass @locale everywhere just makes the dependency on it explicit. It has been there been there before.

Thanks for your response. However, I kind of disagree.

Gettext has already made the decision that locale doesn’t need to be passed in to any of the public methods (the private lgettext/4 is the only exception), but that it’s read from Process instead.
I don’t think that the circumstance that gettext is now used in live view (which is a pretty common use case IMO) changes the way developers expect to use the gettext functions.

1 Like

I can see your point, but gettext was not developed to deal with updates/rerendering. That is a thing LiveView brought to the table and LiveView handles. There might be a time when LiveView has integration with gettext to do what you’d expect it to do, but until then explicitly handling it is in my opinion the way to go.

This is not even the only place LiveView brought limitations. E.g. the same is true with @key vs. assigns[:key]. Before LiveView both were equally valid code in templates, but with LiveView only the first option does work with change tracking.

1 Like

Hm, I just tried it out and both assigns.locale and assigns[:locale] where updated correctly. Maybe your statement was true for an earlier version of live view?

Anway. What I would also “accept” as an solution is the explicit and active invalidation of all previously rendered tags. This way, live view could still be optimized as-is, but on such occasions where a value outside of assigns is modified that still has an effect on the rendered views, one could call sth. along the lines of force_rerender(socket) to cause a complete reevaluation of all tags.

This thread is, in fact, meant as a discussion about improving live view (if that’s not too presumptuous), not only about solving our specific problem :slight_smile:

3 Likes

They update, but they update on any assigns change not just on a change for @key.

force_rerender(socket) sounds great.

It turns out that my first (and probably hacky) attempt to force a full rerender immediately worked:

socket = %{socket | fingerprints: Phoenix.LiveView.Diff.new_fingerprints()}

However, I’d love some input from the people involved with LiveView about whether this would cause any unexpected effects regarding mounted components etc.

2 Likes

Hello,

The following doesn’t work for me:

# In the view
<button phx-click="set_en">English</button>
# In the handler
def handle_event("set_en", _, socket) do
  Gettext.put_locale(MyApp.Gettext, "en")
  socket = %{socket | fingerprints: Phoenix.LiveView.Diff.new_fingerprints()}
  {:noreply, socket}
end

In fact I get {nil, %{}} for the result of new_fingerprints().

Also I wasn’t able to find any documentation about The Diff module and that new_fingerprints() function.

Do you have any documentation or even some code to share?
Thank you!

EDIT 1:
Anyway I tried the following which is working (very trivial though)

# The button to trigger the translation live update
<button phx-click="set_en">English</button>
# the gettext calls in the views
<%= @x && gettext "Hello" %>
# In the handler (x was set to 0 in mount())
def handle_event("set_en", _, socket) do
  Gettext.put_locale(MyApp.Gettext, "en")
  {:noreply, update(socket, x, &(&1 + 1))}
end

EDIT 2:
The following make it even a little cleaner:

# in the handler
def handle_event("set_en", _, socket) do
  Gettext.put_locale(MyApp.Gettext, "en")
  {:noreply, force_rerender(socket)}
end
defp force_rerender(socket, fingerprint \\ :x) do
    update(socket, fingerprint, &(&1 + 1))
  end

Default to :x (as a single character to clutter a little less the view)…