# Lỗi hydrat hóa xảy ra vì HTML được hiển thị trên máy chủ không khớp với HTML được hiển thị trên máy

Lỗi hydrat hóa xảy ra vì HTML được hiển thị trên máy chủ không khớp với HTML được hiển thị trên máy khách, cụ thể là do thuộc tính lang của phần tử không khớp. Điều này xảy ra vì bạn có hai tệp bố cục (src/app/layout.tsx và src/app/\[lang]/layout.tsx) đều hiển thị phần tử , nhưng chúng xử lý thuộc tính lang khác nhau.

Bước 1: Cập nhật src/app/layout.tsx\
Sửa đổi bố cục gốc để chấp nhận tham số lang và áp dụng nó vào thẻ . Vì bố cục gốc áp dụng cho tất cả các tuyến đường, bạn có thể đặt lang mặc định cho các tuyến đường không được bản địa hóa.

```typescript
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
const geistSans = Geist({
  variable: "--font-geist-sans",
  subsets: ["latin"],
});
const geistMono = Geist_Mono({
  variable: "--font-geist-mono",
  subsets: ["latin"],
});
export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};
export default function RootLayout({
  children,
  params,
}: Readonly<{
  children: React.ReactNode;
  params?: { lang?: string }; // Make params optional for non-localized routes
}>) {
  const lang = params?.lang || "en"; // Default to "en" if lang is not provided
  return (
    <html lang={lang}>
      <body
        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
      >
        {children}
      </body>
    </html>
  );
}
```

Bước 2: Cập nhật src/app/\[lang]/layout.tsx\
Xóa thẻ và khỏi bố cục \[lang], vì chúng chỉ nên được định nghĩa trong bố cục gốc. Bố cục \[lang] chỉ nên hiển thị nội dung nằm bên trong .

```typescript
export const supportedLocales = ["en", "vi"];
export function generateStaticParams() {
  return supportedLocales.map((lang) => ({ lang }));
}
export default function LangLayout({
  children,
  params,
}: {
  children: React.ReactNode;
  params: { lang: string };
}) {
  return (
    <>
      <header>
        <nav>
          <a href={`/${params.lang}`}>Home</a>
        </nav>
      </header>
      {children}
    </>
  );
}
```

Ghi chú bổ sung

Metadata:\
Nếu bạn cần đặt siêu dữ liệu cụ thể theo ngôn ngữ (ví dụ: tiêu đề hoặc mô tả) cho các tuyến đường được bản địa hóa, bạn có thể xác định hàm generateMetadata trong src/app/\[lang]/layout.tsx hoặc các trang riêng lẻ trong \[lang]. Ví dụ:

```typescript
export async function generateMetadata({
  params,
}: {
  params: { lang: string };
}): Promise<Metadata> {
  return {
    title: params.lang === "vi" ? "Ứng dụng Next" : "Next App",
    description:
      params.lang === "vi"
        ? "Được tạo bởi create next app"
        : "Generated by create next app",
  };
}
```

Để triển khai thành phần LanguageSwitcher trong src/app/\[lang]/page.tsx, bạn cần tích hợp thành phần này vào trang chủ và đảm bảo thành phần này xử lý đúng việc chuyển đổi ngôn ngữ cho các phiên bản đã dịch của trang chủ. Dựa trên thiết lập hiện tại của bạn, trang chủ được truy xuất bằng truy vấn GraphQL (GET\_HOMEPAGE) và bạn có thể muốn LanguageSwitcher điều hướng đến URL trang chủ đã dịch chính xác (ví dụ: /en hoặc /vi) khi chuyển đổi ngôn ngữ.

Thách thức ở đây là thành phần LanguageSwitcher mong đợi một prop translations chứa một mảng các đối tượng translation có thuộc tính uri và language.code, như được thấy trong trang bài đăng (src/app/\[lang]/post/\[slug]/page.tsx). Tuy nhiên, truy vấn GET\_HOMEPAGE có thể không cung cấp trường translations ở cùng định dạng hoặc có thể không bao gồm bản dịch nào cả. Bạn sẽ cần phải:

Lấy bản dịch trang chủ (nếu có) thông qua truy vấn GET\_HOMEPAGE.\
Truyền bản dịch đến LanguageSwitcher.\
Đảm bảo LanguageSwitcher xây dựng đúng URL cho trang chủ (ví dụ: /en hoặc /vi).\
Hãy triển khai từng bước này.

