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.
Single-record localize/2.
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
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.
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.
Single-record localize/2.
@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.
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.
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.
Same contract as translated_seo_title/2, for _seo_description.
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.