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.
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 | ❯ npx create-next-app@latest |
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,也即在产品中支持多国语言文化和环境风俗,主要包括语言/时间/货币符号等。这篇文章中将只专注于语言部分。
在国际化的具体呈现上,常见的方式是网站默认进入某个语言的官网(通常是英文),并支持选择语言或地区,进行切换网站的不同语言版本。
具体实现方式上,有的网站以语言简称为前缀,如 en.wikipedia.org
, zh.wikipedia.org
;有的网站以语言简称作为路径后缀,如 aws.amazon.com/cn
, aws.amazon.com/jp
,也有以国家地区域名为区分的,如以前的 apple.cn
, apple.jp
。
其中诸如 en, zh, cn, jp
,也即语言编码,在不同版本的语言编码版本中略有不同,具体可参考文章下方参考资料。
在本文案例中,将以 ISO_3166 中的 en
和 zh
编码分别代表英文和中文。
Begin Configure Multi-Languages
The original file structure of the project was:
1 | ├── package.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 | ├── package.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 | { |
zh.json
1 | { |
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 | import 'server-only' |
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 | 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 | <main className="flex min-h-screen flex-col items-center justify-between p-24"> |
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 | import { NextRequest, NextResponse } from "next/server"; |
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 | English: |
We update middleware
as follows:
- Get the
Accept-Language
from the HTTP headers. If it’s empty, then return the default language. - 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 | function getLocale(request: NextRequest) { |
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 | // Get Cookie |
Web Metadata(Page Title/Descriptions..)
When using i18n in web page metadata, add the following code to page.tsx
:
1 | export async function generateMetadata({ params: { lang } } : { params: { lang: string } }) { |
SSG(Static Generation)
When handling i18n in SSG, the code in layout.tsx
is as follows:
1 | interface LangParams { |
Language Switch(Language Switcher or Links)
You can add a language swicher (like a drop-down menu) or some links.
For example,
1 | <div className="space-x-2"> |
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 | import Negotiator from "negotiator"; |
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