Skip to main content

Translation Job Queue

Implementation details for the server-side async translation service described in AD-015.

Job Model

type JobStatus string

const (
StatusQueued JobStatus = "queued"
StatusProcessing JobStatus = "processing"
StatusCompleted JobStatus = "completed"
StatusFailed JobStatus = "failed"
)

type TranslationJob struct {
ID string
WorkspaceSlug string
ProjectID string
ItemName string // file being translated
TargetLocale string
ProviderConfigID string // empty or "platform" = managed identity
Model string // deployment name (e.g., "gpt-4o-mini")
PushID string // links to originating push event
Status JobStatus // queued | processing | completed | failed
Progress int // 0 - 100
TotalBlocks int
DoneBlocks int
BatchSize int // blocks per LLM call (default 20)
Concurrency int // parallel batch calls (default 5)
TokensUsed int
ViaMemory int // blocks recycled from the project content memory (JSON `via_tm`)
ViaAI int // blocks sent to the AI translator
Error string
CreatedAt time.Time
UpdatedAt time.Time
}

IsPlatformProvider() returns true when ProviderConfigID is empty or "platform", indicating the job should use Azure OpenAI with Managed Identity.

Queue Implementations

Two implementations of the Queue interface:

ImplementationBackendUse Case
ChannelQueueGo channels (in-memory)Single-process local development
SQSQueueAmazon SQS (or SQS-compatible)Production; ElasticMQ for local stacks

Interface:

type Queue interface {
Enqueue(ctx context.Context, jobID string) error
Dequeue(ctx context.Context) (jobID string, ack func(), nack func(), err error)
Close() error
}

Dequeue() blocks until a job is available. Workers call ack() after successful processing; nack() re-queues the job for retry on transient failures.

Worker Algorithm

1. Dequeue job ID from queue
2. Load job from JobStore
3. Skip if status != "queued"
4. Check quota (if QuotaStore configured)
5. Mark status = "processing"
6. Load project and blocks from ContentStore
7. Resolve provider:
- Platform → Azure OpenAI with Managed Identity
- User-configured → credentials store lookup
8. Recycle from the project content memory first (recycleBlocks): fill
matching blocks (exact by default), record ViaMemory; the remainder
goes to AI
9. Create AITranslateTool with batch/concurrency config
10. Process the AI remainder in chunks of 50:
a. Run tool on chunk
b. Record token usage in QuotaStore
c. Update progress in JobStore
11. Store translated blocks in ContentStore
12. Record the memory-first split (ViaMemory/ViaAI) via UpdateJobMemorySplit
13. Mark status = "completed" with total token count
14. Always ack (no retry on permanent failures)

Memory-first split

Each job recycles the project content memory before calling paid AI, using the same content-aware recycle the local translate flow runs (recycleBlocks, exact matches by default; the threshold reads from the project recipe). It records the split — ViaMemory blocks filled from memory, ViaAI blocks sent to the translator — via UpdateJobMemorySplit. A server convergence run (AD-022) sums ViaMemory across its jobs and reconcileSplit takes the AI share as the remainder (so ViaMemory + ViaAI = Done), letting the run report a truthful content memory N · AI M split server-side rather than attributing everything to AI.

Provider Resolution

Platform provider (when BOWRAIN_OPENAI_ENDPOINT is set):

PlatformProviderConfig{
Endpoint: os.Getenv("BOWRAIN_OPENAI_ENDPOINT"),
ClientID: os.Getenv("AZURE_CLIENT_ID"), // optional
}

Uses azidentity.ManagedIdentityCredential to acquire tokens with scope https://cognitiveservices.azure.com/.default. Tokens are cached and refreshed automatically by the Azure SDK. Rate limit: 10 req/sec.

User-configured provider: loaded from the credential store by ProviderConfigID. Supports OpenAI, Anthropic, Ollama, Azure OpenAI with explicit API keys. Rate limits vary by provider.

Quota Schema (PostgreSQL)

CREATE TABLE ai_usage (
id BIGSERIAL PRIMARY KEY,
workspace_slug TEXT NOT NULL,
project_id TEXT NOT NULL,
job_id TEXT NOT NULL DEFAULT '',
model TEXT NOT NULL,
prompt_tokens INTEGER NOT NULL DEFAULT 0,
output_tokens INTEGER NOT NULL DEFAULT 0,
total_tokens INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_ai_usage_workspace_period
ON ai_usage(workspace_slug, created_at);

CREATE TABLE ai_quotas (
workspace_slug TEXT PRIMARY KEY,
monthly_limit BIGINT NOT NULL DEFAULT 10000000, -- 10M tokens
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

Billing period: calendar month (1st UTC to end of month). CheckQuota() verifies remaining_tokens >= 0 before allowing a new job. Usage is recorded incrementally per chunk (every 50 blocks) to prevent overrun on large jobs.

API Endpoints

MethodPathDescription
POST/api/v1/workspaces/:ws/jobs/translateCreate async job (202 Accepted)
POST/api/v1/projects/:id/sync/translateProject-scoped translate (anonymous)
GET/api/v1/workspaces/:ws/jobs/:idPoll job status and progress
GET/api/v1/workspaces/:ws/jobsList recent 50 jobs
DELETE/api/v1/workspaces/:ws/jobs/:idCancel job
GET/api/v1/workspaces/:ws/ai/usageQuota summary

Environment Variables

VariableRequiredDescription
BOWRAIN_DATABASE_URLYesPostgreSQL or SQLite connection string
BOWRAIN_DATABASE_AUTHNo"azure" for Entra ID auth
BOWRAIN_OPENAI_ENDPOINTNoAzure OpenAI endpoint (enables platform provider)
AZURE_CLIENT_IDNoUser-assigned managed identity client ID
BOWRAIN_QUEUE_BACKENDNosqs selects the SQS backend
SQS_ENDPOINTNoEndpoint override for ElasticMQ/LocalStack
BOWRAIN_SQS_QUEUE_PREFIXNoOptional queue-name prefix

If SQS is not configured, the server uses an in-memory channel queue (suitable only for single-instance development).

Jobs within a convergence run

Translation jobs are the produce step of a server-side convergence run — convergence as a service (AD-022): a run — started by a push to an on-push project, by kapi up from a connected checkout, or manually — enqueues one job per pending (item, locale) pair for the locales its pass fans out, waits for their completion, and re-derives coverage before the next pass. Jobs link back to the triggering push via push_id.

The kapi up CLI command (and the GitHub Action) coordinates the full round-trip on a connected project: push → server catch-up → pull results.