Ya estemos desarrollando un blog simple o una sofisticada y moderna aplicación de página única (SPA), a menudo, al considerar la internacionalización en una aplicación web, surge una pregunta importante: ¿cómo detectamos la preferencia de idioma de un usuario? Es importante porque siempre queremos proporcionar la mejor experiencia de usuario, y si este ha establecido un conjunto de idiomas preferidos en su navegador, debemos esforzarnos al máximo para presentar nuestro contenido en dichos idiomas.
En este artículo, veremos tres formas diferentes de detectar la configuración regional de un usuario: a través del objeto navigator.languages del navegador (en el cliente), a través del encabezado HTTP Accept-Language (en el servidor) y mediante la geolocalización usando la dirección IP del usuario (en el servidor).
Cliente: El objeto navigator.languages
Los navegadores modernos proporcionan un objeto navigator.language que podemos usar para obtener todos los idiomas que el usuario ha configurado como preferidos en su navegador.

Configuración del idioma en Firefox
Dada la configuración anterior, si abriéramos la consola de Firefox y comprobáramos el valor de navigator.languages, obtendríamos lo siguiente:

Los códigos de las localizaciones coinciden con los de la configuración del navegador
navigator.languages está disponible en todos los navegadores web modernos y, generalmente, es fiable. Así que escribamos una función reutilizable en JavaScript que nos indique los idiomas preferidos del usuario actual.
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(); // Elimina los espacios en blanco al principio y al final del locale.
return opt.languageCodeOnly
? trimmedLocale.split(/-|_/)[0]
: trimmedLocale;
});
}
getBrowserLocales() comprueba la matriz de navigator.languages, y recurre a navigator.language si esta no está disponible. Cabe señalar que en algunos navegadores, como Chrome, navigator.language será el idioma de la IU, que probablemente sea el idioma al que está configurado el sistema operativo. No es lo mismo que navigator.languages, que contiene los idiomas preferidos establecidos por el usuario en el propio navegador.
✋🏽 Atención » Si ofreces compatibilidad con Internet Explorer, tendrás que usar las propiedades navigator.userLanguage y navigator.browserLanguage. Por supuesto, también tendrás que reemplazar todas las instancias de const con var en el código anterior.
Nuestra función también tiene una práctica opción languageCodeOnly, que eliminará los códigos de país de las regiones antes de devolverlos. Esto puede ser útil cuando nuestra aplicación no gestiona realmente las sutilezas regionales de un idioma (si, por ejemplo, solo disponemos de una versión del contenido en inglés).

