Skip to content

To avoid unrecoverable crashes, live_toast must offer a way to not translate. #56

@marcandre

Description

@marcandre

Issue

live_toast can cause an unrecoverable crash from the server (SystemLimitError) when called with too many variable messages.

Example:

LiveToast.send_toast(:error, "Failed to send email to #{email}, got error #{error}")

There is no way that LiveToast can correctly translate this without the broader context.
Current implementation will call dgettext with an ever changing message, each creating an atom, eventually filling the atom table.

Solutions

In my opinion, it is not the duty of LiveToast to do translations. It's not in it's job description, it doesn't have the context, it should not even assume the app uses Gettext and the caller should do the translation however she sees fit before passing the translated message. Also, in the current way, the error messages won't extract automatically as nothing is done at compile time, which makes it harder to keep the translation tables up to date.

In my mind, the very best solution is for LiveToast to remove this "feature" altogether (in a major release).

Assuming the authors are very attached to including translations, one solution is to tweak the API to allow for message_params and title_params options, so the user can pass "Failed to send email to %{email}, got error %{error}", message_params: [email: email, error: error]. This will create a single atom for the message template and avoid the issue, Personally, I find this solution ugly. It's still not explicit, the messages to translate will still not be extracted automatically, and the caller can still make an error unknowingly. Imagine if she's not even using translations herself and has a unilingual app...

An alternative solutions would be to have a setting and/or an option that allows no translation of messages (nor title). Note that if it's just a setting, the end users must make the right choice from the start, otherwise they'll have to go over all their call points to add translation later to be able to change the setting globally.

Instead of an additional setting, LiveToast could ship with a trivial NoTranslate endpoint (or include an example in the doc) and mention it in the doc, along with the potential issue of using translations, but that seems ugliest. I'm including this as it is currently the only way out for current users of LiveToast (short of forking):

defmodule MyApp.Helpers.NoTranslate do
  @moduledoc """
  A no-op Gettext backend for LiveToast that returns messages unchanged.

  LiveToast unconditionally calls Gettext.dgettext() on all messages, which causes
  atom exhaustion when messages contain %{} interpolation patterns. This module
  bypasses translation entirely, returning messages as-is.

  We handle translation ourselves before passing messages to LiveToast.
  """

  @doc """
  Returns the message unchanged without any translation.
  This prevents Gettext from parsing interpolation patterns and creating atoms.
  """
  def dgettext(_domain, message, _bindings \\ %{}) do
    message
  end

  @doc """
  Implement __gettext__ to satisfy Gettext.Backend behaviour.
  LiveToast calls this to get the default locale.
  """
  def __gettext__(:default_locale), do: "en"
  def __gettext__(:interpolation), do: Gettext.Interpolation.Default

  # Implement all the Gettext backend functions that LiveToast might call
  def lgettext(_locale, _domain, _msgctxt, msgid, _bindings) do
    {:ok, msgid}
  end

  def ldgettext(_locale, _domain, msgid, _bindings) do
    {:ok, msgid}
  end

  def ldpgettext(_locale, _domain, _msgctxt, msgid, _bindings) do
    {:ok, msgid}
  end

  def dpgettext(_domain, _msgctxt, msgid, _bindings \\ %{}) do
    {:ok, msgid}
  end

  def gettext(msgid, _bindings \\ %{}) do
    {:ok, msgid}
  end

  def pgettext(_msgctxt, msgid, _bindings \\ %{}) do
    {:ok, msgid}
  end

  def ngettext(msgid, _msgid_plural, _n, _bindings \\ %{}) do
    {:ok, msgid}
  end

  def dngettext(_domain, msgid, _msgid_plural, _n, _bindings \\ %{}) do
    {:ok, msgid}
  end

  def pngettext(_msgctxt, msgid, _msgid_plural, _n, _bindings \\ %{}) do
    {:ok, msgid}
  end

  def dpngettext(_domain, _msgctxt, msgid, _msgid_plural, _n, _bindings \\ %{}) do
    {:ok, msgid}
  end
end

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions