Docs

Multilingual (i18n)

Per-entry locale variants stored in a dedicated locale column. Same slug across all languages — no slug suffixes, no separate tables.

Setup

In the admin, go to Settings → Language and set the Locales field:

en,de,fr

The first value is the default locale. Additional values are translated locales. Save settings — the locale switcher appears automatically in the entry editor.

How it works

Every entry has a locale column in the database. The default locale is stored as an empty string ('') for backwards compatibility — all entries created before enabling i18n are treated as the default locale automatically.

_entries
├── slug: "my-post",  locale: ""    ← default locale (e.g. English)
├── slug: "my-post",  locale: "de"  ← German translation
└── slug: "my-post",  locale: "fr"  ← French translation

Creating translations

  1. Open any entry in the editor.
  2. The locale bar appears below the toolbar with a tab for each configured locale. Solid tabs are existing variants; dashed tabs are untranslated.
  3. Click a dashed tab to create that translation — a blank entry is created for the same slug in that locale.
  4. Fill in the translated content and save.

Each locale variant is independent: it has its own status (draft/published), its own version history, and its own data fields.

Reading locale content

In your Astro pages, use the locale-aware functions from orbiter:collections:

import { getCollection, getLocaleCollection, getLocaleEntry, locale, locales }
  from 'orbiter:collections';

// Default locale only (the primary language)
const posts = await getCollection('posts');

// All German posts
const dePosts = await getLocaleCollection('posts', 'de');

// A specific post in German — falls back to default locale if not translated
const post = await getLocaleEntry('posts', 'my-post', 'de');

Locale-aware static paths

---
import { getCollection, getLocaleEntry, locales } from 'orbiter:collections';

export async function getStaticPaths() {
  const posts = await getCollection('posts');  // default locale entries only

  return posts.flatMap(post =>
    locales.map(loc => ({
      params: { lang: loc, slug: post.slug },
    }))
  );
}

const { lang, slug } = Astro.params;
const post = await getLocaleEntry('posts', slug, lang);
---

<html lang={lang}>
  <h1>{post.data.title}</h1>
</html>

Fallback behaviour

getLocaleEntry() tries the requested locale first. If no translation exists for that locale, it returns the default locale variant. This lets you translate incrementally — untranslated entries fall back gracefully.

Accessing locale config

import { locale, locales } from 'orbiter:collections';
// locale  → 'en'              (the configured default locale)
// locales → ['en', 'de', 'fr'] (all configured locales)

Use locales to build language switchers or generate <link rel="alternate"> hreflang tags.

Admin API — locale query param

The REST API accepts a ?locale= query parameter on all entry endpoints:

# List German entries
GET /api/collections/posts/entries?locale=de

# Get a specific German entry
GET /api/collections/posts/entries/my-post?locale=de

# List all locales for an entry
GET /api/collections/posts/entries/my-post/locales

# Create a German variant
POST /api/collections/posts/entries
{ "slug": "my-post", "locale": "de", "data": { "title": "Mein Beitrag" } }