Zadaj pytanie i otrzymaj streszczenie dokumentu, odwołując się do tej strony i wybranego dostawcy AI
Treść tej strony została przetłumaczona przy użyciu sztucznej inteligencji.
Zobacz ostatnią wersję oryginalnej treści w języku angielskimIf you have an idea for improving this documentation, please feel free to contribute by submitting a pull request on GitHub.
GitHub link to the documentationCopy doc Markdown to clipboard
Tłumaczenie Twojej aplikacji Next.js 15 za pomocą next-intl i Intlayer | Internacjonalizacja (i18n)
Ten przewodnik przeprowadzi Cię przez najlepsze praktyki next-intl w aplikacji Next.js 15 (App Router) oraz pokaże, jak nałożyć Intlayer, aby uzyskać solidne zarządzanie tłumaczeniami i automatyzację.
Zobacz porównanie w next-i18next vs next-intl vs Intlayer.
- Dla juniorów: postępuj krok po kroku, aby uzyskać działającą aplikację wielojęzyczną.
- Dla programistów średniego szczebla: zwróć uwagę na optymalizację payloadu oraz rozdzielenie serwera i klienta.
- Dla seniorów: zwróć uwagę na generowanie statyczne, middleware, integrację SEO oraz haki automatyzacji.
Co omówimy:
- Konfiguracja i struktura plików
- Optymalizacja ładowania wiadomości
- Użycie komponentów klienta i serwera
- Metadane, sitemap, robots dla SEO
- Middleware do routingu lokalizacji
- Dodanie Intlayer na wierzch (CLI i automatyzacja)
Skonfiguruj swoją aplikację za pomocą next-intl
Zainstaluj zależności next-intl -
Skopiuj kod do schowka
npm install next-intlSkopiuj kod do schowka
.├── locales│ ├── en│ │ ├── common.json│ │ └── about.json│ ├── fr│ │ ├── common.json│ │ └── about.json│ └── es│ ├── common.json│ └── about.json└── src ├── i18n.ts ├── middleware.ts ├── app │ └── [locale] │ ├── layout.tsx │ └── about │ └── page.tsx └── components ├── ClientComponentExample.tsx └── ServerComponent.tsxKonfiguracja i ładowanie zawartości
Ładuj tylko te przestrzenie nazw, których potrzebują Twoje trasy, i wczesne waliduj lokalizacje. Utrzymuj komponenty serwera synchroniczne, gdy to możliwe, i przesyłaj do klienta tylko wymagane wiadomości.
Skopiuj kod do schowka
import { getRequestConfig } from "next-intl/server";import { notFound } from "next/navigation";export const locales = ["en", "fr", "es"] as const;export const defaultLocale = "en" as const;async function loadMessages(locale: string) { // Załaduj tylko przestrzenie nazw potrzebne dla twojego layoutu/stron const [common, about] = await Promise.all([ import(`../locales/${locale}/common.json`).then((m) => m.default), import(`../locales/${locale}/about.json`).then((m) => m.default), ]); return { common, about } as const;}export default getRequestConfig(async ({ locale }) => { if (!locales.includes(locale)) notFound(); return { messages: await loadMessages(locale), };});Skopiuj kod do schowka
import type { ReactNode } from "react";import { locales } from "@/i18n";import { getLocaleDirection, unstable_setRequestLocale,} from "next-intl/server";export const dynamic = "force-static";export function generateStaticParams() { return locales.map((locale) => ({ locale }));}export default async function LocaleLayout({ children, params,}: { children: ReactNode; params: { locale: string };}) { const { locale } = params; // Ustaw aktywny język żądania dla tego renderowania po stronie serwera (RSC) unstable_setRequestLocale(locale); const dir = getLocaleDirection(locale); return ( <html lang={locale} dir={dir}> <body>{children}</body> </html> );}Skopiuj kod do schowka
import { getTranslations, getMessages, getFormatter } from "next-intl/server";import { NextIntlClientProvider } from "next-intl";import pick from "lodash/pick";import ServerComponent from "@/components/ServerComponent";import ClientComponentExample from "@/components/ClientComponentExample";export const dynamic = "force-static";export default async function AboutPage({ params,}: { params: { locale: string };}) { const { locale } = params; // Wiadomości są ładowane po stronie serwera. Przekaż do klienta tylko to, co jest potrzebne. const messages = await getMessages(); const clientMessages = pick(messages, ["common", "about"]); // Tłumaczenia/formatowanie wyłącznie po stronie serwera const tAbout = await getTranslations("about"); const tCounter = await getTranslations("about.counter"); const format = await getFormatter(); const initialFormattedCount = format.number(0); return ( <NextIntlClientProvider locale={locale} messages={clientMessages}> <main> <h1>{tAbout("title")}</h1> <ClientComponentExample /> <ServerComponent formattedCount={initialFormattedCount} label={tCounter("label")} increment={tCounter("increment")} /> </main> </NextIntlClientProvider> );}Użycie w komponencie klienckim
Weźmy przykład komponentu klienckiego renderującego licznik.
Tłumaczenia (struktura powtórzona; załaduj je do wiadomości next-intl według własnego uznania)
Skopiuj kod do schowka
{ "counter": { "label": "Counter", "increment": "Increment" }}Skopiuj kod do schowka
{ "counter": { "label": "Compteur", "increment": "Incrémenter" }}Komponent kliencki
Skopiuj kod do schowka
"use client";import React, { useState } from "react";import { useTranslations, useFormatter } from "next-intl";const ClientComponentExample = () => { // Bezpośredni zakres do zagnieżdżonego obiektu const t = useTranslations("about.counter"); const format = useFormatter(); const [count, setCount] = useState(0); return ( <div> <p>{format.number(count)}</p> <button aria-label={t("label")} onClick={() => setCount((count) => count + 1)} > {t("increment")} </button> </div> );};Nie zapomnij dodać komunikatu "about" w wiadomościach klienta na stronie (dołącz tylko przestrzenie nazw, których faktycznie potrzebuje twój klient).
Użycie w komponencie serwerowym
Ten komponent UI jest komponentem serwerowym i może być renderowany pod komponentem klienckim (strona → klient → serwer). Zachowaj synchroniczność, przekazując wcześniej obliczone ciągi znaków.
Skopiuj kod do schowka
type ServerComponentProps = { formattedCount: string; label: string; increment: string;};const ServerComponent = ({ formattedCount, label, increment,}: ServerComponentProps) => { return ( <div> <p>{formattedCount}</p> <button aria-label={label}>{increment}</button> </div> );};Uwagi:
- Oblicz
formattedCountpo stronie serwera (np.const initialFormattedCount = format.number(0)). - Unikaj przekazywania funkcji lub obiektów niepodlegających serializacji do komponentów serwerowych.
Skopiuj kod do schowka
import type { Metadata } from "next";import { locales, defaultLocale } from "@/i18n";import { getTranslations } from "next-intl/server";function localizedPath(locale: string, path: string) { return locale === defaultLocale ? path : "/" + locale + path;}export async function generateMetadata({ params,}: { params: { locale: string };}): Promise<Metadata> { const { locale } = params; const t = await getTranslations({ locale, namespace: "about" }); const url = "/about"; const languages = Object.fromEntries( locales.map((locale) => [locale, localizedPath(locale, url)]) ); return { title: t("title"), description: t("description"), alternates: { canonical: localizedPath(locale, url), languages: { ...languages, "x-default": url }, }, };}// ... Reszta kodu stronySkopiuj kod do schowka
import type { MetadataRoute } from "next";import { locales, defaultLocale } from "@/i18n";const origin = "https://example.com";const formatterLocalizedPath = (locale: string, path: string) => locale === defaultLocale ? origin + path : origin + "/" + locale + path;export default function sitemap(): MetadataRoute.Sitemap { const aboutLanguages = Object.fromEntries( locales.map((l) => [l, formatterLocalizedPath(l, "/about")]) ); return [ { url: formatterLocalizedPath(defaultLocale, "/about"), lastModified: new Date(), changeFrequency: "monthly", priority: 0.7, alternates: { languages: aboutLanguages }, }, ];}Skopiuj kod do schowka
import type { MetadataRoute } from "next";import { locales, defaultLocale } from "@/i18n";const origin = "https://example.com";const withAllLocales = (path: string) => [ path, ...locales .filter((locale) => locale !== defaultLocale) .map((locale) => "/" + locale + path),];export default function robots(): MetadataRoute.Robots { const disallow = [ ...withAllLocales("/dashboard"), ...withAllLocales("/admin"), ]; return { rules: { userAgent: "*", allow: ["/"], disallow }, host: origin, sitemap: origin + "/sitemap.xml", };}Middleware do routingu lokalizacji
Dodaj middleware do obsługi wykrywania i routingu lokalizacji:
Skopiuj kod do schowka
import createMiddleware from "next-intl/middleware";import { locales, defaultLocale } from "@/i18n";export default createMiddleware({ locales: [...locales], defaultLocale, localeDetection: true,});export const config = { // Pomijaj API, wewnętrzne elementy Next i zasoby statyczne matcher: ["/((?!api|_next|.*\\..*).*)"],};Najlepsze praktyki
- Ustaw html
langidir: Wsrc/app/[locale]/layout.tsxobliczdirza pomocągetLocaleDirection(locale)i ustaw<html lang={locale} dir={dir}>. - Podziel wiadomości według przestrzeni nazw: Organizuj JSON według lokalizacji i przestrzeni nazw (np.
common.json,about.json). - Minimalizuj payload klienta: Na stronach wysyłaj do
NextIntlClientProvidertylko wymagane namespace (np.pick(messages, ['common', 'about'])). - Preferuj strony statyczne: Eksportuj
export const dynamic = 'force-static'i generuj statyczne parametry dla wszystkichlocales. - Synchroniczne komponenty serwera: Przekazuj wcześniej obliczone łańcuchy znaków (przetłumaczone etykiety, sformatowane liczby) zamiast wywołań asynchronicznych lub funkcji nieserializowalnych.
Implementacja Intlayer na bazie next-intl
Zainstaluj zależności intlayer:
Skopiuj kod do schowka
npm install intlayer @intlayer/sync-json-plugin --save-devUtwórz plik konfiguracyjny intlayer:
Skopiuj kod do schowka
import { type IntlayerConfig, Locales } from "intlayer";import { syncJSON } from "@intlayer/sync-json-plugin";const config: IntlayerConfig = { internationalization: { locales: [Locales.ENGLISH, Locales.FRENCH, Locales.SPANISH], defaultLocale: Locales.ENGLISH, }, ai: { apiKey: process.env.OPENAI_API_KEY, }, plugins: [ // Zachowaj strukturę folderów per namespace w synchronizacji z Intlayer syncJSON({ format: "icu", source: ({ key, locale }) => `./locales/${locale}/${key}.json`, }), ],};export default config;Dodaj skrypty do package.json:
Skopiuj kod do schowka
{ "scripts": { "i18n:fill": "intlayer fill", "i18n:test": "intlayer test" }}Notatki:
intlayer fill: używa Twojego dostawcy AI do uzupełniania brakujących tłumaczeń na podstawie skonfigurowanych lokalizacji.intlayer test: sprawdza brakujące/nieprawidłowe tłumaczenia (używaj tego w CI).
Możesz konfigurować argumenty i dostawców; zobacz Intlayer CLI.