Languages and language editions

v0.0.1

Language in Mishka CMS has four parts that work together: the languages a site publishes in, the language each page is written in, the editions that tie a page's versions together, and the words that templates ask for. This page explains how they fit. For the steps, see Set a site's languages and language URLs and Translate a page.

The site's languages

Each site keeps an ordered list of language codes in its site.languages setting, such as ["en", "fa", "ar"]. A site can set its own list. A site that sets none reads the global list, and a global list that sets none reads ["en"].

  • The first code is the site's first language, the one it leads with. There is no separate default setting.
  • A master can limit every site. site.allowed_languages names the only codes a site may list, and site.required_languages names codes that every site publishes whether it lists them or not.
  • The CMS trims each code, lowercases it and drops anything that is not a language code. A list with nothing usable left falls back to ["en"].

Language in the address

A page is stored under a path, and that path can hold :lang. A page stored at /:lang/about answers /fa/about only when fa is one of the site's languages. Without that check the capture would take anything, such as /xyz/about. A request whose lang is not one of the site's languages goes to the next matching page, or to the not-found page.

The site.language_urls setting decides how the public address names the language:

StyleFirst languageOther languages
First language unprefixed (default)/blog/fa/blog
Every language prefixed/en/blog/fa/blog

The stored path is the same in both styles, so switching styles rewrites no page. Links made with site_path/2, the editions of a page, the sitemap and redirects all come from one function, so they cannot name one page two ways.

The language plug applies the style to each request and remembers the language a visitor reads in. It reads only the first segment of an address as a language.

  • Unprefixed first language: the address decides. /blog is served in the first language, and /en/blog is sent (301) to /blog.
  • Every language prefixed: an address with no language, such as /blog, is sent (302) to the visitor's remembered language, or to the first language, when a page exists there. The remembered language is used only while the site still lists it.

Editions of a page

A page has a language, which is en unless you set another, and a language_group_id. A new page's group is its own id, which makes it the original. A translation carries the original's id as its group. Within a site, a group holds one page per language, and the CMS refuses a second one. Blog posts, collections, tags and documents use the same two fields.

A site can publish a page in several languages in two ways:

  • One literal path for each language, such as /about and /fa/about. These are two pages. Add translation creates this form.
  • One pattern for all of them, such as /:lang/about. This is one page whose template reads the captured language.

A pattern page answers only the language it is written in. A page marked to answer every language, with serves_all_languages set to true in its Extra (JSON), answers every language of its site. The blog's index is marked, because it holds components that fetch the rows of the captured language. When a literal page and a pattern both answer a language, the literal page wins.

What a response knows

  • @page.language is the language of this response. On a pattern page it is the captured one.
  • <html lang> and dir follow that language. Arabic, Persian, Hebrew, Urdu and a few others read right to left. A live language change keeps them right too. See Changing language without a reload.
  • @page_languages lists the languages the page is written in, each with its public address. It follows the order of the site's list, and it is empty when fewer than two languages are left. The header's language switcher and the head's alternate links are built from it, and an empty list draws no switcher.
  • The head carries a rel="alternate" link with hreflang for each language in that list, one x-default link at the edition in the first language, an og:locale tag for the page's language, and an og:locale:alternate tag for each other language.

A pattern page with another capture, such as /:lang/blog/:slug, cannot swap the language into its own path, because the slug belongs to the edition the visitor is leaving. The extension that owns the record answers instead, through the runtime:page:frame event. Mishka Blog offers a post, a category or a tag in each language it is published in, at that edition's own slug, and Mishka Document does the same for a document and its pages. An edition the visitor may not open is not offered, and the list keeps only the languages the site publishes in. A pattern page that no extension answers for still offers no swapped alternates. See Page frame.

Changing language without a reload

The language switcher in the header links each edition whose address is a path on the site as a live navigation. Picking a language swaps the page, the header and the browser title inside the open connection, with no reload. An edition at an http(s):// address is another site, so it stays an ordinary link and loads in full.

The root layout writes <html lang dir> with each full page load, so a live change would keep the first language's values. The site-main layout carries a hidden element that sets both again when the new page mounts. lang becomes the page's language, and dir becomes rtl for a language written right to left, such as Persian or Arabic, and ltr for the rest (Languages.direction/1). The same happens on Back and Forward and on any other link to a page of the site.

The head follows as well. The title changes with the page, and the canonical, hreflang and share tags are replaced with the new page's. A layout of your own that draws a language switcher needs the same hidden element. See Draw an extension's pages in the site layout.

Rules that keep editions together

  • A new translation starts inactive, because until it is translated it is a copy of the original.
  • The CMS refuses to archive or delete an original while its translations are live, and it says which languages. The list offers a button that archives or deletes those translations at once.
  • A translation cannot come back from the archive unless its original is live.
  • The Pages list shows one row for each page, with a chip for each other edition. A search or a language filter shows every edition, and so does the archive.

Words and the dashboard's language

Two other things use language but are not editions. The words that templates ask for with t() have their own rows for each site, key and language. See Translate interface words with t(). The dashboard has a language of its own, which an admin picks in the topbar's user menu. It offers the site's languages and English, defaults to English, and is kept apart from the language visitors read.

Example: English and Persian

A site lists en, fa and leaves its first language unprefixed. Its Pricing page is stored at /:lang/pricing in English. Its Persian edition is a second page at /fa/pricing in the same group.

  • /pricing serves the English page, and /fa/pricing serves the Persian one, right to left.
  • Each page lists both editions as alternates, and x-default points to /pricing.
  • Archiving the English page is refused while the Persian page is live.