Erkennung deiner Sprache und Region in einer Web-App

Eines der häufigsten Probleme bei der Entwicklung von Webanwendungen ist es, die Sprache und Region eines Nutzers zu erkennen. So machst du es richtig.

Egal, ob wir einen einfachen Blog oder eine ausgeklügelte, moderne Single-Page-Anwendung (SPA) entwickeln – wenn wir i18n in einer Webanwendung einsetzen, taucht oft eine wichtige Frage auf: Wie erkennen wir deine Sprachpräferenz? Das ist wichtig, weil wir dir immer die beste Nutzererfahrung bieten wollen. Wenn du in deinem Browser eine Reihe bevorzugter Sprachen festgelegt hast, wollen wir unser Bestes geben, um unsere Inhalte in diesen bevorzugten Sprachen anzuzeigen.
In diesem Artikel schauen wir uns drei verschiedene Möglichkeiten an, wie du deine Spracheinstellungen erkennen kannst: über das navigator.languages-Objekt des Browsers (auf dem Client), über den Accept-Language HTTP-Header (auf dem Server) und über Geolokalisierung mit der IP-Adresse (auf dem Server).

Client-seitig: Das navigator.languages Objekt

Moderne Browser bieten ein navigator.languages-Objekt, das wir verwenden können, um alle bevorzugten Sprachen abzurufen, die du in deinem Browser festgelegt hast.
Browser-navigator.languages-Objekt für die Spracheinstellungen der Website | Phrase

Die Spracheinstellungen in Firefox

Angesichts der obigen Einstellungen würden wir, wenn wir die Firefox-Konsole öffnen und den Wert von navigator.languages überprüfen, Folgendes erhalten:
Firefox navigator.languages-Objektwert | Phrase

Die Codes für die Gebietsschemata stimmen mit denen in unseren Browsereinstellungen überein

navigator.languages ist in allen modernen Webbrowsern verfügbar und kann in der Regel bedenkenlos verwendet werden. Lass uns also eine wiederverwendbare JavaScript-Funktion schreiben, die uns zeigt, welche Sprache(n) der aktuelle Nutzer bevorzugt.

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

getBrowserLocales() überprüft das navigator.languages<2>-Array und greift auf navigator.language zurück, wenn das Array nicht verfügbar ist. Es ist erwähnenswert, dass in einigen Browsern, wie Chrome, navigator.language die UI-Sprache sein wird, die wahrscheinlich die Sprache ist, auf die das Betriebssystem eingestellt ist. Das ist anders als navigator.languages, was die vom Benutzer festgelegten bevorzugten Sprachen im Browser selbst hat.

✋🏽 Hinweis » Wenn du Internet Explorer unterstützt, musst du die Eigenschaften navigator.userLanguage und navigator.browserLanguage verwenden. Natürlich musst du auch alle Vorkommen von const im obigen Code durch var ersetzen.

Unsere Funktion hat auch eine praktische languageCodeOnly Option, mit der du die Ländercodes von Locales entfernen kannst, bevor sie zurückgegeben werden. Das kann praktisch sein, wenn unsere App die regionalen Nuancen einer Sprache nicht wirklich berücksichtigt, z.B. wenn wir nur eine Version von englischen Inhalten haben.
Mit languageCodeOnly: true erhalten wir die Sprachen ohne Länder | Phrase

Mit languageCodeOnly: true, bekommen wir die Sprachen ohne Ländercodes

Server-seitig: Der Accept-Language HTTP-Header

