Packages

phoenix_kit

1.7.131
1.7.208 1.7.207 1.7.206 1.7.205 1.7.204 1.7.203 1.7.202 1.7.201 1.7.200 1.7.199 1.7.198 1.7.197 1.7.196 1.7.194 1.7.193 1.7.192 1.7.191 1.7.190 1.7.189 1.7.187 1.7.186 1.7.185 1.7.184 1.7.183 1.7.182 1.7.181 1.7.180 1.7.179 1.7.178 1.7.177 1.7.176 1.7.175 1.7.174 1.7.173 1.7.172 1.7.171 1.7.170 1.7.169 1.7.168 1.7.167 1.7.166 1.7.165 1.7.164 1.7.162 1.7.161 1.7.160 1.7.159 1.7.157 1.7.156 1.7.155 1.7.154 1.7.153 1.7.152 1.7.151 1.7.150 1.7.149 1.7.146 1.7.145 1.7.144 1.7.143 1.7.138 1.7.133 1.7.132 1.7.131 1.7.130 1.7.128 1.7.126 1.7.125 1.7.121 1.7.120 1.7.119 1.7.118 1.7.117 1.7.116 1.7.115 1.7.114 1.7.113 1.7.112 1.7.111 1.7.110 1.7.109 1.7.108 1.7.107 1.7.106 1.7.105 1.7.104 1.7.103 1.7.102 1.7.101 1.7.100 1.7.99 1.7.98 1.7.97 1.7.96 1.7.95 1.7.94 1.7.93 1.7.92 1.7.91 1.7.90 1.7.89 1.7.88 1.7.87 1.7.86 1.7.85 1.7.84 1.7.83 1.7.82 1.7.81 1.7.80 1.7.79 1.7.78 1.7.77 1.7.76 1.7.75 1.7.74 1.7.71 1.7.70 1.7.69 1.7.66 1.7.65 1.7.64 1.7.63 1.7.62 1.7.61 1.7.59 1.7.58 1.7.57 1.7.56 1.7.55 1.7.54 1.7.53 1.7.52 1.7.51 1.7.49 1.7.44 1.7.43 1.7.42 1.7.41 1.7.39 1.7.38 1.7.37 1.7.36 1.7.34 1.7.33 1.7.31 1.7.30 1.7.29 1.7.28 1.7.27 1.7.26 1.7.25 1.7.24 1.7.23 1.7.22 1.7.21 1.7.20 1.7.19 1.7.18 1.7.17 1.7.16 1.7.15 1.7.14 1.7.13 1.7.12 1.7.11 1.7.10 1.7.9 1.7.8 1.7.7 1.7.6 1.7.5 1.7.4 1.7.3 1.7.2 1.7.1 1.7.0 1.6.20 1.6.19 1.6.18 1.6.17 1.6.16 1.6.15 1.6.14 1.6.13 1.6.12 1.6.11 1.6.10 1.6.9 1.6.8 1.6.7 1.6.6 1.6.5 1.6.4 1.6.3 1.5.2 1.5.1 1.5.0 1.4.9 1.4.8 1.4.7 1.4.6 1.4.5 1.4.4 1.4.3 1.4.2 1.4.1 1.4.0 1.3.2 1.3.1 1.3.0 1.2.10 1.2.9 1.2.8 1.2.7 1.2.5 1.2.4 1.2.2 1.2.1 1.2.0 1.1.0 1.0.0

A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more

Current section

Files

Jump to
phoenix_kit lib modules ai translatable.ex
Raw

lib/modules/ai/translatable.ex

defmodule PhoenixKit.Modules.AI.Translatable do
@moduledoc """
Behaviour a feature module implements to make a resource AI-translatable
through core's generic translation pipeline
(`PhoenixKit.Modules.AI.TranslateWorker` +
`PhoenixKit.Modules.AI.Translations`).
An adapter is the *only* per-module code needed — the load, the field
extraction, and the persist. Everything else (enqueue, the Oban worker,
the AI call, parsing, broadcasts, the audit log, retry policy) lives in
core and is shared across every consumer.
## Registration
The feature module exposes its adapters via the optional
`ai_translatables/0` callback on `PhoenixKit.Module`, returning
`[{resource_type, adapter_module}]`:
@impl PhoenixKit.Module
def ai_translatables do
[
{"catalogue", PhoenixKitCatalogue.AITranslatable},
{"catalogue_category", PhoenixKitCatalogue.AITranslatable},
{"catalogue_item", PhoenixKitCatalogue.AITranslatable}
]
end
`resource_type` strings MUST be globally unique across all modules —
namespace them (`"catalogue_item"`, not `"item"`). The same adapter
module may serve several resource types; it dispatches on the
`resource_type` argument passed to each callback.
## Storage contract
`put_translation/4` owns the write and MUST be **atomic + merge-safe**.
`enqueue_all_missing/2` dispatches one concurrent job per target language,
so several jobs write the *same row's* translation store at once. The
`resource` struct handed in was loaded BEFORE the (multi-second) AI call,
so it is stale by persist time — merging the new language into that
in-memory struct and doing a plain update will silently drop sibling
languages other jobs committed in the meantime.
Persist against the **current** row, one of:
- a single atomic SQL write to the per-language path, e.g.
`jsonb_set(coalesce(data, '{}'), {translations, <lang>}, <fields>, true)`
via `update_all` (different languages touch different paths → no
conflict); or
- a `Repo.transaction` that re-reads the row `lock: "FOR UPDATE"`, merges,
and writes.
Either keeps the multilang form's edit round-trip working unchanged.
"""
@type resource_type :: String.t()
@type lang :: String.t()
@type fields :: %{optional(String.t()) => String.t()}
@doc "Load a resource by its (type, uuid). `{:error, :resource_not_found}` when absent."
@callback fetch(resource_type(), uuid :: String.t()) :: {:ok, struct()} | {:error, term()}
@doc """
The `%{field_name => text}` to translate, read in `source_lang`.
Return only non-empty fields — empty ones waste tokens and confuse the
model. Field names become the prompt variables + `---FIELD---` markers.
"""
@callback source_fields(resource :: struct(), source_lang :: lang()) :: fields()
@doc """
Persist `fields` into `resource` for `target_lang`. Must merge (not
clobber other languages). `opts` carries `:actor_uuid`.
"""
@callback put_translation(
resource :: struct(),
target_lang :: lang(),
fields :: fields(),
opts :: keyword()
) :: {:ok, struct()} | {:error, term()}
@doc """
Optional extra PubSub topics (besides the core translation topic) to
fan status events out on — e.g. the module's own resource topic so an
already-subscribed LV gets translation lifecycle events too.
"""
@callback pubsub_topics(resource :: struct()) :: [binary()]
@optional_callbacks pubsub_topics: 1
end