# Triển khai i18next client, server component (ok)

### Step 1: Thực hiện 1 trang ở app

app\\\[lang]\layout.tsx

```typescript
import { ReactNode } from 'react';
import LanguageSwitcher from '@/src/components/LanguageSwitcher'
interface RootLayoutProps {
  children: ReactNode;
}
export default async function RootLayout({ children }: RootLayoutProps) {
  return (
    <>
      <header className="flex justify-end p-4">
        <LanguageSwitcher />
      </header>
      {children}
    </>
  );
}
```

app\\\[lang]\page.tsx

```typescript
import ClientComponent from '@/src/components/ClientComponent';
import initI18next from '@/src/lib/i18n/server';
import { fallbackLng, languages } from '@/src/lib/i18n/settings';
export default async function Page({ params }: { params: { lang: string } }) {
  const {lang} = await params;
  const glang = languages.includes(lang) ? lang : fallbackLng;
  const i18n = await initI18next(glang, 'common');
  const t = i18n.getFixedT(lang, 'common');
  console.log('Server: lang=', glang, 't(hello)=', t('hello'));
  return (
    <>
      <ClientComponent lang={glang} />
      <h1>{t('hello')}</h1>
    </>
  );
}
```

app\layout.tsx

```typescript
import { ReactNode } from 'react';
import LanguageSwitcher from '@/src/components/LanguageSwitcher'
interface RootLayoutProps {
  children: ReactNode;
}
export default async function RootLayout({ children }: RootLayoutProps) {
  return (
    <>
      <header className="flex justify-end p-4">
        <LanguageSwitcher />
      </header>
      {children}
    </>
  );
}
```

src\components\ClientComponent.tsx

```typescript
'use client';
import { useEffect, useState } from 'react';
import i18n from '@/src/lib/i18n/client';
import { useTranslation } from 'react-i18next';
export default function ClientComponent({ lang }: { lang: string }) {
  const { t } = useTranslation('common');
  const [isLanguageReady, setIsLanguageReady] = useState(i18n.language === lang);
  useEffect(() => {
    console.log('ClientComponent: lang=', lang, 'i18n.language=', i18n.language);
    if (i18n.language !== lang) {
      // Clear language detector cache to prevent interference
      i18n.services.languageDetector?.cache?.clear?.();
      i18n.changeLanguage(lang, (err) => {
        if (err) console.error(`Failed to change language to ${lang}`, err);
        i18n.loadNamespaces('common', (err) => {
          if (err) console.error(`Failed to load namespace 'common' for ${lang}`, err);
          setIsLanguageReady(true); // Mark language as ready
        });
      });
    } else {
      setIsLanguageReady(true); // Language already matches
    }
  }, [lang]);
  console.log('ClientComponent rendering: t(hello)=', t('hello'));
  return isLanguageReady ? <p>{t('hello')}</p> : null; // Render only when language is ready
}
```

src\components\LanguageSwitcher.tsx

```typescript
'use client';
import { usePathname, useRouter } from 'next/navigation';
import { useTransition } from 'react';
const locales = ['en', 'vi'];
export default function LanguageSwitcher() {
  const router = useRouter();
  const pathname = usePathname();
  const [isPending, startTransition] = useTransition();
  const currentLang = pathname ? pathname.split('/')[1] : 'en';
  const otherLang = locales.find((lng) => lng !== currentLang) || 'en';
  const switchLang = () => {
    if (!pathname) return;
    const segments = pathname.split('/');
    segments[1] = otherLang;
    const newPath = segments.join('/');
    console.log('LanguageSwitcher: Switching to:', newPath);
    startTransition(() => {
      router.push(newPath, { scroll: false });
      router.refresh();
    });
  };
  return (
    <button
      onClick={switchLang}
      disabled={isPending}
      className="px-4 py-2 rounded border bg-gray-100 hover:bg-gray-200 text-sm"
    >
      {otherLang.toUpperCase()}
    </button>
  );
}
```

src\lib\i18n\client.ts

