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.
Summary
Functions
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.
Functions
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.