Détection des paramètres régionaux d’un utilisateur dans une application web

L’un des problèmes les plus courants dans le développement d’applications web est la détection des paramètres régionaux d’un utilisateur. Voici comment le faire correctement.

Que nous développions un simple blog ou une application monopage (SPA) sophistiquée et moderne, il arrive souvent qu’en considérant i18n dans une application web, nous soyons confrontés à une question importante : comment détecter la préférence linguistique d’un utilisateur ? C’est important, car nous voulons toujours offrir la meilleure expérience utilisateur, et si l’utilisateur a défini un ensemble de langues préférées dans son navigateur, nous voulons faire de notre mieux pour présenter notre contenu dans ces langues préférées.
Dans cet article, nous allons passer en revue trois façons différentes de détecter les paramètres régionaux d’un utilisateur : à travers l’objet navigator.languages du navigateur (côté client), à travers l’en-tête HTTP Accept-Language (côté serveur), et à travers la géolocalisation en utilisant l’adresse IP de l’utilisateur (côté serveur).

Côté client : L’objet navigator.languages

Les navigateurs modernes fournissent un objet navigator.languages que nous pouvons utiliser pour obtenir toutes les langues préférées que l’utilisateur ou l’utilisatrice a définies dans son navigateur.
Objet navigator.languages du navigateur pour les paramètres de langue de la page web | Phrase

Les paramètres de langue dans Firefox

Étant donné les paramètres ci-dessus, si nous devions ouvrir la console Firefox et vérifier la valeur de navigator.languages, nous obtiendrions ce qui suit :
Valeur de l'objet navigator.languages de Firefox | Phrase

Les codes des paramètres régionaux correspondent à ceux de nos paramètres de navigateur

navigator.languages est disponible dans tous les navigateurs web modernes et est généralement sûr de s’y fier. Alors écrivons une fonction JavaScript réutilisable qui nous indique la ou les langue(s) préférée(s) de l’utilisateur actuel.

function getBrowserLocales(options = {}) {
  const defaultOptions = {
    languageCodeOnly : false,
  };
  const opt = {
    ...defaultOptions,
    ...options,
  };
  const browserLocales =
    navigator.languages === undefined
      ? [navigator.langue]
      : navigator.langues;
  if (!browserLocales) {
    return undefined;
  }
  return browserLocales.map(locale => {
    const trimmedLocale = locale.trim();
    return opt.languageCodeOnly
      ? trimmedLocale.split(/-|_/)[0]
      : trimmedLocale;
  });
}

getBrowserLocales() vérifie le tableau navigator.languages, en revenant à navigator.language si le tableau n’est pas disponible. Il convient de noter que dans certains navigateurs, comme Chrome, navigator.language sera la langue de l’UI, qui est probablement la langue à laquelle le système d’exploitation est réglé. C’est différent de navigator.languages, qui contient les langues préférées définies par l’utilisateur dans le navigateur lui-même.

✋🏽 Attention » Si vous prenez en charge Internet Explorer, vous devrez utiliser les propriétés navigator.userLanguage et navigator.browserLanguage. Bien sûr, vous devrez également remplacer toutes les instances de const par var dans le code ci-dessus.

Notre fonction propose également une option pratique languageCodeOnly, qui supprime les codes pays des paramètres régionaux avant de les renvoyer. Cela peut être utile lorsque notre application ne gère pas vraiment les nuances régionales d’une langue, par exemple, nous n’avons qu’une seule version du contenu en anglais.
Avec languageCodeOnly: true, nous obtenons les langues sans pays | Phrase

Avec languageCodeOnly: true, nous obtenons les langues sans pays

Côté Serveur : En-tête HTTP Accept-Language

Si l’utilisateur définit ses préférences de langue dans un navigateur moderne, le navigateur enverra, à son tour, un en-tête HTTP qui transmet ces préférences de langue au serveur avec chaque requête. C’est l’en-tête Accept-Language, et il ressemble souvent à ceci : Accept-Language: en-CA,ar-EG;q=0.5.
L’en-tête liste les langues préférées de l’utilisateur, avec un poids défini par une valeur q attribuée à chacune. Lorsqu’une valeur explicite de q n’est pas spécifiée, une valeur par défaut de 1.0 est utilisée. Ainsi, dans la valeur d’en-tête ci-dessus, le client indique que l’utilisateur préfère l’anglais canadien (avec un poids de q = 1.0), puis l’arabe égyptien (avec un poids de q = 0.5).
Nous pouvons utiliser cet en-tête HTTP standard pour déterminer les paramètres régionaux préférés de l’utilisateur. Écrivons une classe appelée HttpAcceptLanguageHeaderLocaleDetector pour ce faire. Nous utiliserons PHP ici, mais vous pouvez utiliser n’importe quelle langue que vous aimez ; l’en-tête Accept-Language devrait être le même (ou suffisamment similaire) dans tous les environnements.

