Skip to content

Translations

Alokai CMS machine-translates localizable fields using Cloudflare Workers AI. Translation is built around a per-field source lifecycle so machine output is always distinguishable from human-authored content and is gated before it ships.

Route: apps/cms/src/server/routes/translations.ts. Model: @cf/google/gemma-4-26b-a4b-it via the AI binding.

Localized value model

Localized content is stored inside the page’s data JSON (in the locale = '_all' page_versions row). Each localizable field becomes a LocalizedValue map keyed by locale code (see apps/cms/src/shared/locale-utils.ts):

{
"title": {
"_localized": true,
"en-US": { "value": "Spring Sale", "_source": "human" },
"es-ES": {
"value": "Venta de Primavera",
"_source": "draft",
"_translatedAt": "2026-06-01T10:00:00Z",
"_confidence": 0.82,
},
},
}

A plain string under a locale key is treated as _source: "human". At delivery time, localizeZones() unwraps each field to the requested locale (falling back to the default locale).

The source lifecycle

human ── translate ──▶ draft ── review ──▶ reviewed ── publish ──▶ live
_sourceMeaning
humanAuthored/edited by a person.
draftAI suggestion, not yet reviewed.
reviewedA person approved the AI suggestion.

By default translation only fills untranslated fields and never overwrites human or reviewed values (the keep_existing flag).

Endpoints

Method & pathPurpose
POST /entries/:id/translateTranslate an entry into one or more target_locales. Optional field_paths for a subset; keep_existing (default true); source_locale. Returns a summary (jobs_created, jobs_completed, jobs_skipped, jobs_failed).
GET /translation-jobsList jobs, filterable by page_id / status. Scoped to the active (space, edition).
GET / POST /glossary, DELETE /glossary/:idManage glossary terms.

The MVP runs translation inline at request time, writing a row per field/locale into translation_jobs (status runningdone / failed). A batch/Durable-Object pipeline is the planned evolution for full-site jobs.

Glossary

The glossary applies before the model is called and always wins:

  • do-not-translate — emit the source term unchanged (brand/product names).
  • per-locale override — emit a fixed translation for a term in a locale.

Publish gate

apps/cms/src/server/utils/publish.ts refuses to publish a locale that still has _source: "draft" fields, throwing UnreviewedTranslationsError (surface a 422 with the offending locales) unless an allowUnreviewedTranslations flag is set. This is what forces machine output through human review before it goes live.