In this guide, you'll learn how to implement internationalization in Astro.js using astro-i18next and manage your translations with SimpleLocalize. This approach leverages the powerful i18next ecosystem with server-side rendering support.
Installation
Install the required dependencies for astro-i18next integration:
npm install astro-i18next i18next
What you get:
- Full i18next ecosystem support
- Server-side rendering with translations
- Automatic locale detection and routing
- Namespace support for organized translations
- Interpolation and pluralization
- Plugin ecosystem compatibility
Configuration
Create an i18next configuration file:
// src/i18n/config.ts
import i18next from "i18next";
export const languages = {
en: "English",
es: "Español",
fr: "Français",
de: "Deutsch"
};
export const defaultLanguage = "en";
export const showDefaultLanguage = false;
i18next.init({
debug: false,
fallbackLng: defaultLanguage,
lng: defaultLanguage,
supportedLngs: Object.keys(languages),
ns: ["common", "navigation", "forms"],
defaultNS: "common",
resources: {
en: {
common: () => import("../locales/en/common.json"),
navigation: () => import("../locales/en/navigation.json"),
forms: () => import("../locales/en/forms.json"),
},
es: {
common: () => import("../locales/es/common.json"),
navigation: () => import("../locales/es/navigation.json"),
forms: () => import("../locales/es/forms.json"),
},
fr: {
common: () => import("../locales/fr/common.json"),
navigation: () => import("../locales/fr/navigation.json"),
forms: () => import("../locales/fr/forms.json"),
},
de: {
common: () => import("../locales/de/common.json"),
navigation: () => import("../locales/de/navigation.json"),
forms: () => import("../locales/de/forms.json"),
},
},
});
export default i18next;
Configure astro-i18next in your astro.config.mjs
:
// astro.config.mjs
import { defineConfig } from "astro/config";
import astroI18next from "docs/integrations/astro-i18next.mdx";
export default defineConfig({
integrations: [
astroI18next({
baseLanguage: "en",
i18next: {
debug: false,
supportedLngs: ["en", "es", "fr", "de"],
fallbackLng: "en",
ns: ["common", "navigation", "forms"],
defaultNS: "common",
},
i18nextPlugins: {
fsBackend: "i18next-fs-backend",
},
}),
],
});
Translation Files Structure
Organize your translations using namespaces:
src/
locales/
en/
common.json
navigation.json
forms.json
es/
common.json
navigation.json
forms.json
fr/
common.json
navigation.json
forms.json
de/
common.json
navigation.json
forms.json
Example translation files:
// src/locales/en/common.json
{
"site": {
"title": "My Astro App",
"description": "A multilingual website built with Astro.js"
},
"welcome": {
"title": "Welcome to our website",
"subtitle": "Experience the power of internationalization",
"cta": "Get started"
},
"footer": {
"copyright": "© {{year}} My Company. All rights reserved.",
"privacy": "Privacy Policy",
"terms": "Terms of Service"
}
}
// src/locales/en/navigation.json
{
"menu": {
"home": "Home",
"about": "About",
"services": "Services",
"contact": "Contact",
"blog": "Blog"
},
"breadcrumb": {
"home": "Home",
"current": "Current Page"
}
}
// src/locales/en/forms.json
{
"contact": {
"title": "Contact Us",
"name": "Full Name",
"email": "Email Address",
"subject": "Subject",
"message": "Your Message",
"submit": "Send Message",
"success": "Thank you for your message!",
"error": "Please fill in all required fields."
},
"newsletter": {
"title": "Subscribe to our newsletter",
"email": "Enter your email",
"subscribe": "Subscribe",
"success": "Successfully subscribed!"
}
}
Page Implementation
Multilingual Pages
Create pages that support multiple languages:
---
// src/pages/[...lang]/index.astro
import { t, changeLanguage } from "i18next";
import { HeadHrefLangs } from "astro-i18next/components";
import Layout from "../../layouts/Layout.astro";
export async function getStaticPaths() {
return [
{ params: { lang: undefined } },
{ params: { lang: "es" } },
{ params: { lang: "fr" } },
{ params: { lang: "de" } },
];
}
const { lang } = Astro.params;
await changeLanguage(lang || "en");
---
<Layout>
<Fragment slot="head">
<HeadHrefLangs />
</Fragment>
<main>
<h1>{t("welcome.title")}</h1>
<p>{t("welcome.subtitle")}</p>
<button class="cta-button">
{t("welcome.cta")}
</button>
<section class="features">
<h2>{t("features.title", "Amazing Features")}</h2>
<div class="feature-grid">
<div class="feature">
<h3>{t("features.fast.title", "Lightning Fast")}</h3>
<p>{t("features.fast.description", "Built for speed and performance")}</p>
</div>
<div class="feature">
<h3>{t("features.seo.title", "SEO Optimized")}</h3>
<p>{t("features.seo.description", "Perfect for search engines")}</p>
</div>
</div>
</section>
</main>
</Layout>
Blog with Translations
---
// src/pages/[...lang]/blog/[slug].astro
import { t, changeLanguage } from "i18next";
import { HeadHrefLangs } from "astro-i18next/components";
import Layout from "../../../layouts/Layout.astro";
import { getCollection } from "astro:content";
export async function getStaticPaths() {
const blogPosts = await getCollection("blog");
const languages = ["en", "es", "fr", "de"];
const paths = [];
for (const post of blogPosts) {
for (const lang of languages) {
paths.push({
params: {
lang: lang === "en" ? undefined : lang,
slug: post.slug
},
props: { post, lang: lang || "en" }
});
}
}
return paths;
}
const { post, lang } = Astro.props;
await changeLanguage(lang);
const { Content } = await post.render();
---
<Layout title={post.data.title}>
<Fragment slot="head">
<HeadHrefLangs />
</Fragment>
<article>
<header>
<nav class="breadcrumb">
<a href={lang === "en" ? "/" : `/${lang}/`}>
{t("navigation:breadcrumb.home")}
</a>
<span>/</span>
<a href={lang === "en" ? "/blog" : `/${lang}/blog`}>
{t("navigation:menu.blog")}
</a>
<span>/</span>
<span>{t("navigation:breadcrumb.current")}</span>
</nav>
<h1>{post.data.title}</h1>
<time datetime={post.data.date.toISOString()}>
{post.data.date.toLocaleDateString(lang, {
year: 'numeric',
month: 'long',
day: 'numeric'
})}
</time>
</header>
<Content />
</article>
</Layout>
Components with Translations
Language Switcher
---
// src/components/LanguageSwitcher.astro
import { localizePath } from "astro-i18next";
import { languages } from "../i18n/config";
const currentPath = Astro.url.pathname;
---
<div class="language-switcher">
<label for="language-select" class="sr-only">
Choose language
</label>
<select id="language-select" onchange="handleLanguageChange(this.value)">
{Object.entries(languages).map(([code, name]) => (
<option
value={localizePath("/", code)}
selected={currentPath.startsWith(`/${code}/`) || (code === "en" && !currentPath.includes("/es/") && !currentPath.includes("/fr/") && !currentPath.includes("/de/"))}
>
{name}
</option>
))}
</select>
</div>
<script>
function handleLanguageChange(path) {
// Get current path without language prefix
const currentPath = window.location.pathname;
const pathWithoutLang = currentPath.replace(/^\/[a-z]{2}(\/|$)/, '/');
// Construct new path
let newPath;
if (path === '/') {
newPath = pathWithoutLang;
} else {
newPath = path.replace(/\/$/, '') + pathWithoutLang;
}
window.location.href = newPath;
}
</script>
<style>
.language-switcher {
margin: 1rem 0;
}
.language-switcher select {
padding: 0.5rem;
border: 1px solid #ccc;
border-radius: 4px;
background: white;
font-size: 1rem;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
</style>
Contact Form with Validation
---
// src/components/ContactForm.astro
import { t } from "i18next";
---
<form class="contact-form" method="POST" action="/api/contact">
<h2>{t("forms:contact.title")}</h2>
<div class="form-group">
<label for="name">{t("forms:contact.name")}</label>
<input
type="text"
id="name"
name="name"
required
placeholder={t("forms:contact.name")}
/>
</div>
<div class="form-group">
<label for="email">{t("forms:contact.email")}</label>
<input
type="email"
id="email"
name="email"
required
placeholder={t("forms:contact.email")}
/>
</div>
<div class="form-group">
<label for="subject">{t("forms:contact.subject")}</label>
<input
type="text"
id="subject"
name="subject"
required
placeholder={t("forms:contact.subject")}
/>
</div>
<div class="form-group">
<label for="message">{t("forms:contact.message")}</label>
<textarea
id="message"
name="message"
rows="5"
required
placeholder={t("forms:contact.message")}
></textarea>
</div>
<button type="submit" class="submit-btn">
{t("forms:contact.submit")}
</button>
</form>
<style>
.contact-form {
max-width: 600px;
margin: 2rem auto;
padding: 2rem;
border: 1px solid #e0e0e0;
border-radius: 8px;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 600;
}
.form-group input,
.form-group textarea {
width: 100%;
padding: 0.75rem;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 1rem;
}
.submit-btn {
background: #007acc;
color: white;
padding: 0.75rem 2rem;
border: none;
border-radius: 4px;
font-size: 1rem;
cursor: pointer;
transition: background 0.2s;
}
.submit-btn:hover {
background: #005999;
}
</style>
Layout with SEO Support
---
// src/layouts/Layout.astro
import { t } from "i18next";
import { HeadHrefLangs } from "astro-i18next/components";
import LanguageSwitcher from "../components/LanguageSwitcher.astro";
export interface Props {
title?: string;
description?: string;
}
const { title, description } = Astro.props;
const pageTitle = title ? `${title} | ${t("site.title")}` : t("site.title");
const pageDescription = description || t("site.description");
---
<!DOCTYPE html>
<html lang={t("site.lang", "en")}>
<head>
<meta charset="UTF-8" />
<meta name="description" content={pageDescription} />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>{pageTitle}</title>
<!-- Automatically generates hreflang tags -->
<HeadHrefLangs />
</head>
<body>
<header>
<nav>
<a href="/">{t("site.title")}</a>
<div class="nav-links">
<a href={t("navigation:menu.home", "/")}>{t("navigation:menu.home")}</a>
<a href={t("navigation:menu.about", "/about")}>{t("navigation:menu.about")}</a>
<a href={t("navigation:menu.services", "/services")}>{t("navigation:menu.services")}</a>
<a href={t("navigation:menu.contact", "/contact")}>{t("navigation:menu.contact")}</a>
</div>
<LanguageSwitcher />
</nav>
</header>
<main>
<slot />
</main>
<footer>
<p>{t("footer.copyright", { year: new Date().getFullYear() })}</p>
<div class="footer-links">
<a href="/privacy">{t("footer.privacy")}</a>
<a href="/terms">{t("footer.terms")}</a>
</div>
</footer>
</body>
</html>
<style>
/* Add your global styles here */
header {
background: #f8f9fa;
padding: 1rem 0;
border-bottom: 1px solid #e0e0e0;
}
nav {
max-width: 1200px;
margin: 0 auto;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 2rem;
}
.nav-links {
display: flex;
gap: 2rem;
}
footer {
background: #333;
color: white;
text-align: center;
padding: 2rem 0;
margin-top: 4rem;
}
.footer-links {
margin-top: 1rem;
display: flex;
justify-content: center;
gap: 2rem;
}
.footer-links a {
color: #ccc;
text-decoration: none;
}
.footer-links a:hover {
color: white;
}
</style>
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/locales/{lang}/{ns}.json
uploadLanguageKey: en
uploadOptions:
- REPLACE_TRANSLATION_IF_FOUND
# Download configuration
downloadPath: ./src/locales/{lang}/{ns}.json
downloadLanguageKeys: ['en', 'es', 'fr', 'de']
downloadFormat: single-language-json
downloadOptions:
- CREATE_DIRECTORIES
Translation workflow:
# 1. Upload source translations to SimpleLocalize
simplelocalize upload
# 2. Translate using SimpleLocalize interface or auto-translation
# (done via web interface)
# 3. Download completed translations
simplelocalize download
# 4. Build and deploy
npm run build
Advanced Features
Pluralization
// src/locales/en/common.json
{
"items": {
"count": "{{count}} item",
"count_plural": "{{count}} items"
},
"notifications": {
"unread": "You have {{count}} unread message",
"unread_plural": "You have {{count}} unread messages"
}
}
Usage in components:
---
import { t } from "i18next";
const itemCount = 5;
const messageCount = 1;
---
<div>
<p>{t("items.count", { count: itemCount })}</p>
<p>{t("notifications.unread", { count: messageCount })}</p>
</div>
Interpolation with HTML
{
"legal": {
"terms": "By signing up, you agree to our <a href='/terms'>Terms of Service</a> and <a href='/privacy'>Privacy Policy</a>."
}
}
Resources
- astro-i18next documentation
- i18next documentation
- SimpleLocalize CLI documentation
- SimpleLocalize Namespaces Guide
Was this helpful?