Paraglide JS Adapter NextJS

Paraglide JS Adapter NextJS

A NextJS integration for ParaglideJS, providing you with everything you need for i18n routing

Dead Simple i18n. Typesafe, Small Footprint, SEO-Friendly and IDE Integration.

#Getting Started

Get started instantly with the Paraglide-Next CLI.

npx @inlang/paraglide-js-adapter-next init
npm install

The CLI will ask you which languages you want to support. This can be changed later.

It will:

  • Create an Inlang Project
  • Create translation files for each of your languages
  • Create a middleware file
  • Create lib/i18n.ts file
  • Update your next.config.js file to use the Paraglide-Next Plugin.
  • Add the <LanguageProvider> wrapper to your app/layout.tsx component.

You can now start your dev-server and visit /de, /ar, or whatever languages you've set up.

#Adding Messages

Your messages live in messages/{languageTag}.json files. You can add messages in these files as key-value pairs of the message ID and the translations.

Use curly braces to add parameters.

// messages/en.json
	// The $schema key is automatically ignored
	"$schema": "",

	"hello_world" : "Hello World!",
	"greetings": "Greetings {name}."

Learn more about the format in the Inlang Message Format Documentation.

#Using Messages in Code

Use messages by importing them from @/paraglide/messages.js. By convention, we do a wildcard import as m.

import * as m from "@/paraglide/messages.js"