```typescript
'use client';
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import resourcesToBackend from 'i18next-resources-to-backend';
import { getOptions } from './settings';
if (!i18n.isInitialized) {
  i18n
    .use(initReactI18next)
    .use(LanguageDetector)
    .use(
      resourcesToBackend(async (lang: string, namespace: string) => {
        try {
          console.log(`Loading translation: ${lang}/${namespace}`);
          const module = await import(`@/src/locales/${lang}/${namespace}.json`);
          return module.default;
        } catch (error) {
          console.error(`Failed to load translation for ${lang}/${namespace}`, error);
          return {};
        }
      })
    )
    .init({
      ...getOptions(),
      detection: {
        order: ['path'], // Only use path for language detection
        caches: [], // Disable caching to avoid stale language
        lookupFromPathIndex: 0, // Extract language from first URL segment (e.g., /vi/)
      },
      ns: ['common'],
      defaultNS: 'common',
    });
  i18n.on('languageChanged', (lng) => {
    console.log('Language changed to:', lng, 'Resources:', i18n.services.resourceStore.data);
  });
}
export default i18n;
```

src\lib\i18n\server.ts

```typescript
import i18next from 'i18next'
import resourcesToBackend from 'i18next-resources-to-backend'
import { getOptions } from './settings'
const initI18next = async (lng: string, ns?: string) => {
  const i18nInstance = i18next.createInstance()
  await i18nInstance
    .use(
      resourcesToBackend((lang: string, namespace: string) =>
        import(`@/src/locales/${lang}/${namespace}.json`)
      )
    )
    .init(getOptions(lng, ns))
  return i18nInstance
}
export default initI18next

```

src\lib\i18n\settings.ts

```typescript
export const fallbackLng = 'en'
export const languages = ['en', 'vi']
export const defaultNS = 'common'
export function getOptions(lng = fallbackLng, ns = defaultNS) {
  return {
    supportedLngs: languages,
    fallbackLng,
    lng,
    ns,
    defaultNS,
    interpolation: {
      escapeValue: false,
    },
  }
}

```

next.config.ts

```typescript
const nextConfig = {
  experimental: {
    turbo: {
      enabled: false,
    },
  },
};
export default nextConfig;
```

src\locales\en\common.json

```json
{
  "hello": "Hello Eng"
}
```

src\locales\vi\common.json

```json
{
  "hello": "Xin chào Việt Nam"
}
```

### Step 2: Thực hiện 1 trang ở pages

app\\\[lang]\layout.tsx

```typescript
import { ReactNode } from 'react';
import LanguageSwitcher from '@/src/components/LanguageSwitcher';
interface RootLayoutProps {
  children: ReactNode;
  params: { lang: string };
}
export default async function RootLayout({ children, params }: RootLayoutProps) {
  return (
    <>
      <header className="flex justify-end p-4">
        <LanguageSwitcher />
      </header>
      {children}
    </>
  );
}
```

app\\\[lang]\page.tsx

```typescript
import ClientComponent from '@/src/components/ClientComponent';
import initI18next from '@/src/lib/i18n/server';
import { fallbackLng, languages } from '@/src/lib/i18n/settings';
export default async function Page({ params }: { params: { lang: string } }) {
  const {lang} = await params;
  const glang = languages.includes(lang) ? lang : fallbackLng;
  const i18n = await initI18next(glang, 'common');
  const t = i18n.getFixedT(lang, 'common');
  console.log('Server: lang=', glang, 't(hello)=', t('hello'));
  return (
    <>
      <ClientComponent lang={glang} />
      <h1>{t('hello')}</h1>
    </>
  );
}
```

pages\\\[lang]\new-page.tsx

