next-intl
pour implémenter tous les besoins d’internationalisation dans React Server Components et partage une technique pour introduire l’interactivité avec une empreinte côté client minimaliste.
Avec l’introduction de Next.js 13 et la version bêta d’App Router, les composants du serveur React sont devenus accessibles au public. Ce nouveau paradigme permet des composants qui ne nécessitent pas les fonctionnalités interactives de React, telles que useState
et useEffect
pour rester uniquement côté serveur.
Un domaine qui bénéficie de cette nouvelle capacité est internationalisation. Traditionnellement, l’internationalisation nécessite un compromis sur les performances, car le chargement de la traduction entraîne des paquets plus volumineux côté client et l’utilisation d’analyseurs de messages affecte les performances de l’exécution client de votre application.
la promesse de Composants du serveur React est que nous pouvons avoir notre gâteau et le manger aussi. Si l’internationalisation est entièrement implémentée côté serveur, nous pouvons atteindre de nouveaux niveaux de performance pour nos applications, laissant le côté client pour les fonctions interactives. Mais comment pouvons-nous travailler avec ce paradigme lorsque nous avons besoin d’états contrôlés de manière interactive qui devraient être reflétés dans des messages internationalisés ?
Dans cet article, nous allons explorer une application multilingue qui affiche des images de photographie de rue d’Unsplash. nous utiliserons next-intl
pour implémenter tous nos besoins d’internationalisation dans React Server Components, et nous examinerons une technique pour introduire l’interactivité avec une empreinte minimaliste côté client.
Obtenir des photos d’Unsplash
L’un des principaux avantages des composants de serveur est la possibilité d’obtenir des données directement à partir des composants via async
/await
. Nous pouvons l’utiliser pour obtenir les photos d’Unsplash dans notre composant de page.
Mais d’abord, nous devons créer notre client API basé sur le SDK officiel Unsplash.
import {createApi} from 'unsplash-js';
export default createApi({
accessKey: process.env.UNSPLASH_ACCESS_KEY
});
Une fois que nous avons notre client API Unsplash, nous pouvons l’utiliser dans notre composant de page.
import {OrderBy} from 'unsplash-js';
import UnsplashApiClient from './UnsplashApiClient';
export default async function Index() {
const topicSlug = 'street-photography';
const [topicRequest, photosRequest] = await Promise.all([
UnsplashApiClient.topics.get({topicIdOrSlug: topicSlug}),
UnsplashApiClient.topics.getPhotos({
topicIdOrSlug: topicSlug,
perPage: 4
})
]);
return (
<PhotoViewer
coverPhoto={topicRequest.response.cover_photo}
photos={photosRequest.response.results}
/>
);
}
Note: nous utilisons Promise.all
pour invoquer les deux demandes, nous devons le faire en parallèle. De cette façon, nous évitons une cascade de demandes.
À ce stade, notre application affiche une simple grille de photos.

L’application utilise actuellement des balises anglaises codées en dur et les dates des photos sont affichées sous forme d’horodatages, ce qui n’est pas (encore) très convivial.
Ajouter l’internationalisation avec next-intl
En plus de l’anglais, nous aimerions que notre application soit disponible en espagnol. La prise en charge des composants serveur est actuellement en version bêta pour next-intl
nous pouvons donc utiliser les instructions d’installation de la dernière version bêta pour configurer notre application pour l’internationalisation.
Formatage des dates
En plus d’ajouter une deuxième langue, nous avons déjà constaté que l’application ne s’adapte pas bien aux utilisateurs anglais car les dates doivent être formatées. Pour une bonne expérience utilisateur, nous aimerions indiquer à l’utilisateur l’heure relative à laquelle la photo a été téléchargée (par exemple, « il y a 8 jours »).
Une fois next-intl
est défini, nous pouvons fixer le format en utilisant le format.relativeTime
dans le composant qui rend chaque photo.
import {useFormatter} from 'next-intl';
export default function PhotoGridItem({photo}) {
const format = useFormatter();
const updatedAt = new Date(photo.updated_at);
return (
<a href={photo.links.html}>
{/* ... */}
<p>{format.relativeTime(updatedAt)}</p>
</div>
</a>
);
}
La date à laquelle une photo a été mise à jour est maintenant plus facile à lire.

