@astrojs/i18n

Last updated: July 31, 2025Author: Jakub Pomykała

In this guide, you'll learn how to implement internationalization in Astro.js using the official @astrojs/i18n package and manage your translations with SimpleLocalize. This approach provides built-in routing, locale detection, and translation utilities.

Installation

Install the required dependencies for Astro.js internationalization:

npm install @astrojs/i18n

What you get:

  • Built-in internationalized routing (/en/, /es/, /fr/)
  • Automatic locale detection and redirection
  • Translation utilities and helpers
  • SEO-friendly URL structure

Astro Configuration

Configure internationalization in your astro.config.mjs:

// astro.config.mjs
import { defineConfig } from 'astro/config';
import { i18n } from '@astrojs/i18n';

export default defineConfig({
  integrations: [
    i18n({
      defaultLocale: 'en',
      locales: ['en', 'es', 'fr', 'de'],
      routing: {
        prefixDefaultLocale: false,
        redirectToDefaultLocale: true
      }
    })
  ],
  i18n: {
    defaultLocale: 'en',
    locales: ['en', 'es', 'fr', 'de']
  }
});

Configuration options:

  • defaultLocale - The fallback language for your site
  • locales - Array of supported language codes
  • prefixDefaultLocale - Whether to prefix default locale URLs
  • redirectToDefaultLocale - Auto-redirect to default locale when needed

Translation Files Setup

Create translation files in your project structure:

src/
  i18n/
    en.json
    es.json
    fr.json
    de.json
  pages/
    [locale]/
      index.astro
      about.astro
    index.astro

Example translation files:

// src/i18n/en.json
{
  "nav.home": "Home",
  "nav.about": "About",
  "nav.contact": "Contact",
  "home.title": "Welcome to our website",
  "home.description": "This is a multilingual Astro.js application",
  "about.title": "About Us",
  "contact.form.name": "Name",
  "contact.form.email": "Email",
  "contact.form.message": "Message",
  "contact.form.submit": "Send Message"
}
// src/i18n/es.json
{
  "nav.home": "Inicio",
  "nav.about": "Acerca de",
  "nav.contact": "Contacto",
  "home.title": "Bienvenido a nuestro sitio web",
  "home.description": "Esta es una aplicación multiidioma de Astro.js",
  "about.title": "Acerca de Nosotros",
  "contact.form.name": "Nombre",
  "contact.form.email": "Correo electrónico",
  "contact.form.message": "Mensaje",
  "contact.form.submit": "Enviar Mensaje"
}

Translation Utilities

Create a translation utility to load and use translations:

// src/utils/i18n.ts
import { getLocale } from 'astro:i18n';

type TranslationKeys = {
  [key: string]: string | TranslationKeys;
};

const translations: Record<string, TranslationKeys> = {
  en: () => import('../i18n/en.json').then(module => module.default),
  es: () => import('../i18n/es.json').then(module => module.default),
  fr: () => import('../i18n/fr.json').then(module => module.default),
  de: () => import('../i18n/de.json').then(module => module.default),
};

export async function getTranslations(locale: string) {
  const translationLoader = translations[locale] || translations['en'];
  return await translationLoader();
}

export function t(translations: TranslationKeys, key: string, fallback?: string): string {
  const keys = key.split('.');
  let current: any = translations;

  for (const k of keys) {
    if (current[k] === undefined) {
      return fallback || key;
    }
    current = current[k];
  }

  return typeof current === 'string' ? current : fallback || key;
}

