if match?({:module, _}, Code.ensure_compiled(Cldr.LocaleDisplay)) do defmodule Cldr.HTML.Locale do @moduledoc """ Implements an HTML Form select specifically localised locale display. """ alias Cldr.Locale @type select_options :: [ {:locales, [atom() | binary(), ...]} | {:locale, Cldr.Locale.locale_name() | Cldr.LanguageTag.t()} | {:collator, function()} | {:mapper, function()} | {:backend, module()} | {:selected, atom() | binary()} | {atom(), any()} ] @type locale :: %{ locale: String.t(), display_name: String.t(), language_tag: Cldr.LanguageTag.t() } @type mapper :: (locale() -> String.t()) @identity :identity # All configurations include these locales # but they shouldn't be presented for # display @dont_include_default [:"en-001", :root, :und] @doc """ Generate an HTML select tag for a locale list that can be used with a `Phoenix.HTML.Form.t`. ## Arguments * A `t:Phoenix.HTML.Form.t/0` form * A `t:Phoenix.HTML.Form.field/0` field * A `t:Keyword.t/0` list of options ## Options For select options see `Phoenix.HTML.Form.options_for_select/2` * `:locales` defines the list of locales to be displayed in the the `select` tag. The list defaults to `Cldr.known_locale_names/0`. If `:backend` is specified then the list of locales known to that backend is returned. If no `:backend` is specified the locales known to `Cldr.default_backend!/0` is returned. * `:locale` defines the locale to be used to localise the description of the list of locales. The default is the locale returned by `Cldr.get_locale/1` If set to `:identity` then each locale in the `:locales` list will be rendered in its own locale. * `:backend` is any backend module. The default is `Cldr.default_backend!/0` * `:collator` is a function used to sort the locales in the selection list. It is passed a list of maps where each map represents a locale. The default collator sorts by `locale_1.display_name < locale_2.display_name`. As a result, default collation sorts by code point which will not return expected results for scripts other than Latin. * `:mapper` is a function that creates the text to be displayed in the select tag for each locale. It is passed a map with three fields: `:display_name`, `:locale` and `:language_tag`. The default mapper is `&{&1.display_name, &1.locale}`. See `t:locale`. * `:selected` identifies the locale that is to be selected by default in the `select` tag. The default is `nil`. This is passed to the form generator. * `:prompt` is a prompt displayed at the top of the select box. This is passed unmodified to the form generator. ## Notes If `:locale` is set to `:identity` then each locale in `:locales` will be used to render its own display name. In this case each locale in `:locales` must also be configured in the `:backend` or an error will be returned. ## Examples Cldr.HTML.Locale.select(:my_form, :locale_list, selected: "en") Cldr.HTML.Locale.select(:my_form, :locale_list, locales: ["zh-Hant", "ar", "fr"], mapper: &({&1.display_name, &1.locale})) """ @spec select( form :: Phoenix.HTML.Form.t(), field :: Phoenix.HTML.Form.field(), select_options ) :: Phoenix.HTML.safe() | {:error, {Cldr.UnknownLocaleError, binary()}} def select(form, field, options \\ []) def select(form, field, options) when is_list(options) do select(form, field, validate_options(options), options[:selected]) end @doc """ Generate a list of options for a locale list that can be used with `Phoenix.HTML.Form.options_for_select/2` or to create a . ## Arguments * A `t:Keyword.t/0` list of options. ## Options See `Cldr.HTML.Locale.select/3` for options. """ @spec locale_options(select_options) :: list(tuple()) | {:error, {Cldr.UnknownLocaleError, binary()}} def locale_options(options \\ []) def locale_options(options) when is_list(options) do options |> validate_options() |> build_locale_options() end # Invalid options defp select(_form, _field, {:error, reason}, _selected) do {:error, reason} end @omit_from_select_options [ :locales, :locale, :mapper, :collator, :backend, :add_likely_subtags, :prefer, :compound_locale ] # Selected locale defp select(form, field, %{locale: locale} = options, _selected) do select_options = options |> Map.drop(@omit_from_select_options) |> Map.to_list() options = build_locale_options(options) {options, select_options} = add_lang_attribute(locale, options, select_options) to_select(form, field, options, select_options) end if function_exported?(Phoenix.HTML.Form, :select, 4) do defp to_select(form, field, options, select_options) do Phoenix.HTML.Form.select(form, field, options, select_options) end else defp to_select(form, field, options, select_options) do {selected, select_options} = Keyword.pop(select_options, :selected) safe_options = options |> Phoenix.HTML.Form.options_for_select(selected) |> Phoenix.HTML.safe_to_string() safe_attrs = [ id: Phoenix.HTML.Form.input_id(form, field), name: Phoenix.HTML.Form.input_name(form, field) ] |> Keyword.merge(select_options) |> Enum.sort() |> Phoenix.HTML.attributes_escape() |> Phoenix.HTML.safe_to_string() [", safe_options, ""] |> IO.iodata_to_binary() |> Phoenix.HTML.raw() end end # For the :identity case, add a :lang attribute to each select option defp add_lang_attribute(@identity, options, select_options) do options = Enum.map(options, fn {key, value} -> [key: key, value: value, lang: value] end) {options, select_options} end # For the non-identity case, add one :lang attribute to the whole select defp add_lang_attribute(locale, options, select_options) do {options, Keyword.put(select_options, :lang, locale)} end defp validate_options(options) do options = Map.new(options) with options <- Map.merge(default_options(), options), {:ok, options} <- validate_locale(options.locale, options), {:ok, options} <- validate_selected(options.selected, options), {:ok, options} <- validate_locales(options.locales, options), {:ok, options} <- validate_identity_locales(options.locale, options) do options end end defp default_options do Map.new( locales: nil, locale: Cldr.get_locale(), backend: nil, collator: &default_collator/1, mapper: &{&1.display_name, &1.locale}, selected: nil, add_likely_subtags: false, compound_locale: false, prefer: :default ) end defp default_collator(locales) do Enum.sort(locales, &default_comparator/2) end # Note that this is not a unicode aware comparison defp default_comparator(locale_1, locale_2) do locale_1.display_name < locale_2.display_name end defp validate_selected(nil, options) do {:ok, options} end defp validate_selected(selected, options) do list_options = options |> Map.take([:add_likely_subtags]) |> Map.to_list() backend = options[:backend] with {:ok, locale} <- Locale.canonical_language_tag(selected, backend, list_options) do {:ok, Map.put(options, :selected, locale)} end end # Return a list of validated locales or an error defp validate_locales(nil, options) do default_locales = Cldr.known_locale_names(options[:backend]) -- @dont_include_default validate_locales(default_locales, options) end defp validate_locales(locales, options) when is_list(locales) do list_options = options |> Map.take([:add_likely_subtags]) |> Map.to_list() backend = options[:backend] Enum.reduce_while(locales, [], fn locale, acc -> case Locale.canonical_language_tag(to_string(locale), backend, list_options) do {:ok, locale} -> {:cont, [locale | acc]} {:error, reason} -> {:halt, {:error, reason}} end end) |> case do {:error, reason} -> {:error, reason} locales -> {:ok, Map.put(options, :locales, locales)} end end defp validate_identity_locales(@identity, options) do Enum.reduce_while(options.locales, {:ok, options}, fn locale, acc -> case Cldr.validate_locale(locale, options.backend) do {:ok, _locale} -> {:cont, acc} {:error, reason} -> {:halt, {:error, reason}} end end) end defp validate_identity_locales(_locale, options) do options end defp validate_locale(:identity, options) do {_locale, backend} = Cldr.locale_and_backend_from(nil, options[:backend]) {:ok, Map.put(options, :backend, backend)} end defp validate_locale(locale, options) do {locale, backend} = Cldr.locale_and_backend_from(locale, options.backend) with {:ok, locale} <- Cldr.validate_locale(locale, backend) do options |> Map.put(:locale, locale) |> Map.put(:backend, locale.backend) |> wrap(:ok) end end defp wrap(term, atom) do {atom, term} end defp maybe_include_selected_locale(%{selected: nil} = options) do options end defp maybe_include_selected_locale(%{locales: locales, selected: selected} = options) do if Enum.any?(locales, &(&1.canonical_locale_name == selected.canonical_locale_name)) do options else Map.put(options, :locales, [selected | locales]) end end defp build_locale_options(options) when is_map(options) do options = maybe_include_selected_locale(options) locales = Map.fetch!(options, :locales) locale = Map.fetch!(options, :locale) collator = Map.fetch!(options, :collator) mapper = Map.fetch!(options, :mapper) display_options = Map.take(options, [:prefer, :compound_locale]) |> Map.to_list() locales |> Enum.map(&display_name(&1, locale, display_options)) |> collator.() |> Enum.map(&mapper.(&1)) end defp display_name(locale, @identity, options) do if is_nil(locale.cldr_locale_name) do raise Cldr.UnknownLocaleError, "The locale #{locale.canonical_locale_name} is not known" end options = Keyword.put(options, :locale, locale) display_name = Cldr.LocaleDisplay.display_name!(locale, options) %{locale: locale.canonical_locale_name, display_name: display_name, language_tag: locale} end defp display_name(locale, _in_locale, options) do display_name = Cldr.LocaleDisplay.display_name!(locale, options) %{locale: locale.canonical_locale_name, display_name: display_name, language_tag: locale} end defimpl Phoenix.HTML.Safe, for: Cldr.LanguageTag do def to_iodata(language_tag) do language_tag.canonical_locale_name end end end end