Multilingual (i18n)
Per-entry locale variants using a slug--locale convention. No separate tables, no routing configuration.
Setup
In the admin, go to Settings → Site and set:
- Primary locale — e.g.
en - Additional locales — comma-separated, e.g.
en,de,fr
The locale switcher appears automatically in the entry editor once additional locales are configured.
The slug--locale convention
my-post ← base / primary entry (e.g. English) my-post--de ← German variant my-post--fr ← French variant
Locale variants are regular entries with a special slug format. They appear in the editor as tabs. Creating a variant duplicates the base entry and appends the locale suffix.
Reading locale content
import { getLocaleCollection, getLocaleEntry, locale, locales } from 'orbiter:collections';
// All German posts
const posts = await getLocaleCollection('posts', 'de');
// A specific German post, with fallback to the base entry
const post = await getLocaleEntry('posts', 'my-post', 'de'); Locale-aware static paths
---
import { getCollection, locales } from 'orbiter:collections';
export async function getStaticPaths() {
const posts = await getCollection('posts');
const base = posts.filter(p => !p.slug.includes('--'));
return base.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 to find slug--locale first. If it doesn't exist, it returns the base entry (slug). This means you can translate incrementally — untranslated entries gracefully fall back to the primary language.
Accessing locale config
import { locale, locales } from 'orbiter:collections';
// locale → 'en' (the primary locale)
// locales → ['en', 'de', 'fr'] Use locales to build language switchers or generate <link rel="alternate"> tags.