View Source PlugLocale.Browser (plug_locale v0.5.1)

Puts locale into assigns storage for Web browser environment.

The most common way of specifying the desired locale is via the URL. In general, there're four methods to do that:

  1. via country-specific domain - https://example.<locale>, such as:
    • https://example.is
    • https://example.de
  2. via subdomain - https://<locale>.example.com, such as:
    • https://en.example.com/welcome
    • https://zh.example.com/welcome
  3. via path - https://example.com/<locale>, such as:
    • https://example.com/en/welcome
    • https://example.com/zh/welcome
  4. via query string - https://example.com?locale=<locale>, such as:
    • https://example.com/welcome?locale=en
    • https://example.com/welcome?locale=zh

Method 1 and method 2 work, but they are complicated to set up and tedious to maintain.

Method 4 isn't recommended. It is ugly and will confuse search engines.

Personally, I think method 3 strikes a good balance between professionalism and convenience. Because of that, this plug will stick on method 3.

Usage

First, we need to integrate this plug with other libraries, or this plug is useless. All you need is to construct a plug pipeline through Plug.Builder. For example:

defmodule DemoWeb.PlugBrowserLocalization do
  use Plug.Builder

  plug PlugLocale.Browser,
    default_locale: "en",
    locales: ["en", "zh"],
    route_identifier: :locale,
    assign_key: :locale

  plug :put_locale

  def put_locale(conn, _opts) do
    if locale = conn.assigns[:locale] do
      # integrate with gettext
      Gettext.put_locale(locale)
    end

    conn
  end
end

Then, use it in router (following one is a Phoenix router, but Plug.Router is supported, too):

defmodule DemoWeb.Router do
  use DemoWeb, :router

  pipeline :browser do
    plug :accepts, ["html"]

    # ...

    plug DemoWeb.PlugBrowserLocalization

    # ...
  end

  scope "/", DemoWeb do
    pipe_through :browser

    get "/", PageController, :index
    # ...
  end

  # Why using :locale?
  # Because it is specified by `:route_identifier` option.
  scope "/:locale", DemoWeb do
    pipe_through :browser

    get "/", PageController, :index
    # ...
  end
end

Options

  • :default_locale - the default locale.
  • :locales - all the supported locales. Default to [].
  • :detect_locale_from - the sources and the order of sources for detecting locale. Available sources are :query, :cookie, :referrer, :accept_language. Default to [:cookie, :referrer, :accept_language].
  • :cast_locale_by - the function for casting extracted or detected locales. Default to nil.
  • :route_identifier - the part for identifying locale in route. Default to :locale.
  • :assign_key - the key for putting value into assigns storage. Default to :locale.
  • :query_key - the key for getting locale from querystring. Default to "locale".
  • :cookie_key - the key for getting locale from cookie. Default to "locale".

about :cast_locale_by option

By default, the value is nil, which means doing nothing. But, in practice, you will need to use something meaningful.

A possible implementation:

defmodule DemoWeb.I18n do
  def cast_locale(locale) do
    case locale do
      # explicit matching on supported locales
      locale when locale in ["en", "zh"] ->
        locale

      # fuzzy matching on en locale
      "en-" <> _ ->
        "en"

      # fuzzy matching on zh locale
      "zh-" <> _ ->
        "zh"

      # fallback for unsupported locales
      _ ->
        "en"
    end
  end
end

Then, use above implementation for plug:

plug `PlugLocale.Browser`,
  default_locale: "en",
  locales: ["en", "zh"],
  cast_locale_by: &DemoWeb.I18n.cast_locale/1,
  # ...

Helper functions

PlugLocale.Browser also provides some helper functions, which will be useful when implementing UI components:

Check out their docs for more details.

an example - a simple locale switcher using build_locale_path/2

