MishkaCmsCore.Runtime.Languages (Mishka CMS Core v0.0.1)

Copy Markdown View Source

The languages a site publishes in, and what the runtime makes of a path segment that names one.

The site's list

site.languages is a setting a site may override — see MishkaCmsCore.Settings — holding codes: ["en", "fa", "ar"]. A site that sets none reads the global value, and a global that sets none reads default/0.

What a master allows, and what a master requires

Two master-only global settings bound every site's list — see allowed/0 and required/0. A site may list only allowed languages (all of them, while none is set), and every required one is part of every site's list whether the site listed it or not. site/1 answers with both applied, so every reader — routing, the language plug, t/1, the translations admin — sees the same list.

A capture named lang

A page stored at /:lang/about answers /fa/about only when fa is one of its site's languages. Without that check the capture takes anything — /xyz/about, /favicon.ico/about — and the page renders as though it had been written in a language nobody wrote it in. So MishkaCmsCore.Runtime.States.WebRunTimeMetaDataState asks admits?/3 before it keeps a match, and a request whose lang is not the site's falls through to the next pattern, or to the 404.

Wherever the capture sits: /docs/:lang/:section and /shop/:category/:lang are checked the same way, and so is a *lang rest capture. No other capture name is ever checked against languages.

Failing safe

A list that cannot be read — no such site, a value that is not a list, a cache that is not there — is default/0. So is one with nothing usable left in it, because a site that answers no language at all would 404 every page that has a lang in its path. Each code is trimmed and downcased, and anything that is not a language code is dropped rather than admitted. The settings page refuses to store such a list (valid?/1); this is for one that reached the store some other way.

Public addresses

site.language_urls decides how an address names its language — see url_mode/1. Every public address the CMS builds comes out of url/3, and a request whose first segment names no language is read through implied/2. A stored path is the same in both modes, so switching modes rewrites no page.

Names

name/1 and names/1 put a code into a sentence a person reads, in the locale of the calling process: "fa" is "Persian", and ["fa", "fr"] is "French and Persian". The names, their alphabetical order and the "and" are Unicode CLDR's, through MishkaCmsCore.Localized; nothing is translated by hand.

See MishkaCmsCore.Runtime.PageEditions for what a page does with its language.

Summary

Functions

The language an admin reads the dashboard in: the one kept in session, else English.

The languages an admin of site_id may read the dashboard in: the site's own, and English.

The session key the language an admin reads the dashboard in is kept under.

Whether a captured value may stand in a pattern. A lang capture must be one of languages; every other capture takes whatever the visitor asked for.

The languages a master lets sites publish in — the global site.allowed_languages. Empty means any language.

The name of the capture whose value must be one of the site's languages.

Whether a stored path captures lang in any segment, as :lang or as *lang.

valid?/1 for a master's own lists, which may be empty — "no limit", "nothing required" — and are not held to the allowed list, being what sets it.

What a site publishes in when nothing says otherwise.

The direction a language is written in, as the value of <html dir>.

The emoji flag a language is written beside in a switcher: the country its code names when it names one, else the country the language is most spoken in, else a globe.

The language a request whose first segment names none is answered in: the site's primary language while it is left unprefixed ("prefix_others"), else nil — as it is when the first segment is one of the site's languages.

Phrases as one, in the order given, joined as the reader's language joins a list (MishkaCmsCore.Localized.list/2).

Every language code name/1 has a name for, in the order of the codes: the languages a list of choices offers.

The language a path names in its first segment, or nil.

A language's name in the language of the calling process, for a sentence a person reads — CLDR's (MishkaCmsCore.Localized.language/2). A code CLDR does not name reads as the code itself, upper-cased.

Several languages as one phrase: each named by name/1, once, in the alphabetical order of the reader's language, joined as that language joins a list (join/1).

The language a site answers in when a request names none — the FIRST of its list.

language when the site still publishes in it, and nil otherwise.

The languages a master has every site publish in — the global site.required_languages. Empty means none is required.

The session key a visitor's chosen language is remembered under.

The languages a site publishes in, by its id — its own list, else the global one, else default/0.

site/1 for the site the routing table knows by name — which is how MishkaCmsCore.Runtime.States.WebRunTimeMetaDataState names one. A name it does not know reads default/0.

The public address a stored path makes on the site site_id with these captures, or nil when it makes none.

url/3 in mode rather than the site's own — for a caller comparing addresses as they would be in either mode.

How the public addresses of site_id name their language — its site.language_urls setting

The values url_mode/1 answers, the default first.

