Đa ngôn ngữ (i18n) sử dụng Next-intl trong project Nextjs 14
- Published on
Next-intl là một thư viện hỗ trợ đa ngôn ngữ cho framework Next.js, giúp bạn dễ dàng quản lý và thực hiện đa ngôn ngữ trong dự án web của mình. Với Next-intl, chúng ta chỉ cần 15 phút để tạo chức năng đa ngôn ngữ (Internationalization - i18n) cho nextjs project.
Trong phiên bản mới nhất tính tới thời điểm hiện tại là Nextjs 14, App Router thực sự là một cải tiến lớn, cơ chế định tuyến mạnh mẽ và linh hoạt hơn Page Router ở các phiên bản trước đó. Vì vậy, bài viết này mình sẽ tạo demo dựa trên Nextjs 14 sử dụng App Router.
Nếu bạn mới bắt đầu với Nextjs hay chưa biết Nextjs từ version 13 trở đi có gì mới thì hãy đọc bài viết này: https://www.nguyenvanthai.com/blog/next-js/co-gi-moi-trong-nextjs-13
Table Of Content
- Cấu trúc thư mục và source code demo
- Sử dụng đa ngôn ngữ trong Server Component và Client Component
- Tạo select box để thay đổi ngôn ngữ
- Dynamic values message
- Resource
Cấu trúc thư mục và source code demo
Nếu bạn chưa có Project Nextjs 14 sử dụng App Router, hãy làm theo hướng dẫn trong Link sau: https://nextjs.org/docs/getting-started/installation
Let's get started!
Chạy câu lệnh npm install next-intl
để install thư viện next-intl và tạo cấu trúc thư mục như sau:
├── next.config.js (1)
└── src
├── app
│ └── [locale]
│ ├── layout.tsx (2)
│ └── page.tsx (3)
├── locales
│ ├── en.json (4)
│ └── vi.json (5)
├── libs
│ ├── i18n.ts (6)
│ └── i18nNavigation.ts (7)
├── configs
│ └── appConfig.ts (8)
└── middleware.ts (9)
Bây giờ chúng ta sẽ lần lượt thêm nội dung files theo structure trên:
1. locales
Đây là thư mục chứa các file JSON chứa nội dung ngôn ngữ được sử dụng trong project. vi.json
là file cho tiếng việt, en.json
là file cho tiếng anh.
locales/vi.json
{
"Index": {
"title": "Xin chào!"
}
}
Tương tự với locales/en.json
{
"Index": {
"title": "Hello world!"
}
}
2. src/configs/appConfig.ts
Đây là nơi chứa các cấu hình cho next-intl
import { LocalePrefix } from 'next-intl/routing';
export type Locale = "vi" | "en";
export type AppConfig = {
name: string;
locales: Locale[];
defaultLocale: Locale;
localePrefix: LocalePrefix;
timeZoneMap: Record<Locale, string>;
};
// localePrefix default là "always"
// "always": luôn luôn hiển thị tiền tố ngôn ngữ (vi|en)
// "as-needed": chỉ hiển thị khi tiền tố ko phải mặc định
// "never": không hiển thị tiền tố ngôn ngữ
const localePrefix: LocalePrefix = "as-needed";
export const appConfig: AppConfig = {
name: "Nextjs Starter",
locales: ["en", "vi"],
defaultLocale: "vi",
localePrefix,
timeZoneMap: {
en: "America/Los_Angeles",
vi: "Asia/Ho_Chi_Minh",
},
};
3. next.config.js
Bây giờ chúng ta cần set up plugin trỏ tới file cấu hình i18n.ts
:
const withNextIntl = require("next-intl/plugin")("./src/libs/i18n.ts");
/** @type {import('next').NextConfig} */
const nextConfig = withNextIntl({
// các cấu hình Next.js khác ...
});
module.exports = nextConfig;
Nếu sử dụng next.config.mjs thì cấu hình như sau:
import createNextIntlPlugin from 'next-intl/plugin';
const withNextIntl = createNextIntlPlugin("./src/libs/i18n.ts");
/** @type {import('next').NextConfig} */
const nextConfig = {};
export default withNextIntl(nextConfig);
4. src/libs/i18n.ts
next-intl sẽ tạo cấu hình mỗi 1 request gửi lên. Tại đây chúng ta có thể cung cấp message và các options khác tùy thuộc vào ngôn ngữ của người dùng.
import { appConfig } from "@/configs/appConfig";
import { getRequestConfig } from "next-intl/server";
import { notFound } from "next/navigation";
export default getRequestConfig(async ({ locale }) => {
// Validate tồn tại `locale` parameter
if (!appConfig.locales.includes(locale as any)) notFound();
// Lấy múi giờ dựa trên locale
const timeZone =
appConfig.timeZoneMap[locale as Locale] ||
"UTC"; // Sử dụng UTC hoặc giá trị mặc định nếu không tìm thấy
return {
// import message từ thư mục locales tại step 1 đã định nghĩa
messages: (await import(`../locales/${locale}.json`)).default,
timeZone,
};
});
5. middleware.ts
Config matcher đảm bảo rằng chỉ những đường dẫn có chứa tiền tố ngôn ngữ mới trigger middleware.
import createMiddleware from "next-intl/middleware";
import { appConfig } from "./configs/appConfig";
import { NextRequest, NextResponse } from "next/server";
const intlMiddleware = createMiddleware({
locales: appConfig.locales,
localePrefix: appConfig.localePrefix,
defaultLocale: appConfig.defaultLocale,
});
export default function middleware(req: NextRequest) {
const response = intlMiddleware(req);
if (response instanceof Response) {
return response;
}
return NextResponse.next();
}
export const config = {
// chỉ match khi đường dẫn chứa locales (vi|en)
matcher: ["/", `'/(${appConfig.locales.join("|")})/:path*'`],
};
6. app/[locale]/layout.tsx
locale
param sẽ được lấy và sử dụng để cấu hình document language.
import {NextIntlClientProvider, useMessages} from 'next-intl';
import {notFound} from 'next/navigation';
export default function LocaleLayout({children, params: {locale}}) {
const messages = useMessages();
return (
<html lang={locale}>
<body>
// provider setting để sử dụng tại Client component
<NextIntlClientProvider locale={locale} messages={messages}>
{children}
</NextIntlClientProvider>
</body>
</html>
);
}
7. app/[locale]/page.tsx
Sử dụng useTranslations để hiển thị message trong page components
import {useTranslations} from 'next-intl';
export default function Index() {
const t = useTranslations('Index');
return <h1>{t('title')}</h1>;
}
Trên đây là các phần basic nhất về việc áp dụng next-intl cho project nextjs 14. Hãy chạy project và test thử.
Sử dụng đa ngôn ngữ trong Server Component và Client Component
1. Server component
Có 2 cách để định nghĩa Server Component trong nextjs 14 đó là:
- Async components
- Non-async, regular components
Async component
Async component thì chúng ta không thể định nghĩa được hooks. Do đó, next-intl cung cấp các async function để lấy các message, locale,... và nhiều thông tin về đa ngôn ngữ đã được định nghĩa.
Ví dụ sử dụng getTranslations
:
import {getTranslations} from 'next-intl/server';
export default async function ProfilePage() {
const user = await fetchUser();
const t = await getTranslations('ProfilePage');
return (
<PageLayout title={t('title', {username: user.name})}>
<UserDetails user={user} />
</PageLayout>
);
}
Bên cạnh đó còn các async function phổ biết khác hỗ trợ cho việc lấy thông tin internationalization:
import {
getTranslations,
getFormatter,
getNow,
getTimeZone,
getMessages,
getLocale
} from 'next-intl/server';
const t = await getTranslations('ProfilePage');
const format = await getFormatter();
const now = await getNow();
const timeZone = await getTimeZone();
const messages = await getMessages();
const locale = await getLocale();
Non-async components
Non-async components được biết đến như 1 shared component
. Component này sẽ không định nghĩa keyyword async
hay sử dụng directive "use client". Nó vừa có thể là Server vừa có thể là Client component tuỳ vào cách sử dụng.
Trong trường hợp đó chúng ta sẽ sử dụng như sau:
import {useTranslations} from 'next-intl';
export default function UserDetails({user}) {
const t = useTranslations('UserProfile');
return (
<section>
<h2>{t('title')}</h2>
<p>{t('followers', {count: user.numFollowers})}</p>
</section>
);
}
nếu bạn import useTranslations
, useFormatter
, useLocale
, useNow
and useTimeZone
từ một shared component. next-intl
sẽ tự động cung cấp cách triển khai tốt nhất cho môi trường mà component đó được thực thi (server hoặc client).
2. Client Component
Có nhiều cách để sử dụng được đa ngôn ngữ trong Client Component. Tuy nhiên cách phổ biến nhất và thuận tiện nhất đó là sử dụng NextIntlClientProvider
của next-intl.
Check lại file [locale]/layout.tsx:
import {NextIntlClientProvider, useMessages} from 'next-intl';
import {notFound} from 'next/navigation';
export default function LocaleLayout({children, params: {locale}}) {
const messages = useMessages();
return (
<html lang={locale}>
<body>
<NextIntlClientProvider locale={locale} messages={messages}>
{children}
</NextIntlClientProvider>
</body>
</html>
);
}
Lúc này, các client component nào muốn sử dụng đa ngôn ngữ chỉ cần sử dụng các hooks được cung cấp bởi next-intl.
Tạo select box để thay đổi ngôn ngữ
Để thay đổi ngôn ngữ page theo 1 select box chúng ta sẽ làm như sau:
1. Tạo file i18nNavigation
File này sẽ export usePathname
, useRouter
từ createSharedPathnamesNavigation
của next-intl cùng với config của chúng ta trong file appConfig
. Nhằm mục đích xử lí logic chuyển hướng trang.
// libs/i18nNavigation/ts
import { createSharedPathnamesNavigation } from "next-intl/navigation";
import { appConfig } from "@/configs/appConfig";
export const { usePathname, useRouter } = createSharedPathnamesNavigation({
locales: appConfig.locales,
localePrefix: appConfig.localePrefix,
});
2. Tạo select box component thay đổi ngôn ngữ
// src/components/LocaleSwitcher.tsx
"use client";
import { useLocale } from "next-intl";
import type { ChangeEventHandler } from "react";
import { usePathname, useRouter } from "@/libs/i18nNavigation";
import { appConfig } from "@/configs/appConfig";
export default function LocaleSwitcher() {
const router = useRouter();
const pathname = usePathname();
const locale = useLocale();
const handleChange: ChangeEventHandler<HTMLSelectElement> = (event) => {
router.push(pathname, { locale: event.target.value });
router.refresh();
};
return (
<select
defaultValue={locale}
onChange={handleChange}
className="border border-gray-300 font-medium focus:outline-none focus-visible:ring"
>
{appConfig.locales.map((elt) => (
<option key={elt} value={elt}>
{elt.toUpperCase()}
</option>
))}
</select>
);
}
Select Box đã được hoàn thành 😊
Dynamic values message
Sẽ có nhiều trường hợp chúng ta cần message có giá trị động, thay đổi giá trị theo 1 hoặc nhiều biến khác nhau thì sẽ làm như sau:
Ví dụ về Dynamic Values
// en.json
"message": "Hello {name}!"
t('message', {name: 'Jane'}); // "Hello Jane!"
Resource
- Nextjs 14: https://nextjs.org/docs
- Next-intl: https://next-intl-docs.vercel.app/docs/getting-started/app-router