# `PhoenixKitCatalogue.Catalogue.Translations`
[🔗](https://github.com/BeamLabEU/phoenix_kit_catalogue/blob/v0.31.3/lib/phoenix_kit_catalogue/catalogue/translations.ex#L1)

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.

# `get_translation`

```elixir
@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`

```elixir
@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`

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

Single-record `localize/2`.

# `set_translation`

```elixir
@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`

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

Same contract as `translated_name/2`, for `:description`.

# `translated_name`

```elixir
@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`

```elixir
@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`

```elixir
@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.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