Giả định\
Truy vấn GET\_HOMEPAGE lấy nội dung trang chủ (data.page.title và data.page.content).\
Truy vấn có thể bao gồm trường dịch cho trang chủ, tương tự như truy vấn GET\_POST, với cấu trúc như sau:

```typescript
[
  {
    "uri": "/vi/",
    "language": { "code": "VI" }
  },
  {
    "uri": "/en/",
    "language": { "code": "EN" }
  }
]
```

Nếu trường bản dịch không khả dụng, URL trang chủ chỉ đơn giản là /en và /vi (vì trang chủ thường không có slug).\
LanguageSwitcher sẽ xử lý cả hai trường hợp (có hoặc không có bản dịch) và điều hướng đến URL trang chủ chính xác.

Bước 1: Cập nhật truy vấn GET\_HOMEPAGE\
Đảm bảo truy vấn GET\_HOMEPAGE bao gồm trường bản dịch để lấy các phiên bản đã dịch của trang chủ. Nếu bạn đang sử dụng CMS như WordPress với WPGraphQL, truy vấn có thể trông như thế này:

```graphql
query GetHomepage {
  page(id: "homepage", idType: URI) {
    title
    content
    translations {
      uri
      language {
        code
      }
    }
  }
}
```

Thay thế id: "homepage" và idType: URI bằng mã định danh phù hợp cho trang chủ của bạn trong CMS (ví dụ: ID trang cụ thể hoặc slug).\
Trường translations phải trả về một mảng các đối tượng translation với uri và language.code, tương tự như truy vấn GET\_POST.\
Nếu CMS của bạn không cung cấp trường translations cho trang chủ, bạn có thể tự xây dựng dữ liệu translation dựa trên supportedLocales (en và vi), vì URL trang chủ có thể dự đoán được (/en và /vi).

Bước 2: Cập nhật src/app/\[lang]/page.tsx\
Sửa đổi thành phần trang chủ để bao gồm LanguageSwitcher và truyền dữ liệu dịch (từ truy vấn GraphQL hoặc được xây dựng thủ công).

Sau đây là src/app/\[lang]/page.tsx đã cập nhật:

```typescript
import { createApolloClient } from "@/lib/apollo";
import { GET_HOMEPAGE } from "@/graphql/queries";
import LanguageSwitcher from "@/components/LanguageSwitcher";
import { supportedLocales } from "./layout"; // Import supportedLocales from layout.tsx
export default async function Home({ params }: { params: { lang: string } }) {
  const { lang } = await params;
  const client = createApolloClient(lang);
  const { data } = await client.query({ query: GET_HOMEPAGE });
  // If translations are not provided by the query, construct them manually
  const translations =
    data.page.translations && data.page.translations.length > 0
      ? data.page.translations
      : supportedLocales.map((locale) => ({
          uri: `/${locale}/`,
          language: { code: locale.toUpperCase() },
        }));

  return (
    <main>
      <LanguageSwitcher currentLang={lang} translations={translations} />
      <h1>{data.page.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: data.page.content }} />
    </main>
  );
}
```

Giải thích:

Lấy bản dịch:\
Truy vấn GET\_HOMEPAGE phải bao gồm trường bản dịch. Nếu nó trả về bản dịch (ví dụ: data.page.translations), hãy sử dụng trực tiếp.\
Bản dịch dự phòng:\
Nếu data.page.translations trống hoặc không được cung cấp, hãy xây dựng một mảng bản dịch dự phòng bằng supportedLocales (en và vi).\
Mỗi đối tượng bản dịch có uri là /${locale}/ (ví dụ: /en/ hoặc /vi/) và language.code viết hoa (EN hoặc VI) để khớp với định dạng truy vấn GET\_POST của bạn.\
Truy cập LanguageSwitcher:\
Truy cập currentLang={lang} (ví dụ: en hoặc vi) và bản dịch cho LanguageSwitcher.\
Nhập supportedLocales:\
Nhập supportedLocales từ src/app/\[lang]/layout.tsx để đảm bảo tính nhất quán với các ngôn ngữ được hỗ trợ của bạn.

