← Back to Blog
Diagram explaining localized routes in Astro

Astro i18n Guide: Localized Routes & Content

• 4 min read

Build a multilingual Astro site with language-prefixed routes, Content Collections, and helpers for localized navigation. Learn how to configure manual routing, create shared dictionaries, render translated posts, and wire hreflang metadata without sacrificing performance or authoring flow. The guide covers Accept-Language negotiation, astro:i18n utilities, and tactics for keeping English and Chinese content in sync.

Astro Internationalization (i18n) Routing Guide

When building global websites, internationalization (i18n) is essential for reaching a wider audience. This guide walks through a clean, practical approach to multi‑language sites in Astro using language‑prefixed routes and Content Collections.


Why Astro for i18n

Astro is a modern static site builder focused on shipping less JavaScript by default. Its Islands Architecture lets you render most of a page as static HTML and hydrate only what’s needed.

While Astro doesn’t ship a built‑in i18n subsystem like some meta‑frameworks, its routing and content APIs make it straightforward to implement robust, flexible localization that fits your project’s shape.


URL Strategies for Locales

Common patterns for localized URLs:

For Astro projects, a path prefix under /[lang]/... is usually the best default:


Suggested Project Layout

Organize pages and strings per locale to keep things predictable:

src/
  pages/
    en/
      index.astro
      about.astro
    zh/
      index.astro
      about.astro
  i18n/
    en.json
    zh.json

Minimal i18n Helper

Start with a tiny helper for UI strings:

// src/utils/i18n.ts
import en from '../i18n/en.json';
import zh from '../i18n/zh.json';

const languages = { en, zh } as const;

export function getLangData(lang: string) {
  return (languages as Record<string, (typeof languages)[keyof typeof languages]>)[lang] || languages.en;
}

Use it in a page:

---
import { getLangData } from '../../utils/i18n';
const { lang } = Astro.params;
const t = getLangData(lang);
---

<html lang={lang}>
  <head>
    <title>{t.title}</title>
  </head>
  <body>
    <h1>{t.hello}</h1>
  </body>
  </html>

Option A: [lang]/blog/[slug] + Content Collections

If you keep your articles in Astro Content Collections, you can locate the correct document by matching both the slug and the locale from params.

---
// src/pages/[lang]/blog/[slug].astro
import { getCollection } from 'astro:content';

export async function getStaticPaths() {
  // Pre‑build both languages for every post
  const posts = await getCollection('posts');
  return posts.flatMap((p) => [
    { params: { lang: 'en', slug: p.data.slugBase }, props: { locale: 'en' } },
    { params: { lang: 'zh', slug: p.data.slugBase }, props: { locale: 'zh' } },
  ]);
}

const { lang, slug } = Astro.params;
const all = await getCollection('posts');
const entry = all.find((p) => p.data.slugBase === slug && p.data.locale === lang);

if (!entry) {
  // Optional: fallback to default locale, or 404
  return Astro.redirect(`/${lang}/blog`);
}

const { Content } = await entry.render();
---

<article>
  <Content />
</article>

Frontmatter for each localized post shares the same slugBase:

---
// en
locale: en
slugBase: astro-i18n-demo
title: "Astro i18n Guide: Localized Routes & Content"
description: "A practical guide to implementing language‑prefixed routes and Content Collections with Astro."
date: 2025-10-23
tags: [astro, i18n, content]
---

---
// zh
locale: zh
slugBase: astro-i18n-demo
title: "Astro i18n 演示:本地化路由与内容"
description: "演示如何通过 [lang] 路由与内容集合来组织多语言文章。"
date: 2025-10-23
tags: [astro, i18n, content]
---

Option B: Directory‑per‑locale Static Pages

For simple sites, you can also duplicate page files under en/ and zh/. This keeps routing very explicit but can lead to more duplication if content grows. Content Collections generally scale better.


Route Param + Default Locale Redirect

Use a top‑level index.astro to redirect visitors to their language (or a default like en).

---
// src/pages/index.astro
const userLang = Astro.request.headers.get('accept-language')
  ?.split(',')[0]
  .split('-')[0];
Astro.redirect(`/${userLang ?? 'en'}/`);
---

Then read lang inside localized pages via Astro.params.lang.


Shared Layout + Localized Nav

Extract common structure into a layout component and pass the current lang.

---
// src/layouts/I18nLayout.astro
const { lang, title } = Astro.props;
---
<html lang={lang}>
  <head>
    <title>{title}</title>
  </head>
  <body>
    <nav>
      <a href={`/en/`}>English</a> | <a href={`/zh/`}>中文</a>
    </nav>
    <slot />
  </body>
</html>

Use it in pages:

---
import I18nLayout from '../../layouts/I18nLayout.astro';
import { getLangData } from '../../utils/i18n';
const { lang } = Astro.params;
const t = getLangData(lang);
---

<I18nLayout lang={lang} title={t.title}>
  <h1>{t.hello}</h1>
</I18nLayout>

SEO Tips

export async function getStaticPaths() {
  const langs = ['en', 'zh'];
  return langs.map((lang) => ({ params: { lang } }));
}

Wrap‑up

Even without a built‑in i18n framework, Astro’s routing + Content Collections make it easy to build fast, localized sites. Use a consistent directory layout, keep a stable slugBase across translations, and centralize shared UI copy for maintainability.

Quick checklist:

— Author: ZhouZhiOu

Zhou Zhiou
Zhou Zhiou
Engineer · Designer

Writes about Astro, Qt, and lighting systems while building bilingual products and immersive installations.

Start a project

Related articles

Join the discussion

Powered by GitHub Discussions via Giscus.

Configure Giscus environment variables to enable comments.

PUBLIC_GISCUS_REPO="owner/name"
PUBLIC_GISCUS_REPO_ID="..."
PUBLIC_GISCUS_CATEGORY="Comments"
PUBLIC_GISCUS_CATEGORY_ID="..."