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 an — locize-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: truefür einheitliche URLs. - Nutzen Sie dynamische
[lang]/-Page-Routes mitgetStaticPaths, damit jede Locale statisch vorgerendert wird. - Synchronisieren Sie Übersetzungen zur Build-Zeit über den
download-Befehl vonlocize-cliin die App. Kein Runtime-CDN-Hop, keine SSR-Erwägungen. - Schreiben Sie einen fünfzeiligen
useTranslations(lang)-Helper insrc/i18n/utils.ts. Astro liefert bewusst keint(); 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-saveMissingaus 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 Siei18next-locize-backend+ daslocize-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-clilädt das aktuelle veröffentlichte JSON vom Locize-CDN nachsrc/i18n/locales/{lng}/{ns}.json. Die committeten JSON-Dateien werden zum Module-Load-Zeitpunkt direkt vonsrc/i18n/ui.tsimportiert 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 downloadLocalesaus 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-appDie relevante Verzeichnisstruktur, nachdem wir i18n + Locize verdrahtet haben:
my-app/
├── astro.config.mjs — Astro'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.jsonAbhängigkeiten installieren
npm install astro
npm install --save-dev locize-cliDas 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/secondusw. Astro.currentLocalesteht kostenlos in jeder Page/Komponente zur Verfügung.- Browser-Sprach-Erkennung via
Astro.preferredLocaleundAstro.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.appist BunnyCDN-gestützt, kostenlos für großzügige monatliche Download-Volumen, nur öffentlich (Default für neue Projekte); Pro unterapi.locize.appist CloudFront-gestützt, kostenpflichtig, unterstützt Private Downloads + individuelle Cache-Steuerung. Setzen Sie--cdn-typeim 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:
npm run syncLocales(inpackage.jsonverdrahtet, das--dry=true-Flag entfernen, um tatsächlich zu pushen).locize-cliliest Ihre lokalensrc/i18n/locales/{lng}/{ns}.jsonund lädt alle Schlüssel hoch, die noch nicht in Locize sind. Manuell oder CI-getrieben, kein Write-Key im Browser.- Statische Extraktion via
i18next-clioder Ähnlichem — scannt Ihren.astro-Quellcode nacht('…')-Aufrufen, schreibt neue Schlüssel in das lokale JSON und synchronisiert dann wie oben hoch. - Die Locize-Web-App — Schlüssel direkt im Editor hinzufügen und vor dem nächsten Build mit
downloadLocalesherunterladen.
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 downloadLocalesaus 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-backendinnerhalb 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 deslocize-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
- Wie man eine Nuxt-4-App mit @nuxtjs/i18n und Locize internationalisiert — Vue-seitiges Gegenstück mit reichhaltigerer Integration (vue-i18n, InContext-Editor, Runtime-saveMissing)
- Wie man eine React-Router-v7-App mit remix-i18next internationalisiert — React-seitiges Gegenstück mit Live-CDN-Fetch via
i18next-locize-backend - Astros eingebautes i18n-Routing — die Routing-Primitive, auf der dieser Walkthrough aufbaut
- Astro-i18n-Rezept — das Build-it-yourself-Muster, in das Locize sich einklinkt
locize-cli— das Build-Zeit-Sync-Tool