Zum Inhalt springen
21. Mai 20267 min readTutorials

Eine Astro-Site mit Locize internationalisieren

Astro ist das Static-First-Web-Framework für inhaltsgetriebene Sites — dateibasiertes Routing, standardmäßig kein JavaScript, „Islands" für opt-in-Interaktivität, ein SSR-on-Demand-Modus für die Teile, die ihn benötigen. Seit v4.0 (Ende 2023, stabil ab v5+, aktuell in v6.x) liefert Astro eingebautes i18n-Routing: URL-Prefixing (/en/, /de/), den Astro.currentLocale-Helper, Browser-Sprach-Erkennung und automatische <link rel="alternate" hreflang>-SEO-Tags.

Was Astros eingebautes i18n nicht liefert, ist eine Translation-Funktion (t()), Pluralisierung oder Message-Interpolation. Astros offizielles i18n-Rezept zeigt das kanonische „Build it yourself"-Muster: eine JSON-Datei pro Locale importieren, fünf Zeilen Helper schreiben, fertig. Genau dort dockt Locize anlocize-cli lädt das aktuelle veröffentlichte JSON aus Locize zur Build-Zeit nach src/i18n/locales/{lng}/{ns}.json, ein winziges ui.ts-Modul fügt diese zu einem flachen Lookup-Baum zusammen, und Astros Static Build greift sie zur Compile-Zeit ab.

Dieser Beitrag führt End-to-End durch die Integration. Der vollständige Code liegt unter github.com/locize/locize-astro-example.

Wenn Sie stattdessen auf Nuxt sind, folgen Sie dem Nuxt-4-Walkthrough. Wenn Sie auf React Router v7 (Framework-Modus) sind, lesen Sie den React-Router-v7-Walkthrough — beide decken reichhaltigere Integrationen ab (InContext-Editor, Runtime-saveMissing), die direkt nicht in Astros Static-by-Default-Modell passen.

TL;DR

  • Konfigurieren Sie Astros eingebautes i18n-Routing in astro.config.mjs — Locale-Liste + prefixDefaultLocale: true für einheitliche URLs.
  • Nutzen Sie dynamische [lang]/-Page-Routes mit getStaticPaths, damit jede Locale statisch vorgerendert wird.
  • Synchronisieren Sie Übersetzungen zur Build-Zeit über den download-Befehl von locize-cli in die App. Kein Runtime-CDN-Hop, keine SSR-Erwägungen.
  • Schreiben Sie einen fünfzeiligen useTranslations(lang)-Helper in src/i18n/utils.ts. Astro liefert bewusst kein t(); dies ist das kanonische Muster aus den eigenen Docs.
  • Bringen Sie neue Schlüssel via npm run syncLocales, i18next-cli Static Extraction oder über die Locize-Web-App ein — was zu Ihrem Workflow passt. Ein Runtime-saveMissing aus der Static-Schicht ist nicht möglich.
  • Brauchen Sie Runtime-saveMissing, Live-CDN-Fetch oder den InContext-Editor? Mounten Sie eine React-/Vue-/Svelte-Island via @astrojs/<framework> und nutzen Sie i18next-locize-backend + das locize-Paket innerhalb der Island.
  • Vollständiges Beispiel: github.com/locize/locize-astro-example

Wie die Teile zusammenpassen

Astro ist static-by-default. Seiten werden zur Build-Zeit zu HTML vorgerendert und als statische Dateien ausgeliefert (oder im SSR-Modus on demand über einen Adapter). Astros eingebautes i18n besitzt die Routing-Schicht — URL-Prefixing, locale-bewusste Redirects, hreflang-Tags, Browser-Detection-Helper — bleibt aber bewusst aus der Message-Translation-Schicht heraus. Genau deshalb endet jedes Astro-i18n-Tutorial mit „und nun schreiben Sie Ihren eigenen t()-Helper": Astros Philosophie ist, Primitiven zu geben, keine Meinungen zur Übersetzung.

