👏Triển khai i18next ở pages (ok)

Đây là source code full nextbrand-example-full-language-pages-to-en

Bổ sung: Chắc chắn! Dưới đây là cách bạn chuyển getStaticProps sang getServerSideProps khi dùng next-i18next để dịch đa ngôn ngữ cho các trang như /[lang]/about.

✅ Mã mẫu getServerSideProps thay cho getStaticProps

tsxCopyEdit// pages/[lang]/about.tsx
import { GetServerSideProps } from 'next';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import i18nextConfig from '../../../next-i18next.config.js';
import { useTranslation } from 'react-i18next';

export default function About() {
  const { t } = useTranslation();

  return (
    <div className="p-4">
      <h1 className="text-2xl font-bold">{t('about_title')}</h1>
      <p className="mt-2">{t('about_content')}</p>
    </div>
  );
}

export const getServerSideProps: GetServerSideProps = async ({ params }) => {
  const locale = params?.lang as string;

  return {
    props: {
      ...(await serverSideTranslations(locale, ['translation'], i18nextConfig)),
    },
  };
};

🔁 So sánh nhanh:

Phương thức

getStaticProps

getServerSideProps

Thời điểm chạy

Build-time

Request-time (mỗi lần truy cập)

Hữu ích khi

Nội dung tĩnh, không đổi nhiều

Cần ngôn ngữ động, dữ liệu luôn cập nhật

Khả năng dịch động tốt?

Có, nhưng cần build lại khi thêm bản dịch

✅ Tốt hơn cho site có i18n động

Sử dụng được với output: 'export'?

✅ Có (tĩnh)

❌ Không dùng được với output: 'export'


⚠️ Lưu ý quan trọng

Nếu bạn đang dùng output: 'export' trong next.config.js, bạn không thể dùng getServerSidePropsNext.js không hỗ trợ SSR khi export static site.

=> Nếu bạn dùng output: 'export', hãy dùng getStaticProps.


👉 Bạn có muốn mình giúp cập nhật lại toàn bộ cấu trúc getStaticProps -> getServerSideProps cho tất cả các trang trong project của bạn (và bỏ output: 'export') không?

Step 1: Cấu hình ở client kết với SSG (cách này hay được sử dụng)

src\components\LanguageSwitcher.tsx

'use client';
import { useRouter } from 'next/router';
import i18nextConfig from '../../next-i18next.config';
import { useEffect, useState } from 'react';
const LanguageSwitcher = () => {
  const router = useRouter();
  const [currentLang, setCurrentLang] = useState<string | null>(null);
  useEffect(() => {
    if (router.isReady && typeof router.query.lang === 'string') {
      setCurrentLang(router.query.lang);
    }
  }, [router]);
  if (!currentLang) return null;
  const handleChangeLanguage = (lng: string) => {
    if (lng === currentLang) return;
    const newPath = router.asPath.replace(`/${currentLang}`, `/${lng}`);
    router.push(newPath);
  };
  return (
    <div className="flex gap-2 mt-4">
      {i18nextConfig.i18n.locales.map((lng) => (
        <button
          key={lng}
          onClick={() => handleChangeLanguage(lng)}
          className={`px-3 py-1 rounded ${
            lng === currentLang ? 'bg-blue-600 text-white' : 'bg-gray-200'
          }`}
        >
          {lng.toUpperCase()}
        </button>
      ))}
    </div>
  );
};
export default LanguageSwitcher;

src\components\Layout.tsx

import { ReactNode } from 'react';
import LanguageSwitcher from './LanguageSwitcher';
export default function Layout({ children }: { children: ReactNode }) {
  return (
    <div className="min-h-screen flex flex-col">
      <LanguageSwitcher />
      <main className="flex-1">{children}</main>
    </div>
  );
}

src\pages[lang]\about.tsx

import { GetStaticPaths, GetStaticProps } from 'next';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import { useTranslation } from 'next-i18next';
import Layout from '@/components/Layout';
import i18nextConfig from '../../../next-i18next.config';
export default function About() {
  const { t } = useTranslation('common');
  return (
    <Layout>
      <div className="p-4">
        <h1 className="text-2xl font-bold">{t('about_title')}</h1>
        <p className="mt-2">{t('about_content')}</p>
      </div>
    </Layout>
  );
}
export const getStaticPaths: GetStaticPaths = async () => {
  return {
    paths: ['en', 'vi'].map((lang) => ({ params: { lang } })),
    fallback: false,
  };
};
export const getStaticProps: GetStaticProps = async ({ params }) => {
  const locale = params?.lang as string;
  return {
    props: {
      ...(await serverSideTranslations(locale, ['common'], i18nextConfig)),
    },
  };
};

