Usamos cookies para mejorar tu experiencia en el sitio
CodeWorlds

Internacjonalizacja z next-intl

W globalnej sieci Metropolii Quantum 2150, gdzie aplikacje muszą obsługiwać użytkowników z całego świata, poznasz

next-intl
- najpopularniejszą bibliotekę do internacjonalizacji (i18n) w Next.js App Router.

Dlaczego next-intl?

Next.js nie dostarcza wbudowanego rozwiązania i18n dla App Router.

next-intl
wypełnia tę lukę oferując:

  • Pełne wsparcie dla Server Components
  • Type-safe tłumaczenia z TypeScript
  • Formatowanie dat, liczb i walut
  • Obsługa pluralizacji
  • ICU Message Format
  • Routing oparty na locale

Instalacja i konfiguracja

Krok 1: Instalacja

1npm install next-intl

Krok 2: Struktura plików tłumaczeń

1messages/
2├── pl.json
3├── en.json
4└── de.json
1// messages/pl.json
2{
3  "common": {
4    "welcome": "Witaj w Metropolii Quantum!",
5    "login": "Zaloguj się",
6    "logout": "Wyloguj się",
7    "loading": "Ładowanie..."
8  },
9  "home": {
10    "title": "Strona główna",
11    "description": "Odkryj przyszłość technologii",
12    "cta": "Rozpocznij przygodę"
13  },
14  "products": {
15    "title": "Produkty",
16    "price": "Cena: {price, number, ::currency/PLN}",
17    "inStock": "{count, plural, =0 {Brak w magazynie} one {# sztuka} few {# sztuki} many {# sztuk} other {# sztuk}}",
18    "addToCart": "Dodaj do koszyka"
19  }
20}
1// messages/en.json
2{
3  "common": {
4    "welcome": "Welcome to Quantum Metropolis!",
5    "login": "Log in",
6    "logout": "Log out",
7    "loading": "Loading..."
8  },
9  "home": {
10    "title": "Home",
11    "description": "Discover the future of technology",
12    "cta": "Start your journey"
13  },
14  "products": {
15    "title": "Products",
16    "price": "Price: {price, number, ::currency/USD}",
17    "inStock": "{count, plural, =0 {Out of stock} one {# item} other {# items}}",
18    "addToCart": "Add to cart"
19  }
20}

Krok 3: Konfiguracja next-intl

1// i18n/config.ts
2export const locales = ['pl', 'en', 'de'] as const;
3export const defaultLocale = 'pl' as const;
4
5export type Locale = (typeof locales)[number];
1// i18n/request.ts
2import { getRequestConfig } from 'next-intl/server';
3import { locales, defaultLocale } from './config';
4
5export default getRequestConfig(async ({ locale }) => {
6  // Walidacja locale
7  if (!locales.includes(locale as any)) {
8    locale = defaultLocale;
9  }
10
11  return {
12    messages: (await import(`../messages/${locale}.json`)).default,
13    timeZone: 'Europe/Warsaw',
14    now: new Date(),
15  };
16});

Krok 4: Middleware

1// middleware.ts
2import createMiddleware from 'next-intl/middleware';
3import { locales, defaultLocale } from './i18n/config';
4
5export default createMiddleware({
6  locales,
7  defaultLocale,
8  localePrefix: 'as-needed', // lub 'always' lub 'never'
9});
10
11export const config = {
12  matcher: [
13    // Wszystkie ścieżki oprócz API, _next, plików statycznych
14    '/((?!api|_next|_vercel|.*\..*).*)',
15  ],
16};

Krok 5: Struktura folderów z [locale]

1app/
2├── [locale]/
3│   ├── layout.tsx
4│   ├── page.tsx
5│   ├── products/
6│   │   ├── page.tsx
7│   │   └── [id]/
8│   │       └── page.tsx
9│   └── about/
10│       └── page.tsx
11├── api/
12└── globals.css

