Jadpo is experimental and not ready for production. These docs follow the compiler's main branch as of 30 Oct 2026. What's built · Install 0.1.0
jadpo
Reference: Configuration and time

Configuration and time

Translations as messages

On this page

Text your server writes for people, such as emails, generated pages and a failure's text for client apps, is translated in Jadpo itself. Each message is a type, each language is a file of wordings, and both live in the domain that uses them.

Where translations live

A domain's translations live in domains/<domain>/translations/, as its emails live in emails/. A message a second domain uses moves to shared/translations/, as anything two domains use does (Sharing and promotion). You move it by hand for now: layout.promotion_required doesn't cover messages yet, so another domain's call of a message is visibility.private_cross_domain until you do.

The root translations/ folder holds only your app's wording for text it didn't write: Jadpo's own (Jadpo's and packages' text) and its library packages'. A .json file there is ignored, like any other file that isn't source.

text
domains/shipping/translations/    order_shipped.jadpo       type OrderShipped = Message { ... }    parcels.jadpo             type Parcels = Message { ... }    en_gb.jadpo               translations { ... }: the English wording    de_de.jadpo               translations { ... }: the German wording    sources.jadpo_record      generated: what each translation was made fromshared/translations/          messages several domains usetranslations/                 the app's wording for Jadpo's and packages' text only    fr_fr.jadpo               translations { jadpo: { ... } }: grouped by whose text it is    sources.jadpo_record      generated: what each of those was made from

Messages

A message is one piece of text to translate. It is a type built on Message, a type base like Failure, Event and EmailMessage, one per file in the domain's translations/ folder, the file named after the type (Files and names). Its fields are the wording's placeholders. Its /// description reaches translators as context when you export the messages (Tooling for translations). Message is a type base, so no type can be called Message, and message names are unique across the project, as a failure's or an email's are.

example
use shipping/types { Formality }/// Subject of the email sent when a parcel leaves the warehouse.type OrderShipped = Message {    formality: Formality    name: Text}
example
/// How many parcels an order has, on the tracking page.type Parcels = Message {    count: PluralCount}
example
/// When a delivery comes, on the tracking page.type DeliveryWindow = Message {    day: PresentationText    hours: PresentationText}

PluralCount is a built-in whole number that picks a plural form, such as "1 parcel" or "2 parcels". It takes a value built on Int, is used directly as a Message field, and is written as {count} in each form's wording. It has no use outside a message.

Languages

Each language is one translations { } declaration, with no name, in a file named after its locale (de_de.jadpo). Jadpo finds it by the file's name, as it finds config { }, and code never refers to it by name. translations is a reserved word, like config and locales.

A file words the messages, the emails and the enums of its own folder's scope: a domain's, or shared/'s. An enum is worded in the scope that declares it. Each member of the file is a message's, an enum's or an email's name in lower_snake_case (order_shipped is OrderShipped), as the message's file name and its call are (Naming). When two kinds share a name in one scope, the first keeps it, and the naming rules report the clash.

