Osy#betaRun it on Osyrin: osyrin.com

Reference / I18n

Which language a reader gets — and switching it

Session.Language · App.Languages · App.CultureOf("sv") · Session.SetLanguage("sv") · /sv/… · language => order.Language;

A reader's language is, in order: their explicit choice (a `/sv/` address prefix or a language switch), their own `[PreferredCulture]`, the browser's Accept-Language, `app.DefaultCulture`, the source language. `Session.Language` reads it, `App.Languages` lists what the app speaks, `Session.SetLanguage` switches — in place, no reload — and a mail names its recipient's with `language =>`.

stable2 examples compiled by CIi18nlanguagesessionui

Summary#

Only a language the app speaks is ever chosen (A language's words — locales/<language>.osy), and the first of these that names one wins:

  1. an explicit choice — an address that starts with the language (/sv/coffee, served in place as /coffee in Swedish), or a switch that called Session.SetLanguage("sv"); remembered from then on;
  2. the signed-in reader's own [PreferredCulture] (its language);
  3. the browser's Accept-Language;
  4. the language of app.DefaultCulture;
  5. the source language — English, or what app.SourceLanguage names (app.SourceLanguage — the language the app is written in).

Signature#

string       Session.Language           // "sv" — the language this reader reads in
List<string> App.Languages              // ["en", "sv"] — the source first, then every translated language
string?      App.CultureOf(string language)          // "sv" → "sv-SE" — the culture the app formats it in, or null
void         Session.SetLanguage(string language)   // a client effect: the page re-renders in place
[Email] component … { language => order.Language; subject => …; … }   // a mail in its recipient's language
MyMail(order, language: "sv")                                           // …or rendered in a chosen one (a preview)

Description#

One choice drives everything. The page's text, its <title>, lang and dir (an Arabic page reads right to left — Right to left — an app that reads in Arabic with no change to the app), a server render, a server function's error message — and the culture numbers and dates format in (Culture formatting — ToString(format, culture)): a Swedish reader sees 1 234,50 kr even when app.DefaultCulture is en-GB, because the language chooses the app's declared culture of that language. A reader whose own culture speaks the chosen language keeps it.

Switching is in place. Session.SetLanguage("sv") fetches that language's table (by content address, cached forever) and every region that shows a translated text re-renders — no reload, no lost state. The choice is kept in the browser and sent with every request, so the server answers in it too.

A mail is in its recipient's language, not the sender's. Declare it on the mail: language => order.Language;. Every text in the mail — its subject, its parts, a shared footer — renders in that language, whoever sends it (a customer at the checkout, staff at the desk, a scheduled job). With none, a mail is in the language of the work sending it. An app that mails about something should record the language it was placed in (Session.Language at the moment it is placed). Every mail also takes an optional last argument, language: — MyMail(order, language: "sv") renders it in Swedish whoever it is for, which is what a desk's preview of the Swedish mail needs.

…and so are its numbers, its money, its dates and its data. Inside a mail, a culture-less format ($"{total:N2}", price.ToString("C"), a date written as text) is in the culture of the MAIL's language — 17,68 kr in a Swedish one, 17.68 kr in an English one — and a [Translatable] value it reads (a product's name) is read in it too, wherever the mail is rendered: a request in another language, a workflow, a job, a test. A mail's language may be named by a culture: language => account.Language; with a [PreferredCulture] holding "sv-SE" is Swedish.

A person's own language is on their account. Give the principal a [PreferredCulture] Culture? Language; and set it when they switch while signed in: a device where they chose nothing (a new phone) reads the app in it once they sign in — every request, not only the first page — and mail about them rather than about an order says language => account.Language;. A choice made on a device still outranks it there.

Words staff wrote for a mail are per language, too. A mail's Editable parts keep the words staff saved per language ([Email] — mail rendered from data): the mail asks for the words saved in ITS language, and a language with none says the template's default as that language's locale file translates it — the English words a shop wrote never reach a Swedish reader.

A server body can switch too. Session.SetLanguage("sv") in a server function puts Swedish in effect for the rest of that run: its data reads and writes ([Translatable] — an entity's own words in every language), its translated texts, and Session.Language itself. Where no request is in flight at all — a workflow, a scheduled job, a test body — Session.Language is the source language and App.Languages is still every language the app speaks. A culture-less format in an app that speaks more than one language follows the language being read (the request's, the mail's, or the one a body chose), in a page and in a function alike.

Which culture a language formats in — App.CultureOf. App.CultureOf("sv") answers the culture the app writes Swedish in: app.DefaultCulture when that is a Swedish one, else the app's first declared culture of Swedish, else null (the app does not speak it). It is the same rule a request in that language and a mail in it format with, so it is what to keep as a person's [PreferredCulture] when they switch — without switching the run's language to find out.

Who is served what. A browser is served only its own language's table. The words of pages a signed-out visitor cannot open (neither [AllowAnonymous] nor [Composable]) are in a separate table served to signed-in readers only, and a mail's words are never served to a browser at all.

Examples#

[Composable]
[AllowAnonymous]
component LanguageSwitch() {
  action Choose(string lang) { Session.SetLanguage(lang); }
  render {
    Row(gap: 1, label: "Language") {
      foreach (var lang in App.Languages.Where(l => l != Session.Language)) {
        Pressable(onClick: () => Choose(lang), label: lang) { Text(lang.ToUpper()); }
      }
    }
  }
}

translations sv {
  accessibleName "Language" => "Språk";
}
string CultureToKeep(string language) {
  // null for a language the app does not speak — then there is nothing to keep
  return App.CultureOf(language) ?? "";
}

See also#

Related

Which words are translated — the compiler finds every text a reader sees

You write your app's words where they belong, in ordinary strings. The compiler finds every one a reader can see — by…

A language's words — locales/<language>.osy

A project's translations are checked-in source: one `locales/<language>.osy` per language, a `translations sv { }`…

app.SourceLanguage — the language the app is written in

`app.SourceLanguage = "es";` says the app's own text is written in Spanish. The page's `<html lang>` then says `es`…

Culture formatting — ToString(format, culture)

Format numbers, currency, percentages and dates for a declared culture — `total.ToString("C", "sv-SE")` → `1 234,56…

Every language's page, for a search engine — hreflang, canonical, sitemap

An app that speaks more than one language tells search engines where each page lives in each one, with nothing written…

[Email] — mail rendered from data

An `[Email]` component is a mail template. It is rendered on the server — from any function, a workflow step included —…

Right to left — an app that reads in Arabic with no change to the app

A language written right to left (Arabic, Hebrew, Persian, Urdu) mirrors the whole page: every horizontal side a style…