<?php
class HttpAcceptLanguageHeaderLocaleDetector
{
  const HTTP_ACCEPT_LANGUAGE_HEADER_KEY = 'HTTP_ACCEPT_LANGUAGE';
  public static function detect()
  {
    $httpAcceptLanguageHeader = static::getHttpAcceptLanguageHeader();
    if ($httpAcceptLanguageHeader == null) {
      return [] ;
    }
    $locales = static::getWeightedLocales($httpAcceptLanguageHeader);
    $sortedLocales = static::sortLocalesByWeight($locales);
    return array_map(function ($weightedLocale) {
      return $weightedLocale['locale'];
    }, $sortedLocales);
  }
  private static function getHttpAcceptLanguageHeader()
  {
    if (isset($_SERVER[static::HTTP_ACCEPT_LANGUAGE_HEADER_KEY])) {
      return trim($_SERVER['HTTP_ACCEPT_LANGUAGE']);
    } else {
      return null;
    }
  }
  private static function getWeightedLocales($httpAcceptLanguageHeader)
  {
    if (strlen($httpAcceptLanguageHeader) == 0) {
      return [];
    }
    $weightedLocales = [];
    // Nous décomposons la chaîne 'en-CA,ar-EG;q=0.5' le long des virgules,
    // et itérer sur le tableau résultant de paramètres régionaux individuels. Une fois
    // nous avons terminé, $weightedLocales devrait ressembler à
    // [['locale' => 'en-CA', 'q' => 1.0], ['locale' => 'ar-EG', 'q' => 0.5]]
    foreach (explode(',', $httpAcceptLanguageHeader) as $locale) {
      // séparer la clé des paramètres régionaux ("ar-EG") de son poids ("q=0.5")
      $localeParts = explode(';', $locale);
      $weightedLocale = ['locale' => $localeParts[0]];
      if (count($localeParts) == 2) {
        // poids explicite par exemple : 'q=0.5'
        $weightParts = explode('=', $localeParts[1]);
        // attraper la partie '0.5' et la convertir en float
        $weightedLocale['q'] = floatval($weightParts[1]);
      } else {
        // aucun poids donné dans la chaîne, c'est-à-dire un poids implicite de 'q=1.0'
        $weightedLocale['q'] = 1.0;
      }
      $weightedLocales[] = $weightedLocale;
    }
    return $weightedLocales;
  }
  /**
   * Trier par valeur `q` de la plus élevée à la plus faible
   */
  private static function trierParametresRegionauxParPoids($locales)
  {
    usort($locales, function ($a, $b) {
      // usort va convertir les valeurs float que nous retournons ici en entiers,
      // ce qui peut perturber notre tri. Donc au lieu de soustraire le `q`,
      // au lieu de soustraire les valeurs et de retourner la différence, nous comparons les valeurs `q` et
      // retournons explicitement des valeurs entières.
      if ($a['q'] == $b['q']) {
        retourner 0;
      }
      if ($a['q'] > $b['q']) {
        retourner -1;
      }
      retourner 1;
    });
    return $locales;
  }
}

Ce long morceau de code n’est en fait pas très compliqué. Dans la seule méthode publique, detect(), notre classe fait ce qui suit :

  1. Obtient la valeur brute de la chaîne de l’en-tête Accept-Language, par exemple "en-CA,ar-EG;q=0.5"
  2. Utilise la méthode utilitaire getWeightedLocales() pour analyser la chaîne d’en-tête en un tableau qui ressemble à [['locale' => 'en-CA', 'q' => 1.0], ['locale' => 'ar-EG', 'q' => 0.5]].
  3. Utilise la méthode utilitaire sortLocalesByWeight() pour trier le tableau ci-dessus de la valeur q la plus élevée à la plus basse.
  4. Extrait les valeurs locale du tableau trié, renvoyant un tableau qui ressemble à ['en-CA', 'ar-EG'].

Nous pouvons maintenant utiliser notre nouvelle classe pour obtenir un joli tableau consommable de codes de paramètres régionaux basés sur l’en-tête HTTP Accept-Language.