Con languageCodeOnly: true, obtenemos los idiomas sin el código de país
Servidor: El encabezado HTTP Accept-Language
Si configuras tus preferencias de idioma en un navegador moderno, el navegador enviará a su vez una cabecera HTTP que transmite estas preferencias de idioma al servidor con cada solicitud. Este es el encabezado Accept-Language, y a menudo tiene un aspecto como este: Accept-Language: en-CA,ar-EG;q=0.5.
El encabezado enumera los idiomas preferidos del usuario, con un peso definido por un valor de q, asignado a cada uno. Cuando no se especifica un valor q explícito, se asume por defecto 1.0. Así que en el valor del encabezado anterior, el cliente está indicando que el usuario prefiere el inglés canadiense (con un peso de q = 1.0) y luego el árabe egipcio (con un peso de q = 0.5).
Podemos usar este encabezado HTTP estándar para determinar las regiones preferidas del usuario. Escribamos una clase llamada HttpAcceptLanguageHeaderLocaleDetector para hacer esto. Aquí usaremos PHP, pero puedes usar el lenguaje que quieras; en cualquier caso, el encabezado Accept-Language debería ser el mismo (o lo bastante parecido) en todos los entornos.
<?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 = [];
// Dividimos la cadena 'en-CA,ar-EG;q=0.5' por las comas,
// y vamos a iterar sobre la matriz resultante de regiones individuales. Una vez
// hemos terminado, $weightedLocales debería verse como
// [['locale' => 'en-CA', 'q' => 1.0], ['locale' => 'ar-EG', 'q' => 0.5]]
foreach (explode(',', $httpAcceptLanguageHeader) as $locale) {
// separar la clave de región ("ar-EG") de su peso ("q=0.5")
$localeParts = explode(';', $locale);
$weightedLocale = ['locale' => $localeParts[0]];
if (count($localeParts) == 2) {
// peso explícito p. ej. 'q=0.5'
$weightParts = explode('=', $localeParts[1]);
// obtener la parte '0.5' y convertirla a un float
$weightedLocale['q'] = floatval($weightParts[1]);
} else {
// no se proporciona peso en la cadena; es decir, un peso implícito de 'q=1.0'
$weightedLocale['q'] = 1.0;
}
$weightedLocales[] = $weightedLocale;
}
return $weightedLocales;
}
/**
* Ordenar por valor `q` de mayor a menor
*/
private static function sortLocalesByWeight($locales)
{
usort($locales, function ($a, $b) {
// usort forzará el tipo de los valores de float que devolvemos aquí a enteros,
// lo que puede arruinar nuestra clasificación. Así que en lugar de restar los valores `q`,
// y devolver la diferencia, comparamos los valores `q` y
// devolvemos explícitamente valores enteros.
if ($a['q'] == $b['q']) {
return 0;
}
if ($a['q'] > $b['q']) {
return -1;
}
return 1;
});
return $locales;
}
}
Este largo fragmento de código en realidad no es muy complicado. En el único método público, detect(), nuestra clase hace lo siguiente:
- Obtiene el valor de cadena sin procesar del encabezado
Accept-Language, por ejemplo,"en-CA,ar-EG;q=0.5" - Utiliza el método auxiliar
getWeightedLocales()para analizar la cadena del encabezado en una matriz que se ve como[['locale' => 'en-CA', 'q' => 1.0], ['locale' => 'ar-EG', 'q' => 0.5]]. - Utiliza el método auxiliar
sortLocalesByWeight()para clasificar la matriz anterior de mayor a menor valorq. - Extrae los valores de
localede la matriz ordenada, devolviendo una matriz que se ve como['en-CA', 'ar-EG'].
Ahora podemos usar nuestra nueva clase para obtener una práctica matriz de códigos de idioma a partir del encabezado HTTP Accept-Language.
<?php $locales = HttpAcceptLanguageHeaderLocaleDetector::detect(); // => ['en-CA', 'ar-EG']
Servidor: Geolocalización por dirección IP
A veces, el encabezado Accept-Language no estará presente en las solicitudes a nuestro servidor. En estos casos, nos puede interesar usar la dirección IP del usuario para determinar su país y detectar la localidad del usuario o su idioma a partir de ese país.
✋🏽 Atención » La geolocalización debe usarse como último recurso para detectar la configuración regional del usuario, ya que a menudo puede provocar una detección incorrecta. Por ejemplo, si vemos que nuestro usuario viene de Canadá, ¿asumimos que prefiere inglés o francés? Ambos son idiomas formales y ampliamente utilizados en el país. Y, por supuesto, el usuario podría pertenecer a una minoría de habla árabe, o ser un visitante de habla española.
Usar MaxMind para la geolocalización
Para determinar el país del usuario a partir de la dirección IP de la solicitud, utilizaremos la API de MaxMind para PHP y la base de datos de geolocalización de MaxMind. MaxMind es una empresa que ofrece algunos productos relacionados con IP, y entre ellos hay dos que son de interés para nosotros aquí.
- Las bases de datos GeoIP2: estas son las bases de datos de geolocalización comerciales de MaxMind, son de baja latencia y requieren suscripción. Puede que quieras actualizar a estas si quieres bases de datos más actualizadas o rápidas.
- Las bases de datos GeoLite2: estas son las bases de datos de geolocalización gratuitas de MaxMind, y aunque se suele decir que son menos precisas que sus contrapartidas comerciales, son más que suficientes si estás empezando. Usaremos una base de datos GeoLite2 aquí. Ten en cuenta que tendrás que dar crédito a Maxmind en tu página web pública y enlazar a su sitio si usas una de sus bases de datos gratuitas.
Para instalar la base de datos, solo regístrate para obtener una cuenta gratuita de MaxMind. Recibirás un correo electrónico con un enlace para iniciar sesión. Sigue el enlace e inicia sesión. Una vez que lo hagas, deberías llegar a la página Resumen de Cuenta.

