Translate your app's text
You end with typed messages in two languages, chosen from the browser's header, with stale translations caught by jadpo check.
On this page
Text your server writes for people, such as a page's headline or a failure's wording, is translated in Jadpo itself. Each message is a type, each language is a file of wordings, and the compiler checks every call, every placeholder and every plural form against them, so a missing translation is a compile error, not a blank on a French customer's screen. This guide puts a parcel-tracking headline into English and French.
You start from an app with an Order entity holding how many parcels an order has,
and a POST / route to place one. Build an API shows how
to write those.
List your languages
locales names the languages your app supports, and the one to use when no other
is chosen. A locale is written Locale.<tag>, in lower case with _ for -, so
Locale.fr_fr is the French of France.
app.jadpo:
application {}locales { default: Locale.en_gb supported: [Locale.en_gb, Locale.fr_fr]}
Declare a message
A message is a type built on Message, one to a file in the domain's translations/
folder. Its fields are the wording's placeholders, and its / comment reaches
translators as context. PluralCount is a built-in whole number that picks the plural
form.
domains/:
/// How many of a customer's parcels are on their way, on the tracking page.type ParcelsOnTheWay = Message { count: PluralCount}
Word it in each language
Each language is a file named after its locale. Each entry takes the message's name in
lower_snake_case. A field of the message becomes a {field} placeholder, and a
PluralCount gives one member per form that language needs: English has one and
other, and French has one, many and other. Leave one out and jadpo check says
which.
order_not_found here is the wording of a failure, which needs the same care:
each failure a route can answer with is worded in every language.
domains/:
translations { order_not_found: "We can't find that order." parcels_on_the_way: { one: "{count} parcel is on its way" other: "{count} parcels are on their way" }}
domains/:
translations { order_not_found: "Nous ne trouvons pas cette commande." parcels_on_the_way: { one: "{count} colis est en route" many: "{count} de colis sont en route" other: "{count} colis sont en route" }}
Word Jadpo's own messages
Jadpo ships its own wording, such as "Too many requests", in en_gb and en_us only.
For any other language jadpo check fails with translation.jadpo_missing until your
app words them. This lists them, with the English to translate from:
jadpo translations builtinRequests invalid_request needs fr_fr The request couldn't be read. Try again. invalid_input needs fr_fr Some of the information sent isn't valid. Check it and try again. route_not_found needs fr_fr We couldn't find what you were looking for. method_not_allowed needs fr_fr That isn't something this address can do. request_too_large needs fr_fr What was sent is too big. Send less and try again. idempotency_key_reused needs fr_fr That request was already sent with different details. Start again. rate_limited needs fr_fr Too many requests. Wait a moment, then try again. unavailable needs fr_fr A service this depends on isn't available right now. Try again soon. internal_fault needs fr_fr Something went wrong on our side. Try again soon. deadline_exceeded needs fr_fr That took too long, and nothing was saved. Try again.Locales text_direction left_to_right ltr right_to_left rtl11 messages: 10 need wording, 2 featuresnext: jadpo check fails until the application words them
They go under jadpo: in a file in the root translations/ folder, which holds only
your app's wording for text it didn't write. text_direction needs no wording.
translations/:
translations { jadpo: { deadline_exceeded: "Cela a pris trop de temps, et rien n'a été enregistré. Réessayez." idempotency_key_reused: "Cette demande 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 demande n'a pas pu être lue. Réessayez." method_not_allowed: "Cette adresse ne permet pas de faire cela." rate_limited: "Trop de demandes. Attendez 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 cela dépend n'est pas disponible pour le moment. Réessayez bientôt." }}
Use the message
The compiler generates one call for each message,
translations.<domain>.<message>(...), which takes every field by name, plus locale:.
It returns PresentationText, so the result can be shown but never stored.
A browser says which languages its reader wants in its Accept-Language header.
locale.from_accept_language gives the best of your supported locales, or default:
when none match, and it never fails. A language the request names in its own input
would be a Locale field instead.
domains/:
/// What a browser says its reader's languages are.type BrowserLanguages = Text { max_length: 2000}
domains/:
use shipping/types { BrowserLanguages }/// What the tracking page asks with.type TrackingRequest = Input { languages: BrowserLanguages { header: "Accept-Language" optional: true }}
domains/:
/// What the tracking page shows.type TrackingPage = Output { headline: PresentationText}
domains/:
use shipping/entities { Order, OrderNotFound }use shipping/types { TrackingPage, TrackingRequest }/// The tracking page's text, in the reader's language.action show_tracking( order_id: Order.id, asked: TrackingRequest) fails OrderNotFound -> TrackingPage { var order: Order = attempt Order.get(order_id) var reader_locale: Locale = locale.from_accept_language(asked.languages) return TrackingPage { headline: translations.shipping.parcels_on_the_way( count: order.parcel_count, locale: reader_locale ) }}
domains/, with the new route and imports:
use shipping/entities { Order }use shipping/types { NewOrder, OrderPlaced, TrackingPage, TrackingRequest }use shipping/actions { place_order, show_tracking }/// Where customers follow their orders.routes Orders { prefix: /orders /// Places an order. POST / { authentication: RouteAuthentication.none input: NewOrder output: OrderPlaced status: SuccessStatus.created run: place_order(input) } /// An order's tracking page. GET /{order_id: Order.id}/tracking { authentication: RouteAuthentication.none input: TrackingRequest output: TrackingPage run: show_tracking(order_id, input) }}
Test it
domains/:
use shipping/types { NewOrder, OrderPlaced, TrackingPage, TrackingRequest }use shipping/actions { place_order }test "the page is in the language the browser asks for" { var order: OrderPlaced = place_order( NewOrder { parcel_count: 2 } ) var page: TrackingPage = attempt show_tracking( order.id, TrackingRequest { languages: "fr-FR,fr;q=0.9" } ) assert page.headline == "2 colis sont en route"}test "a language the app doesn't have falls back on the default" { var order: OrderPlaced = place_order( NewOrder { parcel_count: 1 } ) var page: TrackingPage = attempt show_tracking( order.id, TrackingRequest { languages: "ja" } ) assert page.headline == "1 parcel is on its way"}
jadpo testtested: 2 passed, 2 action testsTry it
Run jadpo dev, place an order with two parcels, then ask for its tracking page with
the id you got. With no header, you get the default language:
curl http://localhost:3000/orders/01a1a65c-e939-7db9-a6b9-1a29cd430671/tracking{"headline":"2 parcels are on their way"}curl -H 'Accept-Language: fr-FR,fr;q=0.9' http://localhost:3000/orders/01a1a65c-e939-7db9-a6b9-1a29cd430671/tracking{"headline":"2 colis sont en route"}Let client apps show failures
An error's message in a response is always the short English text. A client app
shows its reader the wording for the error's code, and jadpo check writes that for
each language beside the OpenAPI document:
curl -H 'Accept-Language: fr' http://localhost:3000/orders/00000000-0000-7000-8000-000000000000/tracking{"error":{"code":"order_not_found","message":"Not found.","request_id":"req_d5cf3d967d337471"}}build/ (written by jadpo check) holds, among the
others:
"order_not_found": { "formats": {}, "text": "Nous ne trouvons pas cette commande." },
Keep translations up to date
When someone changes the English, the French may no longer say the same thing. Jadpo catches that once you record what each translation was made from:
jadpo translations updateWrote domains/shipping/translations/sources.jadpo_record fr_fr.order_not_found recorded fr_fr.parcels_on_the_way recordednext: git diff shows what was recorded
Commit the record, which jadpo translations update writes for each folder and nobody
edits by hand. Now change "{count} parcel is on its way" in en_gb.jadpo, and
jadpo check stops with translation.stale:
error parcels_on_the_way in fr_fr was translated from en_gb wording that has since changed
--> domains/shipping/translations/fr_fr.jadpo:3:5
|3 | parcels_on_the_way: { | ^^^^^^^^^^^^^^^^^^ made from the old `en_gb` wording
- reason
jadpo translations updaterecorded theen_gbwording this message's translation was made from, and theen_gbwording has changed since, so the translation may no longer say the same thing.- next
- Reword
parcels_on_the_wayindomains/to match the newshipping/ translations/ fr_fr.jadpo en_gbwording, then runjadpo translations update(agent)
jadpo translations missing lists everything outstanding, by language file:
jadpo translations missingdomains/shipping/translations/fr_fr.jadpo parcels_on_the_way stale en_gb reworded since it was translated1 outstanding: 1 stale, 1 locale
Reword the French and run jadpo translations update again, and the check passes.
A language that isn't finished
A language still being translated is listed in supported: and also in
in_progress:, as in_progress: [Locale.pl_pl], in locales. It needs no wording for
every message: what it lacks falls back on the default language, and
jadpo translations missing lists what is left. Take it out of in_progress: once it is
done.
An enum's values can have wording of their own, so a template or message shows them in the
reader's language. Emails use these same messages, written without locale:, as
Email shows. To send wording to a translation service and
bring it back, see
Exchanging translations with a service.
Read more
Locales, Translations as messages, Enums with wording, Tooling for translations and Failures for client apps.
Not built yet
Moving a message to shared/ isn't built yet as a promotion, so
when a second domain needs a message, move it there by hand.
Markdown for agents: /guides/guide-translate-your-apps-text.md.Built from Jadpo main at a4c217c. Jadpo's CI compiles this guide's code and runs its tests.