Packages

phoenix_kit

1.7.44
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 controller language.ex
Raw

lib/modules/publishing/web/controller/language.ex

defmodule PhoenixKit.Modules.Publishing.Web.Controller.Language do
@moduledoc """
Language detection and resolution for the publishing controller.
Handles detecting whether URL parameters represent language codes,
resolving language codes to file languages, and determining
canonical URL language codes.
"""
alias PhoenixKit.Modules.Languages
alias PhoenixKit.Modules.Languages.DialectMapper
alias PhoenixKit.Modules.Publishing
alias PhoenixKit.Modules.Publishing.ListingCache
alias PhoenixKit.Modules.Publishing.Storage
# ============================================================================
# Language Detection
# ============================================================================
@doc """
Detects whether the 'language' parameter is actually a language code or a group slug.
This allows the same route pattern (/:language/:group/*path) to work for both:
- Multi-language: /en/my-group/my-post (language=en, group=my-group)
- Single-language: /my-group/my-post (language=my-group, needs adjustment)
Returns {detected_language, adjusted_params}
"""
def detect_language_or_group(language_param, params) do
# First check if it's a known/predefined language
# Then check if content exists for this language in the group (handles unknown languages like "af")
group_slug = params["group"]
cond do
# Known/predefined language - use as-is
valid_language?(language_param) ->
{language_param, params}
# Unknown language code but content exists for it in this group
# This handles files like af.phk, test.phk, etc.
group_slug && has_content_for_language?(group_slug, language_param) ->
{language_param, params}
# Not a language - shift parameters (group slug in language position)
true ->
default_language = get_default_language()
adjusted_params =
case params do
# Pattern: %{"language" => group_slug, "group" => first_path_segment, "path" => rest}
%{"group" => first_segment, "path" => rest} when is_list(rest) ->
%{"group" => language_param, "path" => [first_segment | rest]}
# Pattern: %{"language" => group_slug, "group" => first_path_segment}
%{"group" => first_segment} ->
%{"group" => language_param, "path" => [first_segment]}
# Pattern: %{"language" => group_slug} (just listing)
_ ->
%{"group" => language_param}
end
{default_language, adjusted_params}
end
end
@doc """
Detects if the :group route param is actually a language code by checking if content exists.
Returns {:language_detected, language, adjusted_params} or :not_a_language
"""
def detect_language_in_group_param(
%{"group" => potential_lang, "path" => [_ | _] = path} = _params
)
when is_binary(potential_lang) do
[actual_group | rest_path] = path
group_exists = group_exists?(actual_group)
has_content = has_content_for_language?(actual_group, potential_lang)
# Check if there's a group with slug matching actual_group
# AND if there's content for potential_lang in that group
if group_exists and has_content do
adjusted_params = %{"group" => actual_group, "path" => rest_path}
{:language_detected, potential_lang, adjusted_params}
else
:not_a_language
end
end
def detect_language_in_group_param(_params), do: :not_a_language
# ============================================================================
# Language Validation
# ============================================================================
@doc """
Validates if a code represents a valid language.
"""
def valid_language?(code) when is_binary(code) do
# Check if it's a language code pattern (enabled, disabled, or even unknown)
# This allows access to legacy content in disabled languages
cond do
# Enabled language - definitely valid
Languages.language_enabled?(code) ->
true
# Base code that maps to an enabled dialect
String.length(code) == 2 and not String.contains?(code, "-") ->
dialect = DialectMapper.base_to_dialect(code)
if Languages.language_enabled?(dialect) do
true
else
# Even if disabled, it's still a valid language code pattern
# Check if it's a known language
Languages.get_predefined_language(dialect) != nil
end
# Known but disabled language (full dialect like "fr-FR")
Languages.get_predefined_language(code) != nil ->
true
# Check if it looks like a language code pattern (XX or XX-XX format)
# This allows access to unknown files like legacy imports
looks_like_language_code?(code) ->
true
true ->
false
end
rescue
_ -> false
end
def valid_language?(_), do: false
@doc """
Checks if a string looks like a language code pattern.
Matches: 2-letter codes (en, fr), or dialect codes (en-US, pt-BR)
"""
def looks_like_language_code?(code) when is_binary(code) do
# 2-letter base code
# Dialect code pattern (xx-XX or xx-XXX)
(String.length(code) == 2 and String.match?(code, ~r/^[a-z]{2}$/i)) or
String.match?(code, ~r/^[a-z]{2,3}-[A-Za-z]{2,4}$/i)
end
# ============================================================================
# Language Resolution
# ============================================================================
@doc """
Resolves a language code to an actual file language.
Handles base codes by finding a matching dialect in available languages.
"""
def resolve_language_for_post(language, available_languages) do
cond do
# Direct match - language exactly matches an available file
language in available_languages ->
language
# Base code - try to find a dialect that matches
base_code?(language) ->
find_dialect_for_base_in_files(language, available_languages) ||
DialectMapper.base_to_dialect(language)
# Full dialect code not found - try base code match as fallback
true ->
base = DialectMapper.extract_base(language)
find_dialect_for_base_in_files(base, available_languages) || language
end
end
@doc """
Find a dialect in available files that matches the given base code.
"""
def find_dialect_for_base_in_files(base_code, available_languages) do
base_lower = String.downcase(base_code)
Enum.find(available_languages, fn lang ->
DialectMapper.extract_base(lang) == base_lower
end)
end
# ============================================================================
# Canonical URL Language
# ============================================================================
@doc """
Gets the canonical URL language code for a given language.
If multiple dialects of the same base language are enabled, returns the full dialect.
Otherwise returns the base code for cleaner URLs.
"""
def get_canonical_url_language(language) do
enabled_languages = get_enabled_languages()
# Resolve base code to a specific dialect if needed
resolved_language =
if base_code?(language) do
# Find the matching dialect in enabled languages
find_dialect_for_base(language, enabled_languages) || language
else
language
end
# Now determine if we should use base or full dialect code
Storage.get_display_code(resolved_language, enabled_languages)
end
@doc """
Gets the canonical URL language code for a post's language.
This uses the actual file language (e.g., "en-US") to determine the canonical URL code.
"""
def get_canonical_url_language_for_post(post_language) do
enabled_languages = get_enabled_languages()
Storage.get_display_code(post_language, enabled_languages)
end
# ============================================================================
# Helper Functions
# ============================================================================
@doc """
Gets the list of enabled language codes.
"""
def get_enabled_languages do
Languages.enabled_locale_codes()
rescue
_ -> ["en"]
end
@doc """
Checks if a code is a base (2-letter) language code.
"""
def base_code?(code) when is_binary(code) do
String.length(code) == 2 and not String.contains?(code, "-")
end
def base_code?(_), do: false
@doc """
Find a dialect in enabled languages that matches the given base code.
"""
def find_dialect_for_base(base_code, enabled_languages) do
base_lower = String.downcase(base_code)
Enum.find(enabled_languages, fn lang ->
DialectMapper.extract_base(lang) == base_lower
end)
end
@doc """
Gets the default language.
"""
def get_default_language do
case Languages.get_default_language() do
%{"code" => code} -> code
_ -> "en"
end
end
@doc """
Gets a language's display name.
"""
def get_language_name(code) do
case Languages.get_language(code) do
%{"name" => name} -> name
_ -> String.upcase(code)
end
end
@doc """
Gets a language's flag emoji.
"""
def get_language_flag(code) do
case Languages.get_predefined_language(code) do
%{flag: flag} -> flag
_ -> "🌐"
end
end
# ============================================================================
# Content Checks
# ============================================================================
@doc """
Check if any post in the group has content for the given language.
Uses listing cache when available for fast lookups.
"""
def has_content_for_language?(group_slug, language) do
# Try cache first for fast lookup
case ListingCache.read(group_slug) do
{:ok, posts} ->
Enum.any?(posts, fn post ->
language in (post.available_languages || [])
end)
{:error, _} ->
# Cache miss - fall back to filesystem scan
posts = Publishing.list_posts(group_slug, nil)
Enum.any?(posts, fn post ->
language in (post.available_languages || [])
end)
end
rescue
_ -> false
end
defp group_exists?(group_slug) do
case Enum.find(Publishing.list_groups(), fn group ->
case group["slug"] do
slug when is_binary(slug) ->
String.downcase(slug) == String.downcase(to_string(group_slug))
_ ->
false
end
end) do
nil -> false
_ -> true
end
end
end