This page covers translating your own text: enum values, strings, placeholders, plurals and attribute names.

The text a user reads is written by a person. AshGettext never makes it from a code name, so :awaiting_pickup is not turned into "Awaiting pickup" for you.

Translate an enum

use AshGettext.Enum takes the values and makes the Ash enum for you:

defmodule MyApp.Order.Status do
  use AshGettext.Enum,
    backend: MyApp.Gettext,
    values: [
      placed: [label: "Order placed"],
      awaiting_pickup: [label: "Ready for pickup", description: "Ready at the pickup point"]
    ]
end

MyApp.Order.Status.label(:awaiting_pickup)
#=> "Ожидает выдачи"

label/1 and description/1 are Ash's own functions. They now return the translated text. :backend and :values are required. :text_domain is the catalog file and defaults to "ash".

The key is the label: you wrote on the value. Every value needs one: a value without it fails to compile, and the error lists those values. A description is optional, and its key is the description:

# priv/gettext/ru/LC_MESSAGES/ash.po
msgid "Ready for pickup"
msgstr "Ожидает выдачи"

msgid "Ready at the pickup point"
msgstr "Готов к выдаче в пункте"

The language is Gettext's current locale for the process:

test "the status reads in Russian" do
  Gettext.put_locale(MyApp.Gettext, "ru")

  assert MyApp.Order.Status.label(:awaiting_pickup) == "Ожидает выдачи"
  assert MyApp.Order.Status.description(:awaiting_pickup) == "Готов к выдаче в пункте"
end

With no translation, you get the label as written. A value without a description still returns nil, and so does an empty value (nil). details/1 is Ash's own and is not translated.

Show an enum on a record

Every attribute whose type uses AshGettext.Enum gets a field named <attribute>_label. Load it to get the translated label on the record:

order = Ash.get!(MyApp.Order, id, load: [:status_label], actor: user)

order.status        #=> :awaiting_pickup
order.status_label  #=> "Ожидает выдачи"

An empty value gives nil, a list attribute gives a list of labels, and a field you already named status_label is left alone.

The text is the key

Write the text in your app's own language. Gettext looks it up as written:

AshGettext.translate(MyApp.Gettext, "default", "Order shipped", [], "ru")
#=> "Заказ отправлен"
# priv/gettext/ru/LC_MESSAGES/default.po
msgid "Order shipped"
msgstr "Заказ отправлен"

In your own language the text is also the translation, so that language needs no .po file. Your own language is the backend's default locale, en unless you set another.

With no translation, translate/5 returns the text as you wrote it.

Choose the catalog file

The text domain, the second argument, is the name of the .po file. "default" reads default.po. Use another name to keep a part of the app in its own file:

AshGettext.translate(MyApp.Gettext, "shipping", "Order shipped", [], "ru")
# reads priv/gettext/ru/LC_MESSAGES/shipping.po

Put values in the text

Write %{name} in the text and pass the values:

AshGettext.translate(MyApp.Gettext, "default", "Hello, %{name}", [name: "Ann"], "ru")
#=> "Привет, Ann"
msgid "Hello, %{name}"
msgstr "Привет, %{name}"

Do not build the text with #{...}. The key has to be the same every time.

Translate a count

translate_plural/7 takes the singular, the plural and the count. The count is always available as %{count}:

AshGettext.translate_plural(MyApp.Gettext, "default", "%{count} parcel", "%{count} parcels", 3, [], "ru")
#=> "3 посылки"
msgid "%{count} parcel"
msgid_plural "%{count} parcels"
msgstr[0] "%{count} посылка"
msgstr[1] "%{count} посылки"
msgstr[2] "%{count} посылок"

Each language has its own number of plural forms. Gettext picks the right one.

Show an attribute name

Write the text in the resource's gettext block:

gettext do
  label :date_of_birth, "Date of birth"
end

Then ask for it:

test "the attribute name reads in Russian" do
  Gettext.put_locale(MyApp.Gettext, "ru")

  assert AshGettext.attribute_label(MyApp.Order, :date_of_birth) == "Дата рождения"
end

The language is Gettext's current one; the backend and text domain come from the resource's domain.

# priv/gettext/ru/LC_MESSAGES/ash.po
msgid "Date of birth"
msgstr "Дата рождения"

The key is the text you wrote. An attribute with no written label raises ArgumentError, and so does one the resource does not have. A label for an attribute the resource does not have fails the build.

Know when a translation is missing

translate/5 falls back to the text, so a missing translation looks like a working one. Two ways to find out.

Every miss sends a telemetry event, [:ash_gettext, :translation, :missing], with the backend, text_domain, msgid and locale in its metadata. Log it, or count it:

:telemetry.attach(
  "ash-gettext-missing",
  [:ash_gettext, :translation, :missing],
  &MyApp.Telemetry.log_missing/4,
  nil
)

Or ask in code. The pure functions have an _or_missing twin that tells you:

AshGettext.translate_or_missing(MyApp.Gettext, "default", "Order lost", [], "ru")
#=> {:missing, "Order lost"}

The twins are translate_or_missing/5, translate_plural_or_missing/7 and attribute_label_or_missing/2. They send no event. In your own language nothing is missing. To test, see How to test with AshGettext.

Get the language from the user

Run an action with actor: and loaded _label fields and errors come out in the actor's language. No action changes the process language. With no actor, and in code outside an action, use Gettext.with_locale/3. See Locales.

Add the text to the template

mix gettext.extract finds only Gettext macro calls. Text you pass to translate/5 is not one, so keep the text in a Gettext macro such as dgettext_noop/2.

Enum labels, enum descriptions and attribute labels are different: AshGettext shows them to mix gettext.extract, so they are collected. See Extracting messages.