Krok 6: Root Layout z locale

1// app/[locale]/layout.tsx
2import { NextIntlClientProvider } from 'next-intl';
3import { getMessages } from 'next-intl/server';
4import { locales } from '@/i18n/config';
5import { notFound } from 'next/navigation';
6
7export function generateStaticParams() {
8  return locales.map((locale) => ({ locale }));
9}
10
11export default async function LocaleLayout({
12  children,
13  params,
14}: {
15  children: React.ReactNode;
16  params: Promise<{ locale: string }>;
17}) {
18  const { locale } = await params;
19
20  // Walidacja locale
21  if (!locales.includes(locale as any)) {
22    notFound();
23  }
24
25  const messages = await getMessages();
26
27  return (
28    <html lang={locale}>
29      <body>
30        <NextIntlClientProvider messages={messages}>
31          {children}
32        </NextIntlClientProvider>
33      </body>
34    </html>
35  );
36}

Używanie tłumaczeń

W Server Components

1// app/[locale]/page.tsx
2import { getTranslations } from 'next-intl/server';
3
4export default async function HomePage() {
5  const t = await getTranslations('home');
6  const tCommon = await getTranslations('common');
7
8  return (
9    <main>
10      <h1>{t('title')}</h1>
11      <p>{t('description')}</p>
12      <button>{t('cta')}</button>
13      <p>{tCommon('welcome')}</p>
14    </main>
15  );
16}
17
18// Generowanie metadanych z tłumaczeniami
19export async function generateMetadata({ params }: Props) {
20  const { locale } = await params;
21  const t = await getTranslations({ locale, namespace: 'home' });
22
23  return {
24    title: t('title'),
25    description: t('description'),
26  };
27}

W Client Components

1// components/ProductCard.tsx
2'use client';
3
4import { useTranslations } from 'next-intl';
5
6interface ProductCardProps {
7  product: {
8    id: string;
9    name: string;
10    price: number;
11    stock: number;
12  };
13}
14
15export function ProductCard({ product }: ProductCardProps) {
16  const t = useTranslations('products');
17
18  return (
19    <div className="product-card">
20      <h3>{product.name}</h3>
21      <p>{t('price', { price: product.price })}</p>
22      <p>{t('inStock', { count: product.stock })}</p>
23      <button>{t('addToCart')}</button>
24    </div>
25  );
26}

Formatowanie

Daty i czas

1import { useFormatter } from 'next-intl';
2
3function EventDate({ date }: { date: Date }) {
4  const format = useFormatter();
5
6  return (
7    <div>
8      {/* Pełna data */}
9      <p>{format.dateTime(date, { dateStyle: 'full' })}</p>
10
11      {/* Tylko data */}
12      <p>{format.dateTime(date, {
13        year: 'numeric',
14        month: 'long',
15        day: 'numeric'
16      })}</p>
17
18      {/* Czas względny */}
19      <p>{format.relativeTime(date)}</p>
20
21      {/* Zakres dat */}
22      <p>{format.dateTimeRange(startDate, endDate, {
23        dateStyle: 'medium'
24      })}</p>
25    </div>
26  );
27}

Liczby i waluty

1import { useFormatter } from 'next-intl';
2
3function PriceDisplay({ amount }: { amount: number }) {
4  const format = useFormatter();
5
6  return (
7    <div>
8      {/* Waluta */}
9      <p>{format.number(amount, { style: 'currency', currency: 'PLN' })}</p>
10
11      {/* Procenty */}
12      <p>{format.number(0.25, { style: 'percent' })}</p>
13
14      {/* Jednostki */}
15      <p>{format.number(1500, {
16        style: 'unit',
17        unit: 'kilometer',
18        unitDisplay: 'long'
19      })}</p>
20
21      {/* Listy */}
22      <p>{format.list(['React', 'Next.js', 'TypeScript'], { type: 'conjunction' })}</p>
23    </div>
24  );
25}

