Packages

phoenix_kit

1.7.34
1.7.208 1.7.207 1.7.206 1.7.205 1.7.204 1.7.203 1.7.202 1.7.201 1.7.200 1.7.199 1.7.198 1.7.197 1.7.196 1.7.194 1.7.193 1.7.192 1.7.191 1.7.190 1.7.189 1.7.187 1.7.186 1.7.185 1.7.184 1.7.183 1.7.182 1.7.181 1.7.180 1.7.179 1.7.178 1.7.177 1.7.176 1.7.175 1.7.174 1.7.173 1.7.172 1.7.171 1.7.170 1.7.169 1.7.168 1.7.167 1.7.166 1.7.165 1.7.164 1.7.162 1.7.161 1.7.160 1.7.159 1.7.157 1.7.156 1.7.155 1.7.154 1.7.153 1.7.152 1.7.151 1.7.150 1.7.149 1.7.146 1.7.145 1.7.144 1.7.143 1.7.138 1.7.133 1.7.132 1.7.131 1.7.130 1.7.128 1.7.126 1.7.125 1.7.121 1.7.120 1.7.119 1.7.118 1.7.117 1.7.116 1.7.115 1.7.114 1.7.113 1.7.112 1.7.111 1.7.110 1.7.109 1.7.108 1.7.107 1.7.106 1.7.105 1.7.104 1.7.103 1.7.102 1.7.101 1.7.100 1.7.99 1.7.98 1.7.97 1.7.96 1.7.95 1.7.94 1.7.93 1.7.92 1.7.91 1.7.90 1.7.89 1.7.88 1.7.87 1.7.86 1.7.85 1.7.84 1.7.83 1.7.82 1.7.81 1.7.80 1.7.79 1.7.78 1.7.77 1.7.76 1.7.75 1.7.74 1.7.71 1.7.70 1.7.69 1.7.66 1.7.65 1.7.64 1.7.63 1.7.62 1.7.61 1.7.59 1.7.58 1.7.57 1.7.56 1.7.55 1.7.54 1.7.53 1.7.52 1.7.51 1.7.49 1.7.44 1.7.43 1.7.42 1.7.41 1.7.39 1.7.38 1.7.37 1.7.36 1.7.34 1.7.33 1.7.31 1.7.30 1.7.29 1.7.28 1.7.27 1.7.26 1.7.25 1.7.24 1.7.23 1.7.22 1.7.21 1.7.20 1.7.19 1.7.18 1.7.17 1.7.16 1.7.15 1.7.14 1.7.13 1.7.12 1.7.11 1.7.10 1.7.9 1.7.8 1.7.7 1.7.6 1.7.5 1.7.4 1.7.3 1.7.2 1.7.1 1.7.0 1.6.20 1.6.19 1.6.18 1.6.17 1.6.16 1.6.15 1.6.14 1.6.13 1.6.12 1.6.11 1.6.10 1.6.9 1.6.8 1.6.7 1.6.6 1.6.5 1.6.4 1.6.3 1.5.2 1.5.1 1.5.0 1.4.9 1.4.8 1.4.7 1.4.6 1.4.5 1.4.4 1.4.3 1.4.2 1.4.1 1.4.0 1.3.2 1.3.1 1.3.0 1.2.10 1.2.9 1.2.8 1.2.7 1.2.5 1.2.4 1.2.2 1.2.1 1.2.0 1.1.0 1.0.0

A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more

Current section

Files

Jump to
phoenix_kit lib modules publishing web components language_switcher.ex
Raw

lib/modules/publishing/web/components/language_switcher.ex