Locize ist die Übersetzungsmanagement-Schicht. Die Integration ist rein bundle-basiert zur Runtime:

  • Build-Zeit: locize-cli lädt das aktuelle veröffentlichte JSON vom Locize-CDN nach src/i18n/locales/{lng}/{ns}.json. Die committeten JSON-Dateien werden zum Module-Load-Zeitpunkt direkt von src/i18n/ui.ts importiert und zu einem flachen Lookup-Baum zusammengesetzt.
  • t() ist lokales TypeScript — fünf Zeilen, die Locale-Fallback + {name}-Interpolation behandeln. Keine Runtime-Library, keine Peer-Dep.
  • Updates erfordern einen Rebuild — wie jede andere Astro-Content-Änderung. Führen Sie npm run downloadLocales aus und redeployen Sie.

Dieser letzte Punkt ist der Trade-Off. Ein „Übersetzung in Locize veröffentlicht → Endnutzer sehen sie"-Round-Trip bedeutet, dass der Build erneut ausgeführt werden muss. Wenn Sie stattdessen einen Live-CDN-gestützten Fetch wünschen (veröffentlichen → nächster Seitenaufruf nimmt es auf, kein Redeployment), mounten Sie eine Framework-Island und nutzen i18next-locize-backend darin — der React-Router-v7-Walkthrough behandelt diese Form, und Sie können dasselbe Muster pro Island in Astro übernehmen. Wir kommen am Ende darauf zurück.

Legen wir los

Voraussetzungen

Node.js 22+ (Astro 6 erfordert es), npm/pnpm/yarn und grundlegende Vertrautheit mit Astro. Das Beispiel unten zielt auf Astro 6.3.

Projekt-Layout

Scaffolden Sie eine frische Astro-App oder klonen Sie das Beispiel via git clone:

npm create astro@latest my-app
cd my-app

Die relevante Verzeichnisstruktur, nachdem wir i18n + Locize verdrahtet haben:

my-app/
├── astro.config.mjsAstro's built-in i18n config
├── package.json                — locize-cli download/sync scripts
└── src/
    ├── pages/
    │   ├── index.astro         — root /, redirects to /en/
    │   └── [lang]/
    │       ├── index.astro     — home, renders for each locale via getStaticPaths
    │       └── second.astro    — secondary page, same pattern
    ├── layouts/
    │   └── Layout.astro        — shared <html>/<head>/<body>
    ├── components/
    │   └── LanguagePicker.astro — swaps the /{lang}/ prefix
    └── i18n/
        ├── ui.ts               — assembles namespaced JSON into a flat tree
        ├── utils.ts            — getLangFromUrl + useTranslations helpers
        └── locales/            — locize-cli download target (one JSON per ns)
            ├── en/
            │   ├── common.json
            │   ├── index.json
            │   └── second.json
            └── de/
                ├── common.json
                ├── index.json
                └── second.json

Abhängigkeiten installieren

npm install astro
npm install --save-dev locize-cli

Das war's. Keine Übersetzungs-Runtime-Library, kein Framework-Integrations-Paket. Astros eingebautes i18n + locize-cli + ~50 Zeilen Helper decken die ganze Geschichte ab.

Astros eingebautes i18n in astro.config.mjs konfigurieren

import { defineConfig } from 'astro/config'

export default defineConfig({
  i18n: {
    defaultLocale: 'en',
    locales: ['en', 'de'],
    routing: {
      prefixDefaultLocale: true   // /en/, /de/ — uniform URLs
    }
  }
})

Astro übernimmt nun:

  • URL-Routing für jede Seite unter src/pages/[lang]//en/, /de/, /en/second, /de/second usw.
  • Astro.currentLocale steht kostenlos in jeder Page/Komponente zur Verfügung.
  • Browser-Sprach-Erkennung via Astro.preferredLocale und Astro.preferredLocaleList (nur SSR-/On-Demand-Routes).
  • Automatische <link rel="alternate" hreflang>-Tags, sobald Sie sie aktivieren.

Mit prefixDefaultLocale: true erhält jede Locale ein URL-Prefix — einheitlich, SEO-sauber und einfach mit dynamischen [lang]/-Routes + getStaticPaths zu handhaben. (Wenn Sie die Default-Locale lieber ohne Prefix unter / ausliefern möchten, setzen Sie es auf false und nutzen dateibasierte Pages statt [lang]/. Das Beispiel nutzt die geprefixte Form, weil sie besser skaliert.)

