---
title: "EmDash Multilingual Sites: i18n Built In"
description: "How EmDash handles internationalization. Locale-aware content, translation groups, and multilingual routing."
article_type: child
canonical: https://dashstro.com/learn/emdash-multilingual
---
Internationalization is one of those features that most CMS platforms treat as an afterthought. With WordPress you're buying a plugin. With many headless CMSes you're stitching together locale routing and translation fields manually. EmDash takes a different approach: i18n is part of the core data model, not a bolt-on. Every content entry has a locale. Every entry can belong to a translation group. The query APIs are locale-aware from the start.

## The Core Model: Locale and Translation Groups

Each content entry in EmDash carries a `locale` field that identifies what language it's written in — `en`, `fr`, `de`, and so on using standard IETF language tags. When you create a translated version of an entry, you link it to the original via a `translationGroup` ID. This group ID is what lets EmDash surface the alternate-language version of any given page — for hreflang tags, for language switchers, for alternate URL routing.

The translation group model is flexible. You don't have to translate every entry. An English article can exist without a French counterpart. The site just won't show that article when the active locale is French unless you've configured a fallback.

## Setting Up Locales

Locales are configured in your `astro.config.mjs` via EmDash's `locales` option. You specify a default locale and any additional locales you want to support. Once configured, EmDash's content APIs respect the active locale automatically when you pass a `locale` parameter.

```typescript
// Locale-aware content query
const { entries, cacheHint } = await getEmDashCollection("learn", {
  locale: "fr",
  orderBy: { published_at: "desc" },
  limit: 10,
});

// Fetching a single entry in a specific locale
const { entry, cacheHint: hint } = await getEmDashEntry("learn", slug, {
  locale: "fr",
});

// Getting all translations of an entry (via MCP: content_translations)
// Returns all entries in the same translationGroup
```

## Per-Field Translatability

Not every field needs to be translated. A publication date, a sort order, or a numeric field might be the same regardless of locale. EmDash lets you mark individual fields as translatable or not in the schema. Fields marked as non-translatable sync their value across all locales in a translation group — change it in one locale, it updates everywhere. Fields marked as translatable are independent per locale.

In practice, you'll typically mark text fields (title, body, excerpt, description) as translatable and leave structural fields (sort order, parent reference, category assignments) as shared. This cuts down the translation workload significantly and prevents structural drift between locale variants.

## Multilingual Routing Patterns

The standard routing pattern for multilingual Astro sites uses locale prefixes: `/en/learn/my-article` and `/fr/learn/mon-article`. Each locale gets its own slug. The translation group links them so search engines and language switchers can connect the variants. For the default locale, you can optionally omit the prefix (`/learn/my-article` instead of `/en/learn/my-article`), which is common practice for English-default sites.

## i18n Feature Comparison

Here's how EmDash's i18n capabilities compare to the most common alternatives:

| Feature | EmDash | WordPress + WPML | Contentful |
| --- | --- | --- | --- |
| Locale-aware queries | Built in | Plugin required ($99+/yr) | Built in |
| Translation groups | Built in | Plugin required | Built in |
| Per-field translatability | Yes | Limited (via WPML settings) | Yes |
| Cost to enable | $0 | $99–$199/year | Plan-gated |
| hreflang tag generation | Via EmDashHead | Via plugin | Manual |

## Current State in EmDash v0.1

I want to be honest about where things stand. EmDash v0.1 has the data model and query APIs for i18n in place. The `locale` field on content entries works. Translation groups work. The `content_translations` MCP tool works. The admin UI for managing translations is still being developed — right now, creating a translated entry means creating a new entry via MCP or the API with the `translationOf` parameter, rather than clicking a "Translate" button in the admin panel. The foundation is solid; the editorial UX is coming.

If you're building a multilingual site on EmDash today, you can do it — it just requires working at the API level for translation management. For teams that are already comfortable with MCP-based workflows, this isn't a significant barrier.

:::pullquote
i18n built into the core means you're not paying a tax on multilingualism. The data model handles it from day one, so the decision to go multilingual later doesn't require rearchitecting anything.
:::

## Creating a Translation via MCP

To create a French translation of an existing English article, you'd call `content_create` with the `translationOf` parameter pointing to the original entry's ID and `locale` set to `fr`. EmDash automatically adds the new entry to the same translation group and populates any non-translatable fields from the original. You then provide translated values for the translatable fields.

Multilingual content is one of the highest-leverage moves for reaching a global audience. The barrier to doing it well has historically been tooling cost and complexity. EmDash removes both.

[What Is EmDash?](/learn/what-is-emdash)