```typescript
import ClientComponent from '@/src/components/ClientComponent';
import LanguageSwitcher from '@/src/components/LanguageSwitcher';
import initI18next from '@/src/lib/i18n/server';
import { fallbackLng, languages } from '@/src/lib/i18n/settings';
import { GetServerSideProps } from 'next';
interface NewPageProps {
  lang: string;
  serverTranslation: string;
}
export default function NewPage({ lang, serverTranslation }: NewPageProps) {
  console.log('NewPage Server: lang=', lang, 'serverTranslation=', serverTranslation);
  return (
    <>
      <header className="flex justify-end p-4">
        <LanguageSwitcher />
      </header>
      <ClientComponent lang={lang} />
      <h1>{serverTranslation}</h1>
    </>
  );
}
export const getServerSideProps: GetServerSideProps<NewPageProps> = async (context) => {
  // Ensure lang is a string
  const langParam = Array.isArray(context.params?.lang)
    ? context.params.lang[0] // Take first element if array
    : context.params?.lang || fallbackLng;
  const lang = languages.includes(langParam) ? langParam : fallbackLng;
  const i18n = await initI18next(lang, 'common');
  const t = i18n.getFixedT(lang, 'common');
  const serverTranslation = t('hello');
  console.log('NewPage getServerSideProps: lang=', lang, 't(hello)=', serverTranslation);
  return {
    props: {
      lang,
      serverTranslation,
    },
  };
};
```

src\components\ClientComponent.tsx

```typescript
'use client';
import { useEffect, useState } from 'react';
import i18n from '@/src/lib/i18n/client';
import { useTranslation } from 'react-i18next';
import { fallbackLng } from '@/src/lib/i18n/settings';
export default function ClientComponent({ lang = fallbackLng }: { lang?: string }) {
  const { t } = useTranslation('common');
  const [isLanguageReady, setIsLanguageReady] = useState(i18n.language === lang);
  useEffect(() => {
    console.log('ClientComponent: lang=', lang, 'i18n.language=', i18n.language);
    if (i18n.language !== lang) {
      i18n.services.languageDetector?.cache?.clear?.();
      i18n.changeLanguage(lang, (err) => {
        if (err) console.error(`Failed to change language to ${lang}`, err);
        i18n.loadNamespaces('common', (err) => {
          if (err) console.error(`Failed to load namespace 'common' for ${lang}`, err);
          setIsLanguageReady(true);
        });
      });
    } else {
      setIsLanguageReady(true);
    }
  }, [lang]);
  console.log('ClientComponent rendering: t(hello)=', t('hello'));
  return isLanguageReady ? <p>{t('hello')}</p> : null;
}
```

src\components\LanguageSwitcher.tsx

```typescript
'use client';
import { usePathname, useRouter } from 'next/navigation';
import { useTransition } from 'react';
const locales = ['en', 'vi'];
export default function LanguageSwitcher() {
  const router = useRouter();
  const pathname = usePathname();
  const [isPending, startTransition] = useTransition();
  const currentLang = pathname ? pathname.split('/')[1] : 'en';
  const otherLang = locales.find((lng) => lng !== currentLang) || 'en';
  const switchLang = () => {
    if (!pathname) return;
    const segments = pathname.split('/');
    segments[1] = otherLang;
    const newPath = segments.join('/');
    console.log('LanguageSwitcher: Switching to:', newPath);
    startTransition(() => {
      router.push(newPath, { scroll: false });
    });
  };
  return (
    <button
      onClick={switchLang}
      disabled={isPending}
      className="px-4 py-2 rounded border bg-gray-100 hover:bg-gray-200 text-sm"
    >
      {otherLang.toUpperCase()}
    </button>
  );
}
```

src\lib\i18n\client.ts

```typescript
'use client';
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import resourcesToBackend from 'i18next-resources-to-backend';
import { getOptions } from './settings';
if (!i18n.isInitialized) {
  i18n
    .use(initReactI18next)
    .use(LanguageDetector)
    .use(
      resourcesToBackend(async (lang: string, namespace: string) => {
        try {
          console.log(`Loading translation: ${lang}/${namespace}`);
          const module = await import(`@/src/locales/${lang}/${namespace}.json`);
          return module.default;
        } catch (error) {
          console.error(`Failed to load translation for ${lang}/${namespace}`, error);
          return {};
        }
      })
    )
    .init({
      ...getOptions(),
      detection: {
        order: ['path'], // Only use URL path
        caches: [], // Disable caching
        lookupFromPathIndex: 0,
      },
      ns: ['common'],
      defaultNS: 'common',
    });
  i18n.on('languageChanged', (lng) => {
    console.log('Language changed to:', lng, 'Resources:', i18n.services.resourceStore.data);
  });
}
export default i18n;
```