Indice: Dans une application React traditionnelle qui s’affiche à la fois côté serveur et côté client, il peut être assez difficile de s’assurer que la date relative affichée est synchronisée sur le serveur et le client. Étant donné qu’il s’agit d’environnements différents et qu’ils peuvent se trouver dans des fuseaux horaires différents, vous devez mettre en place un mécanisme pour transférer l’heure du côté serveur vers le côté client. En effectuant le formatage uniquement côté serveur, nous n’avons pas à nous soucier de ce problème en premier lieu.
Salut! ???? Traduire notre application en espagnol
Ensuite, nous pouvons remplacer les balises statiques dans l’en-tête par des messages localisés. Ces balises sont transmises en tant qu’accessoires au PhotoViewer
c’est donc notre chance d’introduire des étiquettes dynamiques via le useTranslations
accrocher.
import {useTranslations} from 'next-intl';
export default function PhotoViewer(/* ... */) {
const t = useTranslations('PhotoViewer');
return (
<>
<Header
title={t('title')}
description={t('description')}
/>
{/* ... */}
</>
);
}
Pour chaque balise internationalisée que nous ajoutons, nous devons nous assurer qu’il existe une entrée appropriée configurée pour toutes les langues.
// en.json
{
"PhotoViewer": {
"title": "Street photography",
"description": "Street photography captures real-life moments and human interactions in public places. It is a way to tell visual stories and freeze fleeting moments of time, turning the ordinary into the extraordinary."
}
}
// es.json
{
"PhotoViewer": {
"title": "Street photography",
"description": "La fotografía callejera capta momentos de la vida real y interacciones humanas en lugares públicos. Es una forma de contar historias visuales y congelar momentos fugaces del tiempo, convirtiendo lo ordinario en lo extraordinario."
}
}
Conseil: next-intl
fournit une intégration TypeScript qui vous aide à vous assurer que vous ne faites référence qu’à des clés de message valides.
Une fois cela fait, nous pouvons visiter la version espagnole de l’application à /es
.

