惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

T
Tenable Blog
H
Heimdal Security Blog
K
Kaspersky official blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
S
Schneier on Security
G
GRAHAM CLULEY
U
Unit 42
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
C
CERT Recently Published Vulnerability Notes
Google DeepMind News
Google DeepMind News
罗磊的独立博客
Stack Overflow Blog
Stack Overflow Blog
阮一峰的网络日志
阮一峰的网络日志
Simon Willison's Weblog
Simon Willison's Weblog
C
Cisco Blogs
Cyberwarzone
Cyberwarzone
T
The Exploit Database - CXSecurity.com
Project Zero
Project Zero
Security Archives - TechRepublic
Security Archives - TechRepublic
www.infosecurity-magazine.com
www.infosecurity-magazine.com
博客园 - 司徒正美
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
V
Visual Studio Blog
博客园 - Franky
Engineering at Meta
Engineering at Meta
WordPress大学
WordPress大学
Jina AI
Jina AI
P
Proofpoint News Feed
P
Proofpoint News Feed
有赞技术团队
有赞技术团队
L
LINUX DO - 最新话题
宝玉的分享
宝玉的分享
N
News and Events Feed by Topic
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
博客园 - 聂微东
T
The Blog of Author Tim Ferriss
Spread Privacy
Spread Privacy
Application and Cybersecurity Blog
Application and Cybersecurity Blog
IT之家
IT之家
S
Security Affairs
博客园 - 叶小钗
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
小众软件
小众软件
N
News | PayPal Newsroom
Cloudbric
Cloudbric
AWS News Blog
AWS News Blog
W
WeLiveSecurity
The Last Watchdog
The Last Watchdog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
NISL@THU
NISL@THU

Amon's Blog

我是一个 HR,碰到疑似“外星人”,居然让我帮他修理星际飞船?! 如何在 GitHub README 中插入视频?原来这么简单 How to embed a video into GitHub-README? So easy! 十年磨一剑,今朝更锃亮:把 Hexo blog + hexo-theme-next 博客升级到最新版本 一篇文章学会 Next.js 实现 i18n 国际化多语言(基于App Router) 《游子》 How to solve error on CentOS "/lib64/libstdc++.so.6 version GLIBCXX_3.4.xx not found" Hexo blog title include special symbols reports error Best Practices for Backend System Refactoring: How to do backend system refactoring efficiently and with high quality 《一个地方,人满为患》 《我》 How to solve error GitHub Permission denied fatal Could not read from remote repository 解决错误 GitHub Permission denied fatal Could not read from remote repository 《山 · 其二》 《山 · 其一》 《地铁》 写于11月27日 从优雅地查看K8s应用日志聊到日志管理 《中国折叠》
Simply way to support multiple languages i18n in Next.js 14 (Based on App Router)
Amon Xu · 2024-04-10 · via Amon's Blog

Introduction:

This post introduces the implementation of i18n internationalization multi-language feature in Next.js 14 (based on App Router), and takes into consideration actual scenarios to optimize and perfect the feature step-by-step. By reading this post, you will immediately get how to implement i18n in Next.js.

hero

Preface

In an era where the Internet world is becoming increasingly flattening, the importance of multi-language products is growing. Fortunately, Next.js allows us to quickly support multi-language with simple configurations and code. However, when we search for how Next.js supports multi-languages on the Internet, we might find a variety of implementations, jumbled information, and excessively clever solutions. Then we become confused and start to question: What’s the issue here?

Today, let’s implement multi-language from 0 to 1 in Next.js and unravel the mystery of multi-language.

We can refer to the i18n introduction in the Next.js official documentation here: https://nextjs.org/docs/app/building-your-application/routing/internationalization, which is quite clear and detailed. This article will be based on this documentation.

Before we begin, let’s take a look at the final running effect: https://next-i18n-demo-two.vercel.app/

Ready to work

First, We create a Next.js app,

1
npx create-next-app@latest

Plese select App Router, I am using TypeScript here.