Übersetzungen zur Build-Zeit von Locize herunterladen

Zu package.json hinzufügen:

{
  "scripts": {
    "downloadLocales": "locize download --project-id=<your-id> --ver=latest --cdn-type=standard --clean=true --path=./src/i18n/locales",
    "syncLocales":     "locize sync --project-id=<your-id> --ver=latest --cdn-type=standard --api-key=<your-write-key> --path=./src/i18n/locales --dry=true"
  }
}

Führen Sie npm run downloadLocales vor npm run build aus (manuell, in CI oder als prebuild-Hook), damit das gebündelte JSON immer frisch ist.

Standard vs. Pro CDN. Locize liefert zwei CDN-Infrastrukturen (vollständiger Vergleich unter CDN-Typen: Standard vs. Pro): Standard unter api.lite.locize.app ist BunnyCDN-gestützt, kostenlos für großzügige monatliche Download-Volumen, nur öffentlich (Default für neue Projekte); Pro unter api.locize.app ist CloudFront-gestützt, kostenpflichtig, unterstützt Private Downloads + individuelle Cache-Steuerung. Setzen Sie --cdn-type im CLI-Flag passend zu Ihrem Projekt.

Der Message-Baum — src/i18n/ui.ts

Locize emittiert ein JSON pro Namespace pro Sprache. Astro kennt das Konzept von i18n-Namespaces nicht, daher fügen wir das On-Disk-Layout zur Module-Load-Zeit in einen einzigen flachen Lookup-Baum zusammen, wobei der Namespace als Prefix in jeden Schlüssel eingebrannt wird:

import enCommon from './locales/en/common.json' with { type: 'json' }
import enIndex  from './locales/en/index.json'  with { type: 'json' }
import enSecond from './locales/en/second.json' with { type: 'json' }
import deCommon from './locales/de/common.json' with { type: 'json' }
import deIndex  from './locales/de/index.json'  with { type: 'json' }
import deSecond from './locales/de/second.json' with { type: 'json' }

type Messages = Record<string, string>

function prefix (ns: string, obj: Messages): Messages {
  return Object.fromEntries(
    Object.entries(obj).map(([k, v]) => [`${ns}.${k}`, v])
  )
}

export const ui = {
  en: { ...prefix('common', enCommon), ...prefix('index', enIndex), ...prefix('second', enSecond) },
  de: { ...prefix('common', deCommon), ...prefix('index', deIndex), ...prefix('second', deSecond) }
} as const

export const defaultLang = 'en'

export const languages = {
  en: 'English',
  de: 'Deutsch'
} as const

export type Lang = keyof typeof ui
export type TranslationKey = keyof typeof ui[typeof defaultLang]

Das Resultat sieht so aus:

ui.en = {
  'common.headTitle': 'Locize + Astro',
  'common.skipToContent': 'Skip to content',
  'index.title': 'Hello, {name}!',
  'second.title': 'The second page',
  // ...
}

TranslationKey wird aus dem Default-Locale-Baum abgeleitet, sodass TypeScript den Schlüssel in .astro-Dateien autovervollständigt und Tippfehler zur Compile-Zeit ablehnt.

Der t()-Helper — src/i18n/utils.ts

import { ui, defaultLang, type Lang, type TranslationKey } from './ui'

export function getLangFromUrl (url: URL): Lang {
  const [, lang] = url.pathname.split('/')
  if (lang && lang in ui) return lang as Lang
  return defaultLang
}

export function useTranslations (lang: Lang) {
  return function t (
    key: TranslationKey,
    values?: Record<string, string | number>
  ): string {
    const raw: string = ui[lang][key] ?? ui[defaultLang][key] ?? key
    if (!values) return raw
    return raw.replace(/\{(\w+)\}/g, (_, k: string) =>
      values[k] !== undefined ? String(values[k]) : `{${k}}`
    )
  }
}

Fünf Zeilen Logik. Fallback-Kette: gesuchter Schlüssel in der aktiven Locale, dann in der Default-Locale, dann der rohe Schlüssel-String. Optionale {name}-Interpolation übergibt Werte über das zweite Argument. Das ist die gesamte Runtime-API.

Dieses Muster ist dasselbe, das Astros offizielles i18n-Rezept zeigt — wir haben lediglich Locizes namespaced On-Disk-Layout darübergelegt.

