Internationalisation avec next-intl¶
Pourquoi next-intl ?¶
Je vous ai montré dans la première partie du cours que pour rendre multilingue une application React, le module react-intl est très pratique. Cependant, react-intl n'est pas idéal dans l'environnement Next.JS. C'est pour ça que je vous parle maintenant de son équivalent, next-intl.
Manuel
Installation¶
Structure du projet¶
Voici la structure de fichiers à mettre en place :
├── messages/
│ ├── fr.json
│ └── en.json
├── i18n/
│ ├── routing.ts
│ └── request.ts
├── proxy.ts
├── app/
│ └── [locale]/
│ ├── layout.tsx
│ └── page.tsx
Étapes de configuration¶
1. Créer les fichiers de traductions¶
{
"app": {
"titre": "Mon application",
"bienvenue": "Bonjour, {nom} !",
"clics": "{nombreclics, plural, =0 {Aucun clic} one {Un clic} other {{nombreclics} clics}}"
}
}
{
"app": {
"titre": "My application",
"bienvenue": "Hello, {nom}!",
"clics": "{nombreclics, plural, =0 {No clicks} one {One click} other {{nombreclics} clicks}}"
}
}
Pensez à la structure JSON du fichier de message comme étant la structure de l'application. Par exemple, si vous avez une composante qui liste les billets vous pourrez avoir ceci dans votre fichier de traduction :
{
"listeBillets": {
"numero": "Numéro du billet",
"desc": "Description",
"demandeur": "Demandeur"
}
}
2. Configurer le routage i18n¶
import { defineRouting } from 'next-intl/routing';
export const routing = defineRouting({
locales: ['fr', 'en'],
defaultLocale: 'fr',
});
import {createNavigation} from 'next-intl/navigation';
import {routing} from './routing';
export const {Link, redirect, usePathname, useRouter, getPathname} =
createNavigation(routing);
3. Configurer le chargement des messages¶
import * as rootParams from 'next/root-params';
import {notFound} from 'next/navigation';
import {getRequestConfig} from 'next-intl/server';
import {hasLocale} from 'next-intl';
import {routing} from './routing';
export default getRequestConfig(async ({locale}) => {
if (!locale) {
const paramValue = await rootParams.locale();
if (hasLocale(routing.locales, paramValue)) {
locale = paramValue;
} else {
notFound();
}
}
return {
locale,
messages: (await import(`../messages/${locale}.json`)).default
};
});
4. Configurer le proxy¶
Le proxy détecte la langue préférée de l'utilisateur et redirige automatiquement vers la bonne URL :
import createMiddleware from 'next-intl/middleware';
import {routing} from './i18n/routing';
export default createMiddleware(routing);
export const config = {
// Match all pathnames except for
// - … if they start with `/api`, `/trpc`, `/_next` or `/_vercel`
// - … the ones containing a dot (e.g. `favicon.ico`)
matcher: '/((?!api|trpc|_next|_vercel|.*\\..*).*)'
};
5. Configurer next.config.ts¶
6. Déplacer les pages sous [locale]¶
Toutes vos pages doivent être dans app/[locale]/ :
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import { locale } from "next/root-params";
import "../globals.css";
import { NextIntlClientProvider } from "next-intl";
import { routing } from "@/i18n/routing";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export function generateStaticParams() {
return routing.locales.map((locale) => ({ locale }));
}
export default async function RootLayout({ children }: LayoutProps<"/[locale]">) {
return (
<html
lang={await locale()}
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
>
<body className="min-h-full flex flex-col"><NextIntlClientProvider>{children}</NextIntlClientProvider></body>
</html>
);
}
Utilisation dans les composants¶
Dans un Server Component¶
import { useTranslations } from 'next-intl';
export default function Page() {
const t = useTranslations();
return <h1>{t('app.titre')}</h1>;
}
Dans un Client Component¶
'use client';
import { useTranslations } from 'next-intl';
export default function MonComposant() {
const t = useTranslations();
return <p>{t('app.titre')}</p>;
}
Message avec variable¶
Message avec pluriel¶
{
"app":{
"clics": "{nombreclics, plural, =0 {Aucun clic} one {Un clic} other {{nombreclics} clics}}"
}
}
Manuel
Formater une date¶
import { useFormatter } from 'next-intl';
const format = useFormatter();
const maintenant = new Date();
<p>{format.dateTime(maintenant, { year: 'numeric', month: 'long', day: '2-digit' })}</p>
Formater une devise (monétaire)¶
import { useFormatter } from 'next-intl';
const format = useFormatter();
const prix = 12.35;
<p>{format.number(prix, { style: 'currency', currency: 'CAD' })}</p>
Formater un pourcentage¶
import { useFormatter } from 'next-intl';
const format = useFormatter();
const rabais = 0.15;
<p>{format.number(rabais, { style: 'percent' })}</p>
Sélecteur de langue¶
Pour changer de langue, next-intl fournit des composants de navigation qui préservent le chemin courant tout en changeant le segment [locale] dans l'URL :
'use client';
import {useLocale} from 'next-intl';
import {useTransition} from 'react';
import {usePathname, useRouter} from '@/i18n/navigation';
import {routing} from '@/i18n/routing';
const labels: Record<string, string> = {
fr: 'Français',
en: 'English',
};
export default function LocaleSwitcher() {
const locale = useLocale();
const router = useRouter();
const pathname = usePathname();
const [isPending, startTransition] = useTransition();
return (
<select
aria-label="Language"
value={locale}
disabled={isPending}
onChange={(e) => {
const nextLocale = e.target.value;
startTransition(() => {
router.replace(pathname, {locale: nextLocale});
});
}}
className="rounded border border-zinc-300 bg-white px-2 py-1 text-sm dark:border-zinc-700 dark:bg-zinc-900"
>
{routing.locales.map((l) => (
<option key={l} value={l}>
{labels[l] ?? l}
</option>
))}
</select>
);
}