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

Answers every request in the language its address names, sends one whose address is not the
site's form of it to the one that is, and remembers which language the visitor is reading.

## The rule

A site's pages are stored under a language — `/:lang/about`, or a literal `/fa/pricing` — and
`site.language_urls` (`MishkaCmsCore.Runtime.Languages.url_mode/1`) says how a public address
names it. Only the FIRST segment is ever read as a language.

With no prefix for the primary language (`"prefix_others"`, the default), the address decides
and nothing about the visitor does:

    /blog        -> served in the primary language, as /en/blog would have been
    /fa/blog     -> served, and `fa` is remembered
    /en/blog     -> 301 /blog            (en is the primary language)
    /            -> served in the primary language, whatever the session says

With a prefix for every language (`"prefix_all"`):

    /fa/pricing  -> served, and `fa` is remembered
    /blog        -> 302 /fa/blog         (after the line above)
    /no-lang     -> served in its page's own language, when a page is stored there

Which language `"prefix_all"` sends a visitor to is their remembered one when the site still
publishes in it, and `MishkaCmsCore.Runtime.Languages.primary/1` otherwise — the first of the
site's list. A remembered language the page has no edition in falls back to that same first
language rather than to a 404: an address that does not name a language is not a request for one.

## What is NOT redirected

Nothing here keeps a list of addresses to leave alone. It asks
`MishkaCmsCore.Runtime.States.WebRunTimeMetaDataState` whether an address exists, which is the same
question the request itself is about to ask, so the answers cannot drift apart:

  * a path that already resolves is served as it stands — a page stored at `/no-lang` keeps its
    address, and moving it under a language would be inventing one it does not have
  * a path that resolves under neither form is left alone and 404s
  * a route Phoenix owns — `/admin/...`, `/api/...`, an asset bundle, `/mish-...` — is neither
    moved nor remembered: it is answered in the visitor's remembered language, else the site's
    first, so its Gettext strings are in the language they were reading, and the language a
    visitor reads pages in is the last page's, never the admin's or a stylesheet's. The admin keeps
    its own, `MishkaCmsCore.Runtime.Languages.admin_session/0`.

## Why it is told which route the CMS answers

`:router` and `:live_view` are required, and only a request the router would hand to that LiveView
is ever redirected, or remembered. Without them, an author who published a page at `/en/admin`
would take a Phoenix route down: `/admin` would resolve under a language, and the rule above would
send every visitor from one to the other. A static route already wins the match — this makes it
win the language rule too.

    plug MishkaCmsCore.Runtime.Plugs.LanguagePlug,
      router: MishkaCmsWeb.Router,
      live_view: MishkaCmsWeb.RuntimeLive

## 301 or 302

The primary language's prefix is dropped with a 301: `/en/blog` is `/blog` for every visitor, so a
browser and a crawler may keep that answer. Sending an address with no language to one is a 302:
where a visitor goes depends on their session, and a 301 would be cached by the browser and keep
sending them to `/en/blog` after they had chosen Persian. A crawler, which carries no session,
always lands on `primary/1` and reads the `hreflang` alternates
`MishkaCmsWeb.RuntimeLive.page_head/1` puts in the head.

## Asked in two places

`call/2` answers an HTTP request. A LIVE navigation inside an open socket never reaches a plug, so
`MishkaCmsWeb.RuntimeLive.handle_params/3` asks `target/3` the same question for `<.link navigate>`
and `push_patch`. One function, so a link cannot behave one way on a fresh load and another on a
click.

# `spoken`

```elixir
@spec spoken(Plug.Conn.t()) :: String.t() | nil
```

The language this connection is answering in, once `call/2` has run.

`MishkaCmsWeb.Router` hands it to `live_session` so a socket knows the language of a page whose
own address does not name one.

# `target`

```elixir
@spec target([String.t()], {String.t(), String.t()}, String.t() | nil) ::
  {:serve, String.t()} | {:redirect, String.t(), 301 | 302}
```

Where a request for `segments` belongs: `{:serve, language}` to answer it where it is, or
`{:redirect, path, status}` to send it to the site's form of its address — `301` for a primary
language's prefix the site leaves off, `302` for a language chosen for the visitor.

`site` is the `{name, id}` pair — the name is what the routing table is keyed by, the id is what
the language settings are keyed by. `remembered` is a language already put through
`MishkaCmsCore.Runtime.Languages.remembered/2`, or `nil`.

    # site.languages ["en", "fa"], site.language_urls "prefix_others"
    target(["fa", "pricing"], {"localhost", id}, nil) #=> {:serve, "fa"}
    target(["en", "blog"], {"localhost", id}, nil)    #=> {:redirect, "/blog", 301}
    target(["blog"], {"localhost", id}, "fa")         #=> {:serve, "en"}

    # site.language_urls "prefix_all"
    target(["blog"], {"localhost", id}, "fa")         #=> {:redirect, "/fa/blog", 302}

---

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