<?php
$locales = HttpAcceptLanguageHeaderLocaleDetector::detect();
// => ['en-CA', 'ar-EG']

Côté serveur: Géolocalisation par l’adresse IP

Parfois, l’en-tête Accept-Language ne sera pas présent dans les requêtes à notre serveur. Dans ces cas, nous pourrions vouloir utiliser l’adresse IP de l’utilisateur pour déterminer le pays de l’utilisateur et en déduire les paramètres régionaux ou la langue de ce pays.

✋🏽 Avertissement » La géolocalisation doit être utilisée en dernier recours lors de la détection des paramètres régionaux de l’utilisateur, car elle peut souvent conduire à une détermination incorrecte des paramètres régionaux. Par exemple, si nous voyons que notre utilisateur vient du Canada, devons-nous supposer que sa langue préférée est l’anglais ou le français ? Les deux sont des langues officielles et largement utilisées dans le pays. Et bien sûr, l’utilisateur pourrait appartenir à une minorité arabophone ou être un visiteur hispanophone.

Utilisation de MaxMind pour la géolocalisation

Pour déterminer le pays de l’utilisateur par l’adresse IP de la requête, nous utiliserons l’API PHP de MaxMind et la base de données de géolocalisation de MaxMind. MaxMind est une entreprise qui propose quelques produits liés aux adresses IP, et parmi eux, deux qui nous intéressent ici :

  • Les bases de données GeoIP2 — ce sont les bases de données de géolocalisation commerciales de MaxMind et elles offrent une faible latence et fonctionnent sur abonnement. Vous voudrez peut-être passer à une édition supérieure si vous souhaitez des bases de données plus à jour ou plus rapides.
  • Les bases de données GeoLite2 — ce sont les bases de données de géolocalisation gratuites de MaxMind, et bien qu’elles soient apparemment moins précises que leurs homologues commerciaux, elles sont plus que suffisantes pour commencer. Nous utiliserons ici une base de données GeoLite2. Notez que vous devrez créditer Maxmind sur votre page web publique et faire un lien vers leur site si vous utilisez l’une de leurs bases de données gratuites.

Pour installer la base de données, il vous suffit de vous inscrire à un compte MaxMind gratuit. Vous recevrez un e-mail avec un lien pour vous connecter. Suivez le lien et connectez-vous. Une fois que vous l’avez fait, vous devriez arriver sur la page Résumé du compte.
Télécharger les bases de données MaxMind | Phrase

Cliquez sur le lien Télécharger les bases de données sur la page Résumé du compte

Cela vous mènera à une page avec la liste des bases de données GeoLite2 gratuites. Récupérez la base de données country binary depuis cet endroit.
base de données binaire de pays MaxMind | Phrase

Nous voulons la base de données binaire des pays pour notre usage

Placez le fichier que vous avez téléchargé quelque part dans votre projet.
Nous aurons également besoin de l’API PHP MaxMind pour le travail avec la base de données. Nous pouvons l’installer avec Composer.

composer require geoip2/geoip2:~2.0

Package de conversion de pays en paramètres régionaux de Peter Kahl

Nous aurons besoin d’un package supplémentaire avant de passer à notre code. Pour déterminer les locales ou les langues d’un pays, nous utiliserons le package country-to-locale de Peter Kahl. Nous pouvons également l’installer en utilisant Composer.

composer require peterkahl/country-to-locale

La classe de détection des paramètres régionaux de l’adresse IP

Avec notre configuration en place, nous pouvons accéder à notre propre classe, IpAddressLocaleDetector.

<?php
require '../vendor/autoload.php';
use GeoIp2\Database\Reader;
use peterkahl\locale\locale;
class IpAddressLocaleDetector
{
  const MAX_MIND_DB_FILEPATH =
    __DIR__ . '/GeoLite2-Country_20200121/GeoLite2-Country.mmdb';
  private static $maxMindDbReader;
  public static function detect()
  {
    $ipAddress = static::getIpAddress();
    try {
      $record = static::getMaxMindDbReader()->country($ipAddress);
      $locales = locale::country2locale($record->country->isoCode);
      $normalizedLocales = str_replace('_', '-', $locales);
      return explode(',', $normalizedLocales);
    } catch (Exception $ex) {
      return null;
    }
  }
  private static function getIpAddress()
  {
    return $_SERVER['REMOTE_ADDR'];
  }
  private static function getMaxMindDbReader()
  {
    if (static::$maxMindDbReader == null) {
      static::$maxMindDbReader = new Reader(static::MAX_MIND_DB_FILEPATH);
    }
    return static::$maxMindDbReader;
  }
}

