Failures for client apps
On this page
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) 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). 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.
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 }}
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:
textis the wording in{field}syntax, a literal brace doubled as in a language file.formatslists only the placeholders the wording writes that the client must format, each as one ofnumber,decimal,date,date_timeorduration.
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):
{ "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:).
Jadpo's own failures (invalid_input, rate_limited and the rest) are
worded with Jadpo's own messages
(Jadpo's 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.
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.
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 | the messages in Built-in emails |
| Magic-link routes | the app writes its own magic-link routes (generate_routes: false, Magic links) |
sign_in_return_invalid |
| Passkeys | the app signs users in with passkeys | 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) | 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). The messages
of privacy requests (Data requests)
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),
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:
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" } }}
Markdown for agents: /docs/failures-for-client-apps.md.Built from Jadpo main at a707efb.