PhoenixKitCatalogue.AITranslatable.Sets (PhoenixKitCatalogue v0.44.2)

Copy Markdown View Source

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

source_fields_pure(set, source_lang)

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