Notre classe est relativement facile à comprendre. Tout comme HttpAcceptLanguageHeaderLocaleDetector, elle a une méthode publique, detect(), qui fait ce qui suit :

  1. Obtenez l’adresse IP de la requête à partir du tableau global $_SERVER.
  2. Cette adresse IP est transmise à la méthode country du Reader de la base de données MaxMind, qui tente de géolocaliser un pays en fonction de l’adresse IP.
  3. Utilise locale::country2locale() de Peter Kahl pour obtenir les langues du pays donné.
  4. Normalise les paramètres régionaux acquis, de sorte que "en_CA,ar_EG" devienne "en-CA,ar-EG".
  5. Retourne les paramètres régionaux qu’il a normalisés sous forme de tableau, par exemple ["en-CA", "ar-EG"].

📖 Allez plus loin » Le Reader de MaxMind a beaucoup plus de méthodes. Consultez la documentation officielle de l’API si vous souhaitez en savoir plus sur les informations disponibles dans les bases de données MaxMind.

Côté serveur: Détection des paramètres régionaux en cascade

Étant donné les deux stratégies de détection côté serveur que nous avons couvertes ci-dessus, nous pouvons écrire une petite fonction detect_user_locales() qui tentera d’abord la stratégie d’en-tête HTTP.

<?php
require './HttpAcceptLanguageHeaderLocaleDetector.php';
require './IpAddressLocaleDetector.php';
function detect_user_locales()
{
  $locales = HttpAcceptLanguageHeaderLocaleDetector::detect();
  if (count($locales) == 0) {
    $locales = IPAddressLocaleDetector::detect();
  }
  if (count($locales) == 0) {
    // revenir à des paramètres régionaux par défaut, l'anglais dans ce cas
    $locales = ['en'];
  }
  return $locales;
}

Si la détection par en-tête HTTP échoue, detect_user_locales() essaiera la détection par géolocalisation IP. Si ce dernier ne porte pas ses fruits, la fonction reviendra à des paramètres régionaux par défaut.
Si elle est gérée avec soin, la détection des paramètres régionaux de l’utilisateur peut aider à offrir une meilleure expérience utilisateur dans nos applications web. Heureusement, l’objet navigator.languages et l’en-tête HTTP Accept-Langauge sont disponibles pour réduire nos tâtonnements en matière de détection des paramètres régionaux.

Si vous et votre équipe travaillez sur une application web internationalisée, découvrez Phrase, une plateforme i18n professionnelle et adaptée aux développeurs. Avec une CLI et une API flexibles, la synchronisation des traductions avec GitHub et l’intégration de Bitbucket, des traductions over-the-air, et bien plus encore, Phrase couvre vos besoins en i18n, afin que vous puissiez vous concentrer sur votre logique métier.

Découvrez toutes les fonctionnalités de Phrase pour les développeurs et voyez par vous-même comment cela peut rationaliser vos flux de travaux de localisation de logiciels.

Posts associés

Software localization blog category featured image | Phrase

Blog post

Le guide ultime de la localisation JavaScript

Démarrez la localisation JavaScript de votre navigateur avec ce guide complet et préparez votre application pour les utilisateurs internationaux.

Software localization blog category featured image | Phrase

Blog post

Traduction over-the-air Flutter avec Phrase

Arrêtez d’attendre l’approbation du magasin d’applications mobiles et obtenez du contenu frais pour vos utilisateurs d’applications Flutter instantanément à travers le monde—avec Phrase over-the-air.

Software localization blog category featured image | Phrase

Blog post

Le guide ultime de la localisation Flutter

Décodons les secrets de la localisation Flutter afin que vous puissiez parler la langue de vos utilisateurs et continuer à coder votre chemin vers la domination mondiale.

Software localization blog category featured image | Phrase

Blog post

Comment utiliser le plug-in de localisation de Phrase Strings pour WordPress

Apprenez à traduire les pages WordPress ou encore des articles dans plusieurs langues avec l’intégration Phrase Strings pour WordPress.

Software localization blog category featured image | Phrase

Blog post

Traduction de l’application iOS over-the-air avec des chaînes Phrase

Si vous voulez vous assurer que tous vos utilisateurs d’application obtiennent le bon texte, la fonctionnalité over-the-air de Phrase peut vous aider à publier vos mises à jour de traduction instantanément.