t() in Pages verwenden

Dynamische [lang]/-Routes + getStaticPaths bedeuten, dass Astro zur Build-Zeit eine HTML-Datei pro Locale vorrendert:

---
// src/pages/[lang]/index.astro
import Layout from '../../layouts/Layout.astro'
import { ui, type Lang } from '../../i18n/ui'
import { useTranslations } from '../../i18n/utils'
import { getRelativeLocaleUrl } from 'astro:i18n'

export function getStaticPaths () {
  return Object.keys(ui).map(lang => ({ params: { lang } }))
}

const { lang } = Astro.params as { lang: Lang }
const t = useTranslations(lang)
---

<Layout lang={lang} title={t('common.headTitle')}>
  <h1>{t('index.title', { name: 'Astro' })}</h1>
  <p>{t('index.subtitle')}</p>
  <p>
    <a href={getRelativeLocaleUrl(lang, 'second/')}>
      {t('index.goto.second')}
    </a>
  </p>
</Layout>

Die getStaticPaths-Funktion mappt jede Locale auf einen statischen Pfad; lang kommt als Route-Param an. Reine Compile-Zeit-Arbeit — keine serverseitige Runtime, keine Hydration-Erwägungen.

Der Language Picker

Astros i18n-Rezept in den Docs zeigt einen einfachen Picker, der das Locale-Prefix in der aktuellen URL austauscht:

---
// src/components/LanguagePicker.astro
import { languages, type Lang } from '../i18n/ui'

interface Props {
  currentLang: Lang
}
const { currentLang } = Astro.props

function pathForLang (target: Lang): string {
  const { pathname } = Astro.url
  const parts = pathname.split('/')
  // "/en/foo/bar" → ["", "en", "foo", "bar"]
  parts[1] = target
  return parts.join('/')
}
---

<nav>
  <ul>
    {Object.entries(languages).map(([lang, label]) => (
      <li>
        {lang === currentLang ? (
          <strong>{label}</strong>
        ) : (
          <a href={pathForLang(lang as Lang)}>{label}</a>
        )}
      </li>
    ))}
  </ul>
</nav>

Legen Sie ihn in src/layouts/Layout.astro ab und er ist auf jeder Seite verfügbar.

Drei Wege, neue Schlüssel in Locize zu bekommen

Astro ist static-by-default — Seiten werden vorgerendert, daher ist ein Runtime-saveMissing-Push von Produktionsnutzern ohne einen SSR-Adapter nicht möglich, und Astro rät ohnehin von Schreibzugriffen aus der Static-Schicht ab. Stattdessen gibt es drei praktikable Pfade:

  1. npm run syncLocales (in package.json verdrahtet, das --dry=true-Flag entfernen, um tatsächlich zu pushen). locize-cli liest Ihre lokalen src/i18n/locales/{lng}/{ns}.json und lädt alle Schlüssel hoch, die noch nicht in Locize sind. Manuell oder CI-getrieben, kein Write-Key im Browser.
  2. Statische Extraktion via i18next-cli oder Ähnlichem — scannt Ihren .astro-Quellcode nach t('…')-Aufrufen, schreibt neue Schlüssel in das lokale JSON und synchronisiert dann wie oben hoch.
  3. Die Locize-Web-App — Schlüssel direkt im Editor hinzufügen und vor dem nächsten Build mit downloadLocales herunterladen.

Wählen Sie, was zu Ihrem Workflow passt. Keiner dieser Pfade erfordert, einen Write-API-Key in Ihrem Build-Artefakt auszuliefern.

Was diese Form bewusst nicht enthält

Drei Dinge, die Sie aus den Nuxt- oder React-Router-v7-Walkthroughs erwarten könnten, fehlen hier bewusst:

  • Kein Runtime-saveMissing. Siehe oben — Astro-Static-Builds können nicht pushen, und eine SSR-Astro-App würde dies aus einer Island heraus tun, nicht aus der Static-Schicht.
  • Kein InContext-Editor auf statischen Seiten. Der locize-Editor benötigt ein DOM, das sich neu rendert, wenn der Editor einen String aktualisiert. Astros Static-Output hat diesen Hook nicht.
  • Kein Live-CDN-Fetch. Übersetzungen werden zur Build-Zeit gebündelt. Um eine frische Übersetzung auszuliefern, führen Sie npm run downloadLocales aus und redeployen. Das passt zu Astros Static-First-Modell und hält das Build-Artefakt in sich geschlossen.

