Mior Agency

Internationalisation dans Next.js 13 avec les composants serveur React

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 useEffectpour 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.

application encadrée
Vous pouvez également consulter la démo interactive. (Grand aperçu)

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.

Une application qui rend une simple grille de photos
(Grand aperçu)

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.

Plus après le saut! Continuez à lire ci-dessous ↓

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-intlnous 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.

L'heure de la photo d'une application avec la date formatée
(Grand aperçu)

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.

La version espagnole de l'application.
(Grand aperçu)

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.

Sélection de l'ordre de l'application avec les photos les plus populaires affichées
(Grand aperçu)

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 :

  1. rendre le select élément avec un composant client interactif.
  2. Rendre l’internationalisé option éléments avec un composant serveur et les transmettre en tant que children vers select é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 OrderBySelectqui 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.

pagination de l'application
(Grand aperçu)

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.

composants d'application
(Grand aperçu)

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

édito fracassant
(Oui oui)

Laissez un commentaire

Derniers Posts
Une Question ? Un Projet ?
Quel que soit votre projet, MIOR AGENCY vous écoute, analyse vos besoins et propose des pistes de travail en conséquence. Vous avancez avec sérénité et confiance.