Wenn du deine Spracheinstellungen in einem modernen Browser festlegst, sendet der Browser im Gegenzug einen HTTP-Header, der diese Spracheinstellungen bei jeder Anfrage an den Server übermittelt. Dies ist der Accept-Language Header, und sieht meistens so aus: Accept-Language: en-CA,ar-EG;q=0.5.
Im Header siehst du deine bevorzugten Sprachen, jeweils mit einem Gewicht, das durch einen q Wert festgelegt ist. Wenn kein expliziter q-Wert angegeben ist, wird der Standardwert 1.0 angenommen. Im obigen Header-Wert gibt der Client an, dass du kanadisches Englisch bevorzugst (mit einem Gewicht von q = 1.0), dann ägyptisches Arabisch (mit einem Gewicht von q = 0.5).
Wir können diesen standardmäßigen HTTP-Header verwenden, um die bevorzugten Spracheinstellungen des Benutzers zu bestimmen. Lass uns eine Klasse namens HttpAcceptLanguageHeaderLocaleDetector schreiben, die das erledigt. Wir verwenden hier PHP, aber du kannst jede Sprache verwenden, die du möchtest; der Accept-Language-Header sollte in allen Umgebungen gleich (oder ähnlich genug) sein.

<?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 = [];
    // Wir zerlegen die Zeichenfolge 'en-CA,ar-EG;q=0.5' an den Kommas,
    // und iteriere über das resultierende Array von einzelnen Locales. Sobald
    // wenn wir fertig sind, sollte $weightedLocales so aussehen
    // [['locale' => 'en-CA', 'q' => 1.0], ['locale' => 'ar-EG', 'q' => 0.5]]
    foreach (explode(',', $httpAcceptLanguageHeader) as $locale) {
      // trenne den Sprachschlüssel ("ar-EG") von seinem Gewicht ("q=0.5")
      $localeParts = explode(';', $locale);
      $weightedLocale = ['locale' => $localeParts[0]];
      if (count($localeParts) == 2) {
        // explizites Gewicht z. B. 'q=0.5'
        $weightParts = explode('=', $localeParts[1]);
        // hol dir den '0.5'-Teil und wandle ihn in eine Fließkommazahl um
        $weightedLocale['q'] = floatval($weightParts[1]);
      } else {
        // kein Gewicht in der Zeichenfolge angegeben, d.h. implizites Gewicht von 'q=1.0'
        $weightedLocale['q'] = 1.0;
      }
      $weightedLocales[] = $weightedLocale;
    }
    return $weightedLocales;
  }
  /**
   * Sortiere nach hoch- bis niedrigem `q`-Wert
   */
  private static function sortLocalesByWeight($locales)
  {
    usort($locales, function ($a, $b) {
      // usort wird die Float-Werte, die wir hier zurückgeben, in Ganzzahlen umwandeln,
      // was unser Sortieren durcheinanderbringen kann. Statt also den `q` zu subtrahieren,
      // Werte und die Differenz zurückzugeben, vergleichen wir die `q`-Werte und
      // Gibt explizit Ganzzahlen zurück.
      if ($a['q'] == $b['q']) {
        return 0;
      }
      if ($a['q'] > $b['q']) {
        return -1;
      }
      return 1;
    });
    return $locales;
  }
}

Dieser lange Code ist eigentlich nicht sehr kompliziert. In der einzigen öffentlichen Methode detect() führt unsere Klasse Folgendes aus:

  1. Holt den rohen Zeichenfolgenwert des Accept-Language Headers, z. B. "en-CA,ar-EG;q=0.5"
  2. Verwende die Hilfsmethode getWeightedLocales(), um die Header-Zeichenfolge in ein Array zu parsen, das wie [['locale' => 'en-CA', 'q' => 1.0], ['locale' => 'ar-EG', 'q' => 0.5]] aussieht.
  3. Verwende die Hilfsmethode sortLocalesByWeight(), um das obige Array von höchstem zu niedrigstem q-Wert zu sortieren.
  4. Zieh die locale-Werte aus dem sortierten Array und gib ein Array zurück, das so aussieht: ['en-CA', 'ar-EG'].

Jetzt kannst du unsere neue Klasse verwenden, um ein schönes, verwendbares Array von Locale-Codes basierend auf dem Accept-Language HTTP-Header zu erhalten.

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

Server-seitig: Geolokalisierung nach IP-Adresse

