Fermer

mars 17, 2023

Internationalisation dans Next.js 13 avec les composants serveur React

Internationalisation dans Next.js 13 avec les composants serveur React


Avec l’introduction de Suivant.js 13 et la version bêta de l’App Router, les composants du serveur React sont devenus accessibles au public. Ce nouveau paradigme permet aux composants qui ne nécessitent pas les fonctionnalités interactives de React, telles que useState et useEffectpour rester côté serveur uniquement.

Un domaine qui bénéficie de cette nouvelle capacité est internationalisation. Traditionnellement, l’internationalisation nécessite un compromis en termes de performances, car le chargement des traductions entraîne des ensembles plus volumineux côté client et l’utilisation d’analyseurs de messages a un impact sur les performances d’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 mise en œuvre côté serveur, nous pouvons atteindre de nouveaux niveaux de performances pour nos applications, laissant le côté client pour les fonctionnalités 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 côté client minimaliste.

Application finale encadrée
Vous pouvez également vérifier le démo interactive. (Grand aperçu)

Récupérer des photos depuis Unsplash

L’un des principaux avantages des composants serveur est la possibilité de récupérer des données directement à partir des composants internes via async/await. Nous pouvons l’utiliser pour récupérer 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 requêtes que nous devons faire en parallèle. De cette façon, nous évitons une cascade de requêtes.

À 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 étiquettes 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 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 obtenir 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 configuré, nous pouvons corriger le formatage en utilisant le format.relativeTime fonction 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>
  );
}

Désormais, la date à laquelle une photo a été mise à jour est 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 configurer un mécanisme pour transférer l’heure du serveur côté client. En effectuant le formatage uniquement côté serveur, nous n’avons pas à nous soucier de ce problème en premier lieu.

¡Hé ! 👋 Traduire notre application en espagnol

Ensuite, nous pouvons remplacer les étiquettes statiques dans l’en-tête par des messages localisés. Ces étiquettes sont transmises en tant qu’accessoires de la 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 étiquette 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é : classement 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 devrions recourir à la récupération de données côté client afin que nous puissions implémenter cette fonctionnalité avec useState. Cependant, cela nous obligerait à déplacer tous nos composants du côté client, ce qui augmenterait la taille du bundle.

Avons-nous une alternative ? Oui. Et c’est une fonctionnalité qui existe sur le Web depuis des lustres : paramètres de recherche (parfois appelé paramètres de requête). Ce qui fait des paramètres de recherche une excellente option pour notre cas d’utilisation, c’est qu’ils peuvent être lus côté serveur.

Alors modifions notre composant de page pour recevoir searchParams via des 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 facilement la valeur du paramètre de recherche, nous aimerions rendre une image interactive select élément depuis l’intérieur d’un composant.

L'ordre de l'application sélectionne 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. Néanmoins, nous aimerions garder les soucis d’internationalisation côté serveur pour réduire la taille du bundle 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 au 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 fournir la localisation option éléments 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 elements est maintenant généré 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 commande est modifiée, nous pouvons souhaiter montrer à l’utilisateur un état de chargement. Réagir 18 introduit le useTransition accrocher, qui est intégré aux composants serveur. Cela nous permet de désactiver le select élément 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>
  );
}

Ajout de plus d’interactivité : commandes de page

Le même modèle que nous avons exploré pour modifier l’ordre peut être appliqué aux contrôles de page en introduisant 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 décimales et les séparateurs de 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 le Syntaxe ICU qui permet d’exprimer ces subtilités de 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 de serveur sont un excellent match 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.

La mise en œuvre de l’internationalisation dans les applications Next.js s’est historiquement accompagnée d’un compromis en termes de performances, mais avec les composants 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 de l'application
(Grand aperçu)

Un autre aspect à noter est que vous voudrez peut-être envisager d’implémenter des états de chargement car la latence du réseau introduit un délai avant que vos 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 aident à réduire la taille du bundle 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 préservent également l’état.
  • Vous pouvez éventuellement intégrer l’historique du navigateur, permettant d’annuler les changements d’état via le bouton de retour.

Notez toutefois qu’il existe également compromis à considérer:

  • 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 de nombreux paramètres de recherche peut affecter la lisibilité.

Vous pouvez consulter l’intégralité code de l’exemple sur GitHub.

Un grand merci à delta de l’olivier de Vercel pour ses commentaires sur cet article !

Lectures complémentaires sur SmashingMag

Éditorial fracassant
(yk, il)






Source link