Haz clic en el enlace Descargar bases de datos en la página Resumen de cuenta
Esto te llevará a una página con la lista de bases de datos GeoLite2 gratuitas. Descarga la base de datos binaria del país de allí.

Queremos la base de datos binaria del país para nuestros propósitos
Coloca el archivo que descargaste en algún lugar de tu proyecto.
También vamos a necesitar la API de PHP de MaxMind para trabajar con la base de datos. Podemos instalar eso con Composer.
composer require geoip2/geoip2:~2.0
Paquete de país a configuración regional de Peter Kahl
Necesitaremos un paquete más antes de llegar a nuestro código. Para determinar las regiones (localidades) o los idiomas de un país, utilizaremos el paquete country-to-locale de Peter Kahl. Podemos instalarlo usando Composer también.
composer require peterkahl/country-to-locale
Clase de detección de ubicación por dirección IP
Con nuestra configuración en su lugar, podemos pasar a nuestra propia clase, 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;
}
}
Nuestra clase es relativamente sencilla. Al igual que HttpAcceptLanguageHeaderLocaleDetector, tiene un método público, detect(), que hace lo siguiente:
- Obtén la dirección IP de la petición desde la matriz global
$_SERVER. - Alimenta esta dirección IP al método
countrydeReaderde la base de datos MaxMind, que intenta geolocalizar un país en función de la dirección IP. - Utiliza
locale::country2locale()de Peter Kahl para obtener los idiomas del país especificado. - Normaliza los locales adquiridos de modo que
"en_CA,ar_EG"se conviertan en"en-CA,ar-EG". - Devuelve las configuraciones regionales que normalizó como una matriz, por ejemplo
["en-CA", "ar-EG"].
📖 Profundiza más » El Reader de MaxMind tiene muchos más métodos. Consulta la documentación oficial de la API si deseas profundizar un poco más en la información disponible en las bases de datos de MaxMind.
Servidor: Detección la localidad o región en cascada
Dadas las dos estrategias de detección del lado del servidor que cubrimos anteriormente, podemos escribir una pequeña función detect_user_locales() que puede intentar primero la estrategia del encabezado 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) {
// usar una configuración regional predeterminada, inglés en este caso
$locales = ['en'];
}
return $locales;
}
Si la detección del encabezado HTTP falla, detect_user_locales() intentará la detección de geolocalización IP. Si lo último no da resultado, la función recurrirá a una configuración regional predeterminada.
Si se maneja con cuidado, detectar la configuración regional del usuario puede ayudar a ofrecer una mejor experiencia de usuario en nuestras aplicaciones web. Afortunadamente, el objeto navigator.languages y el encabezado HTTP Accept-Language están disponibles para reducir las incertidumbres al momento de detectar la configuración regional del usuario.
Si tú y tu equipo estáis trabajando en una aplicación web internacionalizada, echad un vistazo a Phrase, una plataforma de internacionalización profesional pensada para desarrolladores. Con una CLI y API flexibles, sincronización de traducciones con integración de GitHub y Bitbucket, traducciones OTA y mucho más, Phrase se encarga de tu i18n, para que puedas concentrarte en la lógica de tu negocio.
Consulta todas las características de Phrase para desarrolladores y comprueba por ti mismo cómo puede optimizar tus flujos de trabajo de localización de software.


