if Code.ensure_loaded?(Plug) do defmodule Cldr.Plug.SetLocale do @moduledoc """ Sets the Cldr and/or Gettext locales derived from the accept-language header, a query parameter, a url parameter, a body parameter or the session. ## Options * `:apps` - list of apps for which to set locale. See the apps configuration section. * `:from` - where in the request to look for the locale. The default is `[:session, :accept_language]`. The valid options are: * `:accept_language` will parse the `accept-language` header and finds the best matched configured locale * `:path` will look for a locale by examining `conn.path_params` * `:query` will look for a locale by examining `conn.query_params` * `:body` will look for a locale by examining `conn.body_params` * `:cookie` will look for a locale in the request cookie(s) * `:session` will look for a locale in the session * `:default` - the default locale to set if no locale is found by other configured methods. It can be a string like "en" or a `Cldr.LanguageTag` struct. The default is `Cldr.default_locale/1` * `:gettext` - the name of the `Gettext` backend module upon which the locale will be validated. This option is not required if a gettext module is specified in the `:apps` configuration. * `:cldr` - the name of the `Cldr` backend module upon which the locale will be validated. This option is not required if a gettext module is specified in the `:apps` configuration. * `:session_key` - defines the key used to look for the locale in the session. The default is `locale`. * `:put_session?` - defines whether the resolved locale is put in the session. The default is `false`. If set to `true` then two items are stored in the session: `locale.requested_locale_name` is stored under the configured `:session_key` and the configured backend module is stored in the session under the "cldr_backend" key. If a locale is found then `conn.private[:cldr_locale]` is also set. It can be retrieved with `Cldr.Plug.SetLocale.get_cldr_locale/1`. ## App configuration The `:apps` configuration key defines which applications will have their locale *set* by this plug. `Cldr.Plug.SetLocale` can set the locale for `cldr`, `gettext` or both. The basic configuration of the `:app` key is an atom, or list of atoms, containing one or both of these app names. For example: apps: :cldr apps: :gettext apps: [:cldr, :gettext] In each of these cases, the locale is set globally **for the current process**. Sometimes setting the locale for only a specific backend is required. In this case, configure the `:apps` key as a keyword list pairing an application with the required backend module. The value `:global` signifies setting the local for the global context. For example: apps: [cldr: MyApp.Cldr] apps: [gettext: MyAppGettext] apps: [gettext: :global] apps: [cldr: MyApp.Cldr, gettext: MyAppGettext] ## Using Cldr.Plug.SetLocale without Phoenix If you are using `Cldr.Plug.SetLocale` without Phoenix and you plan to use `:path_param` to identify the locale of a request then `Cldr.Plug.SetLocale` must be configured *after* `plug :match` and *before* `plug :dispatch`. For example: defmodule MyRouter do use Plug.Router plug :match plug Cldr.Plug.SetLocale, apps: [:cldr, :gettext], from: [:path, :query], gettext: MyApp.Gettext, cldr: MyApp.Cldr plug :dispatch get "/hello/:locale" do send_resp(conn, 200, "world") end end ## Using Cldr.Plug.SetLocale with Phoenix If you are using `Cldr.Plug.SetLocale` with Phoenix and you plan to use the `:path_param` to identify the locale of a request then `Cldr.Plug.SetLocale` must be configured in the router module, *not* in the endpoint module. This is because `conn.path_params` has not yet been populated in the endpoint. For example: defmodule MyAppWeb.Router do use MyAppWeb, :router pipeline :browser do plug :accepts, ["html"] plug :fetch_session plug Cldr.Plug.SetLocale, apps: [:cldr, :gettext], from: [:path, :query], gettext: MyApp.Gettext, cldr: MyApp.Cldr plug :fetch_flash plug :protect_from_forgery plug :put_secure_browser_headers end scope "/:locale", HelloWeb do pipe_through :browser get "/", PageController, :index end end ## Examples # Will set the global locale for the current process # for both `:cldr` and `:gettext` plug Cldr.Plug.SetLocale, apps: [:cldr, :gettext], from: [:query, :path, :body, :cookie, :accept_language], param: "locale", gettext: GetTextModule, cldr: MyApp.Cldr # Will set the backend-only locale for the current process # for both `:cldr` and `:gettext` plug Cldr.Plug.SetLocale, apps: [cldr: MyApp.Cldr, gettext: GetTextModule], from: [:query, :path, :body, :cookie, :accept_language], param: "locale" # Will set the backend-only locale for the current process # for `:cldr` and globally for `:gettext` plug Cldr.Plug.SetLocale, apps: [cldr: MyApp.Cldr, gettext: :global], from: [:query, :path, :body, :cookie, :accept_language], param: "locale" """ import Plug.Conn require Logger alias Cldr.AcceptLanguage @default_apps [cldr: :global] @default_from [:session, :accept_language, :query, :path] @default_param_name "locale" @private_key :cldr_locale @session_key "cldr_locale" @from_options [:accept_language, :path, :body, :query, :session, :cookie] @app_options [:cldr, :gettext] @language_header "accept-language" @doc false def init(options) do options |> validate_apps(options[:apps]) |> validate_from(options[:from]) |> validate_param(options[:param]) |> validate_cldr(options[:cldr]) |> validate_gettext(options[:gettext]) |> validate_default(options[:default]) |> validate_session_key(options[:session_key]) end @doc false def call(conn, options) do if locale = locale_from_params(conn, options[:from], options) || options[:default] do Enum.each(options[:apps], fn app -> put_locale(app, locale, options) end) put_private(conn, @private_key, locale) else conn end end @doc """ Returns the name of the session key used to store the CLDR locale name. ## Example iex> Cldr.Plug.SetLocale.session_key() "cldr_locale" """ def session_key do @session_key end @doc false def private_key do @private_key end @doc """ Return the locale set by `Cldr.Plug.SetLocale` """ def get_cldr_locale(conn) do conn.private[:cldr_locale] end defp locale_from_params(conn, from, options) do Enum.reduce_while(from, nil, fn param, _acc -> conn |> fetch_param(param, options[:param], options) |> return_if_valid_locale end) end defp fetch_param(conn, :accept_language, _param, options) do case get_req_header(conn, @language_header) do [accept_language] -> AcceptLanguage.best_match(accept_language, options[:cldr]) [accept_language | _] -> AcceptLanguage.best_match(accept_language, options[:cldr]) [] -> nil end end defp fetch_param( %Plug.Conn{query_params: %Plug.Conn.Unfetched{aspect: :query_params}} = conn, :query, param, options ) do conn = fetch_query_params(conn) fetch_param(conn, :query, param, options) end defp fetch_param(conn, :query, param, options) do conn |> Map.get(:query_params) |> Map.get(param) |> Cldr.validate_locale(options[:cldr]) end defp fetch_param(conn, :path, param, options) do conn |> Map.get(:path_params) |> Map.get(param) |> Cldr.validate_locale(options[:cldr]) end defp fetch_param(conn, :body, param, options) do conn |> Map.get(:body_params) |> Map.get(param) |> Cldr.validate_locale(options[:cldr]) end defp fetch_param(conn, :session, _param, options) do conn |> get_session(options[:session_key]) |> Cldr.validate_locale(options[:cldr]) end defp fetch_param(conn, :cookie, param, options) do conn |> Map.get(:cookies) |> Map.get(param) |> Cldr.validate_locale(options[:cldr]) end defp return_if_valid_locale(nil) do {:cont, nil} end defp return_if_valid_locale({:error, _}) do {:cont, nil} end defp return_if_valid_locale({:ok, locale}) do {:halt, locale} end defp put_locale({:cldr, :global}, locale, _options) do Cldr.put_locale(locale) end # Deprecated option :all. Use :global defp put_locale({:cldr, :all}, locale, _options) do Cldr.put_locale(locale) end defp put_locale({:cldr, backend}, locale, _options) do backend.put_locale(locale) end defp put_locale({:gettext, _}, %Cldr.LanguageTag{gettext_locale_name: nil} = locale, _options) do Logger.warn( "Locale #{inspect(locale.requested_locale_name)} does not have a known " <> "Gettext locale. No Gettext locale has been set." ) nil end defp put_locale( {:gettext, :global}, %Cldr.LanguageTag{gettext_locale_name: locale_name}, _options ) do {:ok, apply(Gettext, :put_locale, [locale_name])} end # Deprecated option :all. Use :global defp put_locale( {:gettext, :all}, %Cldr.LanguageTag{gettext_locale_name: locale_name}, _options ) do {:ok, apply(Gettext, :put_locale, [locale_name])} end defp put_locale( {:gettext, backend}, %Cldr.LanguageTag{gettext_locale_name: locale_name}, _options ) do {:ok, apply(Gettext, :put_locale, [backend, locale_name])} end defp validate_apps(options, nil), do: Keyword.put(options, :apps, @default_apps) defp validate_apps(options, app) when is_atom(app) do options |> Keyword.put(:apps, [app]) |> validate_apps([app]) end defp validate_apps(options, apps) when is_list(apps) do app_config = Enum.map(apps, fn {app, scope} -> validate_app_and_scope!(app, scope) {app, scope} app -> validate_app_and_scope!(app, nil) {app, :global} end) Keyword.put(options, :apps, app_config) end defp validate_apps(_options, apps) do raise( ArgumentError, "Invalid apps list: #{inspect(apps)}." ) end defp validate_app_and_scope!(app, nil) when app in @app_options do :ok end defp validate_app_and_scope!(app, :global) when app in @app_options do :ok end # Deprecated option :all. Use :global defp validate_app_and_scope!(app, :all) when app in @app_options do :ok end defp validate_app_and_scope!(:cldr, module) when is_atom(module) do Cldr.validate_backend!(module) :ok end defp validate_app_and_scope!(:gettext, module) when is_atom(module) do Cldr.Code.ensure_compiled?(module) || raise(ArgumentError, "Gettext backend #{inspect module} is unknown") :ok end defp validate_app_and_scope!(app, scope) do raise( ArgumentError, "Invalid app #{inspect(app)} or scope #{inspect(scope)} detected." ) end defp validate_from(options, nil), do: Keyword.put(options, :from, @default_from) defp validate_from(options, from) when is_atom(from) do options |> Keyword.put(:from, [from]) |> validate_from([from]) end defp validate_from(options, from) when is_list(from) do Enum.each(from, fn f -> if f not in @from_options do raise( ArgumentError, "Invalid :from option #{inspect(f)} detected. " <> " Valid :from options are #{inspect(@from_options)}" ) end end) options end defp validate_from(_options, from) do raise( ArgumentError, "Invalid :from list #{inspect(from)} detected. " <> "Valid from options are #{inspect(@from_options)}" ) end defp validate_param(options, nil), do: Keyword.put(options, :param, @default_param_name) defp validate_param(options, param) when is_binary(param), do: options defp validate_param(options, param) when is_atom(param) do validate_from(options, param) end defp validate_param(_options, param) do raise( ArgumentError, "Invalid :param #{inspect(param)} detected. " <> ":param must be a string" ) end defp validate_default(options, nil) do default = options[:cldr].default_locale() Keyword.put(options, :default, default) end defp validate_default(options, default) do case Cldr.validate_locale(default, options[:cldr]) do {:ok, locale} -> Keyword.put(options, :default, locale) {:error, {exception, reason}} -> raise exception, reason end end # No configured gettext. See if there is one configured # on the Cldr backend defp validate_gettext(options, nil) do gettext = options[:cldr].__cldr__(:config).gettext if gettext && get_in(options, [:apps, :gettext]) do Keyword.put(options, :gettext, gettext) else options end end defp validate_gettext(options, gettext) do case Code.ensure_compiled(gettext) do {:error, _} -> raise ArgumentError, "Gettext module #{inspect(gettext)} is not known" {:module, _} -> options end end defp validate_session_key(options, nil), do: Keyword.put(options, :session_key, @session_key) defp validate_session_key(options, session_key) when is_binary(session_key) do IO.warn "The :session_key option is deprecated and will be removed in " <> "a future release", [] options end defp validate_session_key(_options, session_key) do raise( ArgumentError, "Invalid :session_key #{inspect(session_key)} detected. " <> ":session_key must be a string" ) end defp validate_cldr(options, nil) do backend = Keyword.get_lazy(options[:apps], :cldr, &Cldr.default_locale/0) validate_cldr(options, backend) end defp validate_cldr(options, backend) when is_atom(backend) do with {:ok, backend} <- Cldr.validate_backend(backend) do Keyword.put(options, :cldr, backend) else {:error, {exception, reason}} -> raise exception, reason end end end end