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.Next.js nie dostarcza wbudowanego rozwiązania i18n dla App Router.
next-intl wypełnia tę lukę oferując:1npm install next-intl1messages/
2├── pl.json
3├── en.json
4└── de.json1// 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}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});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};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.css1// 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}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}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}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}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}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}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}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!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}next-intl oferuje kompletne rozwiązanie i18n dla Next.js:getTranslationsuseTranslations, useFormatterW globalnej Metropolii Quantum 2150, gdzie użytkownicy mówią w dziesiątkach języków,
next-intl pozwala budować aplikacje dostępne dla wszystkich!