export function getCurrentLocale(url: URL): string {
  const pathname = url.pathname;
  const localeMatch = pathname.match(/^\/([a-z]{2}(?:-[A-Z]{2})?)\//);
  return localeMatch ? localeMatch[1] : 'en';
}

Page Implementation

Dynamic Locale Pages

Create dynamic pages that handle multiple locales:

---
// src/pages/[locale]/index.astro
import Layout from '../../layouts/Layout.astro';
import { getTranslations, t, getCurrentLocale } from '../../utils/i18n';

export async function getStaticPaths() {
  const locales = ['en', 'es', 'fr', 'de'];
  return locales.map(locale => ({
    params: { locale }
  }));
}

const { locale } = Astro.params;
const translations = await getTranslations(locale);
const currentLocale = getCurrentLocale(Astro.url);
---

<Layout title={t(translations, 'home.title')} locale={currentLocale}>
  <main>
    <h1>{t(translations, 'home.title')}</h1>
    <p>{t(translations, 'home.description')}</p>

    <nav>
      <a href={`/${locale}/`}>{t(translations, 'nav.home')}</a>
      <a href={`/${locale}/about`}>{t(translations, 'nav.about')}</a>
      <a href={`/${locale}/contact`}>{t(translations, 'nav.contact')}</a>
    </nav>
  </main>
</Layout>

Default Locale Page

---
// src/pages/index.astro
import Layout from '../layouts/Layout.astro';
import { getTranslations, t } from '../utils/i18n';

const translations = await getTranslations('en');
---

<Layout title={t(translations, 'home.title')} locale="en">
  <main>
    <h1>{t(translations, 'home.title')}</h1>
    <p>{t(translations, 'home.description')}</p>

    <nav>
      <a href="/">{t(translations, 'nav.home')}</a>
      <a href="/about">{t(translations, 'nav.about')}</a>
      <a href="/contact">{t(translations, 'nav.contact')}</a>
    </nav>
  </main>
</Layout>

Language Switcher Component

Create a reusable language switcher:

---
// src/components/LanguageSwitcher.astro
import { getCurrentLocale } from '../utils/i18n';

const currentLocale = getCurrentLocale(Astro.url);
const locales = [
  { code: 'en', name: 'English' },
  { code: 'es', name: 'Español' },
  { code: 'fr', name: 'Français' },
  { code: 'de', name: 'Deutsch' }
];

function getLocalizedPath(targetLocale: string, currentPath: string): string {
  const pathWithoutLocale = currentPath.replace(/^\/[a-z]{2}(\/|$)/, '/');
  return targetLocale === 'en'
    ? pathWithoutLocale
    : `/${targetLocale}${pathWithoutLocale}`;
}
---

<div class="language-switcher">
  {locales.map(locale => (
    <a
      href={getLocalizedPath(locale.code, Astro.url.pathname)}
      class={currentLocale === locale.code ? 'active' : ''}
      hreflang={locale.code}
    >
      {locale.name}
    </a>
  ))}
</div>

<style>
  .language-switcher {
    display: flex;
    gap: 1rem;
    margin: 1rem 0;
  }

  .language-switcher a {
    padding: 0.5rem 1rem;
    text-decoration: none;
    border: 1px solid #ccc;
    border-radius: 4px;
    transition: all 0.2s;
  }

  .language-switcher a:hover,
  .language-switcher a.active {
    background: #007acc;
    color: white;
    border-color: #007acc;
  }
</style>

Layout with SEO

Create a layout that handles internationalization and SEO:

---
// src/layouts/Layout.astro
export interface Props {
  title: string;
  locale: string;
  description?: string;
}

const { title, locale, description } = Astro.props;
const canonicalURL = new URL(Astro.url.pathname, Astro.site);

// Generate alternate language links
const alternateLanguages = ['en', 'es', 'fr', 'de'];
---

<!DOCTYPE html>
<html lang={locale}>
  <head>
    <meta charset="UTF-8" />
    <meta name="description" content={description} />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
    <title>{title}</title>

    <!-- SEO: Canonical and alternate languages -->
    <link rel="canonical" href={canonicalURL} />
    {alternateLanguages.map(lang => (
      <link
        rel="alternate"
        hreflang={lang}
        href={new URL(lang === 'en' ? '/' : `/${lang}/`, Astro.site)}
      />
    ))}
  </head>
  <body>
    <slot />
  </body>
</html>

SimpleLocalize Integration

Set up SimpleLocalize CLI for translation management:

# macOS / Linux / Windows (WSL) curl -s https://get.simplelocalize.io/2.9/install | bash # Windows (PowerShell) . { iwr -useb https://get.simplelocalize.io/2.9/install-windows } | iex; # npm npm install @simplelocalize/cli
# macOS / Linux / Windows (WSL)
curl -s https://get.simplelocalize.io/2.9/install | bash

# Windows (PowerShell)
. { iwr -useb https://get.simplelocalize.io/2.9/install-windows } | iex;

# npm
npm install @simplelocalize/cli

Create simplelocalize.yml in your project root:

apiKey: YOUR_PROJECT_API_KEY

# Upload configuration
uploadFormat: single-language-json
uploadPath: ./src/i18n/{lang}.json
uploadLanguageKey: en

# Download configuration
downloadPath: ./src/i18n/{lang}.json
downloadLanguageKeys: ['en', 'es', 'fr', 'de']
downloadFormat: single-language-json

Build and Deployment

Configure your build process to generate static pages for all locales:

{
  "scripts": {
    "dev": "astro dev",
    "build": "astro build",
    "preview": "astro preview",
    "translations:download": "simplelocalize download",
    "translations:upload": "simplelocalize upload"
  }
}

Deployment workflow:

# 1. Download latest translations
npm run translations:download

# 2. Build your site
npm run build

# 3. Deploy the generated dist/ folder

What happens during build:

  • Astro generates static pages for each locale combination
  • URLs are properly structured (/, /es/, /fr/, /de/)
  • SEO meta tags include proper hreflang attributes
  • All translations are embedded in the static files

Advanced Features

Date and Number Formatting

---
// src/components/FormattedContent.astro
const { locale } = Astro.props;

const formatDate = (date: Date, locale: string) => {
  return new Intl.DateTimeFormat(locale, {
    year: 'numeric',
    month: 'long',
    day: 'numeric'
  }).format(date);
};

const formatCurrency = (amount: number, locale: string) => {
  const currency = locale === 'en' ? 'USD' : 'EUR';
  return new Intl.NumberFormat(locale, {
    style: 'currency',
    currency
  }).format(amount);
};

const currentDate = new Date();
const price = 29.99;
---

<div>
  <p>Date: {formatDate(currentDate, locale)}</p>
  <p>Price: {formatCurrency(price, locale)}</p>
</div>

Content Collections with Translations

// src/content/config.ts
import { defineCollection, z } from 'astro:content';

const blogCollection = defineCollection({
  schema: z.object({
    title: z.string(),
    description: z.string(),
    pubDate: z.date(),
    locale: z.enum(['en', 'es', 'fr', 'de'])
  })
});

export const collections = {
  blog: blogCollection
};

Resources

Was this helpful?