export function Home() {
	return (
			<p>{m.homepage_subtitle({ some: "param" })}</p>

Only messages used in client components are sent to the client. Messages in Server Components don't impact bundle size.

#Localized navigation APIs

While you can now visit /de/some-page you still need to add the language-prefix to every single link. Wouldn't it be nice if that happened automatically?

For this the package provides Localised Navigation APIs. These are exported from @/lib/i18n.js.

To get localized <Link>s you need to replace the ones from next/link with the ones from @/lib/i18n.js. Just find & replace the imports.

- import Link from "next/link"
+ import { Link } from "@/lib/i18n" 

// This now links to /de/about depending on the current language
<Link href="/about"> 

You can do the same for the other navigation APIs.

- import { usePathname, useRouter, redirect, permanentRedirect} from "next/navigation"
+ import { usePathname, useRouter, redirect, permanentRedirect} from "@/lib/i18n"

#Advanced Usage

#Translated Metadata

To return different metadata for each language, we will need to use generateMetadata.

export async function generateMetadata() {
	return {
		title: m.home_metadata_title(),
		description: m.home_metadata_description(),

If you were to use export const metadata your metadata would always end up in the source language.

#Linking to Pages in Other Languages

If you want a Link to be in a specific language you can use the locale prop.

<Link href="/about" locale="de">

This is convenient for constructing language switchers.

If you are using router.push to navigate you can pass locale as an option.

function Component() {
	const router = useRouter()
	return (
		<button onClick={() => router.push("/about", { locale: "de" })}>Go to German About page</button>

#Excluding certain routes from i18n

You can exclude certain routes from i18n using the exclude option on createI18n. You can either pass a string or a regex.

export const { ... } =
		 //array of routes to exclude
		exclude: [
			/^\/api(\/.*)?$/ //excludes all routes starting with /api
			"/admin" //excludes /admin, but not /admin/anything - globs are not supported

Excluded routes won't be prefixed with the language tag & the middleware will not add Link headers to them.

Tip: LLMs are really good at writing regexes.

#Changing the default language

By default, the default language is the sourceLanguageTag defined in project.inlang/settings.json. You can change it with the defaultLanguage option.

export const { ... } =
		defaultLanguage: "de"

#How language-detection works

The adapter follows these steps to determine the language.

  • First, the adapter will try to determine the language based on the URL.
  • If that fails, it will look for a NEXT_LOCALE cookie.
  • If that isn't available either, it will try to negotiate the language based on the Accept-Language header.
  • Finally, it will fall back to the default language.

If a language has been determined once, it will set the NEXT_LOCALE cookie so that future ambiguities don't result in random language switches.

#Translated Pathnames

You can use different pathnames for each language with the pathname option. Pathnames should not include a language prefix or the base path.

export const { ... } =
		pathname: {
			"/about": {
				en: "/about",
				de: "/ueber-uns"

You can use parameters in pathnames with square brackets. You have to use an identical set of parameters in both the canonical and translated pathnames.

You can use double-square brackets for optional parameters and the spread operator to make it a match-all parameter.

pathname: {
	"/articles/[slug]": {
		en: "/articles/[slug]",
		de: "/artikel/[slug]"
	"/admin/[]": {
		en: "/administration/[]",
		de: "/admin/[]"

You can also use a message as a pathname. The translation will be used as the pathname. You can use parameters here too.

// messages/en.json
	"about_pathname": "/about"
// messages/de.json
	"about_pathname": "/ueber-uns"
export const { ... } =
		pathname: {
			"/about": m.about_pathname //pass as reference

Be careful when using translated pathnames in combination with prefix: "never". Links may not work if they are shared between people with different languages.

#Getting a localized Pathname

There are situations where you need to know the localized version of a pathname. You can use the localizePathname function for that.

import { localizePathname } from "@/lib/i18n"
localizePathname("/about", "de") // de/ueber-uns

This does not include the basePath.

Search engines like Google expect you to tell them about translated versions of your pages. The adapter does this by default by adding the Link Header to requests.

You don't need to add the translated versions of your site to your sitemap, although it doesn't hurt if you do.

#Right-to-Left Support

Define a map of all languages to their text-direction & index into it.

import { languageTag, type AvailableLanguageTag } from "@/paraglide/runtime.js"

// This is fully type-safe & forces you to keep it up-to-date
const direction: Record<AvailableLanguageTag, "rtl" | "ltr"> = {
	en: "ltr",
	ar: "rtl",

// src/app/layout.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
	return (
			<html lang={languageTag()} dir={direction[languageTag()]}>

We discourage using the Intl.Locale API for text-direction as it's still poorly supported

#(Legacy) Setup With the Pages Router

The Pages router already comes with i18n support out of the box. Thus, Paraglide doesn't need to provide routing. All the Adapter does in the Pages router is react to the language change & run the compiler.

In next.config.js, add the paraglide plugin.

const { paraglide } = require("@inlang/paraglide-js-adapter-next/plugin")
module.exports = paraglide({
	paraglide: {
		project: "./project.inlang",
		outdir: "./src/paraglide",

Then add an i18n object and specify the locales you want to support. Make sure these match the ones in your project.inlang/settings.json file.

module.exports = paraglide({
	paraglide: {
		project: "./project.inlang",
		outdir: "./src/paraglide",
	i18n: {
		locales: ["en", "de"],
		defaultLocale: "en",

NextJS will automatically prefix all routes with the locale. For example, the route /about will become /en/about for the English locale and /de/about for the German locale. Only the default locale won't be prefixed.

Finally, wrap your _app.js file with the ParaglideJS component.

import { ParaglideJS } from "@inlang/paraglide-js-adapter-next/pages"

export default function App({ Component, pageProps }: AppProps) {
	return (
			<Component {...pageProps} />

You can now use Paraglide's messages in your components.

import * as m from "@/paraglide/messages.js"

export default function Home() {
	return (

Don't forget to set the lang attribute on the html element in src/pages/_document.js.

import { languageTag } from "@/paraglide/runtime"
import { Html, Head, Main, NextScript } from "next/document"

export default function Document() {
	return <Html lang={languageTag()}>...</Html>

#Known Limitations

There are some known limitations with this adapter:

  • output: static isn't supported yet.
  • Evaluating messages in module scope always renders the source language.
  • Server actions that aren't inside a .tsx file will always read the default language unless setLanguageTag(()=>headers().get("x-language-tag")) is called at the top of the file.

#Roadmap to 1.0

  • Static Export support
  • Simplify Setup
  • Cookie & Domain Routing Strategies


You can find example projects in our GitHub repository, or try them on StackBlitz: