PhoenixKitCatalogue.Catalogue.Translations (PhoenixKitCatalogue v0.28.3)

Copy Markdown View Source

Multilang data JSONB helpers — read merged language data from a record and write language-specific overrides through the entity's own update function.

Public surface is re-exported from PhoenixKitCatalogue.Catalogue.

Dialect matching in translated_name/2 / translated_description/2

Deciding whether a requested locale IS the record's own primary language (see primary_locale?/2) mirrors, base code for base code, the SAME lookup Multilang.get_language_data/2 already does when reading the bucket (its private language_entry/3) — deliberately, not by accident: the two must never drift apart, or a dialect-loose caller (a bare base code from a downgraded Gettext locale, e.g.) would read the bucket while an exact caller reads the column, splitting one record's displayed name across two disagreeing code paths.

That mirrored lookup carries one asymmetry inherited as-is from language_entry/3, not designed by this module: among every bucket sharing a requested base code, the PRIMARY bucket always wins the fallback, even when a distinct, more specific sibling of that same base also exists and the primary is no better a match than that sibling. E.g. primary "en-US", siblings "en-US" and "en-GB" both present, requesting an unrelated third dialect "en-CA" (no bucket of its own) resolves to "en-US", not "en-GB". This is left as-is because mirroring the layer below — including its warts — is the entire point; inventing a "better" tiebreak here would just be a second, competing source of truth for what a locale resolves to.

Summary

Functions

Gets translated field data for a record in a specific language. Returns merged data (primary language as base + overrides for the requested language).

Replaces :name (and :description where present) on each record with the locale-resolved display text, so list/detail surfaces can render record.name untouched and still honor the viewer's locale.

Updates the multilang data field for a record with language-specific field data. For primary language: stores ALL fields. For secondary languages: stores only overrides (differences from primary).

Same contract as translated_name/2, for :description.

The display name for locale: the locale's translation override (either the "_name" shape the shared multilang helper writes or the legacy bare "name"), falling back to the primary-language column. Safe on records without translations and on plain maps.

Same contract as translated_seo_title/2, for _seo_description.

The SEO title override for locale, or nil when unset.

Functions

get_translation(record, lang_code)

@spec get_translation(map(), String.t()) :: map()

Gets translated field data for a record in a specific language. Returns merged data (primary language as base + overrides for the requested language).

localize(records, locale)

@spec localize(list(), String.t() | nil) :: list()

Replaces :name (and :description where present) on each record with the locale-resolved display text, so list/detail surfaces can render record.name untouched and still honor the viewer's locale.

Resolve-early by design (the same shape as resolved_group/2): the alternative — threading a locale attr through every table/tile/cell component — spreads the concern across dozens of render sites. Records without a :data map (folders) pass through unchanged, as does everything when locale is nil. Struct identity is preserved (%{record | ...}), and mutations are unaffected: status/move/ reorder writes never take :name from these list structs.

localize_one(record, locale)

@spec localize_one(map() | nil, String.t() | nil) :: map() | nil

Single-record localize/2.

set_translation(record, lang_code, field_data, update_fn, opts \\ [])

@spec set_translation(map(), String.t(), map(), function(), keyword()) ::
  {:ok, term()} | {:error, term()}

Updates the multilang data field for a record with language-specific field data. For primary language: stores ALL fields. For secondary languages: stores only overrides (differences from primary).

update_fn is the entity's update function. It receives (record, attrs) for 2-arity or (record, attrs, opts) for 3-arity when activity-logging opts are provided.

translated_description(record, locale)

@spec translated_description(map() | nil, String.t() | nil) :: String.t() | nil

Same contract as translated_name/2, for :description.

translated_name(record, locale)

@spec translated_name(map() | nil, String.t() | nil) :: String.t() | nil

The display name for locale: the locale's translation override (either the "_name" shape the shared multilang helper writes or the legacy bare "name"), falling back to the primary-language column. Safe on records without translations and on plain maps.

When locale IS the record's own primary language, the column is read FIRST and the bucket is only a fallback for a blank column — a writer that legitimately updates only the column (e.g. the Shopify sync) must not be shadowed forever by a stale primary-language bucket entry. Every other locale is unchanged: bucket override first, then the column.

translated_seo_description(record, locale)

@spec translated_seo_description(map() | nil, String.t() | nil) :: String.t() | nil

Same contract as translated_seo_title/2, for _seo_description.

translated_seo_title(record, locale)

@spec translated_seo_title(map() | nil, String.t() | nil) :: String.t() | nil

The SEO title override for locale, or nil when unset.

Unlike translated_name/2, there is no DB-column fallback — seo_title only ever lives under the multilang data override ("_seo_title"), same storage shape as _name/_description but with no primary-column counterpart.