# `PhoenixKitCatalogue.AITranslatable.Sets`
[🔗](https://github.com/BeamLabEU/phoenix_kit_catalogue/blob/v0.44.2/lib/phoenix_kit_catalogue/ai_translatable/sets.ex#L1)

`PhoenixKitAI.Translatable` adapter for catalogue attribute SETS — the
entities-backed blueprint (`"catalogue_set_label"`, the set's own display
name) and its value records (`"catalogue_set_value"`, each value's title).
See `PhoenixKitCatalogue.Catalogue.AttributeSets` for what a set is; this
module only translates it.

Unlike `PhoenixKitCatalogue.AITranslatable`, both resources here are
entities schemas (`PhoenixKitEntities` / `PhoenixKitEntities.EntityData`),
not catalogue's own — and their generic writers (`update_entity/3`,
`EntityData.update/2`) do not lock the row. Same doctrine as the
item/category adapter: `put_translation/4` re-reads the row `FOR UPDATE`
inside its own transaction before merging, so two concurrent per-language
jobs (de/fr) on the same set or value serialize on the row lock instead of
each overwriting the other's translation from a stale in-memory struct.

The merge logic mirrors the entities package's own translation helpers
(`PhoenixKitEntities.set_entity_translation/3`,
`PhoenixKitEntities.EntityData.set_title_translation/3` — "never drop
other languages"), but does NOT call them directly: both route through
`update_entity/3`/`EntityData.update/2`, which broadcast (PubSub),
activity-log, and fire an async mirror-export Task right after their own
`repo().update/1` — INSIDE this adapter's still-open `FOR UPDATE`
transaction. A subscriber reacting to that broadcast would either block
on the lock or read pre-commit state, and a later failure in this
transaction (the fingerprint write, previously a second bare update)
would roll back a write whose broadcast/log/export already went out.
So the merge here is a bare `Ecto.Changeset.change/2` + `repo().update/1`
on the locked row, folding the fingerprint into the SAME write; each
`put_*` function broadcasts exactly once, after the transaction commits.

# `source_fields_pure`

```elixir
@spec source_fields_pure(struct(), String.t()) :: map()
```

Same extraction as `source_fields/2`, WITHOUT the process-dictionary
capture side effect — see `AITranslatable.source_fields_pure/2` for why
a read-only caller (`TranslationStatus.state/2`/`list/2`, and the
write-time fingerprint fallback below) must not go through the capturing
`@impl` callback.

---

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