# How AshGettext works

This page explains what AshGettext does when you translate text, when a
resource compiles and when an error is raised, and why it has the limits it
has.

## When you translate text

`AshGettext.translate/5` takes the language as an argument. It runs Gettext's own `dgettext` with the backend, the
text domain, the text and the values you pass. It sets the language for that
one call with `Gettext.with_locale/3`, so the process's own language is left
as it was. `translate_plural/7` does the same with `dngettext`.

Gettext returns the text unchanged when it has no translation. In any
language but your own, AshGettext reports that as missing: the
`_or_missing` functions use it, and the plain
functions send a `[:ash_gettext, :translation, :missing]` telemetry event.

Attribute labels are lookups like any other. The key is the text you wrote in
the resource's `gettext` block, and the text domain defaults to `"ash"`.

`use AshGettext.Enum` checks that every value has a written label, then makes an
`Ash.Type.Enum` with the same values. It overrides Ash's `label/1` and
`description/1`: they call Ash's version for the source text, then looks that
text up with `AshGettext.translate/5` in Gettext's current locale. The key is
the label or description as written. A value with no written label fails to
compile.

## When a resource compiles

The `AshGettext.Resource` extension runs a transformer,
`AshGettext.Resource.Transformers.AttachErrorHandler`. It goes through the
resource's actions and sets the `error_handler` of each create, update and
destroy action to `AshGettext.ErrorTranslation.ErrorHandler.translate_error/2`.

- An action that already has an `error_handler` is left alone.
- Read and generic actions are skipped, because Ash has no `error_handler`
  option for them.

Nothing else about the resource changes. No attribute, action or policy is
rewritten.

## When an error is raised

Ash calls the handler once for each error an action collects. The handler does
three things:

1. It reads the domain's settings with `AshGettext.Info.settings/1`. If the
   domain has no `gettext` block, or `translate_errors?` is `false`, the
   error is returned as it is.
2. It works out the language. It asks the domain's `locale_resolver` with
   the action's actor, and cuts the answer to a language the backend has.
   With no actor or no answer it uses Gettext's current language. It never
   changes the process language.
3. It looks the error's `message` up in the backend, under the domain's
   `text_domain`, with the error's `vars` as values.

If the catalog has an entry, the error comes back with the translated message.
If it has none, the error comes back unchanged.

An Ash error with no message, such as a missing required value, takes its
sentence from `AshGettext.AshMessages` instead. It comes back as an
`AshGettext.AshMessages.Translated` that holds the translated sentence and
keeps the original error.

## Why transport does not matter

The handler changes the error struct itself. JSON:API and GraphQL
both render that same struct, so each of them shows the translated message
with no code of its own. An error with nested `errors`, such as an
`Ash.Error.Invalid`, is translated inside as well as at the top.

## The source language

The backend has a source locale: its default locale, `en` unless you set
another. In that locale the msgid is the text, so a lookup always counts as
found, and you keep no catalog for it. In any other locale, an entry that is
missing is reported as missing. `AshGettext.translate_or_missing/5` shows the
difference.

## Why the settings live in the domain

The `gettext do` block is in the domain, not in `config.exs`. Application
config is shared by the whole node, so a test that changes it cannot run in
parallel with another. A domain's settings are read from the compiled module
every time, so they are the same in every process and every test can be
`async: true`.

The list of supported locales is not a setting. It is read from the backend's
catalogs, so it cannot disagree with the files on disk.

## Where the code is

| Module | Job |
|---|---|
| `AshGettext.Domain` | The `gettext` block and its verifier. |
| `AshGettext.Resource` | The extension that attaches the handler. |
| `AshGettext.ErrorTranslation.ErrorHandler` | The `error_handler`. Translates in the action's language. |
| `AshGettext.ErrorTranslation` | Translates an error struct. Pure: no action, no database. |
| `AshGettext.Info` | Reads the `gettext` block. `language/2` asks the resolver, then normalizes. |
| `AshGettext.AshMessages` | The written sentences for Ash's own errors that have no message. |
| `AshGettext.Enum` | `use` it instead of `Ash.Type.Enum` to translate `label/1` and `description/1`. |
| `AshGettext` | Looks text up in the backend: strings, plurals, attribute labels. |

`AshGettext.ErrorTranslation.translate/2` takes everything it needs as
arguments, so you can call it yourself.

## Limits

- **Text passed to `translate/5` is not extracted.** `mix gettext.extract`
  does not see it. Add it to the template by hand. Enum labels and attribute labels
  are extracted.
- **Only write actions are translated.** Ash has no `error_handler` for reads
  or generic actions. Their errors reach the caller as written.
- **A handler you wrote is kept.** If an action declares its own
  `error_handler`, AshGettext does not replace it.
- **The message must be a constant string.** A message built with `#{...}`
  has no stable msgid. `mix gettext.extract` warns about these.
- **An entry that equals its msgid counts as missing.** In a non-source
  locale, a translation that is identical to the msgid cannot be told apart
  from a miss.
- **A resource needs the extension.** The domain block alone translates
  nothing. Set `require_resource_extension?` to find the resources that
  forgot.