Manchmal fehlt der Accept-Language-Header bei Anfragen an unseren Server. In diesen Fällen möchten wir vielleicht die IP-Adresse des Nutzers verwenden, um sein Land zu bestimmen und daraus die Sprache oder Spracheinstellung abzuleiten.

✋🏽 Vorsicht » Die Geolokalisierung sollte nur als letzte Möglichkeit genutzt werden, um deine Spracheinstellung zu erkennen, da das oft zu einer falschen Sprache führen kann. Zum Beispiel, wenn wir sehen, dass du aus Kanada kommst, nehmen wir dann an, dass deine bevorzugte Sprache Englisch oder Französisch ist? Beide sind formelle und weit verbreitete Sprachen im Land. Und natürlich könntest du einer arabischsprachigen Minderheit angehören oder ein spanischsprachiger Besucher sein.

Verwendung von MaxMind für Geolokation

Um das Land des Nutzers anhand der IP-Adresse der Anfrage zu bestimmen, nutzen wir die MaxMind PHP API und die MaxMind GeoIP-Datenbank. MaxMind ist ein Unternehmen, das einige IP-bezogene Produkte anbietet. Darunter sind zwei, die für uns hier interessant sind:

  • Die GeoIP2-Datenbanken — das sind die kommerziellen Geolokalisierungsdatenbanken von MaxMind, die latenzarm und abonnementbasiert sind. Du möchtest vielleicht auf diese upgraden, wenn du aktuellere oder schnellere Datenbanken willst.
  • Die GeoLite2-Datenbanken – das sind die kostenlosen Geolokalisierungsdatenbanken von MaxMind, und obwohl sie laut Berichten weniger genau sind als ihre kommerziellen Pendants, reichen sie mehr als aus, um loszulegen. Wir werden hier eine GeoLite2-Datenbank verwenden. Beachte, dass du Maxmind auf deiner öffentlichen Webseite erwähnen und auf deren Seite verlinken musst, wenn du eine ihrer kostenlosen Datenbanken verwendest.

Um die Datenbank zu installieren, melde dich einfach für ein kostenloses MaxMind-Konto an. Du erhältst eine E-Mail mit einem Anmeldelink. Folge dem Link und melde dich an. Sobald du das getan hast, solltest du auf deiner Kontozusammenfassung-Seite landen.
MaxMind Datenbanken herunterladen | Phrase

Klick auf den Link Datenbanken herunterladen auf der Kontozusammenfassungsseite

Das bringt dich zu einer Seite mit der Liste der kostenlosen GeoLite2-Datenbanken. Hol dir die Länder-Binärdatenbank von dort.
MaxMind Länderdatenbank (Binärformat) | Phrase

Wir benötigen die binäre Länderdatenbank für unsere Zwecke

Lege die Datei, die du heruntergeladen hast, irgendwo in deinem Projekt ab.
Du brauchst außerdem die MaxMind PHP API, um mit der Datenbank zu arbeiten. Wir können das mit Composer installieren.

composer require geoip2/geoip2:~2.0

Peter Kahls Country-to-Locale-Paket

Wir benötigen noch ein weiteres Paket, bevor wir zu unserem Code kommen. Um die locales oder Sprachen eines Landes zu bestimmen, verwenden wir Peter Kahls country-to-locale Paket. Wir können es auch mit Composer installieren.

composer require peterkahl/country-to-locale

IP-Adress-Lokalisierungs-Klasse

Mit unserem Setup kommen wir nun zu unserer eigenen Klasse 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); // Ruft das Land für die angegebene IP-Adresse ab.
      $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;
  }
}

Unsere Klasse ist relativ einfach. Ähnlich wie HttpAcceptLanguageHeaderLocaleDetector hat es eine öffentliche Methode, detect(), die Folgendes tut:

  1. Hol die IP-Adresse der Anfrage aus dem globalen $_SERVER-Array.
  2. Übergibt diese IP-Adresse an die Methode country von Reader in der MaxMind-Datenbank, die versucht, anhand der IP-Adresse ein Land zu ermitteln.
  3. Verwendet Peter Kahls locale::country2locale(), um die Sprachen eines bestimmten Landes zu ermitteln.
  4. Normalisiert die übernommenen Locales, sodass "en_CA,ar_EG" zu "en-CA,ar-EG" wird.
  5. Gibt die normalisierten locales als Array zurück, z.\u00a0B. ["en-CA", "ar-EG"].