defmodule PhoenixKit.Modules.Publishing.Web.Components.LanguageSwitcher do
@moduledoc """
Unified language switcher component for publishing posts.
Displays available languages as a compact inline list with status indicators.
Flexible enough for admin interfaces (with status dots) and public pages (links only).
## Display Format
Admin mode: ● EN | ● FR | ○ ES
- Green dot (●): Published
- Yellow dot (●): Draft
- Gray dot (●): Archived
- Empty dot (○): No translation exists
Public mode: EN | FR | ES
- No status indicators
- Only shows languages with published translations
## Examples
# Admin: publishing listing with edit links and status indicators
<.publishing_language_switcher
languages={@languages}
current_language="en"
on_click="switch_language"
show_status={true}
/>
# Admin: editor with switch functionality
<.publishing_language_switcher
languages={@languages}
current_language={@current_language}
on_click="switch_language"
show_status={true}
show_add={true}
/>
# Public: post page with translation links
<.publishing_language_switcher
languages={@translations}
current_language={@current_language}
show_status={false}
/>
"""
use Phoenix.Component
use Gettext, backend: PhoenixKitWeb.Gettext
@doc """
Renders a compact inline language switcher.
## Attributes
- `languages` - List of language maps with keys: `code`, `name`, `status`, `url`, `exists`
- `current_language` - Currently active language code
- `show_status` - Show status indicator dots (default: true)
- `show_add` - Show "add" option for missing languages (default: false)
- `show_flags` - Show flag emojis (default: false)
- `on_click` - Event name for click handler (optional, uses href if not provided)
- `class` - Additional CSS classes
- `size` - Size variant: :xs, :sm, :md (default: :sm)
## Language Map Structure
Each language in the list should have:
- `code` - Language code (e.g., "en", "fr")
- `name` - Display name (e.g., "English", "French") - optional
- `flag` - Flag emoji (e.g., "🇺🇸") - optional
- `status` - Post status: "published", "draft", "archived", or nil if not exists
- `url` - URL to navigate to (for public mode or href navigation)
- `exists` - Boolean, whether translation file exists (default: inferred from status)
- `enabled` - Boolean, whether the language is enabled in the Languages module (default: true)
- `known` - Boolean, whether the language code is recognized (default: true)
"""
attr :languages, :list, required: true
attr :current_language, :string, default: nil
attr :show_status, :boolean, default: true
attr :show_add, :boolean, default: false
attr :show_flags, :boolean, default: false
attr :on_click, :string, default: nil
attr :phx_target, :any, default: nil
attr :class, :string, default: ""
attr :size, :atom, default: :sm, values: [:xs, :sm, :md]
def publishing_language_switcher(assigns) do
# Filter languages for public mode (only show existing/published)
languages =
if assigns.show_status do
assigns.languages
else
Enum.filter(assigns.languages, fn lang ->
lang_exists?(lang) && lang[:status] == "published"
end)
end
assigns = assign(assigns, :filtered_languages, languages)
~H"""
<div class={["flex items-center flex-wrap", size_gap_class(@size), @class]}>
<%= for {lang, index} <- Enum.with_index(@filtered_languages) do %>
<%= if index > 0 do %>
<span class={["text-base-content/30", size_separator_class(@size)]}>|</span>
<% end %>
<.language_item
lang={lang}
current_language={@current_language}
show_status={@show_status}
show_add={@show_add}
show_flags={@show_flags}
on_click={@on_click}
phx_target={@phx_target}
size={@size}
/>
<% end %>
</div>
"""
end
attr :lang, :map, required: true
attr :current_language, :string, default: nil
attr :show_status, :boolean, default: true
attr :show_add, :boolean, default: false
attr :show_flags, :boolean, default: false
attr :on_click, :string, default: nil
attr :phx_target, :any, default: nil
attr :size, :atom, default: :sm
defp language_item(assigns) do
lang = assigns.lang
is_current = assigns.current_language == lang[:code]
exists = lang_exists?(lang)
status = lang[:status]
enabled = lang_enabled?(lang)
known = lang_known?(lang)
is_primary = lang_is_primary?(lang)
assigns =
assigns
|> assign(:is_current, is_current)
|> assign(:exists, exists)
|> assign(:status, status)
|> assign(:enabled, enabled)
|> assign(:known, known)
|> assign(:is_primary, is_primary)
~H"""
<%= if @on_click do %>
<button
type="button"
phx-click={@on_click}
phx-value-language={@lang[:code]}
phx-value-path={@lang[:path]}
phx-value-post_path={@lang[:post_path]}
phx-value-status={@status}
phx-target={@phx_target}
class={item_classes(@is_current, @exists, @show_add, @size, @enabled, @known)}
title={language_title(@lang, @exists, @status, @show_status, @enabled, @known)}
aria-pressed={to_string(@is_current)}
>
<.language_content
lang={@lang}
exists={@exists}
status={@status}
show_status={@show_status}
show_add={@show_add}
show_flags={@show_flags}
is_current={@is_current}
size={@size}
enabled={@enabled}
known={@known}
is_primary={@is_primary}
/>
</button>
<% else %>
<%= if @lang[:url] do %>
<a
href={@lang[:url]}
class={item_classes(@is_current, @exists, @show_add, @size, @enabled, @known)}
title={language_title(@lang, @exists, @status, @show_status, @enabled, @known)}
>
<.language_content
lang={@lang}
exists={@exists}
status={@status}
show_status={@show_status}
show_add={@show_add}
show_flags={@show_flags}
is_current={@is_current}
size={@size}
enabled={@enabled}
known={@known}
is_primary={@is_primary}
/>
</a>
<% else %>
<span
class={item_classes(@is_current, @exists, @show_add, @size, @enabled, @known)}
title={language_title(@lang, @exists, @status, @show_status, @enabled, @known)}
>
<.language_content
lang={@lang}
exists={@exists}
status={@status}
show_status={@show_status}
show_add={@show_add}
show_flags={@show_flags}
is_current={@is_current}
size={@size}
enabled={@enabled}
known={@known}
is_primary={@is_primary}
/>
</span>
<% end %>
<% end %>
"""
end
attr :lang, :map, required: true
attr :exists, :boolean, required: true
attr :status, :string, default: nil
attr :show_status, :boolean, default: true
attr :show_add, :boolean, default: false
attr :show_flags, :boolean, default: false
attr :is_current, :boolean, default: false
attr :size, :atom, default: :sm
attr :enabled, :boolean, default: true
attr :known, :boolean, default: true
attr :is_primary, :boolean, default: false
defp language_content(assigns) do
~H"""
<span class="inline-flex items-center gap-1">
<%= if @show_status do %>
<span class={status_dot_classes(@exists, @status, @size, @enabled, @known, @is_primary)}>
</span>
<% end %>
<%= if @show_flags && @lang[:flag] do %>
<span class={flag_size_class(@size)}>{@lang[:flag]}</span>
<% end %>
<span class={code_classes(@exists, @status, @is_current, @size, @enabled, @known, @is_primary)}>
{get_display_code(@lang)}
</span>
</span>
"""
end
# Check if language translation exists
defp lang_exists?(%{exists: exists}) when is_boolean(exists), do: exists
defp lang_exists?(%{status: status}) when is_binary(status), do: true
defp lang_exists?(%{"exists" => exists}) when is_boolean(exists), do: exists
defp lang_exists?(%{"status" => status}) when is_binary(status), do: true
defp lang_exists?(_), do: false
# Check if language is enabled in the Languages module (default: true for backwards compat)
defp lang_enabled?(%{enabled: enabled}) when is_boolean(enabled), do: enabled
defp lang_enabled?(%{"enabled" => enabled}) when is_boolean(enabled), do: enabled
defp lang_enabled?(_), do: true
# Check if language code is a recognized/known language (default: true for backwards compat)
defp lang_known?(%{known: known}) when is_boolean(known), do: known
defp lang_known?(%{"known" => known}) when is_boolean(known), do: known
defp lang_known?(_), do: true
# Check if language is the primary language (default: false for backwards compat)
defp lang_is_primary?(%{is_primary: is_primary}) when is_boolean(is_primary), do: is_primary
defp lang_is_primary?(%{"is_primary" => is_primary}) when is_boolean(is_primary), do: is_primary
defp lang_is_primary?(_), do: false
# Get display code for a language
# Uses display_code if provided (for dialect-aware display), otherwise formats code
defp get_display_code(%{display_code: display_code}) when is_binary(display_code) do
String.upcase(display_code)
end
defp get_display_code(%{"display_code" => display_code}) when is_binary(display_code) do
String.upcase(display_code)
end
defp get_display_code(%{code: code}) when is_binary(code) do
format_code(code)
end
defp get_display_code(%{"code" => code}) when is_binary(code) do
format_code(code)
end
defp get_display_code(_), do: ""
# Format language code for display (base code only)
defp format_code(nil), do: ""
defp format_code(code) when is_binary(code) do
code
|> String.split("-")
|> List.first()
|> String.upcase()
end
# Status dot styling
# Dot color follows status regardless of enabled/known state
# (strikethrough on text indicates disabled/unknown, not the dot)
defp status_dot_classes(exists, status, size, _enabled, _known, _is_primary) do
base = ["rounded-full", "inline-block", dot_size_class(size)]
color =
cond do
!exists -> "bg-base-content/20"
status == "published" -> "bg-success"
status == "draft" -> "bg-warning"
status == "archived" -> "bg-base-content/40"
true -> "bg-base-content/20"
end
base ++ [color]
end
# Item container classes
defp item_classes(is_current, exists, show_add, size, enabled, known) do
base = [
"inline-flex items-center rounded transition-colors",
size_padding_class(size)
]
state =
cond do
# Disabled or unknown languages: grey styling but still clickable
is_current and (!enabled or !known) ->
"bg-base-content/10 text-base-content/50 font-semibold cursor-pointer"
(!enabled or !known) and exists ->
"text-base-content/40 hover:bg-base-200/50 cursor-pointer"
is_current ->
"bg-primary/10 text-primary font-semibold"
!exists && show_add ->
"text-success hover:bg-success/10 cursor-pointer"
exists ->
"hover:bg-base-200 cursor-pointer"
true ->
"text-base-content/40"
end
base ++ [state]
end
# Code text classes
# Text color matches the status dot color
defp code_classes(exists, status, is_current, size, enabled, known, is_primary) do
base = [size_text_class(size)]
# Primary language gets bold, current gets semibold, others get medium
weight =
cond do
is_primary -> "font-bold"
is_current -> "font-semibold"
true -> "font-medium"
end
# Add line-through for disabled or unknown languages
decoration = if !enabled or !known, do: "line-through", else: nil
# Text color matches the dot color
color =
cond do
!exists -> "text-base-content/20"
status == "published" -> "text-success"
status == "draft" -> "text-warning"
status == "archived" -> "text-base-content/40"
true -> "text-base-content/20"
end
Enum.filter(base ++ [weight, decoration, color], & &1)
end
# Size-based classes
defp dot_size_class(:xs), do: "w-1.5 h-1.5"
defp dot_size_class(:sm), do: "w-2 h-2"
defp dot_size_class(:md), do: "w-2.5 h-2.5"
defp size_text_class(:xs), do: "text-xs"
defp size_text_class(:sm), do: "text-sm"
defp size_text_class(:md), do: "text-base"
defp size_padding_class(:xs), do: "px-1 py-0.5"
defp size_padding_class(:sm), do: "px-1.5 py-0.5"
defp size_padding_class(:md), do: "px-2 py-1"
defp size_gap_class(:xs), do: "gap-0.5"
defp size_gap_class(:sm), do: "gap-1"
defp size_gap_class(:md), do: "gap-1.5"
defp size_separator_class(:xs), do: "text-xs"
defp size_separator_class(:sm), do: "text-sm"
defp size_separator_class(:md), do: "text-base"
defp flag_size_class(:xs), do: "text-sm"
defp flag_size_class(:sm), do: "text-base"
defp flag_size_class(:md), do: "text-lg"
# Generate title/tooltip text
# Only show status in tooltip when show_status is true (admin mode)
defp language_title(lang, exists, status, show_status, enabled, known) do
name = lang[:name] || lang["name"] || format_code(lang[:code])
if show_status do
build_admin_title(name, exists, status, enabled, known)
else
name
end
end
# Build title for admin mode with status indicators
defp build_admin_title(name, exists, status, enabled, known) do
cond do
!known and exists -> gettext("%{language} (Unknown language file)", language: name)
!enabled and exists -> build_disabled_title(name, status)
!exists -> gettext("Add %{language} translation", language: name)
true -> build_status_title(name, status)
end
end
defp build_disabled_title(name, status) do
status_text = status_label(status)
gettext("%{language} (Disabled - %{status})", language: name, status: status_text)
end
defp build_status_title(name, "published"),
do: gettext("%{language} (Published)", language: name)
defp build_status_title(name, "draft"), do: gettext("%{language} (Draft)", language: name)
defp build_status_title(name, "archived"), do: gettext("%{language} (Archived)", language: name)
defp build_status_title(name, _), do: name
defp status_label("published"), do: gettext("Published")
defp status_label("draft"), do: gettext("Draft")
defp status_label("archived"), do: gettext("Archived")
defp status_label(_), do: gettext("Unknown")
end