PhoenixKitProjects.Translations (PhoenixKitProjects v0.5.1)

Copy Markdown View Source

Public API for AI-driven translation of Project, Task, and Assignment resources. Wraps Oban enqueuing on PhoenixKitProjects.Workers.TranslateResourceWorker and exposes helpers hosts use to drive the language switcher's ai_translate attr.

Design notes

  • Worker does the AI call, this module just enqueues. Host LVs call enqueue/1 from a handle_event clause and subscribe to the projects PubSub topic for status updates.
  • One job per (resource, target_lang) pair. Uniqueness is enforced at the Oban worker level; this module just emits the enqueue call. A second enqueue for an in-flight pair returns {:ok, %{conflict?: true}}.
  • enqueue_all_missing/1 loops over the resource's missing languages and enqueues one job per language. Each runs independently; status events arrive in the order Oban completes them, not the order the languages were enqueued.
  • No "translate all resources in a project" bulk action. That's a level above this module — a host can iterate projects and call enqueue_all_missing/1 per resource if desired.

Status flow

  1. Host calls enqueue/1{:ok, %{conflict?: false}} and a :translation_started PubSub event fires from the worker.
  2. Host's handle_info({:projects, :translation_started, ...}) adds target_lang to its in_flight set, re-renders the switcher with the updated ai_translate.in_flight list → spinner replaces sparkle.
  3. Worker completes → :translation_completed event. Host removes target_lang from in_flight AND from missing (the language now has a translation), re-renders.
  4. On failure: :translation_failed event with :reason. Host removes from in_flight (leaves in missing) and surfaces a flash.

Summary

Functions

Is AI-driven translation usable right now?

Is the default projects translation prompt already provisioned?

Enqueue a translation job for a single resource + target language.

Enqueue one translation job per missing target language.

Generates the default projects translation prompt in the AI prompts catalog. Returns {:ok, prompt} or {:error, changeset}. No-op surface when PhoenixKitAI is unavailable — returns {:error, :ai_not_installed}.

Resolves the default AI endpoint UUID from Settings, or nil.

Resolves the default AI prompt UUID. Prefers the explicit setting; falls back to the prompt matching @translation_prompt_slug if one exists. Returns nil when nothing is wired up.

List configured AI endpoints as {uuid, name} tuples. Empty when AI is unavailable.

List configured AI prompts as {uuid, name} tuples. Empty when AI is unavailable.

Types

enqueue_params()

@type enqueue_params() :: %{
  :resource_type => resource_type(),
  :resource_uuid => String.t(),
  :endpoint_uuid => String.t(),
  :prompt_uuid => String.t(),
  :source_lang => String.t(),
  :target_lang => String.t(),
  optional(:actor_uuid) => String.t() | nil
}

resource_type()

@type resource_type() :: String.t()

Functions

ai_translation_available?()

@spec ai_translation_available?() :: boolean()

Is AI-driven translation usable right now?

Returns true when the optional PhoenixKitAI plugin is loaded, the module is enabled at runtime, and at least one AI endpoint is configured. Hosts use this to gate the AI translate UI surface.

default_translation_prompt_exists?()

@spec default_translation_prompt_exists?() :: boolean()

Is the default projects translation prompt already provisioned?

enqueue(params)

@spec enqueue(enqueue_params()) :: {:ok, %{conflict?: boolean()}} | {:error, term()}

Enqueue a translation job for a single resource + target language.

Returns {:ok, %{conflict?: false}} when the job is freshly enqueued, or {:ok, %{conflict?: true}} when an identical job is already in flight (Oban's unique constraint catches the dup).

Validates required keys and returns {:error, {:invalid, [keys]}} on a malformed input map — saves the host LV from cryptic Oban exceptions at job perform time.

enqueue_all_missing(base_params, missing_langs)

@spec enqueue_all_missing(enqueue_params(), [String.t()]) ::
  {:ok,
   %{
     enqueued: non_neg_integer(),
     conflicts: non_neg_integer(),
     errors: [{String.t(), term()}],
     in_flight: [String.t()]
   }}
  | {:error, term()}

Enqueue one translation job per missing target language.

missing_langs is host-supplied — typically computed as enabled_languages -- ([primary_language] ++ Map.keys(resource.translations)).

Returns {:ok, %{enqueued: N, conflicts: M, errors: [...], in_flight: [...]}}:

  • enqueued — number of jobs newly inserted into Oban.
  • conflicts — number of duplicates (job for that lang already running).
  • errors — list of {lang, reason} for langs whose enqueue raised or returned {:error, _}. Callers should surface this as a partial failure.
  • in_flight — exactly the langs whose enqueue succeeded (newly or by conflict). This is what a host should add to its UI spinner set: failed langs MUST NOT spin because no worker broadcast will arrive to clear them.

generate_default_translation_prompt()

@spec generate_default_translation_prompt() :: {:ok, struct()} | {:error, term()}

Generates the default projects translation prompt in the AI prompts catalog. Returns {:ok, prompt} or {:error, changeset}. No-op surface when PhoenixKitAI is unavailable — returns {:error, :ai_not_installed}.

get_default_ai_endpoint_uuid()

@spec get_default_ai_endpoint_uuid() :: String.t() | nil

Resolves the default AI endpoint UUID from Settings, or nil.

get_default_ai_prompt_uuid()

@spec get_default_ai_prompt_uuid() :: String.t() | nil

Resolves the default AI prompt UUID. Prefers the explicit setting; falls back to the prompt matching @translation_prompt_slug if one exists. Returns nil when nothing is wired up.

list_ai_endpoints()

@spec list_ai_endpoints() :: [{String.t(), String.t()}]

List configured AI endpoints as {uuid, name} tuples. Empty when AI is unavailable.

list_ai_prompts()

@spec list_ai_prompts() :: [{String.t(), String.t()}]

List configured AI prompts as {uuid, name} tuples. Empty when AI is unavailable.