📖 Mehr erfahren » Der MaxMind Reader hat viele weitere Methoden. Sieh dir die offizielle API-Dokumentation an, wenn du etwas tiefer in die Informationen eintauchen möchtest, die in den MaxMind-Datenbanken verfügbar sind.

Serverseitig: Kaskadierende Locale-Erkennung

Angesichts der beiden serverseitigen Erkennungsstrategien, die wir oben behandelt haben, können wir eine kleine detect_user_locales()-Funktion schreiben, die zuerst die HTTP-Header-Strategie versucht.

<?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) {
    // auf eine Standardsprache zurückgreifen, in diesem Fall Englisch
    $locales = ['en'];
  }
  return $locales;
}

Wenn die HTTP-Header-Erkennung fehlschlägt, versucht detect_user_locales(), die IP-Geolokalisierung zu verwenden. Wenn letzteres keine Ergebnisse bringt, wird die Funktion auf eine Standardsprache zurückgreifen.
Wenn du es richtig machst, kann das Erkennen der Sprache des Nutzers dabei helfen, ein besseres Nutzererlebnis in unseren Web-Apps zu schaffen. Gott sei Dank sind das navigator.languages Objekt und der Accept-Language HTTP-Header verfügbar, um unsere Vermutungen bei der Erkennung der Spracheinstellungen zu reduzieren.

Wenn du und dein Team an einer internationalisierten Webanwendung arbeitet, schau dir Phrase für eine professionelle, entwicklerfreundliche i18n-Plattform an. Mit einer flexiblen CLI und API, Übersetzungs-Synchronisierung mit GitHub- und Bitbucket-Integration, Over-the-Air (OTA)-Übersetzungen und vielem mehr bist du mit Phrase in Sachen i18n bestens aufgestellt, sodass du dich auf deine Geschäftslogik konzentrieren kannst.

Schau dir alle Phrase-Funktionen für Entwickler an und sieh selbst, wie sie deine Software-Lokalisierungs-Workflows optimieren kann.

Verwandte Beiträge

Python localization blog post featured image | Phrase

Blog post

Der ultimative Guide zur Python-Lokalisierung

Mit dem Schritt-für-Schritt-Leitfaden zur Vorbereitung von Python-Apps für die Lokalisierung und den gängigen Modulen kann mehrsprachige Unterstützung für eine globale Benutzerbasis implementiert werden.

Vue localization blog post featured image | Phrase

Blog post

Ein umfassender Leitfaden zur Lokalisierung mit Vue

In diesem Leitfaden zur Vue-Lokalisierung erläutern wir, wie die Vue I18n Bibliothek in die App integriert werden kann, um sie für Nutzer weltweit zugänglich zu machen.

Software localization blog category featured image | Phrase

Blog post

Der ultimative Leitfaden zur JavaScript-Lokalisierung

Starte deine Browser-JavaScript-Lokalisierung mit diesem umfassenden Leitfaden und mach deine Anwendung bereit für internationale Nutzer.

Software localization blog category featured image | Phrase

Blog post

Der ultimative Leitfaden zur Flutter-Lokalisierung

Lass uns die Geheimnisse der Flutter-Lokalisierung entschlüsseln, damit du die Sprache deiner Nutzer sprichst und deinen Weg zur globalen Dominanz weiter programmierst.

Software localization blog category featured image | Phrase

Blog post

So wird das Lokalisierungs-Plugin von Phrase Strings für WordPress verwendet

Im Folgenden zeigen wir, wie WordPress-Seiten, -Beiträge und mehr mit der Integration von Phrase Strings für WordPress in mehrere Sprachen übersetzt werden können.