Translating Ash Error messages

What are my options for translating error messages returned by actions? The first thing that comes to mind is translating the errors in the controller like this:
def create(conn, params) do
case Organizations.create_organization(params, scope: conn.assigns.scope) do
{:ok, _org} ->
conn
|> put_flash(:success, Feedback.org_created_successfully())
|> redirect(to: ~p"/")

{:error, errors} ->
translated_errors = translate_errors(errors)

conn
|> assign_errors(translated_errors)
|> redirect(to: ~p"/crear-empresa")
end
end
def create(conn, params) do
case Organizations.create_organization(params, scope: conn.assigns.scope) do
{:ok, _org} ->
conn
|> put_flash(:success, Feedback.org_created_successfully())
|> redirect(to: ~p"/")

{:error, errors} ->
translated_errors = translate_errors(errors)

conn
|> assign_errors(translated_errors)
|> redirect(to: ~p"/crear-empresa")
end
end
However, this feels repetitive and pollutes the controller. Is there a better way to handle this in Ash?
13 Replies
ZachDaniel
ZachDaniel4mo ago
Currently that is the best way TBH
kernel
kernel4mo ago
I personally use dggettext in my web layer not sure if it works for all errors, but it works for field errors pretty well they are already being passed through gettext anyway for the {min} {max} {count} stuff anyway
jart
jart4mo ago
we have definitely talked about adding gettext to Ash's error system but since neither of us speaks another language or has really built anything with translations in it we want to defer to experts
kernel
kernel4mo ago
I have a few apps in japanese, I've seen murmurings about how gettext isn't really that good anyway (from the cldr people), so maybe one day I'll change I don't know if it exists yet, but it would be useful to have a mix task that just extracts the strings from ash and dumps it into a pot file I've had to add the errors I see manually, which isn't the most comprehensive
Joan Gavelán
Joan GavelánOP4mo ago
Would you be so kind to share a code snippet showing how you are doing that? I planned to use gettext as well but I refuse to translate errors in my controller, at least for now @Zach Daniel How about modifying the error messages directly? Is that possible? I don't need to support multiple languages, just Spanish
kernel
kernel4mo ago
gettext is best option as the errors are hardcoded inside the respective ash error modules
kernel
kernel4mo ago
in errors.pot and then the standard phoenix liveview core_components translate_error thing should work
@doc """
Translates an error message using gettext.
"""
def translate_error({msg, opts}) do
# When using gettext, we typically pass the strings we want
# to translate as a static argument:
#
# # Translate the number of files with plural rules
# dngettext("errors", "1 file", "%{count} files", count)
#
# However the error messages in our forms and APIs are generated
# dynamically, so we need to translate them by calling Gettext
# with our gettext backend as first argument. Translations are
# available in the errors.po file (as we use the "errors" domain).
if count = opts[:count] do
Gettext.dngettext(YOURAPP.Gettext, "errors", msg, msg, count, opts)
else
Gettext.dgettext(YOURAPP.Gettext, "errors", msg, opts)
end
end

@doc """
Translates the errors for a field from a keyword list of errors.
"""
def translate_errors(errors, field) when is_list(errors) do
for {^field, {msg, opts}} <- errors, do: translate_error({msg, opts})
end
@doc """
Translates an error message using gettext.
"""
def translate_error({msg, opts}) do
# When using gettext, we typically pass the strings we want
# to translate as a static argument:
#
# # Translate the number of files with plural rules
# dngettext("errors", "1 file", "%{count} files", count)
#
# However the error messages in our forms and APIs are generated
# dynamically, so we need to translate them by calling Gettext
# with our gettext backend as first argument. Translations are
# available in the errors.po file (as we use the "errors" domain).
if count = opts[:count] do
Gettext.dngettext(YOURAPP.Gettext, "errors", msg, msg, count, opts)
else
Gettext.dgettext(YOURAPP.Gettext, "errors", msg, opts)
end
end

@doc """
Translates the errors for a field from a keyword list of errors.
"""
def translate_errors(errors, field) when is_list(errors) do
for {^field, {msg, opts}} <- errors, do: translate_error({msg, opts})
end
Basically
No description
kernel
kernel4mo ago
then you generate po files for whatever language you want
Joan Gavelán
Joan GavelánOP4mo ago
Oh yeah, problem is I'm not using LiveView components I'm using React with Inertia
kernel
kernel4mo ago
would be similar, get an i18n thing for react and generate translations there instead have to create a list once manually of the ash stuff and swap it out in react if you want to go even more budget then you don't even need gettext i18n, just have a function in react land that swaps keys assuming it will be similar to this https://medium.com/@ayatir04/laravel-inertia-js-localization-without-packages-6e7f49fb07c
Joan Gavelán
Joan GavelánOP4mo ago
Yeah that would be another option. Just wanted to see if there was a more "ergonomic way" to do this directly from Ash. So everything is sent to the frontend ready for rendering
kernel
kernel4mo ago
well it's the view layer, so just use gettext on the server to send translations. I've never used inertia so no idea how it all plumbs together maybe register a before_send thing on Plug.Conn and do stuff there so you only define it once
Joan Gavelán
Joan GavelánOP4mo ago
Will look into all that Thanks a lot for your suggestions. I appreciate it.

Did you find this page helpful?