PhoenixKitEcommerce.AITranslatable (PhoenixKitEcommerce v0.4.2)

Copy Markdown View Source

PhoenixKitAI.Translatable adapter for shop products.

Resource identity

resource_type is "shop_product"; resource_uuid is the product uuid.

Fields

%{"title", "description", "body", "seo_title", "seo_description"} from the source language (Translations.get/3), non-empty only. "body" maps to the schema's body_html (the shared prompt vocabulary uses body). The slug is NEVER sourced from or trusted to the AI — it is regenerated locally from the translated title, and only when the target language has no slug yet, so re-translations can't change published URLs. This is a write-once rule for the translation pipeline only: regenerate_slug/2 is the explicit, one-off repair path for a slug already shaped by an older version of this adapter, and it always recomputes from the current title regardless of whether a slug exists. Callers doing a bulk repair are responsible for their own redirect/history bookkeeping — this module has none.

Concurrency

All languages share ONE product row's JSONB maps, so put_translation/4 re-reads the row under FOR UPDATE and merges against the latest committed state (the publishing group-adapter pattern) — concurrent per-language jobs serialize on the row lock and never drop a sibling language. update_product/2 / update_product_translation/3 are deliberately NOT used here: they write a stale in-memory struct without a lock — the exact lost-update race this adapter must prevent.

Slug uniqueness within the language is checked app-side (suffix on collision); there is no DB unique constraint on the JSONB slug map (core migration v47 dropped it), so the check is best-effort across rows.

Prompt

The seo fields are not in the shared translation prompt's vocabulary, so this adapter ships its own prompt (ensure_prompt/0, slug phoenixkit-shop-product-translation). Host forms must pass its uuid per job — the global ai_translation_prompt_uuid setting stays untouched.

Requires the optional phoenix_kit_ai plugin: ensure_prompt/0 returns {:error, :ai_not_installed} when it is absent, and the whole adapter is only reached through duck-typed discovery, which never runs without it.

Summary

Functions

Idempotently creates this adapter's translation prompt and returns its uuid — host forms pass it per job instead of the shared default prompt.

Recomputes and stores lang's slug from its CURRENT title, even when a slug already exists. Explicit, one-off repair path — bypasses the write-once rule put_translation/4 enforces for the translation pipeline. Returns {:error, :no_title} when lang has no title, and {:ok, %{old: slug, new: slug}} (unchanged) when the recomputed slug equals the stored one. Broadcasts Events.broadcast_product_updated/1 only when the slug actually changes.

The resource-type key this adapter registers under.

Functions

ensure_prompt()

@spec ensure_prompt() :: {:ok, String.t()} | {:error, term()}

Idempotently creates this adapter's translation prompt and returns its uuid — host forms pass it per job instead of the shared default prompt.

fetch(arg1, product_uuid)

put_translation(product, target_lang, fields, opts)

regenerate_slug(product_uuid, lang, opts \\ [])

@spec regenerate_slug(String.t(), String.t(), keyword()) ::
  {:ok, %{old: String.t() | nil, new: String.t()}} | {:error, term()}

Recomputes and stores lang's slug from its CURRENT title, even when a slug already exists. Explicit, one-off repair path — bypasses the write-once rule put_translation/4 enforces for the translation pipeline. Returns {:error, :no_title} when lang has no title, and {:ok, %{old: slug, new: slug}} (unchanged) when the recomputed slug equals the stored one. Broadcasts Events.broadcast_product_updated/1 only when the slug actually changes.

opts[:dry_run] (default false): when true, computes and returns the same {:ok, %{old: old, new: new}} result WITHOUT writing anything — no update, no broadcast. Lets a bulk repair task preview what would change.

resource_type()

The resource-type key this adapter registers under.

source_fields(product, source_lang)