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 sitelocales
- Array of supported language codesprefixDefaultLocale
- Whether to prefix default locale URLsredirectToDefaultLocale
- 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
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
- Astro.js Internationalization Guide
- Astro.js @astrojs/i18n Documentation
- SimpleLocalize CLI Documentation