Ash builds its error messages inside the action, so you cannot wrap them in a Gettext call yourself. This page shows how AshGettext translates them for you, which errors it covers, and how to turn it on one domain at a time.
Turn it on
Add the AshGettext.Domain extension and a gettext block to the domain:
defmodule MyApp.Shipping do
use Ash.Domain, extensions: [AshGettext.Domain]
gettext do
backend(MyApp.Gettext)
end
resources do
resource MyApp.Shipping.Parcel
end
endbackend is the only required option. See the
DSL reference for the rest.
Add the AshGettext.Resource extension to each resource:
use Ash.Resource,
domain: MyApp.Shipping,
extensions: [AshGettext.Resource]The domain block says how to translate. The resource extension turns it on for that resource's create, update and destroy actions.
mix ash_gettext.install makes both edits in every domain and resource. Run
it alone to see the edits, and with --apply to write them. If the app has
more than one Gettext backend, name one with --backend MyApp.Gettext.
See one error in two languages
create :create do
accept [:weight]
validate compare(:weight, greater_than_or_equal_to: 30),
message: "must weigh at least %{greater_than_or_equal_to} kg"
end# priv/gettext/ru/LC_MESSAGES/ash.po
msgid "must weigh at least %{greater_than_or_equal_to} kg"
msgstr "должен весить не меньше %{greater_than_or_equal_to} кг"Run the action with actor:. The error comes back in the actor's language:
MyApp.Shipping.Parcel
|> Ash.Changeset.for_create(:create, %{weight: 5}, actor: user)
|> Ash.create()
# {:error, %Ash.Error.Invalid{errors: [%{message: "должен весить не меньше 30 кг"}]}}With no actor, Gettext's current language is used, so wrap the call in
Gettext.with_locale/3. No action changes the process language. The
domain's resolver reads the language off the actor; see Locales.
A resolver must not query the database.
No handler on each action
Without AshGettext, each write action needs its own handler, and a new action ships untranslated if someone forgets it:
create :register do
accept [:email]
error_handler {MyApp.Errors, :translate, []}
endThe resource extension adds the handler to every create, update and destroy
action, including actions you add later. An action that declares its own
error_handler keeps it, and is not translated.
The message is the msgid
An error's message: is looked up in the catalog as written:
validate present(:email), message: "can't be blank"If the catalog for the reader's locale has an entry, the entry replaces the message. If it has none, the message is left as it was. Adding AshGettext can add translations. It never changes a message that has no entry.
Your own language needs no catalog, because the message is already the
text. The .po files hold only the other languages.
Ash's own errors
Some of Ash's errors have no message. Ash prints them from code names: a missing required value reads "attribute delivery_date is required". AshGettext gives each one a written English sentence and translates it:
| Error | Sentence |
|---|---|
Ash.Error.Changes.Required | is required |
Ash.Error.Changes.InvalidAttribute | is invalid |
Ash.Error.Changes.InvalidArgument | is invalid |
Ash.Error.Forbidden.Policy | You are not allowed to do this |
{:error, error} = Ash.create(MyApp.Order, %{}, actor: user)
error = hd(error.errors)
error.message #=> "обязательное поле"
Exception.message(error) # ends with "обязательное поле"
error.field #=> :delivery_date
error.original #=> %Ash.Error.Changes.Required{...}The error comes back as an AshGettext.AshMessages.Translated. It keeps
Ash's error in original, with its field and class. JSON:API renders the
original error with the translated sentence as detail.
mix gettext.extract adds these sentences to the template. A
policy with its own message keeps it, and that message is its msgid. A stale
record error comes from the data layer after the handler, so it is not
translated.
Put values in a message
Write %{name} in the message and pass the values with vars:
InvalidAttribute.exception(
field: :weight,
message: "must weigh at least %{min} kg",
vars: [min: 30]
)msgid "must weigh at least %{min} kg"
msgstr "должен весить не меньше %{min} кг"A message built with #{...} cannot be a msgid, because the catalog needs a
constant key. mix gettext.extract warns about each one so you can rewrite it
with %{name} and vars.
Know which actions are translated
Only create, update and destroy actions are translated. Ash gives those three
an error_handler and gives read and generic actions none. Errors from a
read or a generic action reach the caller as written. If you need a
translated read error, translate it where you render it, with
AshGettext.ErrorTranslation.translate/2.
Choose the catalog file
text_domain names the Gettext domain, which is the .po file the messages
live in. It defaults to "ash", the same catalog that holds enum labels and attribute
names:
gettext do
backend(MyApp.Gettext)
text_domain("shipping")
endMessages are now looked up in priv/gettext/<locale>/LC_MESSAGES/shipping.po.
Roll out one domain at a time
translate_errors? switches translation off for a domain that has a
gettext block:
gettext do
backend(MyApp.Gettext)
translate_errors?(false)
endIts resources keep their messages exactly as written. Remove the line when you are ready.
require_resource_extension? is the opposite check. When true, compiling
the domain warns about every resource that lacks the AshGettext.Resource
extension:
gettext do
backend(MyApp.Gettext)
require_resource_extension?(true)
endThe warning is an error under mix compile --warnings-as-errors. It is off by
default, because Ash extensions are opted into.
Return a coded error
AshGettext.Error is an error with a stable code the client can branch on
and a message that is translated:
{:error,
AshGettext.Error.exception(
code: :weight_too_low,
field: :weight,
message: "must weigh at least %{min} kg",
vars: [min: 30]
)}code is required. Leaving it out raises ArgumentError. field and vars
are optional. The message is translated like any other, so return the error
from a create, update or destroy action.
With :ash_json_api installed, this error renders as:
{
"code": "weight_too_low",
"detail": "must weigh at least 30 kg",
"source": {"pointer": "/data/attributes/weight"},
"meta": {"min": 30}
}code is the atom as a string, detail is the translated message and meta
holds vars. There is no title, so no untranslated English reaches the
reader. source.pointer appears only when field is set.