@@ -374,3 +374,293 @@ export default async function Page({ params: { lang } }) {
374374 return <button >{ dict .products .cart } </button >; // Add to Cart
375375}
376376```
377+
378+ Nhưng trong thực tế việc sử dụng [ next-intl] ( https://next-intl-docs.vercel.app/ ) vẫn dễ dàng và có nhiều lợi ích hơn.
379+
380+ ``` sh
381+ npm install next-intl --save
382+ ```
383+
384+ ``` mjs
385+ import createNextIntlPlugin from " next-intl/plugin" ;
386+
387+ const withNextIntl = createNextIntlPlugin ();
388+
389+ /** @type {import('next').NextConfig} */
390+ const nextConfig = {
391+ async redirects () {
392+ return [
393+ // Basic redirect
394+ {
395+ // redirect to default language
396+ source: " /" ,
397+ destination: " /vi" ,
398+ permanent: true ,
399+ },
400+ ];
401+ },
402+ };
403+
404+ export default withNextIntl (nextConfig);
405+ ```
406+
407+ Cấu trúc cây thư mục như sau
408+
409+ ```
410+ next.config.mjs
411+ src
412+ app
413+ [locale]
414+ global.css
415+ layout.tsx
416+ loading.tsx
417+ page.tsx
418+ middleware.ts
419+ i18n.ts
420+ components
421+ layouts
422+ Footer.tsx
423+ messages
424+ en.json
425+ vi.json
426+ ```
427+
428+ ``` json
429+ {
430+ "layout" : {
431+ "headerTitle" : " NextJS Tutorial 2024"
432+ }
433+ }
434+ ```
435+
436+ Đầu tiên các anh/chị cần move các file sau vào thư mục mới [ locale] :
437+
438+ - global.css
439+ - layout.tsx
440+ - loading.tsx
441+ - page.tsx
442+
443+ ``` tsx
444+ // layout.tsx
445+ import type { Metadata } from " next" ;
446+ import " ./globals.css" ;
447+ import { NextIntlClientProvider } from " next-intl" ;
448+ import { getMessages } from " next-intl/server" ;
449+
450+ export const metadata: Metadata = {
451+ title: " NextJS Tutorial 2024" ,
452+ description: " NextJS courses" ,
453+ };
454+
455+ export default async function RootLayout({
456+ children ,
457+ params : { locale },
458+ }: Readonly <{
459+ children: React .ReactNode ;
460+ params: { locale: string };
461+ }>) {
462+ const messages = await getMessages ({
463+ locale: locale ,
464+ });
465+ return (
466+ <html lang = { locale } >
467+ <body >
468+ <NextIntlClientProvider messages = { messages } >
469+ { children }
470+ </NextIntlClientProvider >
471+ </body >
472+ </html >
473+ );
474+ }
475+ ```
476+
477+ Tạo file i18n.ts
478+
479+ ``` ts
480+ import { notFound } from " next/navigation" ;
481+ import { getRequestConfig } from " next-intl/server" ;
482+
483+ // Can be imported from a shared config
484+ const locales = [" en" , " vi" ];
485+
486+ export default getRequestConfig (async ({ locale }) => {
487+ // Validate that the incoming `locale` parameter is valid
488+ if (! locales .includes (locale as any )) notFound ();
489+
490+ return {
491+ messages: (await import (` ../messages/${locale }.json ` )).default ,
492+ };
493+ });
494+ ```
495+
496+ Cập nhật middleware
497+
498+ ``` ts
499+ import { NextResponse } from " next/server" ;
500+ import type { NextRequest } from " next/server" ;
501+ import { authMiddleware } from " ./app/middlewares/auth.middleware" ;
502+ import { AppCookie , ProtectedRoutes } from " ./shared/constant" ;
503+ import _db from " ../_db" ;
504+ import createIntlMiddleware from " next-intl/middleware" ;
505+ import { redirect } from " next/dist/server/api-utils" ;
506+
507+ // This function can be marked `async` if using `await` inside
508+
509+ export default async function middleware(req : NextRequest ) {
510+ const [, locale, ... segments] = req .nextUrl .pathname .split (" /" );
511+ const path = req .nextUrl .pathname ;
512+
513+ // other middlewares
514+ if (locale != null ) {
515+ console .log (" [Middleware Demo] : " + req .url );
516+
517+ if (ProtectedRoutes .some ((route ) => path .startsWith (route ))) {
518+ // apply auth middleware
519+ const redirectResponse = authMiddleware (req );
520+ if (redirectResponse ) {
521+ return redirectResponse ;
522+ }
523+ }
524+ }
525+
526+ // next-intl middleware
527+ const handleI18nRouting = createIntlMiddleware ({
528+ locales: [" en" , " vi" ],
529+ defaultLocale: " en" ,
530+ localePrefix: " always" ,
531+ });
532+ const response = handleI18nRouting (req );
533+
534+ // fake login
535+ if (path == ` /${locale } ` ) {
536+ response .cookies .set (AppCookie .UserToken , _db .tokens [0 ].token );
537+ }
538+
539+ return response ;
540+ }
541+
542+ // See "Matching Paths" below to learn more
543+ export const config = {
544+ matcher: [
545+ // Paths for internationalization
546+ // "/",
547+ " /(en|vi)/:path*" ,
548+ /*
549+ * Match all request paths except for the ones starting with:
550+ * - _next/static (static files)
551+ * - _next/image (image optimization files)
552+ * - favicon.ico (favicon file)
553+ */
554+ {
555+ source: " /((?!_next/static|_next/image|favicon.ico).*)" ,
556+ missing: [
557+ { type: " header" , key: " next-router-prefetch" },
558+ { type: " header" , key: " purpose" , value: " prefetch" },
559+ ],
560+ },
561+
562+ {
563+ source: " /((?!_next/static|_next/image|favicon.ico).*)" ,
564+ has: [
565+ { type: " header" , key: " next-router-prefetch" },
566+ { type: " header" , key: " purpose" , value: " prefetch" },
567+ ],
568+ },
569+
570+ {
571+ source: " /((?!_next/static|_next/image|favicon.ico).*)" ,
572+ has: [{ type: " header" , key: " x-present" }],
573+ missing: [{ type: " header" , key: " x-missing" , value: " prefetch" }],
574+ },
575+ ],
576+ };
577+ ```
578+
579+ Sử dụng thử
580+
581+ ``` tsx
582+ // src/app/[locale]/page.tsx
583+ import { SlowComponent } from " @/components/SlowComponent" ;
584+ import Link from " next/link" ;
585+ import { Suspense } from " react" ;
586+ import Loading from " ./loading" ;
587+ import { useTranslations } from " next-intl" ;
588+ import { Footer } from " @/layouts/Footer" ;
589+
590+ export default function Home() {
591+ const t = useTranslations (" layout" );
592+
593+ return (
594+ <>
595+ <header className = " container-xl mx-auto p-4" >
596+ <h1 >{ t (" headerTitle" )} </h1 >
597+ </header >
598+ <main className = " container-xl mx-auto p-4" >
599+ <h1 >Home Page</h1 >
600+ <p >Links to other pages with a tag</p >
601+ <ul >
602+ <li >
603+ <a href = " /products" >Products</a >
604+ </li >
605+ <li >
606+ <a href = " /products/mouse-pad-nextjsvietnam" >
607+ Mouse Pad NextJSVietNam
608+ </a >
609+ </li >
610+ <li >
611+ <a href = " /cart" >Cart</a >
612+ </li >
613+ <li >
614+ <a href = " /order" >Order</a >
615+ </li >
616+ <li >
617+ <a href = " /my-account" >My Account</a >
618+ </li >
619+ <li >
620+ <a href = " /my-account/orders" >My orders</a >
621+ </li >
622+ <li >
623+ <a href = " /my-account/orders/1" >My order detail</a >
624+ </li >
625+ </ul >
626+ <p >Links to other pages with Link tag</p >
627+ <ul >
628+ <li >
629+ <Link href = " /products" >Products</Link >
630+ </li >
631+ </ul >
632+ <h2 >Slow Component</h2 >
633+ <Suspense fallback = { <Loading />} >
634+ <SlowComponent ></SlowComponent >
635+ </Suspense >
636+ </main >
637+ <Footer ></Footer >
638+ </>
639+ );
640+ }
641+ ```
642+
643+ Chuyển ngôn ngữ
644+
645+ ``` tsx
646+ // layouts/Footer
647+ " use client" ;
648+
649+ import { useRouter } from " next/navigation" ;
650+
651+ export const Footer = () => {
652+ const router = useRouter ();
653+
654+ const switchLanguage = (language : string ) => {
655+ router .push (language );
656+ };
657+ return (
658+ <footer >
659+ <div >
660+ <button onClick = { () => switchLanguage (" en" )} >English</button >
661+ <button onClick = { () => switchLanguage (" vi" )} >Vietnamese</button >
662+ </div >
663+ </footer >
664+ );
665+ };
666+ ```
0 commit comments