MishkaCmsCore.Runtime.Plugs.LanguagePlug (Mishka CMS Core v0.0.1)

Copy Markdown View Source

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.

Summary

Functions

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

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.

Functions

spoken(conn)

@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(segments, arg, remembered)

@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}