---
title: "Translation System"
description: "How ComStack auto-translates pages at publish time — locale config, translated slugs, canonical_slug grouping, and the repair-translations tool."
url: "https://comstack.ai/docs/concepts/translations/"
---
## 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

| Module | Config field | When translated |
|---|---|---|
| Knowledge pages (markdown docs) | `project.knowledge_pages.locales` | At publish time |
| Custom pages | `project.custom_pages.locales` | At 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:

| Rule | Constraint |
|---|---|
| `default_locale_in_custom_locales` | `default_locale ∈ modules.custom_pages.locales` (when custom pages module is enabled) |
| `default_locale_in_knowledge_locales` | `default_locale ∈ modules.knowledge_base.locales` (when knowledge base module is enabled) |
| `knowledge_subset_of_custom` | `modules.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:

| Field | Purpose |
|---|---|
| `canonical_slug` | Stable cross-locale grouping key — set once at promote time, never overwritten |
| `metadata.slug` | Locale-specific URL path (different per locale) |
| `source_doc_id` | Points 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)

| Field | Translation behaviour |
|---|---|
| `metadata.title` | Translated |
| `metadata.description` | Translated |
| `metadata.slug` | Translated — native-language URL path |
| `content` | Translated markdown |
| `metadata.sidebar.*` | Copied unchanged (structural) |

### Custom pages

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

| Field | Translation behaviour |
|---|---|
| `title`, `description` | Translated |
| `body_source` | Copied byte-identical — author code is never modified |
| `translatable_strings` | The marker map `{ key: defaultText }` is translated value-by-value; keys are preserved |
| `canonical_slug` | Copied identically — custom pages never get per-locale slugs |
| `media_variants`, `og_image` | Copied 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

| Error | Cause | Fix |
|---|---|---|
| `400: Locale config drift` | A locale write violates one of the three invariants | Check which invariant fails; adjust `default_locale` or module locale arrays |
| `400: missing_source_index` | Adding a locale but no live `slug=index` page exists in the default locale | Publish an index page in the default locale first |
| Translation drafts not visible | Fan-out generates drafts; they need a publish run to go live | Run `publish` + `publish-confirm` after adding a locale |
| Duplicate hreflang entries | Two source-locale docs share the same `canonical_slug` | Ensure each page has a unique `canonical_slug` |

## Related

- [Languages](/docs/concepts/languages.md) — all supported locales, BCP-47 codes, RTL languages
- [Publish API](/docs/api/publish.md) — the publish pipeline that triggers translation
- [Tool catalogue](/docs/mcp/tools.md) — `repair-translations` and `publish-confirm`
