👏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
.
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
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 getServerSideProps
vì Next.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
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 getStaticPaths
và getStaticProps
để 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"
và "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-i18next
mà khô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
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
và/public/locales/vi/translation.json
tồn tại (notsrc/locales/...
) nếu dùngi18next-http-backend
.
✅ 3. Đặt lại file dịch vào thư mục public/locales
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.json
vàpublic/locales/vi/translation.json
.✅ Cấu hình
i18n.ts
đúng vớiloadPath
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
và /[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 sanghttp://localhost:3000/vi/about
http://localhost:3000/vi/contact
→ Chuyển sanghttp://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ữ (
en
↔vi
)
✅ 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
[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
)
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)
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ặcgetStaticProps
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:
Layout.tsx
đợi ngôn ngữ đồng bộ xong rồi mới render → tránh mismatchKhông render
t('...')
khii18n.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?