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_source | Meaning |
|---|---|
human | Authored/edited by a person. |
draft | AI suggestion, not yet reviewed. |
reviewed | A 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 & path | Purpose |
|---|---|
POST /entries/:id/translate | Translate 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-jobs | List jobs, filterable by page_id / status. Scoped to the active (space, edition). |
GET / POST /glossary, DELETE /glossary/:id | Manage glossary terms. |
The MVP runs translation inline at request time, writing a row per
field/locale into translation_jobs (status running → done / 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.