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
- Open any entry in the editor.
- The locale bar appears below the toolbar with a tab for each configured locale. Solid tabs are existing variants; dashed tabs are untranslated.
- Click a dashed tab to create that translation — a blank entry is created for the same slug in that locale.
- 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" } }