Current section

Files

Jump to
phoenix_kit_publishing lib phoenix_kit_publishing web controller translations.ex
Raw

lib/phoenix_kit_publishing/web/controller/translations.ex

defmodule PhoenixKit.Modules.Publishing.Web.Controller.Translations do
@moduledoc """
Translation link building for the publishing controller.
Handles building translation/language switcher links for:
- Group listing pages
- Individual post pages
"""
alias PhoenixKit.Modules.Languages
alias PhoenixKit.Modules.Languages.DialectMapper
alias PhoenixKit.Modules.Publishing
alias PhoenixKit.Modules.Publishing.Constants
alias PhoenixKit.Modules.Publishing.LanguageHelpers
alias PhoenixKit.Modules.Publishing.ListingCache
alias PhoenixKit.Modules.Publishing.Web.Controller.Language
alias PhoenixKit.Modules.Publishing.Web.Controller.PostRendering
alias PhoenixKit.Modules.Publishing.Web.HTML, as: PublishingHTML
# ============================================================================
# Listing Page Translations
# ============================================================================
@doc """
Build translation links for group listing page.
Accepts posts to avoid redundant list_posts calls.
"""
def build_listing_translations(group_slug, current_language, posts) do
# Get enabled languages - these are the ONLY languages that should show
enabled_languages = Language.get_enabled_languages()
# Get the primary/default language
primary_language = List.first(enabled_languages) || "en"
# For each enabled language, check if there's published content for it
# Only show languages that are explicitly enabled (not just base code matches)
translations =
enabled_languages
|> Enum.filter(fn lang ->
# Check if this specific language has published content (using passed posts)
has_published_content_for_language?(posts, lang)
end)
|> Enum.map(fn lang ->
# Use display_code helper to determine if we show base or full code
display_code = Publishing.get_display_code(lang, enabled_languages)
%{
code: display_code,
display_code: display_code,
name: Language.get_language_name(lang),
flag: Language.get_language_flag(lang),
url: PublishingHTML.group_listing_path(display_code, group_slug),
current: display_code == current_language
}
end)
# Order: primary first, then the rest alphabetically
if Enum.any?(
translations,
&(&1.code == Publishing.get_display_code(primary_language, enabled_languages))
) do
primary_display = Publishing.get_display_code(primary_language, enabled_languages)
{primary, others} = Enum.split_with(translations, &(&1.code == primary_display))
primary ++ Enum.sort_by(others, & &1.code)
else
Enum.sort_by(translations, & &1.code)
end
|> Enum.uniq_by(& &1.code)
end
# Check if a specific enabled language has published content in the group.
# Requires the language to exist, be published, and have actual content (non-empty title).
defp has_published_content_for_language?(posts, language) do
Enum.any?(posts, fn post ->
language in (post.available_languages || []) and
Map.get(post.language_statuses, language) == "published" and
has_content?(post, language)
end)
end
defp has_content?(post, language) do
title = get_in(post, [:language_titles, language])
title != nil and title != "" and title != Constants.default_title()
end
# ============================================================================
# Post Page Translations
# ============================================================================
@doc """
Build translation links for a post page.
"""
def build_translation_links(group_slug, post, current_language, opts \\ []) do
version = Keyword.get(opts, :version)
# Get enabled languages
enabled_languages = Language.get_enabled_languages()
# Use the post's primary language so it appears first in the switcher
primary_language = LanguageHelpers.get_primary_language()
# Fetch language_slugs from cache for per-language URL slugs
# Falls back to using post.slug for all languages if cache miss
language_slugs = fetch_language_slugs_from_cache(group_slug, post)
# Include ALL available languages that are published
# This allows legacy/disabled languages to still show in the public switcher
# (they'll be styled differently by the component based on enabled/known flags)
available_and_published =
post.available_languages
|> normalize_languages(current_language)
|> Enum.filter(fn lang ->
translation_published_exact?(group_slug, post, lang)
end)
# Remove legacy base codes when dialect content exists
# e.g., if both "en" and "en-CA" exist, remove "en" to avoid duplicates
deduplicated =
deduplicate_base_and_dialect_codes(available_and_published, enabled_languages)
# Order: primary first (if present), then enabled languages, then disabled ones
languages = order_languages_for_public(deduplicated, enabled_languages, primary_language)
languages = deduplicate_languages_by_display_code(languages, enabled_languages, post.language)
Enum.map(languages, fn lang ->
# Use display_code helper to determine if we show base or full code
display_code = Publishing.get_display_code(lang, enabled_languages)
is_enabled = exact_enabled_for_public?(lang, enabled_languages)
is_known = Languages.get_predefined_language(lang) != nil
# Get the URL slug for this specific language
# This enables SEO-friendly localized URLs (e.g., /es/docs/primeros-pasos)
url_slug_for_lang = Map.get(language_slugs, lang, post.slug)
post_with_url_slug = Map.put(post, :url_slug, url_slug_for_lang)
# Build URL with version if viewing a specific version
url =
if version do
PostRendering.build_version_url(group_slug, post_with_url_slug, display_code, version)
else
PublishingHTML.build_post_url(group_slug, post_with_url_slug, display_code)
end
%{
code: display_code,
display_code: display_code,
name: Language.get_language_name(lang),
flag: Language.get_language_flag(lang),
url: url,
current: lang == post.language,
enabled: is_enabled,
known: is_known
}
end)
end
# ============================================================================
# Helper Functions
# ============================================================================
# Order languages for public display: primary first, then enabled, then disabled
defp order_languages_for_public(languages, enabled_languages, primary_language) do
{enabled, disabled} =
Enum.split_with(languages, fn lang ->
exact_enabled_for_public?(lang, enabled_languages)
end)
# Put primary first if present
{primary, other_enabled} = Enum.split_with(enabled, &(&1 == primary_language))
primary ++ Enum.sort(other_enabled) ++ Enum.sort(disabled)
end
# Remove legacy base codes when dialect content of the same language exists
# This prevents showing both "en" and "en-CA" in the switcher
defp deduplicate_base_and_dialect_codes(languages, enabled_languages) do
# Separate base codes and dialect codes
{base_codes, dialect_codes} = Enum.split_with(languages, &Language.base_code?/1)
# Only remove a base code if it's NOT an enabled language AND a dialect exists.
# If both "en" and "en-US" are enabled with content, show both.
filtered_base_codes =
Enum.reject(base_codes, fn base ->
base not in enabled_languages and
Enum.any?(dialect_codes, fn dialect ->
DialectMapper.extract_base(dialect) == base
end)
end)
dialect_codes ++ filtered_base_codes
end
defp deduplicate_languages_by_display_code(languages, enabled_languages, preferred_language) do
languages
|> Enum.group_by(&Publishing.get_display_code(&1, enabled_languages))
|> Enum.map(fn {_display_code, variants} ->
Enum.max_by(variants, fn lang ->
{
lang == preferred_language,
not Language.base_code?(lang),
lang in enabled_languages
}
end)
end)
|> Enum.sort_by(fn lang ->
case Enum.find_index(languages, &(&1 == lang)) do
nil -> length(languages)
idx -> idx
end
end)
end
# Fetches language_slugs map from cache for per-language URL slugs
# Returns a map of language -> url_slug for each available language
defp fetch_language_slugs_from_cache(group_slug, post) do
case ListingCache.find_post_by_mode(group_slug, post) do
{:ok, cached_post} ->
cached_post.language_slugs || %{}
{:error, _} ->
# Cache miss - return empty map (will fall back to post.slug)
%{}
end
end
defp normalize_languages([], current_language), do: [current_language]
defp normalize_languages(languages, _current_language) when is_list(languages), do: languages
# Strict check for public display — distinguishable by name from
# `LanguageHelpers.language_enabled?/2` which is the looser variant.
# Only shows languages that are:
# 1. Directly in the enabled languages list, OR
# 2. Base codes where any dialect of that base is enabled
# This prevents showing en-US/en-GB when only en-CA is enabled.
defp exact_enabled_for_public?(language, enabled_languages) do
cond do
# Direct match - language code exactly matches an enabled language
language in enabled_languages ->
true
# Base code (e.g., "en") - show if any dialect is enabled
Language.base_code?(language) ->
Enum.any?(enabled_languages, fn enabled_lang ->
DialectMapper.extract_base(enabled_lang) == language
end)
# Dialect (e.g., "en-US") not directly enabled - DON'T show
# This is the key difference from language_enabled?
true ->
false
end
end
# Translation is visible if it exists — status comes from the post level
defp translation_published_exact?(_group_slug, post, language) do
language in (post.available_languages || []) and
post_has_content_for_language?(post, language)
end
# Check if a post has actual content for a language (not just an empty content row)
defp post_has_content_for_language?(post, language) do
# On post pages, language_titles may not be available — check the current content
default_title = Constants.default_title()
cond do
# Listing maps have language_titles
is_map(post[:language_titles]) ->
title = Map.get(post.language_titles, language)
title != nil and title != "" and title != default_title
# Post maps: if we're checking the current language, check metadata title
language == post[:language] ->
title = get_in(post, [:metadata, :title])
title != nil and title != "" and title != default_title
# For other languages on post maps, assume content exists if in available_languages
# (the content row was created intentionally)
true ->
true
end
end
end