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

Locales

On this page

A locale is a language as a place writes it, such as British English (Locale.en_gb) or the French of France (Locale.fr_fr). Your app lists the locales it supports, once, in app.jadpo. Formatting and translations accept only those, and writing any other is a compile error. The server's own locale is never used.

example
application {}locales {    default: Locale.en_gb    supported: [Locale.en_gb, Locale.en_us, Locale.fr_fr]}

locales { } is a declaration with no name, like config { }. It takes default:, the locale used when no other is chosen, and supported:, the list of locales the app supports, each once. Each entry is written Locale.<tag>, the tag in lower case with _ for - (Locale.zh_hans is zh-Hans).

A locale the list doesn't have is type.locale_unsupported when it is written out where text is formatted, or as the default: or test: of a Locale setting. A project that formats text and declares no locales is type.temporal_locales_required.

There is no unsupported: setting. A request field typed Locale is decoded like any enum, so a tag the app doesn't support is an invalid_value failure, and the OpenAPI document lists only the supported tags. A setting typed Locale is read the same way when the app starts. To choose a language from a browser's Accept-Language header, call a library function that falls back to default: (Choosing a locale from a request).

Locales still being translated: in_progress:

in_progress: is a third, optional option, written once: a list of the supported locales whose translations aren't finished yet. Every other supported locale must be complete. Only a locale listed here falls back where wording is missing, with no error, because listing it is how you say so (Translations as messages). A locale listed there but not in supported: is type.locale_in_progress_unsupported, and the default can't be listed (type.locale_in_progress_default).

example
application {}locales {    default: Locale.en_gb    supported: [Locale.en_gb, Locale.en_us, Locale.fr_fr, Locale.pl_pl]    in_progress: [Locale.pl_pl]}

A locale the app doesn't support

Text for people is only ever written in a supported locale. A Locale the list doesn't have can still reach text.format, temporal.format_relative or a message while the app runs: one saved with a record before supported: changed, or one written out anywhere but locale:. It is written in the supported locale a browser asking for it would get (Choosing a locale from a request): a supported locale of its language, the one CLDR gives as the language's likely region first, else default:.

A list list.sort_by orders in such a locale is put in that same supported locale's alphabetical order. So jadpo test and a deployable, which carries only its supported locales' sorting rules, give the same order. Sorting by a locale needs locales { }, as formatting does (type.temporal_locales_required), so a deployable never carries sorting rules for a locale the app doesn't support. Changing case isn't text for people in a language, and takes any Locale (Text).

example
application {}locales {    default: Locale.en_gb    supported: [Locale.en_gb, Locale.fr_fr]}
example
/// When an order was placed.type OrderedAt = Instant {}
example
/// When an order was placed, shown to its customer.type PlacedText = Output {    placed: PresentationText}
example
use shop/types { OrderedAt, PlacedText }/// The placing, in the locale saved with the customer's order.action show_placed(ordered_at: OrderedAt, saved_locale: Locale) -> PlacedText {    return PlacedText {        placed: text.format(            ordered_at,            zone: Zone.europe_london,            locale: saved_locale,            format: TimeFormat.date_long        )    }}
example
test "a locale the application doesn't support is written in one it does" {    assert show_placed("2026-09-27T13:30:05Z", Locale.fr_fr).placed == "27 septembre 2026"    assert show_placed("2026-09-27T13:30:05Z", Locale.fr_ca).placed == "27 septembre 2026"    assert show_placed("2026-09-27T13:30:05Z", Locale.de_de).placed == "27 September 2026"}

A deployable carries the locale data of its supported locales only. jadpo build takes from the CLDR data compiled into Jadpo the entries the supported locales reach (the locale, then each locale it falls back to, down to the root), their sorting rules among them, and the data that is the same in every locale, and compiles the deployable with that. jadpo test and jadpo dev keep every locale, and write the same text.

Text direction

Every locale knows which way its text runs. locale.direction(value) gives a value of the built-in enum TextDirection, .left_to_right or .right_to_left, from the script CLDR says the locale most likely uses: Locale.ar_eg and Locale.he_il read right to left, Locale.en_gb left to right. Jadpo's wording of the two values is the HTML values, ltr and rtl, so the email layout's <html dir="{{ locale.direction }}"> writes one of them.

example
application {}locales {    default: Locale.en_gb    supported: [Locale.en_gb, Locale.ar_eg]}
example
test "a locale knows which way its text runs" {    assert locale.direction(Locale.ar_eg) == TextDirection.right_to_left    assert locale.direction(Locale.en_gb) == TextDirection.left_to_right}

Choosing a locale from a request

A browser says which languages its reader wants in its Accept-Language header. locale.from_accept_language(header) gives the Locale it asks for, out of the locales your app supports, and default: when the header names none of them. The call never fails, and it is the only way a request chooses a locale that isn't written in its input.

