howdy/locale

Which language to answer a request in.

A negotiator lists the locales an app supports and where to look for the one a request wants, in order. The first source that names a supported locale wins; when none does, the default is used.

let locales =
  locale.negotiator(["en", "de", "fr-CA", "ar"], default: "en")
  |> locale.from_param("locale")
  |> locale.from_cookie("locale")
  |> locale.from_accept_language

fn show(ctx: Context) {
  let l = locale.detect(ctx, locales)
  controller.html(ctx, page(l))
}

Matching follows the lookup of RFC 4647: de-CH falls back to de. A request for a bare language also takes a regional locale of it, so en matches an app that only supports en-GB.

Add middleware to stamp responses with content-language, and with vary naming the headers the choice came from, so shared caches keep one copy per language. remember stores a visitor’s choice in the cookie named by from_cookie, for a language switcher.

Types

A language, optionally with a script and a region, such as en, pt-BR or zh-Hant-TW. Variants and extensions are dropped when parsing, since nothing in Howdy uses them.

pub opaque type Locale

The locales an app supports and where to look for a request’s choice.

pub opaque type Negotiator

Values

pub fn accept_language(header: String) -> List(String)

The language ranges in an accept-language header, most wanted first. Ranges with q=0, and the * wildcard, are left out.

pub fn default(negotiator: Negotiator) -> Locale
pub fn detect(
  ctx: context.Context(guarded),
  negotiator: Negotiator,
) -> Locale

The locale for a request.

pub fn detect_request(
  request: request.Request(context.Body),
  params: dict.Dict(String, String),
  negotiator: Negotiator,
) -> Locale

The locale for a request outside a handler, such as when a live view mounts. params are the route parameters, if any.

pub fn fallbacks(locale: Locale) -> List(Locale)

The locale and the less specific ones it falls back to, most specific first: zh-Hant-TW, zh-Hant, zh.

pub fn from(
  negotiator: Negotiator,
  find: fn(request.Request(context.Body)) -> option.Option(String),
) -> Negotiator

Look wherever find does, such as in a signed-in user’s saved preference. None, or a tag that is not supported, moves on to the next source. Since a custom source may read any header, middleware sets vary: * to keep its responses out of shared caches.

pub fn from_accept_language(negotiator: Negotiator) -> Negotiator

Look in the accept-language header the browser sends, trying each language in order of preference.

pub fn from_cookie(
  negotiator: Negotiator,
  name: String,
) -> Negotiator

Look in the cookie name, which remember sets.

pub fn from_param(
  negotiator: Negotiator,
  name: String,
) -> Negotiator

Look in the route parameter name, such as locale for routes under /:locale. Route parameters are not known to app-wide middleware; use from_path_segment there.

pub fn from_path_segment(negotiator: Negotiator) -> Negotiator

Look at the first segment of the path, so /de/shop asks for de.

pub fn from_query(
  negotiator: Negotiator,
  name: String,
) -> Negotiator

Look in the query parameter name, such as ?lang=de.

pub fn language(locale: Locale) -> String

The language subtag: pt for pt-BR.

pub fn match(
  negotiator: Negotiator,
  tag: String,
) -> option.Option(Locale)

The supported locale a tag asks for, if any: the tag itself, then each of its fallbacks, then any supported locale of the same language.

pub fn middleware(
  negotiator: Negotiator,
) -> fn(
  context.Context(Nil),
  fn(context.Context(Nil)) -> response.Response(content.Content),
) -> response.Response(content.Content)

Stamp every response with content-language, unless the handler set one, and add to vary the request headers the negotiator reads, so a shared cache keeps a copy per language.

pub fn negotiate(
  negotiator: Negotiator,
  tags: List(String),
) -> Locale

The supported locale for the first of tags that has one, or the default.

pub fn negotiator(
  supported: List(String),
  default default: String,
) -> Negotiator

A negotiator for supported locales, falling back to default, which is supported even when not listed. With no sources added it always answers the default. Invalid tags panic, as they are configuration.

pub fn parse(tag: String) -> Result(Locale, Nil)

Parse a BCP 47 language tag. Case is normalised, so EN-us becomes en-US, and _ is accepted in place of -.

pub fn region(locale: Locale) -> option.Option(String)
pub fn remember(
  answer: response.Response(body),
  negotiator: Negotiator,
  locale: Locale,
) -> response.Response(body)

Store a visitor’s choice in the cookie named by from_cookie, for a year. Panics if the negotiator reads no cookie, since the choice would never be read back.

pub fn right_to_left(locale: Locale) -> Bool

Whether text in this locale runs right to left, as Arabic, Hebrew and Persian do. An explicit script decides when there is one.

pub fn script(locale: Locale) -> option.Option(String)
pub fn supported(negotiator: Negotiator) -> List(Locale)
pub fn to_string(locale: Locale) -> String

The tag, in canonical case: pt-BR.

✨ Search Document