Przełącznik języków

1// components/LocaleSwitcher.tsx
2'use client';
3
4import { useLocale } from 'next-intl';
5import { usePathname, useRouter } from 'next-intl/client';
6import { locales } from '@/i18n/config';
7
8const localeNames: Record<string, string> = {
9  pl: 'Polski',
10  en: 'English',
11  de: 'Deutsch',
12};
13
14export function LocaleSwitcher() {
15  const locale = useLocale();
16  const router = useRouter();
17  const pathname = usePathname();
18
19  const handleChange = (newLocale: string) => {
20    router.replace(pathname, { locale: newLocale });
21  };
22
23  return (
24    <select
25      value={locale}
26      onChange={(e) => handleChange(e.target.value)}
27      className="bg-gray-800 text-white px-3 py-2 rounded"
28    >
29      {locales.map((loc) => (
30        <option key={loc} value={loc}>
31          {localeNames[loc]}
32        </option>
33      ))}
34    </select>
35  );
36}

Linki z zachowaniem locale

1import { Link } from 'next-intl';
2
3function Navigation() {
4  return (
5    <nav>
6      {/* Link automatycznie dodaje prefix locale */}
7      <Link href="/">Home</Link>
8      <Link href="/products">Products</Link>
9      <Link href="/about">About</Link>
10
11      {/* Link do konkretnego locale */}
12      <Link href="/about" locale="en">About (EN)</Link>
13    </nav>
14  );
15}

Type-safe tłumaczenia

1// types/messages.ts
2import pl from '../messages/pl.json';
3
4// Automatyczne typowanie z pliku JSON
5type Messages = typeof pl;
6
7declare global {
8  interface IntlMessages extends Messages {}
9}

Teraz TypeScript będzie podpowiadać dostępne klucze:

1const t = useTranslations('products');
2t('title');     // ✅ OK
3t('price', { price: 100 }); // ✅ OK z parametrem
4t('nonExistent'); // ❌ TypeScript error!

Praktyczny przykład - E-commerce

1// app/[locale]/products/page.tsx
2import { getTranslations } from 'next-intl/server';
3import { ProductCard } from '@/components/ProductCard';
4
5export default async function ProductsPage() {
6  const t = await getTranslations('products');
7  const products = await getProducts();
8
9  return (
10    <div className="container mx-auto py-8">
11      <h1 className="text-3xl font-bold mb-8">{t('title')}</h1>
12
13      <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
14        {products.map((product) => (
15          <ProductCard key={product.id} product={product} />
16        ))}
17      </div>
18    </div>
19  );
20}
1// Rozszerzony messages/pl.json
2{
3  "products": {
4    "title": "Nasze produkty",
5    "filters": {
6      "all": "Wszystkie",
7      "inStock": "Dostępne",
8      "onSale": "W promocji"
9    },
10    "sort": {
11      "label": "Sortuj",
12      "priceAsc": "Cena: od najniższej",
13      "priceDesc": "Cena: od najwyższej",
14      "newest": "Najnowsze"
15    },
16    "empty": "Nie znaleziono produktów",
17    "results": "{count, plural, =0 {Brak wyników} one {# produkt} few {# produkty} many {# produktów} other {# produktów}}"
18  }
19}

Podsumowanie

next-intl
oferuje kompletne rozwiązanie i18n dla Next.js:

  • Server Components - pełne wsparcie z
    getTranslations
  • Client Components - hooki
    useTranslations
    ,
    useFormatter
  • Type Safety - TypeScript podpowiada dostępne klucze
  • Formatowanie - daty, liczby, waluty, listy
  • Routing - automatyczne prefixy locale w URL
  • Pluralizacja - poprawna odmiana dla każdego języka

W globalnej Metropolii Quantum 2150, gdzie użytkownicy mówią w dziesiątkach języków,

next-intl
pozwala budować aplikacje dostępne dla wszystkich!

Ir a CodeWorlds