# `MishkaCmsCore.Runtime.Languages`
[🔗](https://github.com/mishka-group/mishka_cms_core/blob/v0.0.1/lib/mishka_cms_core/runtime/languages.ex#L5)

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.

# `admin`

```elixir
@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`

```elixir
@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`

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

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

# `admits?`

```elixir
@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`

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

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

# `capture`

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

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

# `captures?`

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

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

# `codes_or_empty?`

```elixir
@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`

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

What a site publishes in when nothing says otherwise.

# `direction`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

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

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

# `session`

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

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

# `site`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

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

The values `url_mode/1` answers, the default first.

# `valid?`

```elixir
@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

---

*Consult [api-reference.md](api-reference.md) for complete listing*