Every wording is a plain string whose only syntax is {field}. Write a literal brace twice: {{ writes { and }} writes }. There is no grouping inside a file: the domain is the group, and nesting is only for enum values and plural forms (the root file's groups by owner, below, are the exception). There is no message inside another. A value used again, such as a product name, is a placeholder, because languages whose words change form can't reuse a fragment.

example
translations {    order_shipped: {        formal: "Guten Tag {name}, Ihre Bestellung wurde versandt."        casual: "Hallo {name}, deine Bestellung ist unterwegs!"    }    parcels: {        one: "{count} Paket"        other: "{count} Pakete"    }    delivery_window: "Lieferung am {day}, zwischen {hours}."}

How a message's type maps to its wording in each language's file:

In the type In each language's file
The type's name, OrderShipped The member order_shipped, in the language files of the type's own translations/ folder
No fields A plain string
A Text, PresentationText, Int or Decimal field, or a named type built on Text, Int or Decimal A {field} placeholder; numbers are written the locale's way
A field built on Money A {field, number, currency} placeholder, written as text.format shows the amount in the locale; {field} alone is translation.placeholder_style
A field built on Percentage A {field, number, percent} placeholder, written as text.format shows the percentage in the locale; {field} alone is translation.placeholder_style
One enum field One member per value of the enum; the field is not a placeholder
One PluralCount field One member per plural form the language needs, from Unicode CLDR (English one and other; Polish one, few, many and other); {count} is available in each
Any other field type (Instant, an object, T?) translation.field_type: format it in code and pass the text
More than one enum or PluralCount field translation.too_many_selectors: split the message, so a translator never fills a grid of combinations

The plural forms are the locale's counting forms in CLDR, and no others. There is no exact =0 form: a language that says "no items" differently words it as its own zero form, where CLDR has one.

Dates, money and percentages in a message

Format dates and times in code, with text.format (Showing dates and times to people), and pass them in as PresentationText. An amount of money is a placeholder of its own, written by the same formatter as text.format, so the translator sees a price and each language writes it its own way (Money operations):

example
use orders/types { OrderAmount }/// What an order comes to, on its confirmation page.type OrderTotal = Message {    total: OrderAmount}
example
translations {    order_total: "Your order comes to {total, number, currency}."}
example
translations {    order_total: "Tu pedido suma {total, number, currency}."}
example
use orders/types { OrderAmount }test "a price is written each language's way" {    var total: OrderAmount = OrderAmount(money.from_minor(123450, Currency.mxn))    assert confirm_total(total, Locale.en_gb) == "Your order comes to MX$1,234.50."    assert confirm_total(total, Locale.es_mx) == "Tu pedido suma $1,234.50."}

A percentage is a placeholder of its own in the same way, written {rate, number, percent} (Prices, discounts and rates):

example
use orders/types { DiscountRate }/// The discount on an order, on its confirmation page.type OrderDiscount = Message {    reduction: DiscountRate}
example
translations {    order_discount: "You saved {reduction, number, percent}."}
example
translations {    order_discount: "Ahorraste {reduction, number, percent}."}
example
test "a discount is written each language's way" {    assert confirm_discount(12.5%, Locale.en_gb) == "You saved 12.5%."    assert confirm_discount(12.5%, Locale.es_mx) == "Ahorraste 12.5%."}

Using a message

The compiler generates one call per message, translations.<domain>.<message>(...). It takes every field by name, plus locale:, one of your app's supported locales. A value given without its name is the next field's, in the order the type declares them. The locale is always named.

Each field takes a value of its kind:

  • a field that picks the wording takes its enum;
  • a PluralCount takes any value built on Int;
  • a text field takes a value built on Text or PresentationText;
  • a number field takes a value built on Int or Decimal.

The call returns PresentationText, so the result can be shown but never stored. A message belongs to its domain: another domain may call it only when it is public. So a message two domains use lives in shared/translations/, and is called translations.shared.<message>(...).

In an email template the same call takes the email's locale, and is written without locale:: {{ translations.shipping.parcels(count: parcel_count) }} (Email).

example
use shipping/types { CustomerName, Formality, ParcelCount, ShippingText }/// What a customer is told about their order, in their language.action describe_order(    formality: Formality,    name: CustomerName,    count: ParcelCount,    reader_locale: Locale) -> ShippingText {    return ShippingText {        subject: translations.shipping.order_shipped(            formality: formality,            name: name,            locale: reader_locale        )        parcels: translations.shipping.parcels(count: count, locale: reader_locale)    }}

The text is the language's own. The plural form comes from CLDR's rules for the locale, and numbers are written with its digits and separators, from the same data as text.format. So the interpreter, a native deployable and WebAssembly write the same text.

example
use shipping/types { Formality }test "a message is worded by its enum value and its plural count" {    assert translations.shipping.order_shipped(        formality: Formality.formal,        name: "Ana",        locale: Locale.de_de    ) == "Guten Tag Ana, Ihre Bestellung wurde versandt."    assert translations.shipping.order_shipped(        formality: Formality.casual,        name: "Ana",        locale: Locale.en_gb    ) == "Hi Ana, your order is on its way!"    assert translations.shipping.parcels(count: 1, locale: Locale.en_gb) == "1 parcel"    assert translations.shipping.parcels(count: 1234, locale: Locale.en_gb) == "1,234 parcels"    assert translations.shipping.parcels(count: 1234, locale: Locale.de_de) == "1.234 Pakete"}test "a field given without its name is the next field" {    assert translations.shipping.delivery_window(        "Monday",        "9 to 12",        locale: Locale.de_de    ) == "Lieferung am Monday, zwischen 9 to 12."}

Markdown for agents: /docs/translations-as-messages.md.Built from Jadpo main at a707efb.