227 lines
8.1 KiB
Markdown
227 lines
8.1 KiB
Markdown
|
|
---
|
|||
|
|
title: Internationalization
|
|||
|
|
description: Add support for multiple languages with internationalized routing and localized content.
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
Next.js enables you to configure the routing and rendering of content to support multiple languages. Making your site adaptive to different locales includes translated content (localization) and internationalized routes.
|
|||
|
|
|
|||
|
|
## Terminology
|
|||
|
|
|
|||
|
|
- **Locale:** An identifier for a set of language and formatting preferences. This usually includes the preferred language of the user and possibly their geographic region.
|
|||
|
|
- `en-US`: English as spoken in the United States
|
|||
|
|
- `nl-NL`: Dutch as spoken in the Netherlands
|
|||
|
|
- `nl`: Dutch, no specific region
|
|||
|
|
|
|||
|
|
## Routing Overview
|
|||
|
|
|
|||
|
|
It’s recommended to use the user’s language preferences in the browser to select which locale to use. Changing your preferred language will modify the incoming `Accept-Language` header to your application.
|
|||
|
|
|
|||
|
|
For example, using the following libraries, you can look at an incoming `Request` to determine which locale to select, based on the `Headers`, locales you plan to support, and the default locale.
|
|||
|
|
|
|||
|
|
```js filename="proxy.js"
|
|||
|
|
import { match } from '@formatjs/intl-localematcher'
|
|||
|
|
import Negotiator from 'negotiator'
|
|||
|
|
|
|||
|
|
let headers = { 'accept-language': 'en-US,en;q=0.5' }
|
|||
|
|
let languages = new Negotiator({ headers }).languages()
|
|||
|
|
let locales = ['en-US', 'nl-NL', 'nl']
|
|||
|
|
let defaultLocale = 'en-US'
|
|||
|
|
|
|||
|
|
match(languages, locales, defaultLocale) // -> 'en-US'
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Routing can be internationalized by either the sub-path (`/fr/products`) or domain (`my-site.fr/products`). With this information, you can now redirect the user based on the locale inside [Proxy](/docs/app/api-reference/file-conventions/proxy).
|
|||
|
|
|
|||
|
|
```js filename="proxy.js"
|
|||
|
|
import { NextResponse } from "next/server";
|
|||
|
|
|
|||
|
|
let locales = ['en-US', 'nl-NL', 'nl']
|
|||
|
|
|
|||
|
|
// Get the preferred locale, similar to the above or using a library
|
|||
|
|
function getLocale(request) { ... }
|
|||
|
|
|
|||
|
|
export function proxy(request) {
|
|||
|
|
// Check if there is any supported locale in the pathname
|
|||
|
|
const { pathname } = request.nextUrl
|
|||
|
|
const pathnameHasLocale = locales.some(
|
|||
|
|
(locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
if (pathnameHasLocale) return
|
|||
|
|
|
|||
|
|
// Redirect if there is no locale
|
|||
|
|
const locale = getLocale(request)
|
|||
|
|
request.nextUrl.pathname = `/${locale}${pathname}`
|
|||
|
|
// e.g. incoming request is /products
|
|||
|
|
// The new URL is now /en-US/products
|
|||
|
|
return NextResponse.redirect(request.nextUrl)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export const config = {
|
|||
|
|
matcher: [
|
|||
|
|
// Skip all internal paths (_next)
|
|||
|
|
'/((?!_next).*)',
|
|||
|
|
// Optional: only run on root (/) URL
|
|||
|
|
// '/'
|
|||
|
|
],
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Finally, ensure all special files inside `app/` are nested under `app/[lang]`. This enables the Next.js router to dynamically handle different locales in the route, and forward the `lang` parameter to every layout and page. For example:
|
|||
|
|
|
|||
|
|
```tsx filename="app/[lang]/page.tsx" switcher
|
|||
|
|
// You now have access to the current locale
|
|||
|
|
// e.g. /en-US/products -> `lang` is "en-US"
|
|||
|
|
export default async function Page({ params }: PageProps<'/[lang]'>) {
|
|||
|
|
const { lang } = await params
|
|||
|
|
return ...
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
```jsx filename="app/[lang]/page.js" switcher
|
|||
|
|
// You now have access to the current locale
|
|||
|
|
// e.g. /en-US/products -> `lang` is "en-US"
|
|||
|
|
export default async function Page({ params }) {
|
|||
|
|
const { lang } = await params
|
|||
|
|
return ...
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> **Good to know:** `PageProps` and `LayoutProps` are globally available TypeScript helpers that provide strong typing for route parameters. See [PageProps](/docs/app/api-reference/file-conventions/page#page-props-helper) and [LayoutProps](/docs/app/api-reference/file-conventions/layout#layout-props-helper) for more details.
|
|||
|
|
|
|||
|
|
The root layout can also be nested in the new folder (e.g. `app/[lang]/layout.js`).
|
|||
|
|
|
|||
|
|
## Localization
|
|||
|
|
|
|||
|
|
Changing displayed content based on the user’s preferred locale, or localization, is not something specific to Next.js. The patterns described below would work the same with any web application.
|
|||
|
|
|
|||
|
|
Let’s assume we want to support both English and Dutch content inside our application. We might maintain two different “dictionaries”, which are objects that give us a mapping from some key to a localized string. For example:
|
|||
|
|
|
|||
|
|
```json filename="dictionaries/en.json"
|
|||
|
|
{
|
|||
|
|
"products": {
|
|||
|
|
"cart": "Add to Cart"
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
```json filename="dictionaries/nl.json"
|
|||
|
|
{
|
|||
|
|
"products": {
|
|||
|
|
"cart": "Toevoegen aan Winkelwagen"
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
We can then create a `getDictionary` function to load the translations for the requested locale:
|
|||
|
|
|
|||
|
|
```ts filename="app/[lang]/dictionaries.ts" switcher
|
|||
|
|
import 'server-only'
|
|||
|
|
|
|||
|
|
const dictionaries = {
|
|||
|
|
en: () => import('./dictionaries/en.json').then((module) => module.default),
|
|||
|
|
nl: () => import('./dictionaries/nl.json').then((module) => module.default),
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export type Locale = keyof typeof dictionaries
|
|||
|
|
|
|||
|
|
export const hasLocale = (locale: string): locale is Locale =>
|
|||
|
|
locale in dictionaries
|
|||
|
|
|
|||
|
|
export const getDictionary = async (locale: Locale) => dictionaries[locale]()
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
```js filename="app/[lang]/dictionaries.js" switcher
|
|||
|
|
import 'server-only'
|
|||
|
|
|
|||
|
|
const dictionaries = {
|
|||
|
|
en: () => import('./dictionaries/en.json').then((module) => module.default),
|
|||
|
|
nl: () => import('./dictionaries/nl.json').then((module) => module.default),
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export const hasLocale = (locale) => locale in dictionaries
|
|||
|
|
|
|||
|
|
export const getDictionary = async (locale) => dictionaries[locale]()
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Given the currently selected language, we can fetch the dictionary inside of a layout or page.
|
|||
|
|
|
|||
|
|
Since `lang` is typed as `string`, using `hasLocale` narrows the type to your supported locales. It also ensures a 404 is returned if a translation is missing, rather than a runtime error.
|
|||
|
|
|
|||
|
|
```tsx filename="app/[lang]/page.tsx" switcher
|
|||
|
|
import { notFound } from 'next/navigation'
|
|||
|
|
import { getDictionary, hasLocale } from './dictionaries'
|
|||
|
|
|
|||
|
|
export default async function Page({ params }: PageProps<'/[lang]'>) {
|
|||
|
|
const { lang } = await params
|
|||
|
|
|
|||
|
|
if (!hasLocale(lang)) notFound()
|
|||
|
|
|
|||
|
|
const dict = await getDictionary(lang)
|
|||
|
|
return <button>{dict.products.cart}</button> // Add to Cart
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
```jsx filename="app/[lang]/page.js" switcher
|
|||
|
|
import { notFound } from 'next/navigation'
|
|||
|
|
import { getDictionary, hasLocale } from './dictionaries'
|
|||
|
|
|
|||
|
|
export default async function Page({ params }) {
|
|||
|
|
const { lang } = await params
|
|||
|
|
|
|||
|
|
if (!hasLocale(lang)) notFound()
|
|||
|
|
|
|||
|
|
const dict = await getDictionary(lang)
|
|||
|
|
return <button>{dict.products.cart}</button> // Add to Cart
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Because all layouts and pages in the `app/` directory default to [Server Components](/docs/app/getting-started/server-and-client-components), we do not need to worry about the size of the translation files affecting our client-side JavaScript bundle size. This code will **only run on the server**, and only the resulting HTML will be sent to the browser.
|
|||
|
|
|
|||
|
|
## Static Rendering
|
|||
|
|
|
|||
|
|
To generate static routes for a given set of locales, we can use `generateStaticParams` with any page or layout. This can be global, for example, in the root layout:
|
|||
|
|
|
|||
|
|
```tsx filename="app/[lang]/layout.tsx" switcher
|
|||
|
|
export async function generateStaticParams() {
|
|||
|
|
return [{ lang: 'en-US' }, { lang: 'de' }]
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export default async function RootLayout({
|
|||
|
|
children,
|
|||
|
|
params,
|
|||
|
|
}: LayoutProps<'/[lang]'>) {
|
|||
|
|
return (
|
|||
|
|
<html lang={(await params).lang}>
|
|||
|
|
<body>{children}</body>
|
|||
|
|
</html>
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
```jsx filename="app/[lang]/layout.js" switcher
|
|||
|
|
export async function generateStaticParams() {
|
|||
|
|
return [{ lang: 'en-US' }, { lang: 'de' }]
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export default async function RootLayout({ children, params }) {
|
|||
|
|
return (
|
|||
|
|
<html lang={(await params).lang}>
|
|||
|
|
<body>{children}</body>
|
|||
|
|
</html>
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Resources
|
|||
|
|
|
|||
|
|
- [Minimal i18n routing and translations](https://github.com/vercel/next.js/tree/canary/examples/i18n-routing)
|
|||
|
|
- [`next-intl`](https://next-intl.dev)
|
|||
|
|
- [`next-international`](https://github.com/QuiiBz/next-international)
|
|||
|
|
- [`next-i18n-router`](https://github.com/i18nexus/next-i18n-router)
|
|||
|
|
- [`paraglide-next`](https://inlang.com/m/osslbuzt/paraglide-next-i18n)
|
|||
|
|
- [`lingui`](https://lingui.dev)
|
|||
|
|
- [`tolgee`](https://tolgee.io/apps-integrations/next)
|
|||
|
|
- [`next-intlayer`](https://intlayer.org/doc/environment/nextjs)
|
|||
|
|
- [`gt-next`](https://generaltranslation.com/en/docs/next)
|