---
title: Publish documentation in multiple languages
description: Add translated pages with build-time generation, automatic fallbacks, and a built-in language switcher.
url: https://docs.thally.io/guides/multi-language
---

# Publish documentation in multiple languages

Add translated pages with build-time generation, automatic fallbacks, and a built-in language switcher.

Thally supports build-time multi-language documentation. Translated MDX files live in your repo alongside your primary content — fully version-controlled, statically generated, and indexed by search engines with real locale-specific URLs.

#### Real locale URLs

  `/es/introduction`, `/fr/guides/getting-started`. Each published translation
  has its own crawlable URL.

#### AI-powered translation

  Thally Cloud generates translations using your workspace's AI Credits.
  Self-hosted sites can translate with one command. Code and MDX structure are
  validated.

#### Automatic fallbacks

  Missing translations show the original with a notice. Stale translations stay
  visible with an update notice.

#### Built-in language switcher

  A globe icon appears automatically in the top bar as soon as you configure two
  or more locales.

---

## URL structure

The default locale is served without a URL prefix — fully backward-compatible with any existing links. Secondary locales are prefixed with their locale code:

| URL                          | Content           |
| ---------------------------- | ----------------- |
| `/introduction`              | English (default) |
| `/es/introduction`           | Spanish           |
| `/fr/introduction`           | French            |
| `/guides/getting-started`    | English (default) |
| `/es/guides/getting-started` | Spanish           |

Adding a new locale never breaks existing URLs.

---

## Setup

#### Choose languages in Thally Cloud

For a managed site, open **Site settings → Languages**, choose every language
readers should see, and save. Confirm that Thally may use your workspace's
shared AI Credits to generate translations. You can leave the page while Thally
generates missing or stale translations, validates the complete batch, commits
the MDX files to your connected repository, and publishes the site. Thally
never overwrites team-authored translations.

English remains the source language at unprefixed URLs. The selected languages
go live only after the full batch succeeds. If generation or validation fails,
no translation files are published; fix the reported problem and select
**Refresh translations**. The Languages section shows current, stale, missing,
and human-authored coverage from your connected repository.

#### Configure a self-hosted site

Self-hosted sites can configure the same behavior in `docs.json`:

```json
{
  "i18n": {
    "defaultLocale": "en",
    "locales": [
      { "code": "en", "label": "English" },
      { "code": "es", "label": "Español" },
      { "code": "fr", "label": "Français" }
    ]
  },
  "tabs": [...]
}
```

`defaultLocale` must match the repository's source content language. The
locales array controls the language switcher order.

Once this is saved, a globe icon appears in the top bar automatically.

#### Translate your pages locally with the CLI

This optional, local workflow is separate from Thally Cloud's managed
translation. Run the translate command in your documentation repository to
generate translated MDX files for a locale:

```bash
npx create-thally-docs translate --locale es
```

This calls Claude to translate every content page defined in your `docs.json` and writes the results to `src/content/es/`. You'll see a live progress list as each page completes, and a summary at the end.

**Translate specific pages only:**

```bash
npx create-thally-docs translate --locale es --pages introduction,guides/getting-started
```

**Overwrite existing translations:**

```bash
npx create-thally-docs translate --locale es --force
```

**All options:**

| Flag              | Description                                                                  |
| ----------------- | ---------------------------------------------------------------------------- |
| `--locale <code>` | Target locale — required. Must match a code in your `docs.json` i18n config. |
| `--pages <ids>`   | Comma-separated page IDs to translate. Omit to translate all pages.          |
| `--force`         | Overwrite existing translation files.                                        |
| `--api-key <key>` | Anthropic API key. Falls back to `ANTHROPIC_API_KEY` env var.                |
| `--model <id>`    | Claude model to use. Default: `claude-sonnet-4-6`.                           |
| `--yes`, `-y`     | Skip the confirmation prompt.                                                |

> **Local CLI key only:**
  The CLI command runs in your own environment and calls Claude through the
  Anthropic API. Set `ANTHROPIC_API_KEY` locally or pass `--api-key` to that
  command. These options do not apply to Thally Cloud, which uses workspace AI
  Credits for managed translation.

#### Review and commit local translations

After a local CLI translation, your content directory will look like this:

```
src/content/
├── introduction.mdx            ← primary (English, unchanged)
├── guides/
│   └── getting-started.mdx
├── es/                         ← Spanish translations
│   ├── introduction.mdx
│   └── guides/
│       └── getting-started.mdx
└── fr/                         ← French translations
    └── introduction.mdx
```

Translated files are plain MDX. Edit them with the rest of your content, commit
them to the repository, and deploy normally.

---

## Translation quality

Claude translates with these rules applied automatically:

- **Translates:** all prose text, headings, frontmatter `title`, `description`, and `keywords`
- **Preserves:** MDX component tags verbatim (`<Note>`, `<Steps>`, `<CodeGroup>`, etc.)
- **Preserves:** all code blocks and inline code exactly as written
- **Preserves:** all URLs, file paths, and import statements
- **Preserves:** all frontmatter structure and non-text fields

If a translation needs adjusting, edit the file in `src/content/{locale}/` directly. You can also re-run the command for specific pages:

```bash
npx create-thally-docs translate --locale es --pages guides/getting-started --force
```

---

## Fallback behavior

When a source page exists but its translation is missing, Thally shows the source-language content with an informative banner. A translation left behind after its source page is deleted returns 404.

**Missing translation**: the page hasn't been translated for that locale yet.

> ⚠️ This page hasn't been translated to **Español** yet. Showing the original **English** version.

**Stale translation**: a managed AI translation's recorded source hash differs from the current source.

> This translation may be outdated. [View original →]

Both banners appear only on secondary-locale routes. The primary route is never affected. Human-authored or unmarked translations are not labeled stale from deployment timestamps; review their freshness manually.

### Heading anchors in translated pages

Heading anchor IDs preserve non-Latin characters. A translated heading such as
"Überblick" produces the anchor `#überblick`, and "日本語 API" becomes
`#日本語-api`. This means section links within translated pages use the
translated text, not transliterated ASCII.

For a managed site, select **Refresh translations** in **Site settings →
Languages**. For a local CLI workflow, run the translate command for that page:

```bash
npx create-thally-docs translate --locale es --pages guides/getting-started
```

---

## Locale-specific navigation

By default, every locale shares the root `tabs` navigation. To provide
translated sidebar labels or a different page structure for a locale, add a
`navigation` map under `i18n`:

```json
{
  "i18n": {
    "defaultLocale": "en",
    "locales": [
      { "code": "en", "label": "English" },
      { "code": "es", "label": "Español" }
    ],
    "navigation": {
      "es": [
        {
          "tab": "Empezar",
          "groups": [
            { "group": "Inicio", "pages": ["introduction", "quickstart"] }
          ]
        }
      ]
    }
  }
}
```

Use page IDs without a locale prefix; Thally adds the locale to navigation URLs.

Each entry uses the same `tabs` array structure as the root. When a locale has
no entry in `navigation`, Thally falls back to the root `tabs`. Locale-specific
navigation is reflected in the sidebar, breadcrumbs, and previous/next links.

---

## Language switcher

Once you have two or more locales configured, a **globe icon** appears in the top bar automatically. Clicking it opens a dropdown listing all configured locales.

Selecting a locale navigates to the same page in that locale — for example, switching from `/es/guides/getting-started` to French takes you directly to `/fr/guides/getting-started`. The switcher always keeps you on the current page rather than sending you to a home page.

---

## API Reference

The API Reference tab is auto-generated from your OpenAPI spec. Its content is always in the primary language — OpenAPI specs define machine-readable contracts, not prose content.

When navigating to the API Reference tab from a locale page, the URL prefix is preserved so you remain in the locale context (`/es/api/...`). The sidebar and tab bar stay locale-aware; only the operation content itself comes from the spec unchanged.

> **Localising API content:**
  To change the language of API Reference descriptions and summaries, update
  them directly in your OpenAPI spec file.

---

## SEO

Thally generates crawler metadata from the same language selection used by the site. A locale is added to `hreflang` and the sitemap only when that page has a translated MDX file and neither the source nor the translation is hidden or marked `noindex`:

```html
<link rel="canonical" href="https://docs.example.com/es/introduction" />
<link
  rel="alternate"
  hreflang="en"
  href="https://docs.example.com/introduction"
/>
<link
  rel="alternate"
  hreflang="es"
  href="https://docs.example.com/es/introduction"
/>
<link
  rel="alternate"
  hreflang="fr"
  href="https://docs.example.com/fr/introduction"
/>
<link
  rel="alternate"
  hreflang="x-default"
  href="https://docs.example.com/introduction"
/>
```

Real translations use their own locale URL as canonical. A missing-translation fallback is marked `noindex` and canonicalized to the source URL, so crawlers never index English prose as if it were Spanish or French. The translated content also carries its own `lang` and text direction on the server-rendered content container.

The docs search dialog searches the current language and returns translated page URLs. Agents can request `/api/docs-index?locale=fr`, `/api/search?q=installation&locale=fr`, or `/api/docs/fr/guides/getting-started` for translated content. These APIs omit pages without a published translation in the requested language.

Set `THALLY_SITE_URL` so the correct domain appears in these tags:

```bash
THALLY_SITE_URL=https://docs.example.com
```

---

## Using the MCP tool

If you use the Thally MCP server, you can translate docs directly from your AI assistant:

```json
{
  "name": "translate_docs",
  "arguments": {
    "projectDir": "/path/to/my-docs",
    "locale": "es",
    "force": false
  }
}
```

The MCP tool accepts the same options as the CLI. See [MCP Server](/guides/mcp-server) for setup instructions.

---

## Scaffold with i18n

When creating a new project, you can enable multi-language support from the start:

```bash
npx create-thally-docs my-docs
```

When asked **"Add another documentation language? (y/N):"**, enter `y` and provide your locale codes (e.g. `es,fr,de`). The `i18n` block is written to `docs.json` automatically.

---

#### [CLI Reference](/guides/cli-reference)

  Full reference for `translate` and all other create-thally-docs commands.

#### [MCP Server](/guides/mcp-server)

  Set up the Thally MCP server to translate docs from your AI assistant.