src\lib\i18n\server.ts

```typescript
import i18next from 'i18next'
import resourcesToBackend from 'i18next-resources-to-backend'
import { getOptions } from './settings'
const initI18next = async (lng: string, ns?: string) => {
  const i18nInstance = i18next.createInstance()
  await i18nInstance
    .use(
      resourcesToBackend((lang: string, namespace: string) =>
        import(`@/src/locales/${lang}/${namespace}.json`)
      )
    )
    .init(getOptions(lng, ns))
  return i18nInstance
}
export default initI18next

```

src\lib\i18n\settings.ts

```typescript
export const fallbackLng = 'en'
export const languages = ['en', 'vi']
export const defaultNS = 'common'
export function getOptions(lng = fallbackLng, ns = defaultNS) {
  return {
    supportedLngs: languages,
    fallbackLng,
    lng,
    ns,
    defaultNS,
    interpolation: {
      escapeValue: false,
    },
  }
}

```

### Source code (kiểm tra đã hoạt động tốt app và pages)

{% file src="https://1113999984-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M84NwP_L_qEsBHOa2xo%2Fuploads%2Fb5ra8YVcb5Xmqe5ZDPop%2Flangtest.zip?alt=media&token=9ea7f7ba-9cae-4ea2-8f90-45f58a5cfad5" %}

### Step 3:  Để tạo một **ClientComponent không sử dụng `'use client'`** mà vẫn hỗ trợ **đa ngôn ngữ (i18n)**

Để tạo một **ClientComponent không sử dụng `'use client'`** mà vẫn hỗ trợ **đa ngôn ngữ (i18n)**, bạn cần:

* **Không sử dụng hook như `useTranslation()`** (vì hook chỉ dùng trong client component).
* **Truyền dữ liệu dịch (`t`) từ server xuống dưới dạng prop**.
* **Hoặc tạo một function component async (server component) mà không cần hook.**

src\components\TranslatedComponent.tsx

```typescript
interface TranslatedComponentProps {
  t: (key: string) => string;
}
export default function TranslatedComponent({ t }: TranslatedComponentProps) {
  return (
    <div className="p-4">
      <h2>{t('hello')}</h2>
    </div>
  );
}

```

app\\\[lang]\page.tsx

```typescript
import ClientComponent from '@/src/components/ClientComponent';
import TranslatedComponent from '@/src/components/TranslatedComponent';
import initI18next from '@/src/lib/i18n/server';
import { fallbackLng, languages } from '@/src/lib/i18n/settings';
export default async function Page({ params }: { params: { lang: string } }) {
  const {lang} = await params;
  const glang = languages.includes(lang) ? lang : fallbackLng;
  const i18n = await initI18next(glang, 'common');
  const t = i18n.getFixedT(glang, 'common');
  console.log('Server: lang=', glang, 't(hello)=', t('hello'));
  return (
    <>
      <ClientComponent lang={glang} />
      <TranslatedComponent t={t} />
      <h1>{t('hello')}</h1>
    </>
  );
}
```

{% file src="https://1113999984-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M84NwP_L_qEsBHOa2xo%2Fuploads%2Fs83yDJ378oDRY1mbLUoj%2Flangtest.zip?alt=media&token=328f3276-d70f-4c5a-b950-a4b7c83eb5db" %}

Cách chuyển localhost:3000 sang localhost:3000/en

app\page.tsx

```typescript
import Image from "next/image";
import { redirect } from 'next/navigation';
import { fallbackLng, languages } from '@/src/lib/i18n/settings';
export default async function Home({ params }: { params: { lang: string } }) {
  const {lang} = await params;
  if (!languages.includes(lang)) {
    redirect(`/${fallbackLng}`);
  }
  return (
    <div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
      HomePage
    </div>
  );
}

```

{% file src="https://1113999984-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M84NwP_L_qEsBHOa2xo%2Fuploads%2FryXFy4IX0GGYaTNzYG7c%2Flangtest.zip?alt=media&token=0b40a332-c8e5-4178-a38e-1324738034a1" %}
