Next.jsnext-intl

Đa ngôn ngữ (i18n) sử dụng Next-intl trong project Nextjs 14

By Thái Nguyễn
Picture of the author
Published on
image alt

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

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

// Trong trường hợp báo lỗi dòng này bạn cần sửa 
import type { LocalePrefix } from "next-intl/dist/types/src/shared/types";

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;

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à:

  1. Async components
  2. 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

  1. Nextjs 14: https://nextjs.org/docs
  2. Next-intl: https://next-intl-docs.vercel.app/docs/getting-started/app-router