Whether languages may be stored as a site's list: at least one code, and each of them a language code, in any case.

Functions

admin(arg1)

@spec admin(map()) :: String.t()

The language an admin reads the dashboard in: the one kept in session, else English.

Languages.admin(%{"admin_language" => "fa"}) #=> "fa"
Languages.admin(%{})                         #=> "en"

admin_choices(site_id)

@spec admin_choices(String.t() | nil) :: [String.t()]

The languages an admin of site_id may read the dashboard in: the site's own, and English.

Languages.admin_choices(site.id) #=> ["fa", "de", "en"]

admin_session()

@spec admin_session() :: String.t()

The session key the language an admin reads the dashboard in is kept under.

admits?(arg1, value, languages)

@spec admits?(String.t(), String.t(), [String.t()]) :: boolean()

Whether a captured value may stand in a pattern. A lang capture must be one of languages; every other capture takes whatever the visitor asked for.

allowed()

@spec allowed() :: [String.t()]

The languages a master lets sites publish in — the global site.allowed_languages. Empty means any language.

capture()

@spec capture() :: String.t()

The name of the capture whose value must be one of the site's languages.

captures?(path)

@spec captures?(String.t() | nil) :: boolean()

Whether a stored path captures lang in any segment, as :lang or as *lang.

codes_or_empty?(languages)

@spec codes_or_empty?(term()) :: boolean()

valid?/1 for a master's own lists, which may be empty — "no limit", "nothing required" — and are not held to the allowed list, being what sets it.

default()

@spec default() :: [String.t()]

What a site publishes in when nothing says otherwise.

direction(language)

@spec direction(String.t() | nil) :: String.t()

The direction a language is written in, as the value of <html dir>.

Languages.direction("fa")    #=> "rtl"
Languages.direction("en-gb") #=> "ltr"

flag(language)

@spec flag(String.t() | atom() | nil) :: String.t()

The emoji flag a language is written beside in a switcher: the country its code names when it names one, else the country the language is most spoken in, else a globe.

Languages.flag("fa")     #=> "🇮🇷"
Languages.flag("pt-BR")  #=> "🇧🇷"
Languages.flag("en-us")  #=> "🇺🇸"
Languages.flag("en")     #=> "🇬🇧"
Languages.flag("eo")     #=> "🌐"
Languages.flag(nil)      #=> "🌐"

A flag is a country's and a language is not, so this is a convention and nothing more: en is the United Kingdom's, ar Saudi Arabia's. A site that wants another writes its own switcher.

implied(segments, site_id)

@spec implied([String.t()], String.t() | nil) :: String.t() | nil

The language a request whose first segment names none is answered in: the site's primary language while it is left unprefixed ("prefix_others"), else nil — as it is when the first segment is one of the site's languages.

What MishkaCmsCore.Runtime.States.WebRunTimeMetaDataState.resolve/2 reads /blog as, and what MishkaCmsCore.Runtime.Plugs.LanguagePlug serves it in.

Languages.implied(["blog"], site.id)       #=> "en"
Languages.implied(["fa", "blog"], site.id) #=> nil

join(phrases)

@spec join([String.t()]) :: String.t()

Phrases as one, in the order given, joined as the reader's language joins a list (MishkaCmsCore.Localized.list/2).

Languages.join(["a", "b", "c"]) #=> "a, b, and c"

known()

@spec known() :: [String.t()]

Every language code name/1 has a name for, in the order of the codes: the languages a list of choices offers.

Languages.known() #=> ["af", "am", "ar", …, "zu"]

leading(arg1, languages)

@spec leading([String.t()], [String.t()]) :: String.t() | nil

The language a path names in its first segment, or nil.

Only the first segment, and only against the site's own list — /fa/pricing is Persian, /favicon.ico is not a language and never was.

Languages.leading(["fa", "pricing"], ["en", "fa"]) #=> "fa"
Languages.leading(["pricing"], ["en", "fa"])       #=> nil

name(language)

@spec name(String.t() | atom() | nil) :: String.t() | nil

A language's name in the language of the calling process, for a sentence a person reads — CLDR's (MishkaCmsCore.Localized.language/2). A code CLDR does not name reads as the code itself, upper-cased.

Languages.name("fa")    #=> "Persian"
Languages.name("pt-BR") #=> "Brazilian Portuguese"
Languages.name("fa-IR") #=> "Persian (Iran)"
Languages.name("xx")    #=> "XX"
Languages.name(nil)     #=> nil

names(languages)

@spec names([String.t() | atom() | nil]) :: String.t()