header is Text?, which may hold no value. An input field read with header: "Accept-Language" and optional: true (Headers) is passed straight in, whether the request sent the header or not:

example
type PlaceRequest = Input {    ordered_at: OrderedAt    languages_header: BrowserLanguages {        header: "Accept-Language"        optional: true    }}
example
application {}locales {    default: Locale.en_gb    supported: [Locale.en_gb, Locale.en_us, Locale.fr_fr]}
example
/// What a browser says its reader's languages are.type BrowserLanguages = Text {    max_length: 2000}
example
/// When an order was placed.type OrderedAt = Instant {}
example
/// An order's placing, shown to its customer.type PlacedText = Output {    reply_locale: Locale    placed: PresentationText}
example
use shop/types { BrowserLanguages, OrderedAt, PlacedText }/// The placing, in the language the customer's browser asks for.action show_placed(ordered_at: OrderedAt, languages_header: BrowserLanguages?) -> PlacedText {    var reader_pick: Locale = locale.from_accept_language(languages_header)    return PlacedText {        reply_locale: reader_pick        placed: text.format(            ordered_at,            zone: Zone.europe_london,            locale: reader_pick,            format: TimeFormat.date_long        )    }}
example
test "the browser's languages choose the locale" {    assert show_placed("2026-09-27T13:30:05Z", "fr-FR").reply_locale == Locale.fr_fr    assert show_placed("2026-09-27T13:30:05Z", "fr-CA").reply_locale == Locale.fr_fr    assert show_placed("2026-09-27T13:30:05Z", "en").reply_locale == Locale.en_us    assert show_placed("2026-09-27T13:30:05Z", "en-GB;q=0.3, fr;q=0.9").reply_locale == Locale.fr_fr    assert show_placed(        "2026-09-27T13:30:05Z",        "fr-FR;q=0, en-US;q=0.1"    ).reply_locale == Locale.en_us    assert show_placed("2026-09-27T13:30:05Z", "de-DE, ja;q=0.8").reply_locale == Locale.en_gb    assert show_placed("2026-09-27T13:30:05Z", "fr-FR;q=nonsense").reply_locale == Locale.en_gb    assert show_placed("2026-09-27T13:30:05Z", none).reply_locale == Locale.en_gb    assert show_placed("2026-09-27T13:30:05Z", "fr-FR").placed == "27 septembre 2026"}

The rules are fixed, so every platform gives the same locale for the same header and the same locales { }:

  • Reading the header. As RFC 9110 reads Accept-Language: a list split at commas, each entry a language range (fr-CA, en) with an optional weight, ;q= and a number from 0 to 1 with up to three decimals. Spaces and tabs around entries and the ; are ignored, an empty entry is nothing, and the range is read in any case, _ as - (a client that writes en_GB means it). An entry that does not parse (a weight like 1.5 or abc, a range with an empty part or a part over eight characters, anything after the weight) is skipped and the others are still read. A weight of 0 means "not acceptable", so that tag is skipped too, and *, which names no language, is ignored.
  • Taking the tags. By weight, the heaviest first; tags of equal weight keep the order the header gives them (an entry with no weight has 1). The first tag that matches a supported locale decides, so a lighter tag that matches is chosen over a heavier one that does not.
  • Matching a tag. Against supported: only, never against every Locale. A supported locale with exactly the tag wins, whatever the case (fr-FR is Locale.fr_fr). Otherwise a supported locale of the same language answers, so fr-CA gets fr_fr when French is supported only as fr_fr. Where several are of that language, the one CLDR gives as the language's likely region is chosen (en means en-US, pt means pt-BR), and if none of them is that one, the first in the order supported: lists them. Only the language is compared, never the script, so list a locale a header should reach exactly (zh-TW is Locale.zh_tw); otherwise it is answered by another locale of its language.
  • Falling back. With no tag matching, an empty header or none, the result is default:. It is never the server's own locale, and never a locale that is not supported.
  • A pure function. The same header and locales { } give the same locale in the interpreter, a native deployable and WebAssembly. It reads no clock, store or request, so it may be called in any function, action or test. A query computes nothing, so it cannot call it (query.value_unsupported): choose the locale in the action and pass it in.
  • The call is checked as a standard-library call is. header is always given, by position or as header:, and takes a Text, a type built on Text, none, or any of these that may be none (type.argument_count, type.named_argument_unknown, type.mismatch). A project with no locales { } has nothing to choose among, which is type.temporal_locales_required, as it is for formatting.
  • The name locale is reserved. It is a standard-library namespace now, so a parameter or variable cannot be called locale (semantic.standard_namespace_reserved); the examples on this page call theirs reader_locale. A field or a named argument called locale (locale:) is a different thing and stays.

Markdown for agents: /docs/locales.md.Built from Jadpo main at a707efb.