Skip to content

Commit 76564be

Browse files
committed
feat: enhance i18n documentation with updated project structure, new locale handling functions, and improved component organization for better clarity and usability
1 parent ad1a6be commit 76564be

File tree

2 files changed

+125
-56
lines changed

2 files changed

+125
-56
lines changed

docs/blog/en/i18n_using_next-i18next.md

Lines changed: 111 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -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";
162169
export 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

197220
With 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

445492
Server-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"
448495
import I18nProvider from "@/components/I18nProvider";
449496
import { initI18next } from "@/app/i18n/server";
450497
import 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";
514561
import { 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
552603
Server 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

557608
type 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";
598654
import Link from "next/link";
599655
import { 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

607659
export 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";
663726
import NextLink, { type LinkProps } from "next/link";
664727
import { useParams } from "next/navigation";
728+
import type { ComponentProps, PropsWithChildren } from "react";
665729
import {
666730
defaultLocale,
731+
type Locale,
667732
locales,
668733
localizedPath,
669-
type Locale,
670734
} from "@/i18n.config";
671735

672736
const isExternal = (href: string) => /^https?:\/\//.test(href);
673737

674738
type 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

678743
export 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

Comments
 (0)