# Failures for client apps

A client app can show a failure to its user in the user's language. Word
the failure in its domain's language files under its code (its `code:`
when it has one), as you would a message. Its `client: { }` fields are the
placeholders, and `kind:` and `message:` are not. The wording is never
written on the failure type itself, so the same rule holds for all text,
Jadpo's and packages' failures included. Names are unique across the
project, so a failure and a message never share a key.

Every failure that can reach a response, which is any failure some route's
`fails` lists anywhere in the project, must have wording in every supported
locale. Any other failure may be worded, and needn't be. A client field no
wording writes isn't an error: the client gets it in `details` anyway. The
not-found failure the compiler provides for an entity
([The not-found failure](the-operations-every-entity-gets.md#the-not-found-failure)) is
worded in the entity's domain, as one the project declares is. A project
that declares no `locales { }` words no failures.

A client field that is a date, a time or a number is sent as a value (an
`Instant` stays an instant on the wire,
[Output and status](output-and-status.md)). The client formats it
for its own language before filling the placeholder. So, unlike a
message's, a failure's date or time field can be a placeholder. The field's
type decides how the client formats it:

| Client field type | Format |
|---|---|
| `CalendarDate` | `date` |
| `Instant` | `date_time`, shown in the reader's own zone by the client |
| `ZonedTime` | `date_time`, in the zone it carries |
| `Duration` | `duration` |
| `Int` | `number` |
| `Decimal` | `decimal` |
| `Text`, `PresentationText`, `Uuid`, `Email`, `Url`, `IpAddress` | none: written as it is |
| A type the client cannot format (an object, a list, an enum, `T?`) | `translation.field_type`, as for a message's placeholder |

A type built on one of these is formatted as the one it is built on.

```jadpo
use booking/types { SeatCount, SlotTime }

/// The slot was taken while the booking was made.
type SlotTaken = Failure {
    kind: FailureKind.conflict
    client: {
        next_free: SlotTime
        seats_left: SeatCount
    }
}
```

```jadpo
translations {
    slot_taken: "Ce créneau vient d'être pris. Le prochain est {next_free}, avec {seats_left} places."
}
```

## The file for client apps

`jadpo check` writes, beside each API's OpenAPI document in
`build/openapi/`, one file per supported locale named
`<api>.failures.<locale>.json` (`main.failures.fr_fr.json`), for client
apps to show. It maps each failure code to an object of the same shape,
with its keys sorted as in every generated JSON file, so a client has one
way to read it:

- `text` is the wording in `{field}` syntax, a literal brace doubled as in
  a language file.
- `formats` lists only the placeholders the wording writes that the client
  must format, each as one of `number`, `decimal`, `date`, `date_time` or
  `duration`.

A locale in progress is sent what the server would write for it, its
regional sibling's wording and then the default's, so a client has text for
every code. The example above writes this for `fr_fr` (only `slot_taken`'s
entry is shown; the file also holds Jadpo's own failures, below):

```json
{
  "slot_taken": {
    "formats": {
      "next_free": "date_time",
      "seats_left": "number"
    },
    "text": "Ce créneau vient d'être pris. Le prochain est {next_free}, avec {seats_left} places."
  }
}
```

The server's `message` stays fixed English, for developers and logs
([`message:`](declaring-a-failure.md#message)).

Jadpo's own failures (`invalid_input`, `rate_limited` and the rest) are
worded with Jadpo's own messages
([Jadpo's and packages' text](failures-for-client-apps.md#jadpos-and-packages-text)), and join every
API's file. An API gets the ones its routes can meet: the request failures
for any API with routes, and the failures of signing in for one with a
route that needs a caller. A Jadpo failure's text is your app's root
wording for the locale, else what Jadpo ships in it, else the in-progress
fallback. Your app's own failure under the same code is sent in place of
Jadpo's. So an API with routes has a file for every supported locale,
whether or not the app declares failures of its own.

```jadpo
translations {
    jadpo: {
        deadline_exceeded: "Cela a pris trop de temps, et rien n'a été enregistré. Réessayez."
        idempotency_key_reused: "Cette requête a déjà été envoyée avec d'autres détails. Recommencez."
        internal_fault: "Un problème est survenu de notre côté. Réessayez bientôt."
        invalid_input: "Certaines informations envoyées ne sont pas valides. Vérifiez-les et réessayez."
        invalid_request: "La requête n'a pas pu être lue. Réessayez."
        method_not_allowed: "Cette adresse ne permet pas cette action."
        rate_limited: "Trop de requêtes. Patientez un instant, puis réessayez."
        request_too_large: "Ce qui a été envoyé est trop volumineux. Envoyez moins et réessayez."
        route_not_found: "Nous n'avons pas trouvé ce que vous cherchiez."
        unavailable: "Un service dont dépend l'application est indisponible pour le moment. Réessayez bientôt."
    }
}
```

## Jadpo's and packages' text

Text Jadpo writes itself, its built-in emails and its failures' wording, is
Jadpo's own messages. (Jadpo generates endpoints only, never pages.) Jadpo
ships their wording in `en_gb` and `en_us`, and later releases add locales.

An app supporting any other locale words them itself. Until it does, `jadpo
check` fails with one error for each such locale,
`translation.jadpo_missing`, pointing at the locale in `supported:` (or at
the missing file's name), whose reason lists every missing message. An
`in_progress:` locale falls back instead. The default locale is held to the
same rule: if Jadpo doesn't ship it, the app words the messages itself.
`translation.field_unused` doesn't apply to Jadpo's messages.

Only the messages of features your app has switched on count. Rewording a
message of a feature that is off, such as magic-link wording without
`magic_link:`, is an error saying the feature is off
(`translation.feature_off`). `jadpo translations builtin` lists only the
switched-on features' messages, with their fields.

Every Jadpo message may use `{app_name}`, the application's
`display_name:`. An app that needs the name translated rewords the
messages. The magic-link email's messages are listed in
[Built-in emails](built-in-emails.md).

Each of Jadpo's messages has a key, a kind (a message, a failure or an
enum), the feature that switches it on, the fields it may write besides
`{app_name}`, a description for translators and the wording Jadpo ships.
Code can't call them: `translations.jadpo` isn't a scope code can call. An
email template's `{{ locale.direction }}` writes the wording of Jadpo's
`TextDirection` enum.

A feature is switched on by what the project has. A message's name is its
key in the `jadpo: { }` group, and a failure's is its code:

| Feature | On when | Its messages |
|---|---|---|
| Requests | an API serves routes | `invalid_request`, `invalid_input`, `route_not_found`, `method_not_allowed`, `request_too_large`, `idempotency_key_reused`, `rate_limited`, `unavailable`, `internal_fault`, `deadline_exceeded` |
| Signing in | a route needs a caller | `authentication_required`, `invalid_credentials`, `ambiguous_credentials`, `not_permitted`, `authentication_unavailable` |
| Signing users in | an API accepts a browser session or an API token | `user_disabled` |
| Signing services in | an API accepts a service key | `service_disabled` |
| Browser sessions | an API accepts browser sessions and a route that needs a caller writes | `csrf_rejected` |
| Locales | the app declares `locales { }` | `text_direction`, the wording of `TextDirection` |
| Magic links | the app signs people in with [magic links](magic-links.md) | the messages in [Built-in emails](built-in-emails.md) |
| Magic-link routes | the app writes its own magic-link routes (`generate_routes: false`, [Magic links](magic-links.md)) | `sign_in_return_invalid` |
| Passkeys | the app signs users in with [passkeys](passkeys.md) | `passkey_challenge_expired`, `passkey_rejected`, `passkey_already_registered`, `passkey_limit_reached`, `passkey_not_found` |
| External identity tokens | an API accepts external identity tokens ([Ways to sign in](ways-to-sign-in.md)) | `authentication_misconfigured` |

The failures of signing in that only some methods can give
(`user_disabled`, `service_disabled`, `csrf_rejected`) are on only where an
API accepts those methods, by the same rule as the OpenAPI document's
responses. An API's file for clients carries the Jadpo failures of its own
features, and the application needs those of all its APIs together.

The wording for an idempotency key whose first request is still running
(`conflict`, 409) is your app's: `conflict` is a code an app declares too,
so Jadpo doesn't word it.

The twelve magic-link messages are on once `magic_link:` is declared
([Built-in emails](built-in-emails.md)). The messages
of privacy requests ([Data requests](data-requests.md))
join the catalogue with that feature.

`TextDirection`'s wording, `ltr` and `rtl`, is the same HTML values in every
language, so Jadpo ships it in every locale and no app locale has to word
it. An email template's `{{ locale.direction }}` writes it.

## A library package's text

A library package, one that adds code to your app rather than calling an
outside service ([Kinds of package](kinds-of-package.md)),
carries translations as a domain does: a `translations/` folder with its
message types, its failures' wording and one `<locale>.jadpo` per language
it supports. Your app calls a package's message as
`Postcodes.translations.<message>(...)`. A provider package shows users no
text, because its failures are always mapped to the app's own, so it
carries none.

## The root wording file

The root `translations/<locale>.jadpo` holds your app's wording for text it
didn't write, grouped by whose text it is: `jadpo: { }` for Jadpo's own, and
the package's name for a package's. This is the one place a language file
has groups, and each group is checked against its owner's message and
failure types. A member outside any group is `translation.wording_shape`,
and a group that names no owner is `translation.group_unknown`.

The root folder holds language files and nothing else
(`layout.wrong_folder` for any other declaration), and a root file with no
`locales { }` in the project is `translation.locales_required`.

A wording of a Jadpo message is checked as any wording is: it may write
only `{app_name}` and the message's own fields. `text_direction`, Jadpo's
wording of its `TextDirection` enum, has one wording for `left_to_right` and
one for `right_to_left`.

Here the booking app above rewords what Jadpo ships in English and gives
`text_direction` its wording:

```jadpo
translations {
    jadpo: {
        rate_limited: "Steady on: {app_name} is busy. Try again in a minute."
        text_direction: {
            left_to_right: "ltr"
            right_to_left: "rtl"
        }
    }
}
```
