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.
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.
use shipping/types { Formality }/// Subject of the email sent when a parcel leaves the warehouse.type OrderShipped = Message { formality: Formality name: Text}
/// How many parcels an order has, on the tracking page.type Parcels = Message { count: PluralCount}
/// 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.
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):
use orders/types { OrderAmount }/// What an order comes to, on its confirmation page.type OrderTotal = Message { total: OrderAmount}
translations { order_total: "Your order comes to {total, number, currency}."}
translations { order_total: "Tu pedido suma {total, number, currency}."}
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):
use orders/types { DiscountRate }/// The discount on an order, on its confirmation page.type OrderDiscount = Message { reduction: DiscountRate}
translations { order_discount: "You saved {reduction, number, percent}."}
translations { order_discount: "Ahorraste {reduction, number, percent}."}
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
PluralCounttakes any value built onInt; - a text field takes a value built on
TextorPresentationText; - a number field takes a value built on
IntorDecimal.
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).
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.
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.