@@ -80,21 +80,28 @@ Here's the project structure we'll be creating:
8080``` bash
8181.
8282├── i18n.config.ts
83- └── src
83+ └── src # Src is optional
8484 ├── locales
8585 │ ├── en
8686 │ │ ├── common.json
8787 │ │ └── about.json
8888 │ └── fr
8989 │ ├── common.json
9090 │ └── about.json
91+ ├── types
92+ │ └── i18next.d.ts
9193 ├── app
9294 │ ├── proxy.ts
9395 │ ├── i18n
9496 │ │ └── server.ts
9597 │ └── [locale]
9698 │ ├── layout.tsx
97- │ └── about.tsx
99+ │ ├── (home) # / (Route Group to not pollute all pages with home messages)
100+ │ │ ├── layout.tsx
101+ │ │ └── page.tsx
102+ │ └── about # /about
103+ │ ├── layout.tsx
104+ │ └── page.tsx
98105 └── components
99106 ├── I18nProvider.tsx
100107 ├── ClientComponent.tsx
@@ -162,6 +169,16 @@ const ORIGIN = "https://example.com";
162169export function absoluteUrl(locale : string , path : string ) {
163170 return ` ${ORIGIN }${localizedPath (locale , path )} ` ;
164171}
172+
173+ // Used to set the locale cookie in the browser
174+ export function getCookie(locale : Locale ) {
175+ return [
176+ ` NEXT_LOCALE=${locale } ` ,
177+ " Path=/" ,
178+ ` Max-Age=${60 * 60 * 24 * 365 } ` , // 1 year
179+ " SameSite=Lax" ,
180+ ].join (" ; " );
181+ }
165182```
166183
167184### Step 3: Centralize Translation Namespaces
@@ -192,7 +209,13 @@ declare module "i18next" {
192209}
193210```
194211
195- > Tip: Store this declaration under ` src/types ` (create the folder if it doesn't exist). Next.js already includes ` src ` in ` tsconfig.json ` , so the augmentation is picked up automatically.
212+ > Tip: Store this declaration under ` src/types ` (create the folder if it doesn't exist). Next.js already includes ` src ` in ` tsconfig.json ` , so the augmentation is picked up automatically. If not, add the following to your ` tsconfig.json ` file:
213+
214+ ``` json5 fileName="tsconfig.json"
215+ {
216+ " include" : [" src/types/**/*.ts" ],
217+ }
218+ ```
196219
197220With this in place you can rely on autocomplete and compile-time checks:
198221
@@ -404,15 +427,37 @@ Organizing translations by namespace (e.g., `common.json`, `about.json`) enables
404427
405428``` json fileName="src/locales/en/common.json"
406429{
407- "welcome " : " Welcome " ,
408- "greeting " : " Hello, world! "
430+ "appTitle " : " Next.js i18n App " ,
431+ "appDescription " : " Example Next.js application with internationalization using i18next "
409432}
410433```
411434
412435``` json fileName="src/locales/fr/common.json"
413436{
437+ "appTitle" : " Application Next.js i18n" ,
438+ "appDescription" : " Exemple d'application Next.js avec internationalisation utilisant i18next"
439+ }
440+ ```
441+
442+ ``` json fileName="src/locales/en/home.json"
443+ {
444+ "title" : " Home" ,
445+ "description" : " Home page description" ,
446+ "welcome" : " Welcome" ,
447+ "greeting" : " Hello, world!" ,
448+ "aboutPage" : " About Page" ,
449+ "documentation" : " Documentation"
450+ }
451+ ```
452+
453+ ``` json fileName="src/locales/fr/home.json"
454+ {
455+ "title" : " Accueil" ,
456+ "description" : " Description de la page d'accueil" ,
414457 "welcome" : " Bienvenue" ,
415- "greeting" : " Bonjour le monde!"
458+ "greeting" : " Bonjour le monde!" ,
459+ "aboutPage" : " Page À propos" ,
460+ "documentation" : " Documentation"
416461}
417462```
418463
@@ -422,7 +467,8 @@ Organizing translations by namespace (e.g., `common.json`, `about.json`) enables
422467 "description" : " About page description" ,
423468 "counter" : {
424469 "label" : " Counter" ,
425- "increment" : " Increment"
470+ "increment" : " Increment" ,
471+ "description" : " Click the button to increase the counter"
426472 }
427473}
428474```
@@ -433,7 +479,8 @@ Organizing translations by namespace (e.g., `common.json`, `about.json`) enables
433479 "description" : " Description de la page À propos" ,
434480 "counter" : {
435481 "label" : " Compteur" ,
436- "increment" : " Incrémenter"
482+ "increment" : " Incrémenter" ,
483+ "description" : " Cliquez sur le bouton pour augmenter le compteur"
437484 }
438485}
439486```
@@ -444,7 +491,7 @@ Create a page component that initializes i18next on the server and passes transl
444491
445492Server-side initialization loads translations before the page renders, improving SEO and preventing FOUC. By passing pre-loaded resources to the client provider, we avoid duplicate fetching and ensure smooth hydration.
446493
447- ``` tsx fileName="src/app/[locale]/about.tsx"
494+ ``` tsx fileName="src/app/[locale]/about/index .tsx"
448495import I18nProvider from " @/components/I18nProvider" ;
449496import { initI18next } from " @/app/i18n/server" ;
450497import type { Locale } from " @/i18n.config" ;
@@ -510,7 +557,7 @@ Client components need React hooks to access translations. The `useTranslation`
510557``` tsx fileName="src/components/ClientComponent.tsx"
511558" use client" ;
512559
513- import React , { useState } from " react" ;
560+ import { useState } from " react" ;
514561import { useTranslation } from " react-i18next" ;
515562
516563/**
@@ -529,10 +576,14 @@ const ClientComponent = () => {
529576 const numberFormat = new Intl .NumberFormat (i18n .language );
530577
531578 return (
532- <div >
579+ <div className = " flex flex-col items-center gap-4 " >
533580 { /* Format number using locale-specific formatting */ }
534- <p >{ numberFormat .format (count )} </p >
581+ <p className = " text-5xl font-bold text-white m-0" >
582+ { numberFormat .format (count )}
583+ </p >
535584 <button
585+ type = " button"
586+ className = " flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
536587 aria-label = { t (" counter.label" )}
537588 onClick = { () => setCount ((c ) => c + 1 )}
538589 >
@@ -552,12 +603,11 @@ Server components cannot use React hooks, so they receive translations via props
552603Server components that might be nested under client boundaries need to be synchronous. By passing translated strings and locale information as props, we avoid async operations and ensure proper rendering.
553604
554605``` tsx fileName="src/components/ServerComponent.tsx"
555- import type { TFunction } from " react- i18next" ;
606+ import type { TFunction } from " i18next" ;
556607
557608type ServerComponentProps = {
558609 // Translation function passed from parent server component
559610 // Server components can't use hooks, so translations come via props
560- // Strongly typed to the "about" namespace for compile-time safety
561611 t: TFunction <" about" >;
562612 locale: string ;
563613 count: number ;
@@ -574,10 +624,17 @@ const ServerComponent = ({ t, locale, count }: ServerComponentProps) => {
574624 const formatted = new Intl .NumberFormat (locale ).format (count );
575625
576626 return (
577- <div >
578- <p >{ formatted } </p >
627+ <div className = " flex flex-col items-center gap-4 " >
628+ <p className = " text-5xl font-bold text-white m-0 " >{ formatted } </p >
579629 { /* Use translation function passed as prop */ }
580- <button aria-label = { t (" counter.label" )} >{ t (" counter.increment" )} </button >
630+ <div className = " flex flex-col items-center gap-2" >
631+ <span className = " text-xl font-semibold text-white" >
632+ { t (" counter.label" )}
633+ </span >
634+ <span className = " text-sm opacity-80 italic" >
635+ { t (" counter.description" )}
636+ </span >
637+ </div >
581638 </div >
582639 );
583640};
@@ -594,22 +651,28 @@ To change the language of your content in Next.js, the recommended way is to use
594651``` tsx fileName="src/components/LocaleSwitcher.tsx"
595652" use client" ;
596653
597- import { useMemo } from " react" ;
598654import Link from " next/link" ;
599655import { useParams , usePathname } from " next/navigation" ;
600- import { locales , defaultLocale , type Locale } from " @/i18n.config" ;
601-
602- const localeLabels: Record <Locale , string > = {
603- en: " English" ,
604- fr: " Français" ,
605- };
656+ import { useMemo } from " react" ;
657+ import { defaultLocale , getCookie , type Locale , locales } from " @/i18n.config" ;
606658
607659export default function LocaleSwitcher() {
608660 const params = useParams ();
609661 const pathname = usePathname ();
610662
611663 const activeLocale = (params ?.locale as Locale | undefined ) ?? defaultLocale ;
612664
665+ const getLocaleLabel = (locale : Locale ): string => {
666+ try {
667+ const displayNames = new Intl .DisplayNames ([locale ], {
668+ type: " language" ,
669+ });
670+ return displayNames .of (locale ) ?? locale .toUpperCase ();
671+ } catch {
672+ return locale .toUpperCase ();
673+ }
674+ };
675+
613676 const basePath = useMemo (() => {
614677 if (! pathname ) return " /" ;
615678
@@ -618,6 +681,7 @@ export default function LocaleSwitcher() {
618681 if (segments .length === 0 ) return " /" ;
619682
620683 const maybeLocale = segments [0 ] as Locale ;
684+
621685 if ((locales as readonly string []).includes (maybeLocale )) {
622686 const rest = segments .slice (1 ).join (" /" );
623687 return rest ? ` /${rest } ` : " /" ;
@@ -626,27 +690,27 @@ export default function LocaleSwitcher() {
626690 return pathname ;
627691 }, [pathname ]);
628692
629- const hrefForLocale = (nextLocale : Locale ) => {
630- if (nextLocale === defaultLocale ) {
631- return basePath ;
632- }
633- return basePath === " /" ? ` /${nextLocale } ` : ` /${nextLocale }${basePath } ` ;
634- };
635-
636693 return (
637694 <nav aria-label = " Language selector" >
638- <ul >
639- { (locales as readonly Locale []).map ((locale ) => (
640- <li key = { locale } >
641- <Link
642- href = { hrefForLocale (locale )}
643- aria-current = { locale === activeLocale ? " page" : undefined }
644- >
645- { localeLabels [locale ] ?? locale .toUpperCase ()}
646- </Link >
647- </li >
648- ))}
649- </ul >
695+ { (locales as readonly Locale []).map ((locale ) => {
696+ const isActive = locale === activeLocale ;
697+
698+ const href =
699+ locale === defaultLocale ? basePath : ` /${locale }${basePath } ` ;
700+
701+ return (
702+ <Link
703+ key = { locale }
704+ href = { href }
705+ aria-current = { isActive ? " page" : undefined }
706+ onClick = { () => {
707+ document .cookie = getCookie (locale );
708+ }}
709+ >
710+ { getLocaleLabel (locale )}
711+ </Link >
712+ );
713+ })}
650714 </nav >
651715 );
652716}
@@ -659,20 +723,21 @@ Reusing localized URLs across your app keeps navigation consistent and SEO-frien
659723``` tsx fileName="src/components/LocalizedLink.tsx"
660724" use client" ;
661725
662- import type { PropsWithChildren } from " react" ;
663726import NextLink , { type LinkProps } from " next/link" ;
664727import { useParams } from " next/navigation" ;
728+ import type { ComponentProps , PropsWithChildren } from " react" ;
665729import {
666730 defaultLocale ,
731+ type Locale ,
667732 locales ,
668733 localizedPath ,
669- type Locale ,
670734} from " @/i18n.config" ;
671735
672736const isExternal = (href : string ) => / ^ https? :\/\/ / .test (href );
673737
674738type LocalizedLinkProps = PropsWithChildren <
675- Omit <LinkProps , " href" > & { href: string ; locale? : Locale }
739+ Omit <LinkProps , " href" > &
740+ Omit <ComponentProps <" a" >, " href" > & { href: string ; locale? : Locale }
676741>;
677742
678743export default function LocalizedLink({
@@ -683,7 +748,6 @@ export default function LocalizedLink({
683748}: LocalizedLinkProps ) {
684749 const params = useParams ();
685750 const fallback = (params ?.locale as Locale | undefined ) ?? defaultLocale ;
686-
687751 const normalizedLocale = (locales as readonly string []).includes (fallback )
688752 ? ((locale ?? fallback ) as Locale )
689753 : defaultLocale ;
0 commit comments