Skip to content
On this pageThe Core Model: Locale and Translation Groups
  1. The Core Model: Locale and Translation Groups
  2. Setting Up Locales
  3. Per-Field Translatability
  4. Multilingual Routing Patterns
  5. i18n Feature Comparison
  6. Current State in EmDash v0.1
  7. Creating a Translation via MCP

EmDash Multilingual Sites: i18n Built In

Ben 4 min read

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.

// 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:

FeatureEmDashWordPress + WPMLContentful
Locale-aware queriesBuilt inPlugin required ($99+/yr)Built in
Translation groupsBuilt inPlugin requiredBuilt in
Per-field translatabilityYesLimited (via WPML settings)Yes
Cost to enable$0$99–$199/yearPlan-gated
hreflang tag generationVia EmDashHeadVia pluginManual

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.

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?