PhoenixKitProjects. Workers. TranslateResourceWorker
(PhoenixKitProjects v0.5.0)
Copy Markdown
View Source
Oban worker that translates a Project / Task / Assignment's
translatable fields from a source language to a single target
language, then writes the result into the resource's translations
JSONB map.
Templates use the same Project schema as regular projects (with
is_template: true), so resource_type of "project" or
"template" both route through Project. The distinction shows up
only in the activity-log entry and the user-facing flash text.
Job arg shape
%{
"resource_type" => "project" | "template" | "task" | "assignment",
"resource_uuid" => uuid,
"endpoint_uuid" => uuid, # PhoenixKitAI endpoint
"prompt_uuid" => uuid, # PhoenixKitAI prompt template
"source_lang" => "en",
"target_lang" => "es",
"actor_uuid" => uuid_or_nil
}Storage
Writes to resource.translations[target_lang] via the resource's
existing changeset, preserving any pre-existing translations on
other languages. The shape matches what the multilang form writes
(e.g. %{"et" => %{"name" => "...", "description" => "..."}}) so
the form's edit round-trip works unchanged after a translation
finishes.
Broadcasts
{:projects, :translation_started, %{...}}before the AI call{:projects, :translation_completed, %{...}}after a successful write{:projects, :translation_failed, %{...}}on any failure path
Payload always includes resource_type, resource_uuid,
target_lang. :translation_failed adds reason (a normalised
atom or {atom, term} tuple from PhoenixKit.Modules.AI.Translation).
Uniqueness
One job in flight per (resource_uuid, target_lang) pair so a
double-click in the UI doesn't burn AI tokens twice. Other target
languages on the same resource can run concurrently.
period: :infinity is deliberate: Oban's unique default is a
60-second window, which would let a second enqueue slip through
for a translation that runs longer than a minute (entirely possible
for a multi-field AI call). With :infinity the dedup is bounded by
the in-flight states instead of time — a duplicate is rejected as
long as the original is still available/scheduled/executing/
retryable, and a fresh job is allowed only once the previous one
reaches a terminal state (so re-translating later still works).
Auto-completion
Mirrors publishing's TranslatePostWorker shape (queue, retry
policy, activity-log action) but writes a much smaller surface
(no version model, no per-language status, no cache invalidation).
Each module owns its own broadcast topic + activity-log action;
the actual AI orchestration lives in
PhoenixKit.Modules.AI.Translation.