Packages
phoenix_kit
1.7.80
1.7.210
1.7.209
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
Current section
Files
lib/modules/shop/translations.ex
defmodule PhoenixKit.Modules.Shop.Translations do
@moduledoc """
Localized fields helper for Shop module.
All translatable fields are stored as JSONB maps directly in the field:
%Product{
title: %{"en" => "Planter", "ru" => "Кашпо"},
slug: %{"en" => "planter", "ru" => "kashpo"},
description: %{"en" => "Modern pot", "ru" => "Современное кашпо"}
}
## Fallback Chain
When retrieving a translated field, the fallback chain is:
1. Exact language match (e.g., "ru")
2. Default language from Languages module
3. First available value in the map
## Usage Examples
# Get translated field with automatic fallback
Translations.get(product, :title, "ru")
#=> "Кашпо"
Translations.get(product, :title, "fr")
#=> "Planter" (fallback to default or first available)
# Set a single translated field
product = Translations.put(product, :title, "es", "Maceta")
# Build changeset attrs for localized field update
attrs = Translations.changeset_attrs(product, :title, "ru", "Новое кашпо")
#=> %{title: %{"en" => "Planter", "ru" => "Новое кашпо"}}
"""
alias PhoenixKit.Modules.Languages
alias PhoenixKit.Settings
@product_fields [:title, :slug, :description, :body_html, :seo_title, :seo_description]
@category_fields [:name, :slug, :description]
# ============================================================================
# Language Configuration
# ============================================================================
@doc """
Returns the default/master language code.
Checks Languages module first, falls back to Settings content language,
then defaults to "en".
## Examples
iex> Translations.default_language()
"en"
"""
@spec default_language() :: String.t()
def default_language do
if languages_enabled?() do
case Languages.get_default_language() do
%{code: code} -> code
_ -> "en"
end
else
Settings.get_content_language() || "en"
end
end
@doc """
Returns list of enabled language codes.
When Languages module is enabled, returns all enabled language codes.
Otherwise returns only the default language.
## Examples
iex> Translations.enabled_languages()
["en", "es", "ru"]
# When Languages module disabled:
iex> Translations.enabled_languages()
["en"]
"""
@spec enabled_languages() :: [String.t()]
def enabled_languages do
if languages_enabled?() do
Languages.get_enabled_language_codes()
else
[default_language()]
end
end
@doc """
Checks if Languages module is enabled.
"""
@spec languages_enabled?() :: boolean()
def languages_enabled? do
Code.ensure_loaded?(Languages) and function_exported?(Languages, :enabled?, 0) and
Languages.enabled?()
end
# ============================================================================
# Reading Translations (New Localized Fields Approach)
# ============================================================================
@doc """
Gets a localized value with automatic fallback chain.
Fallback order:
1. Exact language match
2. Default language
3. First available value
## Parameters
- `entity` - Product or Category struct
- `field` - Field atom (e.g., :title, :name, :slug)
- `language` - Language code (e.g., "ru", "en")
## Examples
iex> product = %Product{title: %{"en" => "Planter", "ru" => "Кашпо"}}
iex> Translations.get(product, :title, "ru")
"Кашпо"
iex> Translations.get(product, :title, "fr")
"Planter" # Falls back to default or first available
"""
@spec get(struct(), atom(), String.t()) :: any()
def get(entity, field, language) do
field_map = Map.get(entity, field) || %{}
field_map[language] ||
field_map[default_language()] ||
first_available(field_map)
end
@doc """
Gets the localized slug with fallback.
Convenience function for URL slug retrieval.
## Examples
iex> Translations.get_slug(product, "es")
"maceta-geometrica"
"""
@spec get_slug(struct(), String.t()) :: String.t() | nil
def get_slug(entity, language) do
get(entity, :slug, language)
end
@doc """
Gets all values for a specific language from the entity's localized fields.
Returns a map of field => value for the given language.
## Examples
iex> Translations.get_all_for_language(product, "ru", [:title, :slug, :description])
%{title: "Кашпо", slug: "kashpo", description: "Описание"}
"""
@spec get_all_for_language(struct(), String.t(), [atom()]) :: map()
def get_all_for_language(entity, language, fields) do
Enum.reduce(fields, %{}, fn field, acc ->
value = get(entity, field, language)
Map.put(acc, field, value)
end)
end
# ============================================================================
# Writing Translations
# ============================================================================
@doc """
Sets a localized value for a language.
Returns the updated entity struct (not persisted to database).
## Examples
iex> product = Translations.put(product, :title, "ru", "Новое кашпо")
%Product{title: %{"en" => "Planter", "ru" => "Новое кашпо"}}
"""
@spec put(struct(), atom(), String.t(), any()) :: struct()
def put(entity, field, language, value) do
current = Map.get(entity, field) || %{}
updated = Map.put(current, language, value)
Map.put(entity, field, updated)
end
@doc """
Builds changeset attrs for localized field update.
Merges the new value into the existing field map for the given language.
## Examples
iex> Translations.changeset_attrs(product, :title, "ru", "Новое кашпо")
%{title: %{"en" => "Planter", "ru" => "Новое кашпо"}}
"""
@spec changeset_attrs(struct(), atom(), String.t(), any()) :: map()
def changeset_attrs(entity, field, language, value) do
current = Map.get(entity, field) || %{}
updated = Map.put(current, language, value)
%{field => updated}
end
@doc """
Builds changeset attrs for multiple localized fields at once.
## Examples
iex> Translations.changeset_attrs_multi(product, "ru", %{title: "Кашпо", slug: "kashpo"})
%{title: %{"en" => "Planter", "ru" => "Кашпо"}, slug: %{"en" => "planter", "ru" => "kashpo"}}
"""
@spec changeset_attrs_multi(struct(), String.t(), map()) :: map()
def changeset_attrs_multi(entity, language, field_values) do
Enum.reduce(field_values, %{}, fn {field, value}, acc ->
Map.merge(acc, changeset_attrs(entity, field, language, value))
end)
end
@doc """
Sets multiple translated fields for a language.
Returns the updated entity struct (not persisted to database).
## Examples
iex> product = Translations.put_all(product, "es", %{title: "Maceta", slug: "maceta"})
%Product{title: %{"en" => "Planter", "es" => "Maceta"}, ...}
"""
@spec put_all(struct(), String.t(), map()) :: struct()
def put_all(entity, language, field_values) do
Enum.reduce(field_values, entity, fn {field, value}, acc ->
put(acc, field, language, value)
end)
end
# ============================================================================
# Inspection Helpers
# ============================================================================
@doc """
Gets all languages that have a value for a field.
## Examples
iex> Translations.available_languages(product, :title)
["en", "ru"]
"""
@spec available_languages(struct(), atom()) :: [String.t()]
def available_languages(entity, field) do
field_map = Map.get(entity, field) || %{}
field_map
|> Map.keys()
|> Enum.filter(fn lang ->
value = Map.get(field_map, lang)
value != nil and value != ""
end)
end
@doc """
Checks if translation exists for language in a specific field.
## Examples
iex> Translations.has_translation?(product, :title, "ru")
true
iex> Translations.has_translation?(product, :title, "zh")
false
"""
@spec has_translation?(struct(), atom(), String.t()) :: boolean()
def has_translation?(entity, field, language) do
field_map = Map.get(entity, field) || %{}
value = Map.get(field_map, language)
value != nil and value != ""
end
@doc """
Gets translation completeness for a language across all translatable fields.
## Examples
iex> Translations.translation_status(product, "ru")
%{complete: 4, total: 6, percentage: 67, missing: [:body_html, :seo_description]}
"""
@spec translation_status(struct(), String.t(), [atom()] | nil) :: map()
def translation_status(entity, language, required_fields \\ nil) do
fields = required_fields || translatable_fields(entity)
present =
Enum.filter(fields, fn field ->
has_translation?(entity, field, language)
end)
missing = fields -- present
present_count = Enum.count(present)
total_count = Enum.count(fields)
%{
complete: present_count,
total: total_count,
percentage: if(total_count > 0, do: round(present_count / total_count * 100), else: 0),
missing: missing
}
end
# ============================================================================
# Field Definitions
# ============================================================================
@doc """
Returns the list of translatable fields for products.
"""
@spec product_fields() :: [atom()]
def product_fields, do: @product_fields
@doc """
Returns the list of translatable fields for categories.
"""
@spec category_fields() :: [atom()]
def category_fields, do: @category_fields
@doc """
Returns translatable fields based on entity type.
"""
@spec translatable_fields(struct()) :: [atom()]
def translatable_fields(%{__struct__: PhoenixKit.Modules.Shop.Product}), do: @product_fields
def translatable_fields(%{__struct__: PhoenixKit.Modules.Shop.Category}), do: @category_fields
def translatable_fields(_), do: []
# ============================================================================
# Legacy Compatibility (Deprecated)
# ============================================================================
@doc """
DEPRECATED: Use `get/3` instead.
This function exists for backward compatibility during migration.
"""
@spec get_field(struct(), atom(), String.t()) :: any()
def get_field(entity, field, language) do
get(entity, field, language)
end
@doc """
DEPRECATED: Use `put/4` instead.
This function exists for backward compatibility during migration.
"""
@spec put_field(struct(), atom(), String.t(), any()) :: struct()
def put_field(entity, field, language, value) do
put(entity, field, language, value)
end
@doc """
DEPRECATED: Use `changeset_attrs_multi/3` instead.
Builds changeset attrs for updating translations.
This function adapts the old API to the new localized fields approach.
"""
@spec translation_changeset_attrs(map() | nil, String.t(), map()) :: map()
def translation_changeset_attrs(_current_translations, _language, _params) do
# This function is no longer applicable in the new approach
# where each field is its own map.
# Kept for compilation but should not be used.
%{}
end
# ============================================================================
# Private Helpers
# ============================================================================
defp first_available(map) when map == %{}, do: nil
defp first_available(map) do
case Enum.at(map, 0) do
{_key, value} -> value
nil -> nil
end
end
end