1
2
3
4
5
6
7
8
❯ npx create-next-app@latest
✔ What is your project named? … `next-i18n-demo`
✔ Would you like to use TypeScript? … No / `Yes`
✔ Would you like to use ESLint? … No / `Yes`
✔ Would you like to use Tailwind CSS? … No / `Yes
✔ Would you like to use `src/` directory? … No / `Yes`
✔ Would you like to use App Router? (recommended) … No / `Yes`
✔ Would you like to customize the default import alias (@/*)? … `No` / Yes

Run locally,

1
npm run dev

Open http://localhost:3000 and you will see that it is running okay.

Internationalization introduction

Before we start it, let’s briefly introduce about internationalization. internationalization, aka i18n, this means supporting multiple languages, cultures, and customs in products, mainly including language, time, currency symbols, etc. This article will focus only on the language part.

In terms of internationalization, a common approach is for a website to default to a certain language’s official site (usually English), and to support the selection of language or region, allowing for a switch to different language versions of the site.

Specifically, some websites use a language abbreviation as a prefix, such as en.wikipedia.org, zh.wikipedia.org; some use it as a path suffix, such as aws.amazon.com/cn, aws.amazon.com/jp, and others distinguish based on the country or region domain name, such as apple.cn, apple.jp.

Among these, en, zh, cn, jp, etc., are language codes, which can vary slightly in different versions. You can refer to the reference materials at the end of the article for specifics.

In this article’s particular case, the ISO_3166 codes en and zh will be used to represent English and Chinese respectively.

Begin to configure Multi-Languages

The original file structure of the project was:

1
2
3
4
5
6
7
8
9
10
11
12
├── package.json
├── public
│   ├── next.svg
│   └── vercel.svg
├── src
│   └── app
│   ├── favicon.ico
│   ├── globals.css
│   ├── layout.tsx
│   └── page.tsx
├── tailwind.config.ts
└── tsconfig.json

We create a new folder named [lang] in the app directory, then move laytout.tsx and page.tsx from the app directory to [locales].

The file structure after moving is:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
├── package.json
├── postcss.config.mjs
├── public
│   ├── next.svg
│   └── vercel.svg
├── src
│   └── app
│   ├── [lang]
│   │   ├── layout.tsx
│   │   └── page.tsx
│   ├── favicon.ico
│   └── globals.css
├── tailwind.config.ts
└── tsconfig.json

Tips:

Please modify the reference position of globals.css in layout.tsx.

Next, we define json resource files for different languages, which you can put in your preferred file directory. I put it in public/dictionaries. The file format is as follows:

en.json

1
2
3
4
5
6
7
8
9
10
{
"page": {
"title": "Next.js i18n Demo",
"desc": "How to implement i18n with Next.js (based on App Router)"
},
"home": {
"title": "Hello, Next.js i18n",
"desc": "This is a demo of Next.js i18n"
}
}

zh.json

1
2
3
4
5
6
7
8
9
10
{
"page": {
"title": "Next.js i18n 示例",
"desc": "搞懂 Next.js 实现 i18n 国际化多语言(基于App Router)"
},
"home": {
"title": "你好, Next.js i18n",
"desc": "这是一个 Next.js i18n 示例"
}
}

Then, we create a file to load the multi-language resource files and get the corresponding language text.

Add dictionaries.js in the app/[lang] directory. Make sure the file directory and file name are correct and match.

1
2
3
4
5
6
7
8
import 'server-only'

const dictionaries = {
en: () => import('./dictionaries/en.json').then((module) => module.default),
zh: () => import('./dictionaries/zh.json').then((module) => module.default),
}

export const getDictionary = async (locale) => dictionaries[locale]()

Using Multi-Languages

We use the multi-language feature on the pages.tsx page.

First, add the lang parameter for the function, add async for the function,

1
2
3
export default async function Home({ params: { lang } }: { params: { lang: string } }) {
...
}

Use it on the page, add the multi-language fuction call,

1
const t = await getDictionary(lang);

For convenience, I clean up the default code on page.tsx and only retain the text display.

1
2
3
4
5
6
<main className="flex min-h-screen flex-col items-center justify-between p-24">
<p className="fixed left-0 top-0 flex w-full justify-center border-b border-gray-300 bg-gradient-to-b from-zinc-200 pb-6 pt-8 backdrop-blur-2xl dark:border-neutral-800 dark:bg-zinc-800/30 dark:from-inherit lg:static lg:w-auto lg:rounded-xl lg:border lg:bg-gray-200 lg:p-4 lg:dark:bg-zinc-800/30">
{t.home.title}
</p>
{t.home.desc}
</main>

Restart the program or wait for the program to hot update successfully, open different language pages http://localhost:3000/en http://localhost:3000/zh to check the effect.

Setting the Default Language

It looks pretty good, but careful friends will find that opening http://localhost:3000 will result in a 404 error. To solve this problem, we need to set a default language when no language is selected.

For this, we can create a middleware.ts in the src directory, and then copy the code from the documentation.

The core logic is simple:

Check whether there is a certain language identifier in URL’s pathname. If so, return directly. Otherwise, get the appropriate language and redirect the URL to /${locale}${pathname}

The focus is on the getLocale function. We need to specify the suitable language. For now, let’s deal with this simply: use the default defaultLocale = "en" .

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import { NextRequest, NextResponse } from "next/server";

let locales = ["en", "zh"];
let defaultLocale = "en";


function getLocale(request: NextRequest) {
return defaultLocale;
}

export function middleware(request: NextRequest) {

const { pathname } = request.nextUrl;
const pathnameHasLocale = locales.some(
(locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
);

if (pathnameHasLocale) return;


const locale = getLocale(request);
request.nextUrl.pathname = `/${locale}${pathname}`;


return NextResponse.redirect(request.nextUrl);
}

export const config = {
matcher: [

"/((?!_next).*)",


],
};

After the program updates, we open http://localhost:3000/ and see that it will automatically redirect to the default language page.

Optimization of Getting the Default Language

In the previous step, while get the default language, we treated it simply as defaultLocale = "en". A more graceful way is: Set the default language based on the user’s system or browser language.

We can achieve this by getting the Accept-Language field from the browser’s HTTP headers. The data format is approximately as follows:

1
2
3
4
English:
accept-language: en-US,en;q=0.5
Chinese:
accept-language: zh-CN,zh-Hans;q=0.9

We update middleware as follows:

  1. Get the Accept-Language from the HTTP headers. If it’s empty, then return the default language.
  2. Parse the language list in Accept-Language and match to get the corresponding language based on the configured language list. (If there is no match, return the default language)

Install dependencies @formatjs/intl-localematcher, negotiator, @types/negotiator, and implement the following logic:

1
2
3
4
5
6
7
function getLocale(request: NextRequest) {
const acceptLang = request.headers.get("Accept-Language");
if (!acceptLang) return defaultLocale;
const headers = { "accept-language": acceptLang };
const languages = new Negotiator({ headers }).languages();
return match(languages, locales, defaultLocale);
}

By changing the system language, open http://localhost:3000 and it will automatically redirect to the page with the same system language. Test successfully.

Other Handling of Multi-Language

Storing the Language

Going a step further, we can store the user’s web page language in the cookies and use it on the next visit:

1
2
3
4
5
6
7

if (request.cookies.has(cookieName)) {
return request.cookies.get(cookieName)!.value;
}


response.cookies.set(cookieName, locale);

Web Metadata(Page Title/Descriptions..)

When using i18n in web page metadata, add the following code to page.tsx:

1
2
3
4
5
6
7
export async function generateMetadata({ params: { lang } } : { params: { lang: string } }) {
const t = await getDictionary(lang);
return {
title: t.page.title,
description: t.page.desc,
};
}

SSG(Static Generation)

When handling i18n in SSG, the code in layout.tsx is as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
interface LangParams {
lang: string;
}

export async function generateStaticParams() {
return [{ lang: "en" }, { lang: "zh" }];
}

export default function RootLayout({
children,
params,
}: Readonly<{
children: React.ReactNode;
params: LangParams;
}>) {
return (
<html lang={params.lang}>
<body className={inter.className}>{children}</body>
</html>
);
}

Language Switch(Language Switcher or Links)

You can add a language swicher (like a drop-down menu) or some links.

For example,

1
2
3
4
5
<div className="space-x-2">
<Link href="/en">English</Link>
<span>|</span>
<Link href="/zh">Chinese</Link>
</div>

End

Through the learning of the above steps, we initially familiarize and practice using multi-language in Next.js. A journey of thousand miles begins with a single step. The work of i18n is not limited to these, and of course, other areas need improvement which I’d leave to you, the reader.

Finally, here is the complete code of middleware.ts :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import Negotiator from "negotiator";
import { match } from "@formatjs/intl-localematcher";
import { NextRequest, NextResponse } from "next/server";

const locales = ["en", "zh"];
const defaultLocale = "zh";
const cookieName = "i18nlang";


function getLocale(request: NextRequest): string {

if (request.cookies.has(cookieName))
return request.cookies.get(cookieName)!.value;

const acceptLang = request.headers.get("Accept-Language");
if (!acceptLang) return defaultLocale;

const headers = { "accept-language": acceptLang };
const languages = new Negotiator({ headers }).languages();
return match(languages, locales, defaultLocale);
}

export function middleware(request: NextRequest) {
if (request.nextUrl.pathname.startsWith("/_next")) return NextResponse.next();


const { pathname } = request.nextUrl;
const pathnameHasLocale = locales.some(
(locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
);

if (pathnameHasLocale) return;


const locale = getLocale(request);
request.nextUrl.pathname = `/${locale}${pathname}`;


const response = NextResponse.redirect(request.nextUrl);

response.cookies.set(cookieName, locale);
return response;
}

export const config = {
matcher: [

"/((?!_next).*)",


],
};

You can get the full code from https://github.com/xumeng/next-i18n-demo .

The finally running demo: https://next-i18n-demo-two.vercel.app/


Reference:

https://nextjs.org/docs/app/building-your-application/routing/internationalization

https://en.wikipedia.org/wiki/ISO_3166

https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes

https://en.wikipedia.org/wiki/IETF_language_tag

https://www.alchemysoftware.com/livedocs/ezscript/Topics/Catalyst/Language.htm