Jusqu’ici, tout va bien!
Ajout d’interactivité : tri dynamique des photos
Par défaut, l’API Unsplash renvoie les photos les plus populaires. Nous voulons que l’utilisateur puisse changer l’ordre pour afficher les photos les plus récentes en premier.
Ici, la question se pose de savoir si nous devons recourir à la récupération de données côté client afin de pouvoir implémenter cette fonction avec useState
. Cependant, cela nous obligerait à déplacer tous nos composants du côté client, ce qui entraînerait une taille de package plus importante.
Avons-nous une alternative ? Oui. Et c’est une fonctionnalité qui existe depuis longtemps sur le Web : les paramètres de recherche (parfois appelés paramètres de requête). Ce qui fait des paramètres de recherche un excellent choix pour notre cas d’utilisation, c’est qu’ils peuvent être lus côté serveur.
Alors modifions notre composant de page pour recevoir searchParams
grâce aux accessoires.
export default async function Index({searchParams}) {
const orderBy = searchParams.orderBy || OrderBy.POPULAR;
const [/* ... */, photosRequest] = await Promise.all([
/* ... */,
UnsplashApiClient.topics.getPhotos({orderBy, /* ... */})
]);
Après ce changement, l’utilisateur peut accéder à /?orderBy=latest
pour modifier l’ordre des photos affichées.
Pour permettre à l’utilisateur de modifier plus facilement la valeur du paramètre de recherche, nous aimerions rendre une image interactive select
élément à l’intérieur d’un composant.

Nous pouvons marquer le composant avec 'use client';
pour attacher un gestionnaire d’événements et traiter les événements de modification à partir du select
élément. Cependant, nous aimerions garder les problèmes d’internationalisation côté serveur pour réduire la taille du paquet client.
Examinons le balisage requis pour notre select
élément.
<select>
<option value="popular">Popular</option>
<option value="latest">Latest</option>
</select>
Nous pouvons diviser ce balisage en deux parties :
- rendre le
select
élément avec un composant client interactif. - Rendre l’internationalisé
option
éléments avec un composant serveur et les transmettre en tant quechildren
versselect
élément.
Mettons en œuvre le select
élément pour le côté client.
'use client';
import {useRouter} from 'next-intl/client';
export default function OrderBySelect({orderBy, children}) {
const router = useRouter();
function onChange(event) {
// The `useRouter` hook from `next-intl` automatically
// considers a potential locale prefix of the pathname.
router.replace('/?orderBy=' + event.target.value);
}
return (
<select defaultValue={orderBy} onChange={onChange}>
{children}
</select>
);
}
Maintenant, utilisons notre composant dans PhotoViewer
et indiquer l’emplacement option
des articles comme children
.
import {useTranslations} from 'next-intl';
import OrderBySelect from './OrderBySelect';
export default function PhotoViewer({orderBy, /* ... */}) {
const t = useTranslations('PhotoViewer');
return (
<>
{/* ... */}
<OrderBySelect orderBy={orderBy}>
<option value="popular">{t('orderBy.popular')}</option>
<option value="latest">{t('orderBy.latest')}</option>
</OrderBySelect>
</>
);
}
Avec ce modèle, le balisage pour le option
Les éléments sont désormais générés côté serveur et transmis au OrderBySelect
qui gère l’événement de modification côté client.
Conseil: Étant donné que nous devons attendre que le balisage mis à jour soit généré côté serveur lorsque la demande est modifiée, nous pouvons souhaiter montrer à l’utilisateur un état de chargement. React 18 a introduit le useTransition
hook, qui est intégré aux composants serveur. Cela nous permet de désactiver le select
en attendant une réponse du serveur.
import {useRouter} from 'next-intl/client';
import {useTransition} from 'react';
export default function OrderBySelect({orderBy, children}) {
const [isTransitioning, startTransition] = useTransition();
const router = useRouter();
function onChange(event) {
startTransition(() => {
router.replace('/?orderBy=' + event.target.value);
});
}
return (
<select disabled={isTransitioning} /* ... */>
{children}
</select>
);
}
Ajoutez plus d’interactivité : contrôles de page
Le même modèle que nous avons exploré pour modifier l’ordre peut être appliqué aux contrôles de page en entrant un page
paramètre de recherche.

Notez que les langues ont des règles différentes pour gérer les séparateurs décimaux et des milliers. De plus, les langues ont différentes formes de pluralisation : alors que l’anglais ne fait qu’une distinction grammaticale entre un et zéro/plusieurs éléments, par exemple, le croate a une forme distincte pour « peu » d’éléments.
next-intl
utilise la syntaxe ICU qui permet d’exprimer ces subtilités du langage.
// en.json
{
"Pagination": {
"info": "Page {page, number} of {totalPages, number} ({totalElements, plural, =1 {one result} other {# results}} in total)",
// ...
}
}
Cette fois, nous n’avons pas besoin de marquer un composant avec 'use client';
. Au lieu de cela, nous pouvons implémenter cela avec des balises d’ancrage régulières.
import {ArrowLeftIcon, ArrowRightIcon} from '@heroicons/react/24/solid';
import {Link, useTranslations} from 'next-intl';
export default function Pagination({pageInfo, orderBy}) {
const t = useTranslations('Pagination');
const totalPages = Math.ceil(pageInfo.totalElements / pageInfo.size);
function getHref(page) {
return {
// Since we're using `Link` from next-intl, a potential locale
// prefix of the pathname is automatically considered.
pathname: '/',
// Keep a potentially existing `orderBy` parameter.
query: {orderBy, page}
};
}
return (
<>
{pageInfo.page > 1 && (
<Link aria-label={t('prev')} href={getHref(pageInfo.page - 1)}>
<ArrowLeftIcon />
</Link>
)}
<p>{t('info', {...pageInfo, totalPages})}</p>
{pageInfo.page < totalPages && (
<Link aria-label={t('prev')} href={getHref(pageInfo.page + 1)}>
<ArrowRightIcon />
</Link>
)}
</>
);
}
conclusion
Les composants serveur sont une excellente combinaison pour l’internationalisation
L’internationalisation est une partie importante de l’expérience utilisateur, que vous preniez en charge plusieurs langues ou que vous souhaitiez maîtriser les subtilités d’une seule langue. une bibliothèque comme next-intl
peut aider dans les deux cas.
Historiquement, la mise en œuvre de l’internationalisation dans les applications Next.js a compromis les performances, mais avec les composants côté serveur, ce n’est plus le cas. Cependant, cela peut prendre un certain temps pour explorer et apprendre des modèles qui vous aideront à garder vos problèmes d’internationalisation côté serveur.
Dans notre application de visionneuse de photographie de rue, nous n’avions besoin que d’un seul composant côté client : OrderBySelect
.

Une autre chose à garder à l’esprit est que vous voudrez peut-être envisager de mettre en œuvre des états de charge, car la latence du réseau introduit un délai avant que les utilisateurs ne voient le résultat de leurs actions.
Les paramètres de recherche sont une excellente alternative à useState
Les paramètres de recherche sont un excellent moyen d’implémenter des fonctionnalités interactives dans les applications Next.js, car ils permettent de réduire la taille du package côté client.
Outre les performances, il existe d’autres avantages de l’utilisation des paramètres de recherche:
- Les URL avec des paramètres de recherche peuvent être partagées tout en préservant l’état de l’application.
- Les signets conservent également leur état.
- En option, il peut être intégré à l’historique du navigateur, vous permettant d’annuler les changements de statut via le bouton de retour.
Notez toutefois qu’il existe également compromis à envisager:
- Les valeurs des paramètres de recherche sont des chaînes, vous devrez donc peut-être sérialiser et désérialiser les types de données.
- L’URL fait partie de l’interface utilisateur, donc l’utilisation d’un trop grand nombre de paramètres de recherche peut affecter la lisibilité.
Vous pouvez consulter le code complet de l’exemple sur GitHub.
Merci beaucoup à delta des olives de Vercel pour ses commentaires sur cet article.
Lecture supplémentaire sur SmashingMag

(Oui oui)