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.10/install | bash
# Windows (PowerShell)
. { iwr -useb https://get.simplelocalize.io/2.10/install-windows } | iex;
# npm
npm install @simplelocalize/cli
# macOS / Linux / Windows (WSL)
curl -s https://get.simplelocalize.io/2.10/install | bash
# Windows (PowerShell)
. { iwr -useb https://get.simplelocalize.io/2.10/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>."
  }
}