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.
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).
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).
application {}locales { default: Locale.en_gb supported: [Locale.en_gb, Locale.fr_fr]}
/// When an order was placed.type OrderedAt = Instant {}
/// When an order was placed, shown to its customer.type PlacedText = Output { placed: PresentationText}
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 ) }}
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.
application {}locales { default: Locale.en_gb supported: [Locale.en_gb, Locale.ar_eg]}
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:
type PlaceRequest = Input { ordered_at: OrderedAt languages_header: BrowserLanguages { header: "Accept-Language" optional: true }}
application {}locales { default: Locale.en_gb supported: [Locale.en_gb, Locale.en_us, Locale.fr_fr]}
/// What a browser says its reader's languages are.type BrowserLanguages = Text { max_length: 2000}
/// When an order was placed.type OrderedAt = Instant {}
/// An order's placing, shown to its customer.type PlacedText = Output { reply_locale: Locale placed: PresentationText}
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 ) }}
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 writesen_GBmeans it). An entry that does not parse (a weight like1.5orabc, 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 of0means "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 everyLocale. A supported locale with exactly the tag wins, whatever the case (fr-FRisLocale.fr_fr). Otherwise a supported locale of the same language answers, sofr-CAgetsfr_frwhen French is supported only asfr_fr. Where several are of that language, the one CLDR gives as the language's likely region is chosen (enmeansen-US,ptmeanspt-BR), and if none of them is that one, the first in the ordersupported:lists them. Only the language is compared, never the script, so list a locale a header should reach exactly (zh-TWisLocale.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 isdefault:. 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.
headeris always given, by position or asheader:, and takes aText, a type built onText,none, or any of these that may benone(type.argument_count,type.named_argument_unknown,type.mismatch). A project with nolocales { }has nothing to choose among, which istype.temporal_locales_required, as it is for formatting. - The name
localeis reserved. It is a standard-library namespace now, so a parameter or variable cannot be calledlocale(semantic.standard_namespace_reserved); the examples on this page call theirsreader_locale. A field or a named argument calledlocale(locale:) is a different thing and stays.
Markdown for agents: /docs/locales.md.Built from Jadpo main at a707efb.