Several languages as one phrase: each named by name/1, once, in the alphabetical order of the reader's language, joined as that language joins a list (join/1).

Languages.names(["fa", "fr"])       #=> "French and Persian"
Languages.names(["de", "fa", "fr"]) #=> "French, German, and Persian"
Languages.names([])                 #=> ""

primary(site_id)

@spec primary(String.t() | nil) :: String.t()

The language a site answers in when a request names none — the FIRST of its list.

There is no separate "default language" setting, and there should not be: site.languages is already ordered and already validated, so moving a code to the front of it is how an admin changes what the site leads with. A second setting could disagree with the list, and then one of them would be wrong.

Languages.primary(site.id) #=> "en"

remembered(language, site_id)

@spec remembered(term(), String.t() | nil) :: String.t() | nil

language when the site still publishes in it, and nil otherwise.

What a remembered language is put through before it is used. A visitor's session outlives the setting that justified it: a site that drops fa from its list would otherwise keep sending everyone who ever chose it to Persian addresses that now 404.

Languages.remembered("fa", site.id) #=> "fa"
Languages.remembered("de", site.id) #=> nil

required()

@spec required() :: [String.t()]

The languages a master has every site publish in — the global site.required_languages. Empty means none is required.

session()

@spec session() :: String.t()

The session key a visitor's chosen language is remembered under.

site(site_id)

@spec site(String.t() | nil) :: [String.t()]

The languages a site publishes in, by its id — its own list, else the global one, else default/0.

Languages.site(site.id)
#=> ["en", "fa"]

site_named(name)

@spec site_named(String.t()) :: [String.t()]

site/1 for the site the routing table knows by name — which is how MishkaCmsCore.Runtime.States.WebRunTimeMetaDataState names one. A name it does not know reads default/0.

url(path, captures, site_id)

@spec url(String.t(), map(), String.t() | nil) :: String.t() | nil

The public address a stored path makes on the site site_id with these captures, or nil when it makes none.

path is what a page is stored under — a literal, or a pattern holding :name and *name segments. Three steps:

  1. each capture is filled from captures, by its name as a string — percent-encoded, a *name one segment at a time — and a capture captures does not hold is nil;
  2. a lang capture whose value is not one of the site's languages is nil — the resolver would refuse that address;
  3. in "prefix_others" (url_mode/1), a FIRST segment that is the primary language is dropped: /en is /, /en/blog/x is /blog/x. Only the first segment is ever a language prefix; a lang anywhere else in the path stays as it is.

Every public address the CMS builds comes out of this — a link in a template (MishkaCmsCore.Runtime.LiveViewHelpers.site_path/2), a page's editions, the sitemap, robots.txt, a redirect — so no two of them can name one page differently.

# site.languages ["en", "fa"], site.language_urls "prefix_others"
Languages.url("/:lang/blog", %{"lang" => "en"}, site.id)  #=> "/blog"
Languages.url("/:lang/blog", %{"lang" => "fa"}, site.id)  #=> "/fa/blog"
Languages.url("/:lang/blog", %{"lang" => "de"}, site.id)  #=> nil
Languages.url("/en/pricing", %{}, site.id)                #=> "/pricing"
Languages.url("/:lang/blog/:slug", %{"lang" => "fa"}, site.id) #=> nil

url(path, captures, site_id, mode)

@spec url(String.t(), map(), String.t() | nil, String.t()) :: String.t() | nil

url/3 in mode rather than the site's own — for a caller comparing addresses as they would be in either mode.

url_mode(site_id)

@spec url_mode(String.t() | nil) :: String.t()

How the public addresses of site_id name their language — its site.language_urls setting:

  • "prefix_others" — the primary language (primary/1) has no prefix, every other has its own: /blog, /fa/blog;
  • "prefix_all" — every language has its own: /en/blog, /fa/blog.

Any other value reads "prefix_others".

Languages.url_mode(site.id) #=> "prefix_others"

url_modes()

@spec url_modes() :: [String.t()]

The values url_mode/1 answers, the default first.

valid?(languages)

@spec valid?(term()) :: boolean()

Whether languages may be stored as a site's list: at least one code, and each of them a language code, in any case.

What site.languages is held to when it is written — see MishkaCmsCore.Settings.cast/2 — so an admin who types English, Persian, or empties the field, is told it was not saved rather than finding every :lang page answering English alone.

Languages.valid?(["en", "FA", "pt-br"]) #=> true
Languages.valid?(["en", "Persian"])     #=> false
Languages.valid?([])                    #=> false