defmodule PhoenixKitWeb.Live.Modules.Languages do @moduledoc """ Languages module settings LiveView for PhoenixKit admin panel. Provides interface for managing available languages and localization settings. """ use PhoenixKitWeb, :live_view alias PhoenixKit.Config alias PhoenixKit.Modules.Languages alias PhoenixKit.Modules.Publishing alias PhoenixKit.Modules.Publishing.ListingCache alias PhoenixKit.Settings alias PhoenixKit.Utils.Routes @default_locale Config.default_locale() def mount(_params, session, socket) do # Attach locale hook for automatic locale handling # Get current path for navigation current_path = get_current_path(socket, session) # Get project title from settings project_title = Settings.get_project_title() # Load languages configuration ml_config = Languages.get_config() display_languages = Languages.get_display_languages() # Get ALL languages grouped by continent -> country for display grouped_languages = Languages.get_languages_grouped_by_continent() # Get enabled language codes and default code enabled_codes = get_enabled_codes(display_languages) default_code = get_default_code(display_languages) # Calculate covered countries covered_countries = get_covered_countries(enabled_codes, grouped_languages) socket = socket |> assign(:current_path, current_path) |> assign(:page_title, "Languages") |> assign(:project_title, project_title) |> assign(:ml_enabled, ml_config.enabled) |> assign(:languages, display_languages) |> assign(:grouped_languages, grouped_languages) |> assign(:enabled_codes, enabled_codes) |> assign(:default_code, default_code) |> assign(:language_count, length(display_languages)) |> assign(:enabled_count, length(enabled_codes)) |> assign(:covered_countries, covered_countries) |> assign(:covered_count, length(covered_countries)) # Search filter |> assign(:search_query, "") # Switcher preview settings |> assign(:switcher_show_names, true) |> assign(:switcher_show_flags, true) |> assign(:switcher_goto_home, false) |> assign(:switcher_hide_current, false) |> assign(:switcher_show_native_names, false) # Backend tab settings |> assign(:admin_languages, load_admin_languages()) {:ok, socket} end defp load_admin_languages do admin_languages_json = Settings.get_setting_cached("admin_languages", nil) || Jason.encode!([@default_locale]) case Jason.decode(admin_languages_json) do {:ok, codes} when is_list(codes) -> codes _ -> [@default_locale] end end def handle_params(_params, _uri, socket) do active_tab = case socket.assigns.live_action do :backend -> "backend" # Default to frontend for :index and :frontend _ -> "frontend" end {:noreply, assign(socket, :active_tab, active_tab)} end def handle_event("toggle_languages", _params, socket) do # Toggle languages new_enabled = !socket.assigns.ml_enabled result = if new_enabled do Languages.enable_system() else Languages.disable_system() end case result do {:ok, _} -> # Regenerate all blog caches since language availability changed regenerate_all_blog_caches() # Reload configuration to get fresh data socket = socket |> reload_display_languages(new_enabled) |> put_flash( :info, if(new_enabled, do: "Languages enabled with default English", else: "Languages disabled" ) ) {:noreply, socket} {:error, _changeset} -> socket = put_flash(socket, :error, "Failed to update languages") {:noreply, socket} end end def handle_event("set_default", %{"code" => code}, socket) do case Languages.set_default_language(code) do {:ok, _config} -> language = Enum.find(socket.assigns.languages, &(&1.code == code)) socket = socket |> reload_display_languages() |> put_flash(:info, "#{language.name} set as default language") {:noreply, socket} {:error, reason} when is_binary(reason) -> socket = put_flash(socket, :error, reason) {:noreply, socket} {:error, _changeset} -> socket = put_flash(socket, :error, "Failed to set default language") {:noreply, socket} end end def handle_event("toggle_switcher_setting", %{"setting" => setting}, socket) do setting_atom = String.to_atom(setting) current_value = socket.assigns[setting_atom] {:noreply, assign(socket, setting_atom, !current_value)} end def handle_event("search_countries", %{"value" => query}, socket) do {:noreply, assign(socket, :search_query, query)} end def handle_event("toggle_language_availability", %{"code" => code}, socket) do # Check if language is currently enabled is_enabled = code in socket.assigns.enabled_codes result = if is_enabled do # Disable the language (remove from config) Languages.remove_language(code) else # Enable the language (add to config) Languages.add_language(code) end case result do {:ok, _config} -> # Regenerate all blog caches since language availability changed regenerate_all_blog_caches() predefined_lang = Languages.get_predefined_language(code) language_name = (predefined_lang && predefined_lang.name) || code action = if is_enabled, do: "disabled", else: "enabled" socket = socket |> reload_display_languages() |> put_flash(:info, "#{language_name} #{action}") {:noreply, socket} {:error, reason} when is_binary(reason) -> socket = put_flash(socket, :error, reason) {:noreply, socket} {:error, _changeset} -> socket = put_flash(socket, :error, "Failed to update language") {:noreply, socket} end end # Backend tab event handlers def handle_event( "add_admin_language_to_form", %{"admin_language_code_input" => language_code}, socket ) when language_code != "" do current_languages = socket.assigns.admin_languages || [@default_locale] # Toggle: add if not present, remove if already present if language_code in current_languages do # Remove language updated_languages = Enum.filter(current_languages, &(&1 != language_code)) language = Languages.get_predefined_language(language_code) language_name = if language, do: language.name, else: String.upcase(language_code) socket = socket |> assign(:admin_languages, updated_languages) |> put_flash(:info, "#{language_name} removed. Remember to save settings.") {:noreply, socket} else # Add language updated_languages = current_languages ++ [language_code] language = Languages.get_predefined_language(language_code) language_name = if language, do: language.name, else: String.upcase(language_code) socket = socket |> assign(:admin_languages, updated_languages) |> put_flash(:info, "#{language_name} added. Remember to save settings.") {:noreply, socket} end end def handle_event("add_admin_language_to_form", _params, socket) do {:noreply, socket} end def handle_event("remove_admin_language_from_form", %{"code" => code}, socket) do current_languages = socket.assigns.admin_languages || [@default_locale] # Prevent removing the last language if length(current_languages) <= 1 do socket = put_flash( socket, :warning, "Cannot remove the last language. At least one language must be configured." ) {:noreply, socket} else updated_languages = Enum.filter(current_languages, &(&1 != code)) language = Languages.get_predefined_language(code) language_name = if language, do: language.name, else: String.upcase(code) socket = socket |> assign(:admin_languages, updated_languages) |> put_flash(:info, "#{language_name} removed. Remember to save settings.") {:noreply, socket} end end def handle_event("save_admin_languages", _params, socket) do admin_languages = socket.assigns.admin_languages || [@default_locale] case Settings.update_setting("admin_languages", Jason.encode!(admin_languages)) do {:ok, _} -> socket = put_flash(socket, :info, "Admin panel languages saved successfully.") {:noreply, socket} {:error, _} -> socket = put_flash(socket, :error, "Failed to save admin panel languages.") {:noreply, socket} end end defp get_current_path(_socket, _session) do # For LanguagesLive, return the settings path Routes.path("/admin/settings/languages") end # Helper function to reload display languages from the Languages module defp reload_display_languages(socket, enabled \\ nil) do enabled = enabled || Languages.enabled?() display_languages = Languages.get_display_languages() grouped_languages = Languages.get_languages_grouped_by_continent() enabled_codes = get_enabled_codes(display_languages) default_code = get_default_code(display_languages) covered_countries = get_covered_countries(enabled_codes, grouped_languages) # Sync the admin_languages setting so the admin navbar updates too sync_admin_languages(display_languages) socket |> assign(:ml_enabled, enabled) |> assign(:languages, display_languages) |> assign(:grouped_languages, grouped_languages) |> assign(:enabled_codes, enabled_codes) |> assign(:default_code, default_code) |> assign(:language_count, length(display_languages)) |> assign(:enabled_count, length(enabled_codes)) |> assign(:covered_countries, covered_countries) |> assign(:covered_count, length(covered_countries)) end # Sync the admin_languages setting with the current display languages defp sync_admin_languages(display_languages) do enabled_codes = display_languages |> Enum.filter(& &1.is_enabled) |> Enum.map(& &1.code) Settings.update_json_setting("admin_languages", enabled_codes) end # Helper function to generate the language switcher code based on current settings defp generate_switcher_code(show_flags, show_names, goto_home, hide_current, show_native_names) do flags_line = if show_flags, do: "\n show_flags={true}", else: "" names_line = if show_names, do: "\n show_names={true}", else: "" home_line = if goto_home, do: "\n goto_home={true}", else: "" hide_line = if hide_current, do: "\n hide_current={true}", else: "" native_names_line = if show_native_names, do: "\n show_native_names={true}", else: "" """ <.language_switcher_dropdown current_locale={@current_locale}#{flags_line}#{names_line}#{native_names_line}#{home_line}#{hide_line} /> """ end # Get list of enabled language codes from display languages defp get_enabled_codes(display_languages) do display_languages |> Enum.filter(& &1.is_enabled) |> Enum.map(& &1.code) end # Get the default language code defp get_default_code(display_languages) do case Enum.find(display_languages, & &1.is_default) do %{code: code} -> code _ -> nil end end # Count enabled languages in a list defp count_enabled(languages, enabled_codes) do Enum.count(languages, fn lang -> lang.code in enabled_codes end) end # Count countries in a continent that have ALL their languages enabled (covered) defp count_covered_countries_in_continent(countries, enabled_codes) do Enum.count(countries, fn {_country, _flag, languages} -> languages != [] and Enum.all?(languages, fn lang -> lang.code in enabled_codes end) end) end # Filter grouped languages by search query (continent -> country -> languages structure) defp filter_grouped_languages(grouped_languages, ""), do: grouped_languages defp filter_grouped_languages(grouped_languages, nil), do: grouped_languages defp filter_grouped_languages(grouped_languages, query) do query_downcase = String.downcase(query) grouped_languages |> Enum.map(fn {continent, countries} -> filtered_countries = Enum.filter(countries, fn {country, _flag, languages} -> # Match country name or any language in the group String.contains?(String.downcase(country), query_downcase) or Enum.any?(languages, fn lang -> String.contains?(String.downcase(lang.name), query_downcase) or String.contains?(String.downcase(lang.native), query_downcase) or String.contains?(String.downcase(lang.code), query_downcase) end) end) {continent, filtered_countries} end) |> Enum.reject(fn {_continent, countries} -> countries == [] end) end # Get list of countries that have ALL their languages enabled defp get_covered_countries(enabled_codes, grouped_languages) do grouped_languages |> Enum.flat_map(fn {_continent, countries} -> countries |> Enum.filter(fn {_country, _flag, languages} -> # Country is covered only if ALL its languages are enabled languages != [] and Enum.all?(languages, fn lang -> lang.code in enabled_codes end) end) |> Enum.map(fn {country, flag, _langs} -> {country, flag} end) end) |> Enum.uniq() |> Enum.sort_by(fn {country, _} -> country end) end # Regenerate listing caches for all blogs when language settings change defp regenerate_all_blog_caches do if Publishing.enabled?() do Publishing.list_groups() |> Enum.each(fn blog -> ListingCache.regenerate(blog["slug"]) end) end end end