Bước 3: Đảm bảo khả năng tương thích của LanguageSwitcher\
LanguageSwitcher (src/components/LanguageSwitcher.tsx) từ phản hồi trước đó đã được thiết kế để xử lý các bản dịch có giá trị uri như /vi/ và giá trị language.code viết hoa (VI). Tuy nhiên, hãy xác minh xem nó có hoạt động chính xác với trang chủ không.

Sau đây là mã LanguageSwitcher để tham khảo (không cần thay đổi nếu bạn đang sử dụng phiên bản đã sửa):

```typescript
"use client";
import { usePathname, useRouter } from "next/navigation";
type-lheTranslation = {
  uri: string;
  language: {
    code: string;
  };
};
type Props = {
  currentLang: string;
  translations: Translation[];
};
const supportedLocales = ["en", "vi"];
export default function LanguageSwitcher({ currentLang, translations }: Props) {
  const router = useRouter();
  const pathname = usePathname();

  const handleSwitch = (lang: string) => {
    if (lang === currentLang) return;

    // Find the translation for the target language (case-insensitive)
    const translation = translations.find(
      (t) => t.language.code.toLowerCase() === lang
    );
    if (translation) {
      // Strip the language prefix and trailing slash from the uri
      let uri = translation.uri;
      const langPrefix = `/${translation.language.code.toLowerCase()}/`;
      if (uri.startsWith(langPrefix)) {
        uri = uri.replace(langPrefix, "/");
      }
      // Remove trailing slash
      uri = uri.endsWith("/") ? uri.slice(0, -1) : uri;
      // Ensure uri starts with /
      uri = uri.startsWith("/") ? uri : `/${uri}`;
      router.push(`/${lang}${uri}`);
    } else {
      // Fallback: Replace the language prefix in the current pathname
      const newPath = pathname.replace(/^\/[^\/]+/, `/${lang}`);
      router.push(newPath);
    }
  };

  return (
    <div className="flex gap-2 text-sm">
      {supportedLocales.map((lang) => (
        <button
          key={lang}
          onClick={() => handleSwitch(lang)}
          className={`px-2 py-1 rounded ${
            lang === currentLang ? "bg-blue-600 text-white" : "bg-gray-200"
          }`}
        >
          {lang.toUpperCase()}
        </button>
      ))}
    </div>
  );
}
```

Cách thức hoạt động cho Trang chủ:

Đối với currentLang = 'en' và translations = \[{ uri: '/vi/', language: { code: 'VI' } }, { uri: '/en/', language: { code: 'EN' } }], nhấp vào nút "VI":\
Tìm bản dịch trong đó language.code.toLowerCase() === 'vi' (khớp với VI).\
Lấy uri = '/vi/' và xóa tiền tố ngôn ngữ (/vi/), tạo thành chuỗi rỗng ('').\
Xóa dấu gạch chéo cuối, vẫn tạo thành ''.\
Xây dựng URL dưới dạng /vi + '' = /vi.\
Điều hướng đến /vi.

Bước 4: Kiểm tra việc triển khai\
Chạy ứng dụng:\
Khởi động ứng dụng Next.js của bạn (npm run dev) và điều hướng đến <http://localhost:3000/en>.\
Kiểm tra việc chuyển đổi ngôn ngữ:\
Kiểm tra xem LanguageSwitcher có xuất hiện trên trang chủ với các nút "EN" và "VI" không.\
Nhấp vào nút "VI" và đảm bảo nó chuyển hướng đến <http://localhost:3000/vi>.\
Kiểm tra xem nội dung trang chủ (tiêu đề và nội dung) có cập nhật lên phiên bản tiếng Việt không.\
Kiểm tra ngược lại (chuyển từ /vi sang /en).

**Debug Translations**:

* Add a console.log in page.tsx to inspect the translations data:

```
const { data } = await client.query({ query: GET_HOMEPAGE });
console.log("Homepage Translations:", data.page.translations);
```

Check the console output to confirm the translations structure. If it’s empty, the fallback logic (supportedLocales.map) will be used.

Kiểm tra lỗi:\
Đảm bảo không có lỗi nào trong bảng điều khiển trình duyệt hoặc thiết bị đầu cuối khi chuyển đổi ngôn ngữ.\
Nếu dữ liệu bản dịch có định dạng không mong muốn (ví dụ: thiếu uri hoặc language.code), hãy điều chỉnh logic dự phòng hoặc truy vấn GraphQL cho phù hợp.

