Astro doesn’t have built-in internationalization (i18n) support, but you can build your own i18n solution. In this recipe, you’ll learn how to use content collections and dynamic routing to serve content for different languages.
This example serves each language at its own subpath, e.g. example.com/en/blog for English and example.com/fr/blog for French.
If you prefer the default language to not be visible in the URL unlike other languages, there are instructions to hide the default language below.
folder structure
src
└─ pages
├─ en
│ ├─about.astro
│ └─index.astro
├─ tw
│ ├─about.astro
│ └─index.astro
└─ index.astro
src/pages/index.astro
to redirect to your default language.for SSG
// src/pages/index.astro
---
---
<meta http-equiv="refresh" content="0;url=/en/" />
for SSR
---
return Astro.redirect('/en/');
---
src/content/
for each type of content you want to include and add subdirectories for each supported language. For example, to support English and French blog posts:src
└─ content
└─ blog
├─ en
│ ├─post-1.md
│ └─post-2.md
├─ tw
│ ├─post-1.md
│ └─post-2.md
└─ index.astro
src/content/config.ts
file and export a collection for each type of content.// src/content/config.ts
import { defineCollection, z } from 'astro:content';
const blogCollection = defineCollection({
schema: z.object({
title: z.string(),
author: z.string(),
date: z.date()
})
});
export const collections = {
'blog': blogCollection
};
// src/pages/[lang]/blog/[...slug].astro
---
import { getCollection } from 'astro:content'
export async function getStaticPaths() {
const pages = await getCollection('blog')
const paths = pages.map(page => {
const [lang, ...slug] = page.slug.split('/');
return { params: { lang, slug: slug.join('/') || undefined }, props: page }
})
return paths;
}
const { lang, slug } = Astro.params;
const page = Astro.props;
const formattedDate = page.data.date.toLocaleString(lang);
const { Content } = await page.render();
---
<h1>{page.data.title}</h1>
<p>by {page.data.author} • {formattedDate}</p>
<Content/>