src\pages[lang]\index.tsx

import { GetStaticPaths, GetStaticProps } from 'next';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import { useTranslation } from 'react-i18next';
import Link from 'next/link';
import i18nextConfig from '../../../next-i18next.config';
export default function Home() {
  const { t } = useTranslation();
  return (
    <div className="p-4">
      <h1 className="text-2xl font-bold mb-2">{t('welcome')}</h1>
      <p>{t('description')}</p>
      <div className="mt-4 space-x-4">
        <Link href="/en/about" className="text-blue-500 underline">About (EN)</Link>
        <Link href="/vi/about" className="text-blue-500 underline">About (VI)</Link>
      </div>
    </div>
  );
}
export const getStaticPaths: GetStaticPaths = async () => {
  return {
    paths: ['en', 'vi'].map((lang) => ({ params: { lang } })),
    fallback: false,
  };
};
export const getStaticProps: GetStaticProps = async ({ params }) => {
  const locale = params?.lang as string;
  return {
    props: {
      ...(await serverSideTranslations(locale, ['translation'], i18nextConfig)),
    },
  };
};

src\pages\_app.tsx

import { appWithTranslation } from 'next-i18next';
import type { AppProps } from 'next/app';
import '../styles/globals.css';
function App({ Component, pageProps }: AppProps) {
  return <Component {...pageProps} />;
}
export default appWithTranslation(App);

next-i18next.config.js

// next-i18next.config.js
const path = require('path');
/** @type {import('next-i18next').UserConfig} */
module.exports = {
  i18n: {
    defaultLocale: 'en',
    locales: ['en', 'vi'],
  },
  localeDetection: false,
  localePath: path.resolve('./public/locales'),
};

next.config.js

module.exports = {
  reactStrictMode: true,
  // output: 'export',
  images: {
    unoptimized: true,
  },
};

package.json

{
  "name": "next-i18next",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev --turbopack",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "i18next": "^25.1.3",
    "next": "15.3.2",
    "next-i18next": "^15.4.2",
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    "react-i18next": "^15.5.1"
  },
  "devDependencies": {
    "@tailwindcss/postcss": "^4",
    "@types/node": "^20",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "tailwindcss": "^4",
    "typescript": "^5"
  }
}

public\locales\en\common.json

{
  "welcome": "Welcome to my Next.js app!",
  "description": "This is a multilingual app using i18next.",
  "about_title": "About Us",
  "about_content": "This is the About page in English."
}
{
  "welcome": "Chào mừng đến với ứng dụng Next.js của tôi!",
  "description": "Đây là một ứng dụng đa ngôn ngữ sử dụng i18next.",
  "about_title": "Giới thiệu",
  "about_content": "Đây là trang giới thiệu bằng tiếng Việt."
}

— Cách khai báo Layout cho tất cả các trang không, không phải thủ công cho mỗi trang about.tsx, contact.tsx

src\components\Footer.tsx

import React from 'react'
export default function Footer() {
  return (
    <>
    Footer
    </>
  )
}

src\components\LanguageSwitcher.tsx

'use client';
import { useRouter } from 'next/router';
import i18nextConfig from '../../next-i18next.config';
import { useEffect, useState } from 'react';
const LanguageSwitcher = () => {
  const router = useRouter();
  const [currentLang, setCurrentLang] = useState<string | null>(null);
  useEffect(() => {
    if (router.isReady && typeof router.query.lang === 'string') {
      setCurrentLang(router.query.lang);
    }
  }, [router]);
  if (!currentLang) return null;
  const handleChangeLanguage = (lng: string) => {
    if (lng === currentLang) return;
    const newPath = router.asPath.replace(`/${currentLang}`, `/${lng}`);
    router.push(newPath);
  };
  return (
    <div className="flex gap-2 mt-4">
      {i18nextConfig.i18n.locales.map((lng) => (
        <button
          key={lng}
          onClick={() => handleChangeLanguage(lng)}
          className={`px-3 py-1 rounded ${
            lng === currentLang ? 'bg-blue-600 text-white' : 'bg-gray-200'
          }`}
        >
          {lng.toUpperCase()}
        </button>
      ))}
    </div>
  );
};
export default LanguageSwitcher;

