Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

Several languages: an overview

Internationalisation (i18n) is preparing a site to work in several languages. The App Router has no built-in language switching: you assemble it from familiar parts — a dynamic segment, translation files and the proxy (proxy.ts). Here is the general scheme.

The language in the address

The language becomes the first segment of the address: /ru/posts, /en/posts. For that, all pages move into an app/[lang] folder:

app/
└── [lang]/
    ├── layout.tsx
    ├── page.tsx           /ru, /en
    └── posts/
        └── page.tsx       /ru/posts, /en/posts

Now every page receives lang in params.

Dictionaries

Translations are kept in JSON files, one per language:

{
  "posts": "Posts",
  "newPost": "New post"
}

This is dictionaries/en.json; dictionaries/ru.json has the same keys with Russian text. The function that loads a dictionary:

// lib/dictionaries.ts
import 'server-only';
 
const dictionaries = {
  ru: () => import('@/dictionaries/ru.json').then((module) => module.default),
  en: () => import('@/dictionaries/en.json').then((module) => module.default),
};
 
export type Locale = keyof typeof dictionaries;
 
export async function getDictionary(locale: Locale) {
  return dictionaries[locale]();
}

The server-only package (installed with npm install server-only) guarantees that the file never reaches the browser.

A translation on a page

// app/[lang]/posts/page.tsx
import { getDictionary, type Locale } from '@/lib/dictionaries';
 
export default async function PostsPage({ params }: { params: Promise<{ lang: Locale }> }) {
  const { lang } = await params;
  const t = await getDictionary(lang);
 
  return <h1>{t.posts}</h1>;
}

/en/posts shows "Posts", and /ru/posts shows the Russian word. The lang attribute of the html tag in the app/[lang]/layout.tsx layout also comes from params.

Choosing the default language

An address without a language, such as /posts, is handled by the proxy: it reads the accept-language header, picks a suitable language and redirects, for example to /en/posts. If the browser's language is not supported, the default language is used.

Ready-made libraries

The manual scheme is fine for a couple of languages and simple strings. For plural forms ("1 post", "5 posts"), dates, numbers and a language switcher, the next-intl library is more convenient — it is built on the same principles but does a lot for you.

Common mistake

Forgetting to check the lang value. The address /de/posts also lands in the [lang] folder, and dictionaries['de'] turns out undefined — the page crashes. Check the language against the list of supported ones and call notFound() if it is not there.

Thanks for reading!

Donate to the site — we work hard for you.