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:
- Exact language match (e.g., "ru")
- Default language from Languages module
- 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" => "Новое кашпо"}}
Summary
Functions
Gets all languages that have a value for a field.
Returns the list of translatable fields for categories.
Builds changeset attrs for localized field update.
Builds changeset attrs for multiple localized fields at once.
Returns the default/master language code.
Returns list of enabled language codes.
Gets a localized value with automatic fallback chain.
Gets all values for a specific language from the entity's localized fields.
Storefront-only wrapper around get/3: runs the resolved value through
PhoenixKitEcommerce.NamePrefix.strip/1, which hides a shop-configured
vocabulary prefix ("3D Printed Costume Masks" -> "Costume Masks") when
the shop_name_prefixes setting names one — a no-op by default.
DEPRECATED: Use get/3 instead.
Gets the localized slug with fallback.
Checks if translation exists for language in a specific field.
Checks if Languages module is enabled.
Returns the list of translatable fields for products.
Sets a localized value for a language.
Sets multiple translated fields for a language.
DEPRECATED: Use put/4 instead.
Returns translatable fields based on entity type.
DEPRECATED: Use changeset_attrs_multi/3 instead.
Gets translation completeness for a language across all translatable fields.
Functions
Gets all languages that have a value for a field.
Examples
iex> Translations.available_languages(product, :title)
["en", "ru"]
@spec category_fields() :: [atom()]
Returns the list of translatable fields for categories.
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" => "Новое кашпо"}}
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 default_language() :: String.t()
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 enabled_languages() :: [String.t()]
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"]
Gets a localized value with automatic fallback chain.
Fallback order:
- Exact language match
- Default language
- First available value
Parameters
entity- Product or Category structfield- 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
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: "Описание"}
Storefront-only wrapper around get/3: runs the resolved value through
PhoenixKitEcommerce.NamePrefix.strip/1, which hides a shop-configured
vocabulary prefix ("3D Printed Costume Masks" -> "Costume Masks") when
the shop_name_prefixes setting names one — a no-op by default.
Call this ONLY for a display field on a genuine public storefront
page (:title on a product, :name on a category), reading a real
%Product{}/%Category{} struct. Never call it for :slug — slugs
are URLs and must never be rewritten — and never from an admin surface
or the Shopify sync path: an operator editing a category must see what
is actually stored, and the sync must compare and write the raw
Shopify-sourced value.
A cart line, order line item, or invoice's stored product_title/
"name" is a DIFFERENT kind of value — a snapshot taken at add-to-cart
time, not a live read of the product — and is never touched by this
function at all (it isn't a %Product{}/%Category{} field access in
the first place). The snapshot itself stays exactly as stored, same as
a snapshotted price; but DISPLAYING it on a storefront page (the cart,
checkout, and order-confirmation pages, and the customer's own order
history) still applies the prefix, by calling
PhoenixKitEcommerce.NamePrefix.strip/1 directly on the snapshot
string — the shopper is not shopping from a stored record, so there is
no earlier "what they actually saw" to preserve; they are looking at
the same storefront naming as every other page. Only a persisted
WRITE, and any value compared against or sent to Shopify, must stay
raw.
Options
:prefixes— a precomputedNamePrefix.prefixes/0list. Every call without it reads the setting through core's settings cache (a process round-trip), so a template rendering a LIST of products or categories resolves the list once and passes it to everyget_display/4call inside the loop.
Examples
iex> Translations.get_display(category, :name, "en")
"Costume Masks"
iex> prefixes = NamePrefix.prefixes()
iex> Translations.get_display(category, :name, "en", prefixes: prefixes)
"Costume Masks"
DEPRECATED: Use get/3 instead.
This function exists for backward compatibility during migration.
Gets the localized slug with fallback.
Convenience function for URL slug retrieval.
Examples
iex> Translations.get_slug(product, "es")
"maceta-geometrica"
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 languages_enabled?() :: boolean()
Checks if Languages module is enabled.
@spec product_fields() :: [atom()]
Returns the list of translatable fields for products.
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" => "Новое кашпо"}}
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"}, ...}
DEPRECATED: Use put/4 instead.
This function exists for backward compatibility during migration.
Returns translatable fields based on entity type.
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.
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]}