src\components\Layout.tsx

import { ReactNode } from 'react';
import LanguageSwitcher from './LanguageSwitcher';
// src/components/layout/Layout.tsx
import React from 'react';
import Navbar from './Navbar';
import Footer from './Footer';
type Props = {
  children: React.ReactNode;
};
export default function Layout({ children }: Props) {
  return (
    <div className="flex flex-col min-h-screen">
      <Navbar />
      <LanguageSwitcher />
      <main className="flex-1">{children}</main>
      <Footer />
    </div>
  );
}

src\components\Navbar.tsx

import React from 'react'
export default function Navbar() {
  return (
    <>
      Navbar
    </>
  )
}

src\pages[lang]\about.tsx

import { GetStaticPaths, GetStaticProps } from 'next';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import { useTranslation } from 'next-i18next';
import i18nextConfig from '../../../next-i18next.config';
export default function About() {
  const { t } = useTranslation('common');
  return (
    <div className="p-4">
      <h1 className="text-2xl font-bold">{t('about_title')}</h1>
      <p className="mt-2">{t('about_content')}</p>
    </div>
  );
}
export const getStaticPaths: GetStaticPaths = async () => {
  return {
    paths: ['en', 'vi'].map((lang) => ({ params: { lang } })),
    fallback: false,
  };
};
export const getStaticProps: GetStaticProps = async ({ params }) => {
  const locale = params?.lang as string;
  return {
    props: {
      ...(await serverSideTranslations(locale, ['common'], i18nextConfig)),
    },
  };
};

src\pages[lang]\index.tsx

import { GetStaticPaths, GetStaticProps } from 'next';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import { useTranslation } from 'react-i18next';
import Link from 'next/link';
import i18nextConfig from '../../../next-i18next.config';
export default function Home() {
  const { t } = useTranslation();
  return (
    <div className="p-4">
      <h1 className="text-2xl font-bold mb-2">{t('welcome')}</h1>
      <p>{t('description')}</p>
      <div className="mt-4 space-x-4">
        <Link href="/en/about" className="text-blue-500 underline">About (EN)</Link>
        <Link href="/vi/about" className="text-blue-500 underline">About (VI)</Link>
      </div>
    </div>
  );
}
export const getStaticPaths: GetStaticPaths = async () => {
  return {
    paths: ['en', 'vi'].map((lang) => ({ params: { lang } })),
    fallback: false,
  };
};
export const getStaticProps: GetStaticProps = async ({ params }) => {
  const locale = params?.lang as string;
  return {
    props: {
      ...(await serverSideTranslations(locale, ['translation'], i18nextConfig)),
    },
  };
};

src\pages\_app.tsx

import { appWithTranslation } from 'next-i18next';
import type { AppProps } from 'next/app';
import '../styles/globals.css';
import Layout from '@/components/Layout';
function App({ Component, pageProps }: AppProps) {
  return <Layout><Component {...pageProps} /></Layout>;
}
export default appWithTranslation(App);

src\pages\_document.tsx

import { Html, Head, Main, NextScript } from "next/document";
export default function Document() {
  return (
    <Html lang="en">
      <Head />
      <body className="antialiased">
        <Main />
        <NextScript />
      </body>
    </Html>
  );
}

Step 2: Cấu hình ở client

next.config.ts

// next.config.js
const nextConfig = {
  reactStrictMode: true,
  // output: 'export', // vẫn giữ xuất tĩnh
  images: {
    unoptimized: true,
    remotePatterns: [
      {
        protocol: 'https',
        hostname: '**',
      },
    ],
  },
};
module.exports = nextConfig;


next-i18next.config.js

// next-i18next.config.js
module.exports = {
  i18n: {
    defaultLocale: 'en',
    locales: ['en', 'vi'],
    localeDetection: true
  }
};

src\pages\_app.tsx

import "@/styles/globals.css";
import { I18nextProvider } from 'react-i18next';
import i18n from '@/lib/i18n';
import type { AppProps } from "next/app";
import { Layout } from "@/components/layout/Layout"
import { useEffect, useState } from "react";
export default function App({ Component, pageProps }: AppProps) {
  const [i18nInstance, setI18nInstance] = useState(i18n);
  // Ensure i18n is only initialized on the client side
  useEffect(() => {
    setI18nInstance(i18n);
  }, []);
  return (
    <I18nextProvider i18n={i18nInstance}>
      <Layout>
        <Component {...pageProps} />
      </Layout>
    </I18nextProvider>
  );
}

