Multi-language content with automatic AI translation, locale routing, and hreflang.
Configure locales
export default defineConfig({
defaultLocale: 'en',
locales: ['en', 'da'],
collections: [
defineCollection({
name: 'posts',
sourceLocale: 'en',
locales: ['en', 'da'],
translatable: true,
fields: [
{ name: 'title', type: 'text', required: true },
{ name: 'content', type: 'richtext' },
],
}),
],
});How translations work
Each translation is a separate document linked via translationGroup — a shared UUID connecting all language versions:
// content/posts/hello-world.json (English)
{
"slug": "hello-world",
"locale": "en",
"translationGroup": "abc-123",
"data": { "title": "Hello, World!" }
}
// content/posts/hello-world-da.json (Danish)
{
"slug": "hello-world-da",
"locale": "da",
"translationGroup": "abc-123",
"data": { "title": "Hej, Verden!" }
}Admin UI translation workflow
In the admin UI:
- Open a document — see the locale badge showing current language
- Click "+ Add translation" to create a new locale version
- The AI translator auto-translates all fields
- Review and publish the translation
Translations appear grouped in the document list and the editor shows a locale switcher.
AI translation
import { createAi } from '@webhouse/cms-ai';
const ai = await createAi();
const result = await ai.content.translate(
sourceDoc.data,
'da',
{ collection: collectionConfig },
);
// result.fields contains translated dataLocale routing in Next.js
Use a [locale] route segment:
app/
[locale]/
blog/
[slug]/page.tsx
page.tsx
layout.tsxWith a middleware for locale detection:
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
const LOCALES = ['en', 'da'];
const DEFAULT = 'en';
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
const hasLocale = LOCALES.some(l => pathname.startsWith(`/${l}/`) || pathname === `/${l}`);
if (hasLocale) return;
const preferred = request.headers.get('accept-language')?.split(',')[0]?.split('-')[0] ?? DEFAULT;
const locale = LOCALES.includes(preferred) ? preferred : DEFAULT;
return NextResponse.redirect(new URL(`/${locale}${pathname}`, request.url));
}