<ul>
  <li>
    <a
      href={PlugLocale.Browser.build_locale_path(@conn, "en")}
      aria-label="switch to locale - en"
    >
      English
    </a>
  </li>
  <li>
    <a
      href={PlugLocale.Browser.build_locale_path(@conn, "zh")}
      aria-label="switch to locale - zh"
    >
      中文
    </a>
  </li>
</ul>

How it works?

This plug will try to:

  1. extract locale from URL, and check if the locale is supported:
    • If it succeeds, put locale into assigns storage.
    • If it fails, jump to step 2.
  2. detect locale from Web browser environment, then redirect to the path corresponding to detected locale.

Extract locale from URL

For example, the locale extracted from https://example.com/en/welcome is en.

Detect locale from Web browser environment

By default, local is detected from multiple sources:

  • query (whose key is specified by :query_key option)
  • cookie (whose key is specified by :cookie_key option)
  • HTTP request header - Referer
  • HTTP request header - Accept-Language

If all detections fail, fallback to default locale.

Examples

When:

  • :default_locale option is set to "en"
  • :locales option is set to ["en", "zh"]

For users in an English-speaking environment:

  • https://example.com/en will be responded directly.
  • https://example.com/ will be redirected to https://example.com/en.
  • https://example.com/path will be redirected to https://example.com/en/path.
  • https://example.com/unknown will be redirected to https://example.com/en.
  • ...

For users in an Chinese-speaking environment:

  • https://example.com/zh will be responded directly.
  • https://example.com/ will be redirected to https://example.com/zh.
  • https://example.com/path will be redirected to https://example.com/zh/path.
  • https://example.com/unknown will be redirected to https://example.com/zh.
  • ...

Summary

Functions

Builds a localized path for current connection.

Builds a localized url for current connection.

Puts a response cookie for locale in the connection.

Functions

Link to this function

build_locale_path(conn, locale)

View Source
@spec build_locale_path(Plug.Conn.t(), String.t()) :: String.t()

Builds a localized path for current connection.

Note: the locale passed to this function won't be casted by the function which is specified by :cast_locale_by option.

Examples

# the request path of conn is /posts/7
iex> build_locale_path(conn, "en")
"/en/posts/7"

# the request path of conn is /en/posts/7
iex> build_locale_path(conn, "zh")
"/zh/posts/7"
Link to this function

build_locale_url(conn, locale)

View Source
@spec build_locale_url(Plug.Conn.t(), String.t()) :: String.t()

Builds a localized url for current connection.

Note: the locale passed to this function won't be casted by the function which is specified by :cast_locale_by option.

Examples

# the request path of conn is /posts/7
iex> build_locale_path(conn, "en")
"http://www.example.com/en/posts/7"

# the request path of conn is /en/posts/7
iex> build_locale_path(conn, "zh")
"http://www.example.com/zh/posts/7"
Link to this function

put_locale_resp_cookie(conn, locale, opts \\ [])

View Source

Puts a response cookie for locale in the connection.

This is a simple wrapper around Plug.Conn.put_resp_cookie/4. See its docs for more details.

Examples

iex> put_locale_resp_cookie(conn, "en")
iex> put_locale_resp_cookie(conn, "zh", max_age: 365 * 24 * 60 * 60)

Use cases

Use this function to persistent current locale into cookie, then subsequent requests can directly read the locale from the cookie.

defmodule DemoWeb.PlugBrowserLocalization do
  use Plug.Builder

  plug PlugLocale.Browser,
    default_locale: "en",
    locales: ["en", "zh"],
    route_identifier: :locale,
    assign_key: :locale

  plug :put_locale

  def put_locale(conn, _opts) do
    if locale = conn.assigns[:locale] do
      # integrate with gettext
      Gettext.put_locale(locale)

      # persistent current locale into cookie
      PlugLocale.Browser.put_locale_resp_cookie(
        conn,
        locale,
        max_age: 365 * 24 * 60 * 60
      )
    else
      conn
    end
  end
end