Alle drei dieser Punkte funktionieren innerhalb von Framework-Islands. Mounten Sie eine React-, Vue-, Svelte-, Solid- oder Preact-Island via der passenden @astrojs/<framework>-Integration und nutzen Sie in dieser Island i18next-locize-backend + das locize-Paket direkt. Die Astro-Static-Schicht erledigt das Routing + die Page-Chrome; die Islands übernehmen die Teile, die eine Runtime benötigen.

Wann eine Framework-Island sinnvoll ist

Die rein statische Integration deckt die meisten Content-Sites ab — Blogs, Marketing-Pages, Docs, Landing-Pages. Fügen Sie eine Framework-Island hinzu, wenn Sie Folgendes benötigen:

  • Live-Übersetzungs-Updates ohne Redeployment. Nutzen Sie i18next-locize-backend innerhalb der Island, um Übersetzungen bei jedem Besuch (oder pro Route, wenn Sie cachen) vom Locize-CDN zu holen.
  • InContext-Editing für Übersetzer. Das ?incontext=true-Overlay des locize-Pakets benötigt eine Runtime, die bei String-Änderungen neu rendert — nur innerhalb einer hydrierten Island verfügbar.
  • Runtime-saveMissing. Dieselbe Einschränkung — eine hydrierte Island kann neue Schlüssel pushen; statisches HTML kann das nicht.

Das Muster sieht so aus (für eine React-Island):

---
// src/pages/[lang]/interactive.astro
import Layout from '../../layouts/Layout.astro'
import I18nIsland from '../../components/I18nIsland.tsx'
---

<Layout lang={Astro.currentLocale}>
  <I18nIsland client:load lang={Astro.currentLocale} />
</Layout>
// src/components/I18nIsland.tsx
import i18next from 'i18next'
import Backend from 'i18next-locize-backend'
import { initReactI18next, useTranslation } from 'react-i18next'

i18next
  .use(Backend)
  .use(initReactI18next)
  .init({
    fallbackLng: 'en',
    saveMissing: import.meta.env.DEV,
    backend: {
      projectId: import.meta.env.PUBLIC_LOCIZE_PROJECT_ID,
      apiKey: import.meta.env.DEV ? import.meta.env.LOCIZE_API_KEY : undefined,
      version: 'latest'
    }
  })

export default function I18nIsland ({ lang }: { lang: string }) {
  const { t, i18n } = useTranslation()
  if (i18n.language !== lang) i18n.changeLanguage(lang)
  return <h2>{t('island.heading')}</h2>
}

client:load weist Astro an, diese Komponente sofort beim Page-Load zu hydrieren; Sie können client:visible nutzen, um die Hydration zu verzögern, bis die Island in den Viewport gelangt. Die genaue Konfiguration von i18next-locize-backend ist im React-Router-v7-Walkthrough dokumentiert — dasselbe Backend-Wiring funktioniert in jedem React-/Vue-/Svelte-/Solid-Kontext, auch innerhalb einer Astro-Island.

🎉 Glückwunsch

Ein funktionierendes Astro 6 + Locize-Setup gibt Ihnen:

  • Static-First-i18n ohne Flash unübersetzter Inhalte (alles wird pro Locale vorgerendert)
  • SEO-sauberes URL-Prefixing via Astros eingebautem i18n-Routing
  • Build-Zeit-Übersetzungs-Sync via locize-cli — serverless-freundlich, kein per-Request-CDN-Hop
  • Typsicheres t() mit Autovervollständigung auf Übersetzungs-Schlüsseln
  • Einen Ausstiegspfad zu dynamischen Islands, wenn Sie Runtime-Updates oder InContext-Editing benötigen

🧑‍💻 Der vollständige Code: github.com/locize/locize-astro-example.

Die Gründer von Locize sind auch die Macher von i18next — durch die Nutzung von Locize unterstützen Sie direkt die Zukunft von i18next.

Siehe auch