Đa ngôn ngữ không còn là “nỗi đau” đầu mỗi khi khởi tạo dự án
Nếu bạn từng làm i18n trên Page Router, hẳn bạn còn nhớ cảm giác chật vật khi cấu hình thủ công để SEO không bị lỗi hoặc tránh việc trang bị reload khi chuyển vùng. Với App Router, mọi thứ đã thay đổi. Cách tiếp cận mới yêu cầu chúng ta tư duy khác đi về routing và Server Components.
Trong số các thư viện hiện nay, next-intl nổi lên như một ứng cử viên sáng giá nhất. Nó nhẹ (chỉ khoảng 10kb gzipped), hỗ trợ tuyệt vời cho cả Server và Client Components. Quan trọng hơn, nó giúp bạn quản lý bản dịch một cách khoa học, thay vì để các file JSON rải rác khắp nơi.
Thiết lập nhanh trong 5 phút
Đầu tiên, hãy thêm thư viện vào project của bạn:
npm install next-intl
1. Cấu trúc thư mục chuẩn
Để i18n hoạt động mượt mà, chúng ta cần đưa toàn bộ route vào dynamic segment [locale]. Đây là quy chuẩn giúp Next.js nhận diện ngôn ngữ trực tiếp từ URL. Cấu trúc thư mục lý tưởng sẽ như sau:
├── messages (Chứa file dịch)
│ ├── en.json
│ └── vi.json
├── src
│ ├── i18n.ts (Config loader)
│ ├── middleware.ts (Bộ lọc điều hướng)
│ └── app
│ └── [locale]
│ ├── layout.tsx
│ └── page.tsx
├── next.config.mjs
2. Cấu hình để máy thực sự hiểu bạn
Hãy tạo file messages/vi.json. Đây là nơi lưu trữ linh hồn của ứng dụng:
{
"Index": {
"title": "Chào mừng bạn đến với itfromzero!",
"description": "Học lập trình từ con số 0."
}
}
Tiếp theo, thiết lập src/i18n.ts. File này đóng vai trò cầu nối để load dữ liệu tương ứng với ngôn ngữ người dùng đang truy cập:
import {notFound} from 'next/navigation';
import {getRequestConfig} from 'next-intl/server';
const locales = ['en', 'vi'];
export default getRequestConfig(async ({locale}) => {
if (!locales.includes(locale as any)) notFound();
return {
messages: (await import(`../messages/${locale}.json`)).default
};
});
Đừng quên src/middleware.ts. Nó giúp tự động nhận diện ngôn ngữ trình duyệt hoặc cookie để redirect người dùng về đúng URL (ví dụ: /vi hoặc /en):
import createMiddleware from 'next-intl/middleware';
export default createMiddleware({
locales: ['en', 'vi'],
defaultLocale: 'vi'
});
export const config = {
// Bỏ qua các file hệ thống và api
matcher: ['/', '/(vi|en)/:path*']
};
Đi sâu vào các thành phần cốt lõi
Middleware: “Người gác cổng” thông minh
Khi người dùng gõ yourdomain.com, middleware sẽ ngay lập tức kiểm tra header accept-language. Nếu họ dùng trình duyệt tiếng Việt, nó sẽ tự động đẩy sang /vi. Một mẹo nhỏ: hãy luôn đặt localePrefix: 'always'. Việc này giúp URL luôn tường minh, cực kỳ có lợi cho SEO vì Google Bot có thể index riêng biệt từng phiên bản ngôn ngữ.
Sức mạnh của Server Components
Điểm cộng lớn nhất của next-intl là khả năng chạy trực tiếp trên Server Components. Bạn không cần thêm dòng 'use client' vô nghĩa chỉ để hiển thị một câu chào. Việc này giúp giảm dung lượng JavaScript gửi xuống trình duyệt, tăng tốc độ load trang đáng kể.
// src/app/[locale]/page.tsx
import {useTranslations} from 'next-intl';
export default function Index() {
const t = useTranslations('Index');
return (
<div>
<h1>{t('title')}</h1>
<p>{t('description')}</p>
</div>
);
}
Kinh nghiệm thực tế: Khi file JSON của bạn lên tới hàng nghìn dòng, việc quản lý sẽ rất khó khăn. Mình thường dùng JSON Formatter để kiểm tra cú pháp nhanh. Chỉ cần thiếu một dấu phẩy, toàn bộ app có thể bị crash ngay lập tức.
Nâng cấp Type-safe: Nói không với lỗi Typo
Gõ sai key là lỗi cực kỳ phổ biến. Ví dụ: JSON là title nhưng trong code bạn gõ titel. Thay vì để giao diện hiển thị những đoạn mã thô kệch, hãy dùng TypeScript để bắt lỗi ngay lúc code.
Chỉ cần tạo file global.d.ts với nội dung sau:
// global.d.ts
type Messages = typeof import('./messages/vi.json');
declare interface IntlMessages extends Messages {}
Bây giờ, VS Code sẽ tự động gợi ý (Intellisense) từng key cho bạn. Nếu gõ sai, IDE sẽ báo đỏ ngay lập tức. Đây là tiêu chuẩn bắt buộc cho các dự án thực tế quy mô lớn.
Xử lý chuyển đổi ngôn ngữ (Language Switcher) mượt mà
Đừng dùng thẻ <a> để đổi ngôn ngữ vì nó sẽ làm reload trang, gây trải nghiệm ngắt quãng. Thay vào đó, hãy tận dụng các hàm điều hướng được tối ưu sẵn từ next-intl.
Tạo file src/navigation.ts để tái sử dụng:
import {createSharedPathnamesNavigation} from 'next-intl/navigation';
export const locales = ['en', 'vi'] as const;
export const {Link, redirect, usePathname, useRouter} = createSharedPathnamesNavigation({locales});
Sau đó, component chọn ngôn ngữ sẽ đơn giản như thế này:
'use client';
import {usePathname, useRouter} from '@/navigation';
export default function LocaleSwitcher() {
const pathname = usePathname();
const router = useRouter();
const changeLocale = (nextLocale: string) => {
// Chuyển vùng mà vẫn giữ nguyên route hiện tại
router.replace(pathname, {locale: nextLocale});
};
return (
<select onChange={(e) => changeLocale(e.target.value)}>
<option value="vi">Tiếng Việt</option>
<option value="en">English</option>
</select>
);
}
Những lưu ý “xương máu” khi triển khai
- SEO Metadata: Hãy dùng
generateMetadatađể dịch cả thẻ meta. Một website đa ngôn ngữ mà tiêu đề trang nào cũng giống nhau sẽ bị Google đánh giá thấp. - Xử lý số ít/số nhiều: Đừng dùng if-else trong code. Hãy tận dụng ICU format của
next-intl:"items": "{count, plural, =0 {Trống} one {1 mục} other {# mục}}". - Định dạng số và ngày tháng: Luôn dùng hàm
format.dateTimethay vì.toLocaleString(). Điều này giúp tránh lỗi HydrationMismatch giữa Server và Client do khác biệt múi giờ.
Triển khai i18n ngay từ ngày đầu tiên sẽ giúp bạn tiết kiệm hàng tuần làm việc sau này. Dù cấu hình ban đầu hơi tốn công, nhưng sự chuyên nghiệp và khả năng tiếp cận người dùng toàn cầu là phần thưởng hoàn toàn xứng đáng.