Bước 5: Xử lý các trường hợp ngoại lệ\
Không có bản dịch trong GET\_HOMEPAGE:\
Nếu data.page.translations trống hoặc không được cung cấp, mảng bản dịch dự phòng (supportedLocales.map(...)) đảm bảo LanguageSwitcher vẫn hoạt động bằng cách điều hướng đến /en hoặc /vi.\
Xác minh rằng bản dự phòng này hoạt động bằng cách tạm thời xóa trường bản dịch khỏi phản hồi truy vấn GET\_HOMEPAGE (ví dụ: mô phỏng phản hồi trống).\
Định dạng uri không mong muốn:\
Nếu uri trong bản dịch không phải là /vi/ (ví dụ: là / hoặc trang chủ), hãy điều chỉnh LanguageSwitcher để xử lý:

```typescript
if (translation) {
  let uri = translation.uri;
  const langPrefix = `/${translation.language.code.toLowerCase()}/`;
  if (uri.startsWith(langPrefix)) {
    uri = uri.replace(langPrefix, "/");
  }
  // Handle special cases like uri = '/' or 'homepage'
  uri = uri === "/" || uri === "/homepage" ? "" : uri;
  uri = uri.endsWith("/") ? uri.slice(0, -1) : uri;
  uri = uri.startsWith("/") ? uri : `/${uri}`;
  router.push(`/${lang}${uri}`);
}
```

Tạo tĩnh:\
Trang chủ (src/app/\[lang]/page.tsx) nằm trong một tuyến động (\[lang]) và layout.tsx của bạn đã định nghĩa generateStaticParams để tạo đường dẫn tĩnh cho /en và /vi. Điều này đủ cho trang chủ, vì vậy không cần thay đổi trừ khi bạn thêm nhiều tham số động hơn.\
Cấu hình CMS:\
Nếu truy vấn GET\_HOMEPAGE trả về các giá trị uri không có tiền tố ngôn ngữ (ví dụ: / thay vì /vi/), bạn có thể cần điều chỉnh LanguageSwitcher để bỏ qua logic loại bỏ tiền tố cho trang chủ.\
Nếu language.code là chữ thường (vi thay vì VI), hãy xóa .toUpperCase() trong cấu trúc bản dịch dự phòng trong page.tsx.

Bước 6: Các cải tiến tùy chọn\
Tắt các nút cho bản dịch bị thiếu:\
Nếu một số ngôn ngữ không có bản dịch trang chủ, hãy tắt các nút tương ứng trong LanguageSwitcher (như đã hiển thị trong các phản hồi trước):

```typescript
return (
  <div className="flex gap-2 text-sm">
    {supportedLocales.map((lang) => {
      const hasTranslation = translations.some(
        (t) => t.language.code.toLowerCase() === lang
      );
      return (
        <button
          key={lang}
          onClick={() => handleSwitch(lang)}
          disabled={!hasTranslation}
          className={`px-2 py-1 rounded ${
            lang === currentLang
              ? "bg-blue-600 text-white"
              : hasTranslation
              ? "bg-gray-200"
              : "bg-gray-100 text-gray-400 cursor-not-allowed"
          }`}
        >
          {lang.toUpperCase()}
        </button>
      );
    })}
  </div>
);
```

SEO và Siêu dữ liệu:\
Thêm siêu dữ liệu dành riêng cho ngôn ngữ cho trang chủ bằng cách xác định hàm generateMetadata trong page.tsx:

```typescript
import { Metadata } from "next";

export async function generateMetadata({
  params,
}: {
  params: { lang: string };
}): Promise<Metadata> {
  const { lang } = await params;
  const client = createApolloClient(lang);
  const { data } = await client.query({ query: GET_HOMEPAGE });

  return {
    title: data.page.title,
    description: data.page.content.slice(0, 160), // Example: Use first 160 chars of content
    alternates: {
      languages: {
        en: "/en",
        vi: "/vi",
      },
    },
  };
}
```

Điều này cải thiện SEO bằng cách đặt tiêu đề trang và cung cấp URL thay thế cho mỗi ngôn ngữ.
