defmodule Sayfa.Blocks.LanguageSwitcher do @moduledoc """ Language switcher block. Renders links to translations of the current page. Returns `""` if only one language is configured. Uses `hreflang_alternates` from content metadata for content pages, and constructs alternate URLs from language config for list/index pages. Falls back to each language's home path (`/` for the default language, `/lang/` for others) when no translation links are available. ## Assigns - `:site` — site config map (used for languages) - `:content` — current `Sayfa.Content` struct (nil on list/home pages) - `:lang` — current language atom - `:page_url` — current page URL (for list pages) - `:variant` — optional atom (`:desktop`, `:mobile`, or `:default`) for unique IDs (default: `:default`) ## Examples <%= @block.(:language_switcher, []) %> <%= @block.(:language_switcher, variant: :desktop) %> <%= @block.(:language_switcher, variant: :mobile) %> """ @behaviour Sayfa.Behaviours.Block alias Sayfa.Block @impl true def name, do: :language_switcher @impl true def render(assigns) do site = Map.get(assigns, :site, %{}) languages = Map.get(site, :languages, []) if length(languages) <= 1 do "" else content = Map.get(assigns, :content) current_lang = Map.get(assigns, :lang, Map.get(site, :default_lang, :en)) variant = Map.get(assigns, :variant, :default) alternates = build_alternates(content, assigns, languages, current_lang, site) render_switcher(alternates, current_lang, languages, variant) end end defp build_alternates(content, _assigns, languages, _current_lang, site) when not is_nil(content) do case content.meta["hreflang_alternates"] do alternates when is_list(alternates) and alternates != [] -> Map.new(alternates, fn {lang_str, url} -> {String.to_atom(lang_str), url} end) _ -> build_home_alternates(languages, site) end end defp build_alternates(nil, assigns, languages, current_lang, site) do case Map.get(assigns, :archive_alternates) do alternates when is_map(alternates) and map_size(alternates) > 0 -> alternates _ -> page_url = Map.get(assigns, :page_url) if page_url do construct_url_alternates(page_url, languages, current_lang, site) else build_home_alternates(languages, site) end end end defp build_home_alternates(languages, site) do default_lang = Map.get(site, :default_lang, :en) Map.new(Keyword.keys(languages), fn lang -> url = if lang == default_lang, do: "/", else: "/#{lang}/" {lang, url} end) end defp construct_url_alternates(page_url, languages, current_lang, site) do default_lang = Map.get(site, :default_lang, :en) Map.new(Keyword.keys(languages), fn lang -> url = construct_alternate_url(page_url, lang, current_lang, default_lang) {lang, url} end) end defp construct_alternate_url(page_url, target_lang, current_lang, default_lang) do stripped = if current_lang != default_lang do prefix = "/#{current_lang}/" if String.starts_with?(page_url, prefix) do "/" <> String.trim_leading(page_url, prefix) else page_url end else page_url end if target_lang == default_lang do stripped else "/#{target_lang}#{stripped}" end end defp render_switcher(alternates, _current_lang, _languages, _variant) when map_size(alternates) <= 1, do: "" defp render_switcher(alternates, current_lang, languages, variant) do items = languages |> Keyword.keys() |> Enum.map(fn lang -> case Map.get(alternates, lang) do nil -> nil url -> {lang, get_language_name(lang, languages), url} end end) |> Enum.reject(&is_nil/1) if length(items) <= 1 do "" else render_dropdown(items, current_lang, variant) end end defp render_dropdown(items, current_lang, variant) do current_code = current_lang |> to_string() |> String.upcase() dropdown_items = Enum.map_join(items, "", &render_dropdown_item(&1, current_lang)) # Generate unique IDs based on variant id_suffix = if variant == :default, do: "", else: "-#{variant}" switcher_id = "lang-switcher#{id_suffix}" toggle_id = "lang-toggle#{id_suffix}" menu_id = "lang-menu#{id_suffix}" globe_svg = ~s() chevron_svg = ~s() ~s(
) <> ~s() <> ~s() <> ~s(
) end defp render_dropdown_item({lang, lang_name, _url}, lang) do ~s(#{Block.escape_html(lang_name)}) end defp render_dropdown_item({_lang, lang_name, url}, _current_lang) do ~s(#{Block.escape_html(lang_name)}) end defp get_language_name(lang, languages) do languages |> Keyword.get(lang, []) |> Keyword.get(:name, lang |> to_string() |> String.upcase()) end end