Xơn Space

    My bullshit stories

    Back

    Đa ngôn ngữ trong NextJS với (i18n)

    logo

    Hoàng Sơn

    published at:18/07/2024 at 4:39 AM- view

    thumbail

    Làm thế nào để cấu hình đa ngôn ngữ đối với NextJS ?

    Trong hướng dẫn này, mình sẽ hướng dẫn cấu hình và triển khai đa ngôn ngữ (i18n) trong ứng dụng NextJS của mọi người.

    1. Bắt đầu

    > Việc cấu hình đa ngôn ngữ (i18n) còn tuỳ thuộc vào version của NextJS mà mọi người đang sử dụng:

    • App Router
    • Page Router

    > Một vài example mà mọi người có thể tham khảo: Nhấn vào đây

    1.1 App Router

    > Đối với App router, sẽ có 2 loại cấu hình, tuỳ thuộc vào mục đích của mọi người.

    Có i18n routing

    Để sử dụng các unique pathname cho mỗi ngôn ngữ mà ứng dụng của mọi người đang muốn triển khai, thì next-intl sẽ xử lý được những vấn đề như sau

    • Dựa trên prefix ở pathname (ví dụ: /en/about hoặc /vi/about ...)
    • Dựa trên tên miền (ví dụ: en.example.com/about hoặc vi.example.com/about)

    Trong cả hai trường hợp trên, next-intl tích hợp với App Router bằng cách sử dụng một [locale] ở layout cao nhất, vì vậy có thể được sử dụng để cung cấp nội dung đối với những ngôn ngữ mà mọi người đang triển khai.

    • Đầu tiên, mọi người cần setup một ứng dụng bằng NextJS App Router trước nhé, nếu chưa thì hãy tham khảo ở document này nhé.
    npm install next-intl
    
    • Ta sẽ cấu hình folder của source theo cấu trúc sau:
    ├── dictionaries (1)
    │   ├── en.json
    │   └── vi.json
    │   └── ...
    ├── next.config.mjs (2)
    └── src 
    	├── i18n.ts (3) 
    	├── middleware.ts (4) 
    	└── app 
    		└── [locale] 
    			├── layout.tsx (5) 
    			└── page.tsx (6)
    

    1) Dictionaries

    • Nội dung có thể được lưu trữ ở local folder hoặc download từ các remote resources (ví dụ hệ thống translation management chẳng hạn).
    • Tuỳ theo nhu cầu và mục đích sử dụng mà bạn chọn cách lưu trữ phù hợp nhé.
    • Với mình, thì mình sẽ chọn cách đơn giản là lưu ở local folder, tạo ra những file JSON với từng ngôn ngữ (vi.json, en.json,...).
    // dictionaries/vi.json
    { 
    	"HomePage": {
    		 "title": "Hello world!",
    		 "description": "Xin Chàoo !"
    	},
    	...
    }
    

    2) Set up next config

    • setup plugin tạo ra các alias để cung cấp cấu hình i18n của mọi người cho các Server Components
    await import("./src/env.js");
    import nextIntl from "next-intl/plugin";
    
    const withNextIntl = nextIntl();
    const nextConfig = {...};
    
    export default withNextIntl(nextConfig);
    

    3) i18n

    • next-intl tạo object dùng để được sử dụng để cung cấp các dictionaries và options khác dựa trên ngôn ngữ mà mọi người cấu hình ở dictionaries để sử dụng ở Server Components
    import { getRequestConfig } from "next-intl/server";
    import { notFound } from "next/navigation";
    
    // Can be imported from a shared config
    const locales: string[] = ["vi", "en"];
    
    export default getRequestConfig(async ({ locale }) => {
    	// Validate that the incoming `locale` parameter is valid
    	if (!locales.includes(locale)) notFound();
    	
    	return {
    		messages: (await import(`../dictionaries/${locale}.json`)).default
    	}
    });
    
    

    4) middlewares

    • Xử lí routing,etc... với từng locale mà mọi người định nghĩa.
    import createMiddleware from 'next-intl/middleware';
    
    export default createMiddleware({
    	locales: ["vi", "en"],
    	defaultLocale: "vi",
    });
    
    export const config = {
    	// Match only internationalized pathnames
    	matcher: ['/', '/(vi|en)/:path*']
    }
    
    • Ngoài ra, mọi người cũng có thể kết hợp nhiều middlewares lại với nhau, ví dụ như sử dụng middlewares như NextAuth, Clerk, ... với Next-Intl middlewares...
    • Tham khảo một vài ví dụ về combine middlewares ở đây nha

    > Ví dụ về Clerk middleware

    import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";
    import createMiddleware from "next-intl/middleware";
    
    const intlMiddleware = createMiddleware({
    	locales: ["vi", "en"],
    	defaultLocale: "vi",
    });
    
    const isProtectedRoute = createRouteMatcher(["/(auth)(.*)"]);
    export default clerkMiddleware((auth, req) => {
    	if (isProtectedRoute(req)) auth().protect();
    	return intlMiddleware(req);
    });
    
    export const config = {
    	matcher: ["/", "/(vi|en)/:path*"],
    };
    

    5) 'app/[locale]/layout.tsx'

    • Mọi người có thể sử dụng phần này để pass config từ i18n.ts đến các components khác thông qua NextIntlClientProvider.
    import {NextIntlClientProvider} from 'next-intl';
    import {getMessages} from 'next-intl/server';
    
    export function generateStaticParams() {
    	return locales.map((locale) => ({ locale }));
    }
    
    export default async function LocaleLayout({
      children,
      params: {locale}
    }: {
      children: React.ReactNode;
      params: {locale: string};
    }) {
      // Providing all messages to the client
      // side is the easiest way to get started
      const messages = await getMessages();
     
      return (
        <html lang={locale}>
          <body>
            <NextIntlClientProvider messages={messages}>
              {children}
            </NextIntlClientProvider>
          </body>
        </html>
      );
    }
    

    Lưu ý rằng là NextIntlClientProvider sẽ tự động kế thừa những config từ i18n.ts ở đây nhé !

    ""

    6) Sử dụng

    • Sử dụng useTranslation ở bất cứ components nào trong applications của mọi người, miễn là phải nằm trong NextIntlClientProvider nhé.
    import {useTranslations} from 'next-intl';
     
    export default function HomePage() {
      const t = useTranslations('HomePage');
      return <h1>{t('title')}</h1>;
    }
    
    import {useTranslations} from 'next-intl';
     
    export default async function HomePage() {
      const t = await getTranslation('HomePage');
      return <h1>{t('title')}</h1>;
    }
    

    Không có i18n routing

    • Đối với việc không triển khai routing với i18n, thì các bước thực hiện đơn giản hơn việc chia routing
    • Cũng thực hiện theo những step 1, step 2,step 3 như cách chia routing, tuy nhiên, ở đây sẽ không cần config middlewares.

    4) 'app/layout.tsx'

    import {NextIntlClientProvider} from 'next-intl';
    import {getLocale, getMessages} from 'next-intl/server';
     
    export default async function RootLayout({
      children
    }: {
      children: React.ReactNode;
    }) {
      const locale = await getLocale();
     
      // Providing all messages to the client
      // side is the easiest way to get started
      const messages = await getMessages();
     
      return (
        <html lang={locale}>
          <body>
            <NextIntlClientProvider messages={messages}>
              {children}
            </NextIntlClientProvider>
          </body>
        </html>
      );
    }
    

    5) Sử dụng

    • Sử dụng useTranslation ở bất cứ components nào trong applications của mọi người, miễn là phải nằm trong NextIntlClientProvider nhé.
    import {useTranslations} from 'next-intl';
     
    export default function HomePage() {
      const t = useTranslations('HomePage');
      return <h1>{t('title')}</h1>;
    }
    

    2. Tổng kết

    • Vậy là đã xong về những bước cơ bản để setup một dự án NextJS đa ngôn ngữ với i18n.
    • Những gì mình viết và chia sẻ đều được vận dụng từ dự án thực tế mà mình đã làm và tham khảo tài liệu của Next-Intl
    • Ngoài ra, docs của Next-Intl có những kiến thức hay khác như là:
      • sử dụng với Server & Client Components
      • sử dụng với metadata
      • MDX
      • Navigation
      • Middlewares
      • ...

    Hi vọng những gì mình chia sẻ ở trên, sẽ giúp ích cho mọi người trong quá trình làm việc. Cảm ơn vì đã xem bài viết của mình nhé. Se yaaa !

    nextjs

    i18n

    next-intl

    frontend

    react

    Categories:

    Frontend ,NextJS

    logo

    Hoàng Sơn

    Cảm ơn bạn đã dành thời gian đọc qua bài viết trên của mình, nếu có bất kỳ câu hỏi gì, thì cứ nhắn tin cho mình nhé. Hi vọng mình đã giúp ích cho bạn 'một phần nào đấy'.

    There are 0 comments on this post

    Comment

    Your email address will not be published. Required fields are marked * are required.