|
defmodule HelloWeb.Plugs.SetLocale do |
|
@moduledoc """ |
|
This plug is responsible for setting the locale in the session based on the |
|
`lang` query parameter, existing session locale or `accept-language` HTTP |
|
header. |
|
|
|
## Behavior |
|
|
|
- If the `lang` query parameter is present, it overwrites any existing locale |
|
in the session and updates the session with the new locale. |
|
- If the `lang` query parameter is not set, the plug tries to determine the |
|
locale in the following order: |
|
1. It reads the locale from the session state, if it exists. |
|
2. If the locale is not found in the session state, it infers the locale |
|
from the `accept-language` header. |
|
3. If the `accept-language` header is not present or does not match any |
|
supported locales, it defaults to English ("en"). |
|
""" |
|
|
|
@behaviour Plug |
|
|
|
import Plug.Conn |
|
|
|
@default_locale "en" |
|
|
|
# language => gettext_locale |
|
@supported_locales %{ |
|
"en" => "en", |
|
"pt" => "pt_BR", |
|
"de" => "de_AT" |
|
} |
|
|
|
@impl true |
|
def init(opts), do: opts |
|
|
|
@impl true |
|
def call(conn, opts) do |
|
locale = |
|
Keyword.get(opts, :force_locale) || |
|
get_params_locale(conn) || |
|
get_session_locale(conn) || |
|
get_browser_locale(conn) || |
|
@default_locale |
|
|
|
Gettext.put_locale(HelloWeb.Gettext, locale) |
|
|
|
conn |
|
|> put_session(:locale, locale) |
|
|> assign(:locale, locale) |
|
end |
|
|
|
@doc false |
|
def get_params_locale(conn) do |
|
conn.params["lang"] |> parse_language() |> locale_for_language() |
|
end |
|
|
|
@doc false |
|
def get_session_locale(conn) do |
|
get_session(conn, :locale) |> parse_language() |> locale_for_language() |
|
end |
|
|
|
@doc false |
|
def get_browser_locale(conn) do |
|
accept_language = Plug.Conn.get_req_header(conn, "accept-language") |
|
|
|
case accept_language do |
|
[header | _] -> parse_accept_language(header) |
|
_ -> nil |
|
end |
|
end |
|
|
|
defp parse_accept_language(header) do |
|
header |
|
|> String.split(",") |
|
|> Enum.map(fn language -> |
|
# Split the language by semicolon to handle q-values (ignored). |
|
# For simplicity, we ignore the q-values and assume the languages are |
|
# sorted in preferred order. This is what most browsers do anyway. |
|
# https://developer.mozilla.org/en-US/docs/Glossary/Quality_values |
|
[language | _] = String.split(language, ";") |
|
|
|
parse_language(language) |
|
end) |
|
|> Enum.find_value(&locale_for_language/1) |
|
end |
|
|
|
defp parse_language(nil), do: nil |
|
|
|
defp parse_language(language) when is_binary(language) do |
|
String.split(language, ~r/[-_]/, parts: 2) |> List.first() |> String.downcase() |
|
end |
|
|
|
defp locale_for_language(language) do |
|
@supported_locales[language] |
|
end |
|
end |