src\pages[lang]\about.tsx

import { useRouter } from 'next/router';
import { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import Head from 'next/head';
export default function About() {
  const { t, i18n } = useTranslation();
  const router = useRouter();
  const { lang } = router.query;
  useEffect(() => {
    if (typeof lang === 'string') {
      i18n.changeLanguage(lang);
    }
  }, [lang]);
  return (
    <div className="container mx-auto p-4">
      <Head>
        <title>{t('about_title')}</title>
        <meta name="description" content={t('about_content')} />
      </Head>
      <h1 className="text-3xl font-bold mb-4">{t('about_title')}</h1>
      <p>{t('about_content')}</p>
    </div>
  );
}

src\locales\en\translation.json

{
  "welcome": "Welcome to my Next.js app!",
  "description": "This is a multilingual app using i18next.",
  "about_title": "About Us",
  "about_content": "This page provides information about our application."
}
{
  "welcome": "Chào mừng đến với ứng dụng Next.js của tôi!",
  "description": "Đây là một ứng dụng đa ngôn ngữ sử dụng i18next.",
  "about_title": "Về chúng tôi",
  "about_content": "Trang này cung cấp thông tin về ứng dụng của chúng tôi."
}

src\lib\i18n\index.ts

// src/lib/i18n.ts
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import HttpBackend from 'i18next-http-backend';
import LanguageDetector from 'i18next-browser-languagedetector';
i18n
  .use(HttpBackend)
  .use(LanguageDetector)
  .use(initReactI18next)
  .init({
    fallbackLng: 'en',
    supportedLngs: ['en', 'vi'],
    ns: ['translation'],
    defaultNS: 'translation',
    interpolation: {
      escapeValue: false,
    },
    backend: {
      loadPath: '/locales/{{lng}}/{{ns}}.json',
    },
  });
export default i18n;

src\components\LanguageSwitcher.tsx

// components/LanguageSwitcher.tsx
import { useRouter } from 'next/router';
const LanguageSwitcher = () => {
  const router = useRouter();
  const { pathname, asPath, query } = router;
  const changeLanguage = (lang: string) => {
    // Current path: /[lang]/about → split to ['', 'en', 'about']
    const segments = asPath.split('/');
    if (segments.length > 1) {
      segments[1] = lang; // change lang segment
    }
    const newPath = segments.join('/');
    router.push(newPath);
  };
  return (
    <div className="space-x-2">
      <button
        onClick={() => changeLanguage('en')}
        className="px-4 py-2 bg-blue-1 text-white rounded hover:bg-blue-1"
      >
        English
      </button>
      <button
        onClick={() => changeLanguage('vi')}
        className="px-4 py-2 bg-blue-1 text-white rounded hover:bg-blue-1"
      >
        Tiếng Việt
      </button>
    </div>
  );
};
export default LanguageSwitcher;

src\components\layout\Layout.tsx

import { useTranslation } from 'react-i18next';
import Navbar from "./Navbar";
import Footer from './Footer';
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
interface LayoutProps {
  children: React.ReactNode
}
export function Layout({ children }: LayoutProps) {
  const { t, i18n } = useTranslation();
  const router = useRouter();
  const { lang } = router.query;
  const [ready, setReady] = useState(false);
  useEffect(() => {
    if (typeof lang === 'string') {
      i18n.changeLanguage(lang).then(() => {
        setReady(true);
      });
    } else {
      setReady(true); // fallback nếu không có lang
    }
  }, [lang]);
  if (!ready) return null; // hoặc loading spinner
  return (
    <div className="min-h-screen flex flex-col">
      <Navbar />
      <p className="mb-4">{t('description')}</p>
      <div className="flex-1">{children}</div>
      <Footer />
    </div>
  )
}

Để tạo một trang About.tsx tại đường dẫn src/pages/[lang]/About.tsx có hỗ trợ đa ngôn ngữ với i18next trong Next.js, bạn cần thực hiện các bước sau:


✅ 1. Cập nhật file dịch

src/locales/en/translation.json:

jsonCopyEdit{
  "welcome": "Welcome to my Next.js app!",
  "description": "This is a multilingual app using i18next.",
  "about_title": "About Us",
  "about_content": "This page provides information about our application."
}

src/locales/vi/translation.json:

jsonCopyEdit{
  "welcome": "Chào mừng đến với ứng dụng Next.js của tôi!",
  "description": "Đây là một ứng dụng đa ngôn ngữ sử dụng i18next.",
  "about_title": "Về chúng tôi",
  "about_content": "Trang này cung cấp thông tin về ứng dụng của chúng tôi."
}

✅ 2. Tạo file src/pages/[lang]/About.tsx

tsxCopyEditimport { useRouter } from 'next/router';
import { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import Head from 'next/head';

export default function About() {
  const { t, i18n } = useTranslation();
  const router = useRouter();
  const { lang } = router.query;

  useEffect(() => {
    if (typeof lang === 'string') {
      i18n.changeLanguage(lang);
    }
  }, [lang]);

  return (
    <div className="container mx-auto p-4">
      <Head>
        <title>{t('about_title')}</title>
        <meta name="description" content={t('about_content')} />
      </Head>
      <h1 className="text-3xl font-bold mb-4">{t('about_title')}</h1>
      <p>{t('about_content')}</p>
    </div>
  );
}

✅ 3. (Tuỳ chọn) Cấu hình route fallback

Nếu bạn không muốn tạo từng file như [lang]/About.tsx, có thể dùng getStaticPathsgetStaticProps để tạo trang động cho các ngôn ngữ, nhưng ở đây bạn đã đi theo hướng thư mục [lang], thì chỉ cần đảm bảo đường dẫn như /en/About, /vi/About là hợp lệ.


✅ 4. (Gợi ý) Tạo điều hướng giữa các ngôn ngữ

Bạn có thể thêm các nút chuyển ngôn ngữ như sau trong trang About:

tsxCopyEdit<div className="space-x-2 mt-4">
  <button
    className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
    onClick={() => router.push(`/en/About`)}
  >
    English
  </button>
  <button
    className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
    onClick={() => router.push(`/vi/About`)}
  >
    Tiếng Việt
  </button>
</div>

✅ Kết luận

Giờ bạn đã có trang About hỗ trợ đa ngôn ngữ với URL rõ ràng như:

  • http://localhost:3000/en/About

  • http://localhost:3000/vi/About

Nếu bạn cần giúp tạo thêm cấu trúc tự động với getStaticPaths, getStaticProps hoặc next-i18next, mình cũng có thể hỗ trợ.

4o

You said:

Lỗi không dịch vẫn là about_title và about_content

ChatGPT said:

Cảm ơn bạn đã cung cấp chi tiết. Nếu trong trang /[lang]/About.tsx bạn thấy nội dung vẫn hiện là "about_title""about_content" thay vì nội dung đã dịch, thì có thể do một trong các nguyên nhân sau. Mình sẽ hướng dẫn bạn từng bước để khắc phục:


✅ 1. Đảm bảo i18n đã load đúng ngôn ngữ và namespace

Bạn cần chắc chắn rằng i18n đã load được file dịch khi trang được render. Trong setup hiện tại, nếu bạn dùng react-i18nextkhông khai báo namespace hoặc không bật chế độ fallback, thì đôi khi nó không tìm thấy translation.json.


✅ 2. Kiểm tra lại file i18n.ts trong lib/i18n.ts

Dưới đây là một cấu hình mẫu đầy đủ để bạn đối chiếu:

tsCopyEdit// src/lib/i18n.ts
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import HttpBackend from 'i18next-http-backend';
import LanguageDetector from 'i18next-browser-languagedetector';

i18n
  .use(HttpBackend)
  .use(LanguageDetector)
  .use(initReactI18next)
  .init({
    fallbackLng: 'en',
    supportedLngs: ['en', 'vi'],
    ns: ['translation'],
    defaultNS: 'translation',
    interpolation: {
      escapeValue: false,
    },
    backend: {
      loadPath: '/locales/{{lng}}/{{ns}}.json',
    },
  });

export default i18n;

✅ Đảm bảo: /public/locales/en/translation.json/public/locales/vi/translation.json tồn tại (not src/locales/...) nếu dùng i18next-http-backend.


✅ 3. Đặt lại file dịch vào thư mục public/locales

Chuyển 2 file dịch sang:

swiftCopyEdit/public/locales/en/translation.json
/public/locales/vi/translation.json

i18next sẽ tự động tải các file này theo đường dẫn bạn cấu hình ở loadPath.


✅ 4. Bật debug để kiểm tra lỗi

Trong init của i18n, bật debug: true để xem i18next có log gì bất thường không:

tsCopyEditinit({
  // ...
  debug: true,
  // ...
})

Sau đó mở DevTools console để xem có thông báo nào như "missing translation" không.


✅ 5. Thêm kiểm tra namespace thủ công (tuỳ chọn)

Trong About.tsx, bạn có thể thử gọi:

tsCopyEditconst { t, i18n, ready } = useTranslation('translation');

và kiểm tra trong JSX:

tsxCopyEdit{ready ? (
  <>
    <h1>{t('about_title')}</h1>
    <p>{t('about_content')}</p>
  </>
) : (
  <p>Loading...</p>
)}

✅ Kết luận

Tóm tắt lại để sửa lỗi:

  • ✅ Đảm bảo các file dịch nằm trong public/locales/en/translation.jsonpublic/locales/vi/translation.json.

  • ✅ Cấu hình i18n.ts đúng với loadPath trỏ về /locales/{{lng}}/{{ns}}.json.

  • ✅ Sử dụng useTranslation('translation') nếu dùng namespace mặc định.


Nếu bạn chia sẻ file i18n.ts, mình có thể giúp rà lỗi kỹ hơn. Bạn cũng có thể gửi ảnh console/log nếu vẫn không dịch được.

4o

You said:

Tạo điều hướng giữa các ngôn ngữ động ví dụ trang about, contact

ChatGPT said:

Để tạo điều hướng giữa các ngôn ngữ một cách động trong một ứng dụng Next.js đa ngôn ngữ sử dụng i18next, ví dụ cho các trang như /[lang]/about/[lang]/contact, bạn có thể làm theo cách dưới đây:


✅ Mục tiêu

Ví dụ bạn đang ở:

  • http://localhost:3000/en/about → Chuyển sang http://localhost:3000/vi/about

  • http://localhost:3000/vi/contact → Chuyển sang http://localhost:3000/en/contact

➕ Yêu cầu:

  • Đường dẫn vẫn giữ nguyên trang (about, contact)

  • Chỉ đổi phần ngôn ngữ (envi)


✅ 1. Tạo điều hướng đổi ngôn ngữ động

Thêm component nút đổi ngôn ngữ có logic lấy tên trang hiện tại và thay lang:

tsxCopyEdit// components/LanguageSwitcher.tsx
import { useRouter } from 'next/router';

const LanguageSwitcher = () => {
  const router = useRouter();
  const { pathname, asPath, query } = router;

  const changeLanguage = (lang: string) => {
    // Current path: /[lang]/about → split to ['', 'en', 'about']
    const segments = asPath.split('/');
    if (segments.length > 1) {
      segments[1] = lang; // change lang segment
    }
    const newPath = segments.join('/');
    router.push(newPath);
  };

  return (
    <div className="space-x-2 mt-4">
      <button
        onClick={() => changeLanguage('en')}
        className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
      >
        English
      </button>
      <button
        onClick={() => changeLanguage('vi')}
        className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
      >
        Tiếng Việt
      </button>
    </div>
  );
};

export default LanguageSwitcher;

✅ 2. Sử dụng trong các trang như About, Contact

Ví dụ src/pages/[lang]/about.tsx:

tsxCopyEditimport { useRouter } from 'next/router';
import { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import Head from 'next/head';
import LanguageSwitcher from '@/components/LanguageSwitcher';

export default function About() {
  const { t, i18n } = useTranslation();
  const router = useRouter();
  const { lang } = router.query;

  useEffect(() => {
    if (typeof lang === 'string') {
      i18n.changeLanguage(lang);
    }
  }, [lang]);

  return (
    <div className="container mx-auto p-4">
      <Head>
        <title>{t('about_title')}</title>
        <meta name="description" content={t('about_content')} />
      </Head>
      <h1 className="text-3xl font-bold mb-4">{t('about_title')}</h1>
      <p>{t('about_content')}</p>
      <LanguageSwitcher />
    </div>
  );
}

Làm tương tự với contact.tsx.


✅ 3. Đảm bảo các route có dạng [lang]/about, [lang]/contact

  • File src/pages/[lang]/about.tsx

  • File src/pages/[lang]/contact.tsx


✅ 4. Thêm từ khóa vào file dịch

jsonCopyEdit// en/translation.json
{
  "about_title": "About Us",
  "about_content": "This page provides information about our application.",
  "contact_title": "Contact Us",
  "contact_content": "Feel free to reach out to us."
}

// vi/translation.json
{
  "about_title": "Về chúng tôi",
  "about_content": "Trang này cung cấp thông tin về ứng dụng của chúng tôi.",
  "contact_title": "Liên hệ",
  "contact_content": "Hãy liên hệ với chúng tôi nếu bạn cần hỗ trợ."
}

✅ Kết quả

  • Bạn đang ở /en/about → Click "Tiếng Việt" → /vi/about

  • Bạn đang ở /vi/contact → Click "English" → /en/contact


Nếu bạn muốn thêm hỗ trợ chuyển đổi ngôn ngữ toàn cục hoặc làm thêm route rewrite, mình có thể hỗ trợ tiếp.

4o

You said:

src\components\layout\Layout.tsx import { useTranslation } from 'react-i18next'; import Navbar from "./Navbar"; import Footer from './Footer'; interface LayoutProps { children: React.ReactNode } export function Layout({ children }: LayoutProps) { const { t, i18n } = useTranslation(); const changeLanguage = (lng: string) => { i18n.changeLanguage(lng); }; return ( <div className="min-h-screen flex flex-col"> <p className="mb-4">{t('description')}</p> <div className="flex-1">{children}</div> </div> ) } Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client. This can happen if a SSR-ed Client Component used: This is a multilingual app using i18next. description

ChatGPT said:

Lỗi bạn đang gặp:

rustCopyEditHydration failed because the server rendered HTML didn't match the client.

❗️Nghĩa là nội dung được render ở server khác với nội dung render ở client → điều này thường xảy ra với i18next vì nó không đồng bộ ngôn ngữ khi render SSR (server-side render).


🎯 Nguyên nhân chính

i18next khi dùng với react-i18next không tự động đồng bộ ngôn ngữ với router param ([lang]) trong SSR. Kết quả là:

  • Server có thể render bằng tiếng Anh mặc định (en)

  • Client sau khi load thấy phải là tiếng Việt (vi) → gây "Hydration mismatch"


✅ Cách sửa đơn giản (cho Layout.tsx)

➤ 1. Đợi đến khi ngôn ngữ được đồng bộ mới render

Thêm logic kiểm tra ngôn ngữ:

tsxCopyEditimport { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import Navbar from "./Navbar";
import Footer from './Footer';
import { useRouter } from 'next/router';

interface LayoutProps {
  children: React.ReactNode
}

export function Layout({ children }: LayoutProps) {
  const { t, i18n } = useTranslation();
  const router = useRouter();
  const { lang } = router.query;
  const [ready, setReady] = useState(false);

  useEffect(() => {
    if (typeof lang === 'string') {
      i18n.changeLanguage(lang).then(() => {
        setReady(true);
      });
    } else {
      setReady(true); // fallback nếu không có lang
    }
  }, [lang]);

  if (!ready) return null; // hoặc loading spinner

  return (
    <div className="min-h-screen flex flex-col">
      <p className="mb-4">{t('description')}</p>
      <div className="flex-1">{children}</div>
    </div>
  )
}

✅ Mẹo: sử dụng suppressHydrationWarning nếu cần (chỉ tạm thời)

Thêm thuộc tính suppressHydrationWarning vào phần tử văn bản có thể mismatch:

tsxCopyEdit<p suppressHydrationWarning>{t('description')}</p>

⚠️ Cảnh báo: Cách này chỉ giấu lỗi, không sửa triệt để. Cách đúng là đồng bộ ngôn ngữ trước khi render.


✅ Giải pháp nâng cao (nếu muốn làm chuẩn SSR với i18n)

Bạn có thể:

  • Dùng next-i18next (hỗ trợ SSR out of the box)

  • Tự setup getServerSideProps hoặc getStaticProps và truyền i18n config

Mình có thể hướng dẫn nếu bạn muốn chuyển sang next-i18next.


✅ Kết luận

Bạn chỉ cần đảm bảo:

  1. Layout.tsx đợi ngôn ngữ đồng bộ xong rồi mới render → tránh mismatch

  2. Không render t('...') khi i18n.language chưa được thiết lập đúng


Bạn muốn mình hướng dẫn cách chuyển sang next-i18next hoặc làm layout dùng SSR chuẩn không?

Last updated

Was this helpful?