Skip to content

Translation System

What this is

ComStack has two translation systems. This page covers page translation — automatic translation of documentation and custom pages at publish time. For live voice agent translation (real-time language detection during visitor conversations), see the voice agent documentation.

How it works

When you publish, every promoted page is automatically translated to all locales configured for your project. The pipeline runs:

  1. Promote drafts to live
  2. Auto-translate each promoted page to all configured locales (in parallel)
  3. Promote the generated translation drafts to live
  4. Build and deploy the site

Both the publish-confirm MCP tool and the REST API publish endpoint run the same translation pipeline — there is no difference in translation behaviour between the two paths.

When to use it

The translation system runs automatically on every publish. Configure it by setting locales on your project modules:

  • Knowledge pages: modules.knowledge_base.locales
  • Custom pages: modules.custom_pages.locales

Locale configuration

Knowledge pages vs custom pages

ModuleConfig fieldWhen translated
Knowledge pages (markdown docs)project.knowledge_pages.localesAt publish time
Custom pagesproject.custom_pages.localesAt publish time

Custom page locales should always be a superset of knowledge page locales.

Locale config invariants

Three constraints are enforced at every write to default_locale or module locales:

RuleConstraint
default_locale_in_custom_localesdefault_locale ∈ modules.custom_pages.locales (when custom pages module is enabled)
default_locale_in_knowledge_localesdefault_locale ∈ modules.knowledge_base.locales (when knowledge base module is enabled)
knowledge_subset_of_custommodules.knowledge_base.locales ⊆ modules.custom_pages.locales (when both modules are enabled)

Any violation returns 400 with { error: "Locale config drift", violations: [...] }.

Adding a locale

PUT /api/projects/:projectId/modules/knowledge_base (or /custom_pages) with a locales array that adds new codes triggers an automatic fan-out:

  • For each new locale × each live source-locale page: generate a translated draft
  • Drafts stay in draft state — run publish + publish-confirm to promote them
  • Idempotent: pairs where a live or draft variant already exists for (canonical_slug, locale) are skipped

A preview endpoint (POST /api/projects/:projectId/modules/knowledge_base/locales/preview) returns the expected manifest without writing anything.

Translated slugs and canonical_slug

Each locale gets its own native-language URL path. An English page at /docs/getting-started might translate to /es/docs/empezando.

The canonical_slug field groups all locale variants of the same page:

FieldPurpose
canonical_slugStable cross-locale grouping key — set once at promote time, never overwritten
metadata.slugLocale-specific URL path (different per locale)
source_doc_idPoints to the source doc this translation was generated from

canonical_slug is set once and preserved. This ensures:

  • A translated variant keeps its source-locale canonical
  • A renamed source page stays in the same hreflang group as its existing translations
  • The promote step supersedes prior live docs by both (metadata.slug, language) and (canonical_slug, language), so re-translated pages with a changed slug replace the old variant cleanly

Exception: slug === "index" is never translated — the homepage slug stays "index" in every locale.

What gets translated per page type

Knowledge pages (markdown)

FieldTranslation behaviour
metadata.titleTranslated
metadata.descriptionTranslated
metadata.slugTranslated — native-language URL path
contentTranslated markdown
metadata.sidebar.*Copied unchanged (structural)

Custom pages

Custom pages have a distinct pattern because the body source must never be modified:

FieldTranslation behaviour
title, descriptionTranslated
body_sourceCopied byte-identical — author code is never modified
translatable_stringsThe marker map { key: defaultText } is translated value-by-value; keys are preserved
canonical_slugCopied identically — custom pages never get per-locale slugs
media_variants, og_imageCopied identically

Field-level translate: false

Doc templates can declare individual fields with translate: false. Fields so declared are copied verbatim from the source locale to all target locales instead of being translated. Applies to title, description, slug, and content when declared.

Repairing missing translations

Use repair-translations (MCP tool, manager role) to backfill missing locale siblings on demand:

  • For every live source-locale page, generate any active-locale variant that doesn’t yet exist
  • Promote and deploy
  • Idempotent: pairs with an existing variant are skipped

Options:

  • dry_run: true — preview the manifest without writing anything
  • doc_id — scope to a single source page
  • locale — scope to a single target locale

repair-translations includes a drift guard: a translated variant whose canonical_slug doesn’t match its source’s is flagged in drift_skipped[] and skipped rather than re-drafted as a duplicate. Drift is not healed by this tool — a targeted canonical_slug data-fix is needed first.

Common errors

ErrorCauseFix
400: Locale config driftA locale write violates one of the three invariantsCheck which invariant fails; adjust default_locale or module locale arrays
400: missing_source_indexAdding a locale but no live slug=index page exists in the default localePublish an index page in the default locale first
Translation drafts not visibleFan-out generates drafts; they need a publish run to go liveRun publish + publish-confirm after adding a locale
Duplicate hreflang entriesTwo source-locale docs share the same canonical_slugEnsure each page has a unique canonical_slug
  • Languages — all supported locales, BCP-47 codes, RTL languages
  • Publish API — the publish pipeline that triggers translation
  • Tool cataloguerepair-translations and publish-confirm

Last updated: