Skip to content

Commit 28812fe

Browse files
author
misostack
committed
i18n support
1 parent 854d77a commit 28812fe

File tree

1 file changed

+290
-0
lines changed

1 file changed

+290
-0
lines changed

content/post/khoa-hoc-nextjs-bai-03-advanced-routing.md

Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)