Tolgee Next.js Setup
In diesem Artikel erfährst du, wie du Tolgee in eine Next.js-App einbindest, die den App Router und next-intl
verwendet.
Die offizielle Anleitung von Tolgee findest du hier.
Ich habe das Setup an mehreren Stellen verbessert:
- Übersetzungen werden serverseitig über ein CDN geladen.
- Clientseitig greift das Next.js-Projekt direkt auf die Übersetzungsdateien zu – keine externen CDN-Requests.
- Fallback auf lokale Übersetzungen, falls Tolgee nicht erreichbar ist.
- Caching der Übersetzungen für bessere Performance.
👉 Beispielrepo: genaumann/next-tolgee-intl
👉 Demo Seite: next-tolgee-intl.vercel.app
Grundlagen
Sprachen
- Deutsch (
de
) – Standardsprache - Englisch (
en
)
Tolgee-Namespaces
common
(Standard)app
Voraussetzungen
- Next.js-Projekt mit App Router
- Vercel CLI mit verknüpftem Vercel-Projekt (optional)
- Tolgee-Projekt mit zwei Sprachen
- Aktiviertes Tolgee CDN
- API-Key für dein Tolgee-Projekt
Benötigte Scopes:keys.view
,translations.view
next-intl-Routing
next-intl
übernimmt das Routing.
de
ist die Standardsprache:
- Deutsch:
/contact
- Englisch:
/en/contact
Installation
npm install next-intl @tolgee/react @tolgee/core @tolgee/web
npm install -D tsx dotenv-cli
Umgebungsvariablen
Füge die folgenden Variablen in .env.local
ein:
NEXT_PUBLIC_TOLGEE_API_KEY=<dein-tolgee-api-key>
NEXT_PUBLIC_TOLGEE_API_URL=https://api.tolgee.io
TOLGEE_CDN_URL=https://cdn.tolg.ee/<id>
NEXT_PUBLIC
-Variablen sind im Browser verfügbar.
Stelle sicher, dass sie nur in Entwicklungsumgebungen gesetzt sind.
vercel env add NEXT_PUBLIC_TOLGEE_API_KEY development
vercel env add NEXT_PUBLIC_TOLGEE_API_URL development
vercel env add TOLGEE_CDN_URL
Tolgee-Einrichtung
Fetch-Plugin
Das Fetch-Plugin kapselt den HTTP-Abruf der Übersetzungen.
Es nutzt serverseitig fetch()
und fällt bei Bedarf auf lokale JSON-Dateien zurück.
'use server'
type FetchI18nParams = {
isCdn: boolean
namespace?: string
language: string
}
export const fetchI18n = async ({
namespace,
language,
isCdn
}: FetchI18nParams): Promise<Record<string, string> | null> => {
const origin =
process.env.VERCEL_ENV === 'production'
? `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL || ''}`
: process.env.VERCEL_ENV === 'preview'
? `https://${process.env.VERCEL_BRANCH_URL}`
: 'http://localhost:3000'
const cdn = process.env.TOLGEE_CDN_URL
const url = isCdn
? `${cdn}/${namespace ? `${namespace}/` : ''}${language}.json`
: `${origin}/i18n/${namespace ? `${namespace}/` : ''}${language}.json`
const result = await fetch(url, {
next: {
revalidate: 3600, // cache for 1 hour
tags: ['i18n']
}
})
if (!result.ok) {
return null
}
return await result.json()
}
Plugin-Wrapper
Dieses Wrapper-Modul bindet das Fetch-Plugin als Tolgee-Plugin ein und exportiert es für Client und Server.
import type {TolgeePlugin, BackendMiddleware} from '@tolgee/core'
import {fetchI18n} from './fetch'
type LoaderFn = (params: {
namespace?: string
language: string
}) => Promise<Record<string, string> | undefined> | Record<string, string>
interface BackendOptions {
loader: LoaderFn
}
export function CreateFunctionBackend({loader}: BackendOptions): TolgeePlugin {
return (tolgee, tools) => {
const backend: BackendMiddleware = {
async getRecord({namespace, language}) {
try {
const data = await loader({namespace, language})
if (!data || typeof data !== 'object') {
throw new Error('Loader function did not return a valid object')
}
return data
} catch (error) {
console.error('Error in Tolgee backend loader:', error)
throw error
}
}
}
tools.addBackend(backend)
return tolgee
}
}
type FetchTolgeeParams = {
namespace?: string
language: string
}
export const fetchTolgee = async ({
namespace,
language
}: FetchTolgeeParams): Promise<Record<string, string>> => {
const cdnData = await fetchI18n({
namespace,
language,
isCdn: true
})
if (!cdnData) {
return (
(await fetchI18n({
namespace,
language,
isCdn: false
})) || {}
)
}
return cdnData
}
Shared Tolgee Setup
Zentrale Factory, die die Tolgee-Instanz mit allen Optionen (API-URL, Sprachen, Plugins etc.) erstellt.
So vermeiden wir doppelten Code zwischen Client und Server.
import {DevTools, Tolgee, FormatSimple} from '@tolgee/web'
import {CreateFunctionBackend, fetchTolgee} from './plugin'
export enum LOCALES {
de = 'de',
en = 'en'
}
export const NAMESPACES = ['common', 'app']
export function TolgeeBase() {
const tolgee = Tolgee()
.use(FormatSimple()) // Super light formatter, which will enable you to pass variables into translations.
.use(DevTools()) // automatically omitted in production builds - useful in development
.use(CreateFunctionBackend({loader: fetchTolgee})) // custom backend loader
if (process.env.NODE_ENV === 'development') {
const apiKey = process.env.NEXT_PUBLIC_TOLGEE_API_KEY
const apiUrl = process.env.NEXT_PUBLIC_TOLGEE_API_URL
tolgee.updateDefaults({
// add api key and api url
apiKey,
apiUrl
})
}
return tolgee
}
Tolgee Client
Reaktiver Client-Wrapper: Exportiert den React-Context und initialisiert Tolgee im Browser.
Hier wird außerdem das In-Memory-Caching aktiviert, um wiederholte Netzwerkaufrufe zu sparen.
'use client'
import {ReactNode, useEffect} from 'react'
import {
CachePublicRecord,
TolgeeProvider,
TolgeeStaticData
} from '@tolgee/react'
import {useRouter} from 'next/navigation'
import {TolgeeBase} from './base'
type Props = {
staticData: TolgeeStaticData | CachePublicRecord[]
language: string
children: ReactNode
}
const tolgee = TolgeeBase().init({
defaultNs: 'common'
})
export const TolgeeNextProvider = ({language, staticData, children}: Props) => {
const router = useRouter()
useEffect(() => {
const {unsubscribe} = tolgee.on('permanentChange', () => {
router.refresh()
})
return () => unsubscribe()
}, [tolgee, router])
return (
<TolgeeProvider tolgee={tolgee} ssr={{language, staticData}}>
{children}
</TolgeeProvider>
)
}
Tolgee Server
Die Datei exportiert initTolgeeServer()
für serverseitige Routen / RSC.
Außerdem enthält sie eine überarbeitete getTranslate(namespace?)
-Hilfsfunktion, sodass du Namespaces direkt angeben kannst.
import {getLocale} from 'next-intl/server'
import {
CombinedOptions,
createServerInstance,
DefaultParamType,
TranslationKey
} from '@tolgee/react/server'
import {TolgeeBase} from './base'
export const {getTolgee, T} = createServerInstance({
getLocale: getLocale,
createTolgee: async language => {
return TolgeeBase().init({
observerOptions: {
fullKeyEncode: true
},
defaultNs: 'common',
language
})
}
})
export const getTranslate = async (
ns?: string,
opts?: CombinedOptions<DefaultParamType>
) => {
const tolgee = await getTolgee()
if (ns) {
await tolgee.addActiveNs(ns)
}
return ns
? (key: TranslationKey, options?: CombinedOptions<DefaultParamType>) =>
tolgee.t(key, {ns, ...opts, ...options})
: (key: TranslationKey, options?: CombinedOptions<DefaultParamType>) =>
tolgee.t(key, {...opts, ...options})
}
Tolgee Provider im Root-Layout
Der Provider macht den Tolgee-Context im gesamten Client-Baum verfügbar und synchronisiert neue Keys on-the-fly.
import {ReactNode} from 'react'
import {notFound} from 'next/navigation'
import {TolgeeNextProvider} from '@/lib/tolgee/client'
import {getTolgee} from '@/lib/tolgee/server'
import {LOCALES} from '@/lib/tolgee/base'
import '@/app/globals.css'
type Props = {
children: ReactNode
params: Promise<{locale: LOCALES}>
}
export default async function LocaleLayout({children, params}: Props) {
const {locale} = await params
if (!locale || !Object.keys(LOCALES).includes(locale)) {
notFound()
}
const tolgee = await getTolgee()
const records = await tolgee.loadRequired() // load default common namespace
return (
<html lang={locale}>
<body>
<TolgeeNextProvider language={locale} staticData={records}>
{children}
</TolgeeNextProvider>
</body>
</html>
)
}
next-intl-Einrichtung
Request-Konfiguration
Diese Factory bestimmt anhand des Requests, welche Locale aktiv ist.
messages
bleibt leer, weil Tolgee die Übersetzungen liefert.
import {getRequestConfig} from 'next-intl/server'
import {LOCALES} from '@/lib/tolgee/base'
export default getRequestConfig(async ({requestLocale}) => {
const locale = await requestLocale
return {
locale: locale || LOCALES.de,
messages: {}
}
})
Routing-Konfiguration
Definiert alle verfügbaren Locales, den Standard-Locale und optional Redirect-Verhalten.
import {defineRouting} from 'next-intl/routing'
import {createNavigation} from 'next-intl/navigation'
import {LOCALES} from '@/lib/tolgee/base'
export const routing = defineRouting({
locales: Object.keys(LOCALES),
defaultLocale: LOCALES.de,
localePrefix: 'as-needed'
})
export const {Link, redirect, usePathname, useRouter, getPathname} =
createNavigation(routing)
Middleware
Fängt jede eingehende Anfrage ab, liest das Locale-Segment (/en/...
) und injiziert es in den Request-Context von next-intl
.
import createMiddleware from 'next-intl/middleware'
import {routing} from '@/i18n/routing'
export default createMiddleware(routing)
export const config = {
// Skip all paths that should not be internationalized
matcher: ['/((?!api|_next|.*\\..*).*)']
}
Next.js-Konfiguration
Bindet die next-intl
-Einstellungen in das Next-Config-Objekt ein, aktiviert typed routes und opt-in-features.
import {NextConfig} from 'next'
import createNextIntlPlugin from 'next-intl/plugin'
const nextConfig: NextConfig = {}
const withNextIntl = createNextIntlPlugin()
export default withNextIntl(nextConfig)
Build-Script
Skripte für CI/CD: Lädt alle Übersetzungen während des Builds aus dem Tolgee-CDN in public/i18n/...
.
Dadurch ist die App auch offline bzw. ohne CDN lauffähig.
import {LOCALES, NAMESPACES} from '@/lib/tolgee/base'
import fs from 'fs/promises'
import path from 'path'
const fetchTolgee = async () => {
const cdn = process.env.TOLGEE_CDN_URL
if (!cdn) {
throw new Error('TOLGEE_CDN_URL is not defined')
}
for (const locale of Object.values(LOCALES)) {
for (const ns of NAMESPACES) {
const localeDir = path.join(process.cwd(), 'public', 'i18n', ns)
await fs.mkdir(localeDir, {recursive: true})
const response = await fetch(`${cdn}/${ns}/${locale}.json`)
if (!response.ok) {
throw new Error(
`Failed to fetch ${ns} for ${locale}: ${response.statusText}`
)
}
const data = await response.json()
const filePath = path.join(localeDir, `${locale}.json`)
await fs.writeFile(filePath, JSON.stringify(data, null, 2))
console.log(`[${locale}:${ns}]: Fetched and saved translations`)
}
}
}
fetchTolgee()
package.json
{
"scripts": {
"prebuild": "tsx scripts/fetchTolgee.ts"
}
}
Verwendung
Clientseitig
Beispiel-RSC mit Client-Hook useTranslate()
– greift live auf den React-Context zu.
'use client'
import {useTranslate} from '@tolgee/react'
export default function Page() {
const {t} = useTranslate()
// const {t} = useTranslate('app') // app-Namespace
return (
<div className="flex flex-col h-full items-center justify-center gap-4">
<h1 className="text-6xl">{t('welcome')}</h1>
</div>
)
}
Serverseitig
Serverkomponente: Übersetzungen stehen schon vor dem Stream bereit, kein FOUC.
import {getTranslate} from '@/lib/tolgee/server'
export default async function Page() {
const t = await getTranslate()
// const t = await getTranslate('app') // app-Namespace
return (
<div className="flex flex-col h-full items-center justify-center gap-4">
<h1 className="text-6xl">{t('welcome')}</h1>
</div>
)
}