Jadpo is experimental and not ready for production. These docs follow the compiler's main branch as of 4 Nov 2026. What's built · Install 0.2.0
jadpo
Guides: all 21

Guides

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 /orders 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:

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/shipping/translations/parcels_on_the_way.jadpo:

jadpo
/// 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/shipping/translations/en_gb.jadpo:

jadpo
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/shipping/translations/fr_fr.jadpo:

jadpo
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:

shell
jadpo translations builtin
output
Requests  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/fr_fr.jadpo:

jadpo
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/shipping/types/browser_languages.jadpo:

jadpo
/// What a browser says its reader's languages are.type BrowserLanguages = Text {    max_length: 2000}

domains/shipping/types/tracking_request.jadpo:

jadpo
use shipping/types { BrowserLanguages }/// What the tracking page asks with.type TrackingRequest = Input {    languages: BrowserLanguages {        header: "Accept-Language"        optional: true    }}

domains/shipping/types/tracking_page.jadpo:

jadpo
/// What the tracking page shows.type TrackingPage = Output {    headline: PresentationText}

domains/shipping/actions/show_tracking.jadpo:

jadpo
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/shipping/routes/orders.jadpo, with the new route and imports:

jadpo
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/shipping/actions/show_tracking.test.jadpo:

jadpo
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"}
shell
jadpo test
output
tested: 2 passed, 2 action tests

Try 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:

shell
curl http://localhost:3000/orders/01a1a65c-e939-7db9-a6b9-1a29cd430671/tracking
text
{"headline":"2 parcels are on their way"}
shell
curl -H 'Accept-Language: fr-FR,fr;q=0.9' http://localhost:3000/orders/01a1a65c-e939-7db9-a6b9-1a29cd430671/tracking
text
{"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:

shell
curl -H 'Accept-Language: fr' http://localhost:3000/orders/00000000-0000-7000-8000-000000000000/tracking
text
{"error":{"code":"order_not_found","message":"Not found.","request_id":"req_d5cf3d967d337471"}}

build/openapi/main.failures.fr_fr.json (written by jadpo check) holds, among the others:

text
  "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:

shell
jadpo translations update
output
Wrote 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:

jadpo check

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 update recorded the en_gb wording this message's translation was made from, and the en_gb wording has changed since, so the translation may no longer say the same thing.
next
Reword parcels_on_the_way in domains/shipping/translations/fr_fr.jadpo to match the new en_gb wording, then run jadpo translations update (agent)

jadpo translations missing lists everything outstanding, by language file:

shell
jadpo translations missing
output
domains/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/translations/ 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.