{"id":133298,"date":"2020-01-30T16:40:13","date_gmt":"2020-01-30T15:40:13","guid":{"rendered":"https:\/\/phrase.com\/blog\/posts\/detectando-el-idioma-de-un-usuario-en-una-aplicacion-web\/"},"modified":"2026-02-26T16:50:48","modified_gmt":"2026-02-26T15:50:48","slug":"detecting-a-users-locale","status":"publish","type":"post","link":"https:\/\/phrase.com\/es\/blog\/posts\/detecting-a-users-locale\/","title":{"rendered":"Detectar la localidad del usuario en una aplicaci\u00f3n web"},"content":{"rendered":"<p>Ya estemos desarrollando un blog simple o una sofisticada y moderna aplicaci\u00f3n de p\u00e1gina \u00fanica (SPA), a menudo, al considerar la <a href=\"https:\/\/phrase.com\/blog\/posts\/i18n-a-simple-definition\/\">internacionalizaci\u00f3n<\/a> en una aplicaci\u00f3n web, surge una pregunta importante: \u00bfc\u00f3mo 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\u00e1ximo para presentar nuestro contenido en dichos idiomas.<br \/>\nEn este art\u00edculo, veremos tres formas diferentes de detectar la configuraci\u00f3n regional de un usuario: a trav\u00e9s del objeto <code>navigator.language<\/code>s del navegador (en el cliente), a trav\u00e9s del encabezado HTTP <code>Accept-Language<\/code> (en el servidor) y mediante la geolocalizaci\u00f3n usando la direcci\u00f3n IP del usuario (en el servidor).<\/p>\n<div id=\"ez-toc-container\" class=\"ez-toc-v2_0_81 counter-hierarchy ez-toc-counter ez-toc-grey ez-toc-container-direction\">\n<div class=\"ez-toc-title-container\">\n<p class=\"ez-toc-title\" style=\"cursor:inherit\">Overview<\/p>\n<span class=\"ez-toc-title-toggle\"><a href=\"#\" class=\"ez-toc-pull-right ez-toc-btn ez-toc-btn-xs ez-toc-btn-default ez-toc-toggle\" aria-label=\"Alternar tabla de contenidos\"><span class=\"ez-toc-js-icon-con\"><span class=\"\"><span class=\"eztoc-hide\" style=\"display:none;\">Toggle<\/span><span class=\"ez-toc-icon-toggle-span\"><svg style=\"fill: #999;color:#999\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" class=\"list-377408\" width=\"20px\" height=\"20px\" viewBox=\"0 0 24 24\" fill=\"none\"><path d=\"M6 6H4v2h2V6zm14 0H8v2h12V6zM4 11h2v2H4v-2zm16 0H8v2h12v-2zM4 16h2v2H4v-2zm16 0H8v2h12v-2z\" fill=\"currentColor\"><\/path><\/svg><svg style=\"fill: #999;color:#999\" class=\"arrow-unsorted-368013\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"10px\" height=\"10px\" viewBox=\"0 0 24 24\" version=\"1.2\" baseProfile=\"tiny\"><path d=\"M18.2 9.3l-6.2-6.3-6.2 6.3c-.2.2-.3.4-.3.7s.1.5.3.7c.2.2.4.3.7.3h11c.3 0 .5-.1.7-.3.2-.2.3-.5.3-.7s-.1-.5-.3-.7zM5.8 14.7l6.2 6.3 6.2-6.3c.2-.2.3-.5.3-.7s-.1-.5-.3-.7c-.2-.2-.4-.3-.7-.3h-11c-.3 0-.5.1-.7.3-.2.2-.3.5-.3.7s.1.5.3.7z\"\/><\/svg><\/span><\/span><\/span><\/a><\/span><\/div>\n<nav><ul class='ez-toc-list ez-toc-list-level-1 ' ><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-1\" href=\"https:\/\/phrase.com\/es\/blog\/posts\/detecting-a-users-locale\/#cliente-el-objeto-navigatorlanguages\" >Cliente: El objeto navigator.languages<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-2\" href=\"https:\/\/phrase.com\/es\/blog\/posts\/detecting-a-users-locale\/#servidor-el-encabezado-http-accept-language\" >Servidor: El encabezado HTTP Accept-Language<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-3\" href=\"https:\/\/phrase.com\/es\/blog\/posts\/detecting-a-users-locale\/#servidor-geolocalizacion-por-direccion-ip\" >Servidor: Geolocalizaci\u00f3n por direcci\u00f3n IP<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-4\" href=\"https:\/\/phrase.com\/es\/blog\/posts\/detecting-a-users-locale\/#usar-maxmind-para-la-geolocalizacion\" >Usar MaxMind para la geolocalizaci\u00f3n<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-5\" href=\"https:\/\/phrase.com\/es\/blog\/posts\/detecting-a-users-locale\/#paquete-de-pais-a-configuracion-regional-de-peter-kahl\" >Paquete de pa\u00eds a configuraci\u00f3n regional de Peter Kahl<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-6\" href=\"https:\/\/phrase.com\/es\/blog\/posts\/detecting-a-users-locale\/#clase-de-deteccion-de-ubicacion-por-direccion-ip\" >Clase de detecci\u00f3n de ubicaci\u00f3n por direcci\u00f3n IP<\/a><\/li><\/ul><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-7\" href=\"https:\/\/phrase.com\/es\/blog\/posts\/detecting-a-users-locale\/#servidor-deteccion-la-localidad-o-region-en-cascada\" >Servidor: Detecci\u00f3n la localidad o regi\u00f3n en cascada<\/a><\/li><\/ul><\/nav><\/div>\n<h2><span class=\"ez-toc-section\" id=\"cliente-el-objeto-navigatorlanguages\"><\/span>Cliente: El objeto <code>navigator.language<\/code>s<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Los navegadores modernos proporcionan un objeto <code>navigator.language<\/code> que podemos usar para obtener todos los idiomas que el usuario ha configurado como preferidos en su navegador.<br \/>\n<img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-9669 size-large\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2020\/01\/detectinguserlocale202001-firefox-browser-settings-1024x531.png\" alt=\"Objeto navigator.languages del navegador para los ajustes de idioma de la p\u00e1gina web | Phrase\" width=\"1024\" height=\"531\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2020\/01\/detectinguserlocale202001-firefox-browser-settings-1024x531.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2020\/01\/detectinguserlocale202001-firefox-browser-settings-300x155.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2020\/01\/detectinguserlocale202001-firefox-browser-settings-768x398.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2020\/01\/detectinguserlocale202001-firefox-browser-settings.png 1320w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/p>\n<p style=\"text-align: center\"><em>Configuraci\u00f3n del idioma en Firefox<\/em><\/p>\n<p>Dada la configuraci\u00f3n anterior, si abri\u00e9ramos la consola de Firefox y comprob\u00e1ramos el valor de <code>navigator.languages<\/code>, obtendr\u00edamos lo siguiente:<br \/>\n<img loading=\"lazy\" decoding=\"async\" class=\"wp-image-9671 size-full aligncenter\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2020\/01\/detectinguserlocale202001-navigator-languages.png\" alt=\"Valor del objeto navigator.languages de Firefox | Phrase\" width=\"440\" height=\"80\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2020\/01\/detectinguserlocale202001-navigator-languages.png 440w, https:\/\/phrase.com\/wp-content\/uploads\/2020\/01\/detectinguserlocale202001-navigator-languages-300x55.png 300w\" sizes=\"auto, (max-width: 440px) 100vw, 440px\" \/><\/p>\n<p style=\"text-align: center\"><em>Los c\u00f3digos de las localizaciones coinciden con los de la configuraci\u00f3n del navegador<\/em><\/p>\n<p><code>navigator.languages<\/code> est\u00e1 <a href=\"https:\/\/caniuse.com\/#search=navigator%20languages\">disponible en todos los navegadores web modernos<\/a> y, generalmente, es fiable. As\u00ed que escribamos una funci\u00f3n reutilizable en <a href=\"https:\/\/phrase.com\/es\/blog\/posts\/step-step-guide-javascript-localization\/\">JavaScript<\/a> que nos indique los idiomas preferidos del usuario actual.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\">function getBrowserLocales(options = {}) {\n  const defaultOptions = {\n    languageCodeOnly: false,\n  };\n  const opt = {\n    ...defaultOptions,\n    ...options,\n  };\n  const browserLocales =\n    navigator.languages === undefined\n      ? [navigator.language]\n      : navigator.languages;\n  if (!browserLocales) {\n    return undefined;\n  }\n  return browserLocales.map(locale =&gt; {\n    const trimmedLocale = locale.trim(); \/\/ Elimina los espacios en blanco al principio y al final del locale.\n    return opt.languageCodeOnly\n      ? trimmedLocale.split(\/-|_\/)[0]\n      : trimmedLocale;\n  });\n}<\/pre>\n<p><code>getBrowserLocales()<\/code> comprueba la matriz de <code>navigator.languages<\/code>, y recurre a <code>navigator.language<\/code> si esta no est\u00e1 disponible. Cabe se\u00f1alar que en algunos navegadores, como Chrome, <code>navigator.language<\/code> ser\u00e1 el idioma de la <em>IU<\/em>, que probablemente sea el idioma al que est\u00e1 configurado el <em>sistema operativo<\/em>. No es lo mismo que <code>navigator.languages<\/code>, que contiene los idiomas preferidos establecidos por el usuario en el propio <em>navegador<\/em>.<\/p>\n<p>\u270b\ud83c\udffd <em>Atenci\u00f3n \u00bb<\/em> Si ofreces compatibilidad con Internet Explorer, tendr\u00e1s que usar las propiedades <code>navigator.userLanguage<\/code> y <code>navigator.browserLanguage<\/code>. Por supuesto, tambi\u00e9n tendr\u00e1s que reemplazar todas las instancias de <code>const<\/code> con <code>var<\/code> en el c\u00f3digo anterior.<\/p>\n<p>Nuestra funci\u00f3n tambi\u00e9n tiene una pr\u00e1ctica opci\u00f3n <code>languageCodeOnly<\/code>, que eliminar\u00e1 los c\u00f3digos de pa\u00eds de las regiones antes de devolverlos. Esto puede ser \u00fatil cuando nuestra aplicaci\u00f3n no gestiona realmente las sutilezas regionales de un idioma (si, por ejemplo, solo disponemos de una versi\u00f3n del contenido en ingl\u00e9s).<br \/>\n<img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-9672 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2020\/01\/detectinguserlocale202001-language-code-only.png\" alt=\"Con languageCodeOnly: true, obtenemos los idiomas sin el c\u00f3digo de pa\u00eds | Phrase\" width=\"658\" height=\"158\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2020\/01\/detectinguserlocale202001-language-code-only.png 658w, https:\/\/phrase.com\/wp-content\/uploads\/2020\/01\/detectinguserlocale202001-language-code-only-300x72.png 300w\" sizes=\"auto, (max-width: 658px) 100vw, 658px\" \/><\/p>\n<p style=\"text-align: center\"><em>Con languageCodeOnly: true, obtenemos los idiomas sin el c\u00f3digo de pa\u00eds<\/em><\/p>\n<h2><span class=\"ez-toc-section\" id=\"servidor-el-encabezado-http-accept-language\"><\/span>Servidor: El encabezado HTTP <code>Accept-Language<\/code><span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Si configuras tus preferencias de idioma en un navegador moderno, el navegador enviar\u00e1 a su vez una cabecera HTTP que transmite estas preferencias de idioma al servidor con cada solicitud. Este es el encabezado <code>Accept-Language<\/code>, y a menudo tiene un aspecto como este: <code>Accept-Language: en-CA,ar-EG;q=0.5<\/code>.<br \/>\nEl encabezado enumera los idiomas preferidos del usuario, con un peso definido por un valor de <code>q<\/code>, asignado a cada uno. Cuando no se especifica un valor <code>q<\/code> expl\u00edcito, se asume por defecto <code>1.0<\/code>. As\u00ed que en el valor del encabezado anterior, el cliente est\u00e1 indicando que el usuario prefiere el ingl\u00e9s canadiense (con un peso de <code>q = 1.0<\/code>) y luego el \u00e1rabe egipcio (con un peso de <code>q = 0.5<\/code>).<br \/>\nPodemos usar este encabezado HTTP est\u00e1ndar para determinar las regiones preferidas del usuario. Escribamos una clase llamada <code>HttpAcceptLanguageHeaderLocaleDetector<\/code> para hacer esto. Aqu\u00ed usaremos PHP, pero puedes usar el lenguaje que quieras; en cualquier caso, el encabezado <code>Accept-Language<\/code> deber\u00eda ser el mismo (o lo bastante parecido) en todos los entornos.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"php\">&lt;?php\nclass HttpAcceptLanguageHeaderLocaleDetector\n{\n  const HTTP_ACCEPT_LANGUAGE_HEADER_KEY = 'HTTP_ACCEPT_LANGUAGE';\n  public static function detect()\n  {\n    $httpAcceptLanguageHeader = static::getHttpAcceptLanguageHeader();\n    if ($httpAcceptLanguageHeader == null) {\n      return [];\n    }\n    $locales = static::getWeightedLocales($httpAcceptLanguageHeader);\n    $sortedLocales = static::sortLocalesByWeight($locales);\n    return array_map(function ($weightedLocale) {\n      return $weightedLocale['locale'];\n    }, $sortedLocales);\n  }\n  private static function getHttpAcceptLanguageHeader()\n  {\n    if (isset($_SERVER[static::HTTP_ACCEPT_LANGUAGE_HEADER_KEY])) {\n      return trim($_SERVER['HTTP_ACCEPT_LANGUAGE']);\n    } else {\n      return null;\n    }\n  }\n  private static function getWeightedLocales($httpAcceptLanguageHeader)\n  {\n    if (strlen($httpAcceptLanguageHeader) == 0) {\n      return [];\n    }\n    $weightedLocales = [];\n    \/\/ Dividimos la cadena 'en-CA,ar-EG;q=0.5' por las comas,\n    \/\/ y vamos a iterar sobre la matriz resultante de regiones individuales. Una vez\n    \/\/ hemos terminado, $weightedLocales deber\u00eda verse como\n    \/\/ [['locale' =&gt; 'en-CA', 'q' =&gt; 1.0], ['locale' =&gt; 'ar-EG', 'q' =&gt; 0.5]]\n    foreach (explode(',', $httpAcceptLanguageHeader) as $locale) {\n      \/\/ separar la clave de regi\u00f3n (\"ar-EG\") de su peso (\"q=0.5\")\n      $localeParts = explode(';', $locale);\n      $weightedLocale = ['locale' =&gt; $localeParts[0]];\n      if (count($localeParts) == 2) {\n        \/\/ peso expl\u00edcito p. ej. 'q=0.5'\n        $weightParts = explode('=', $localeParts[1]);\n        \/\/ obtener la parte '0.5' y convertirla a un float\n        $weightedLocale['q'] = floatval($weightParts[1]);\n      } else {\n        \/\/ no se proporciona peso en la cadena; es decir, un peso impl\u00edcito de 'q=1.0'\n        $weightedLocale['q'] = 1.0;\n      }\n      $weightedLocales[] = $weightedLocale;\n    }\n    return $weightedLocales;\n  }\n  \/**\n   * Ordenar por valor `q` de mayor a menor\n   *\/\n  private static function sortLocalesByWeight($locales)\n  {\n    usort($locales, function ($a, $b) {\n      \/\/ usort forzar\u00e1 el tipo de los valores de float que devolvemos aqu\u00ed a enteros,\n      \/\/ lo que puede arruinar nuestra clasificaci\u00f3n. As\u00ed que en lugar de restar los valores `q`,\n      \/\/ y devolver la diferencia, comparamos los valores `q` y\n      \/\/ devolvemos expl\u00edcitamente valores enteros.\n      if ($a['q'] == $b['q']) {\n        return 0;\n      }\n      if ($a['q'] &gt; $b['q']) {\n        return -1;\n      }\n      return 1;\n    });\n    return $locales;\n  }\n}<\/pre>\n<p>Este largo fragmento de c\u00f3digo en realidad no es muy complicado. En el \u00fanico m\u00e9todo p\u00fablico, <code>detect()<\/code>, nuestra clase hace lo siguiente:<\/p>\n<ol>\n<li>Obtiene el valor de cadena sin procesar del encabezado <code>Accept-Language<\/code>, por ejemplo, <code>\"en-CA,ar-EG;q=0.5\"<\/code><\/li>\n<li>Utiliza el m\u00e9todo auxiliar <code>getWeightedLocales()<\/code> para analizar la cadena del encabezado en una matriz que se ve como <code>[['locale' =&gt; 'en-CA', 'q' =&gt; 1.0], ['locale' =&gt; 'ar-EG', 'q' =&gt; 0.5]]<\/code>.<\/li>\n<li>Utiliza el m\u00e9todo auxiliar <code>sortLocalesByWeight()<\/code> para clasificar la matriz anterior de mayor a menor valor <code>q<\/code>.<\/li>\n<li>Extrae los valores de <code>locale<\/code> de la matriz ordenada, devolviendo una matriz que se ve como <code>['en-CA', 'ar-EG']<\/code>.<\/li>\n<\/ol>\n<p>Ahora podemos usar nuestra nueva clase para obtener una pr\u00e1ctica matriz de c\u00f3digos de idioma a partir del encabezado HTTP <code>Accept-Language<\/code>.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"php\">&lt;?php\n$locales = HttpAcceptLanguageHeaderLocaleDetector::detect();\n\/\/ =&gt; ['en-CA', 'ar-EG']<\/pre>\n<h2><span class=\"ez-toc-section\" id=\"servidor-geolocalizacion-por-direccion-ip\"><\/span>Servidor: Geolocalizaci\u00f3n por direcci\u00f3n IP<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>A veces, el encabezado <code>Accept-Language<\/code> no estar\u00e1 presente en las solicitudes a nuestro servidor. En estos casos, nos puede interesar usar la direcci\u00f3n IP del usuario para determinar su pa\u00eds y detectar la localidad del usuario o su idioma a partir de ese pa\u00eds.<\/p>\n<p>\u270b\ud83c\udffd <em>Atenci\u00f3n \u00bb<\/em> La geolocalizaci\u00f3n debe usarse como \u00faltimo recurso para detectar la configuraci\u00f3n regional del usuario, ya que a menudo puede provocar una detecci\u00f3n incorrecta. Por ejemplo, si vemos que nuestro usuario viene de Canad\u00e1, \u00bfasumimos que prefiere ingl\u00e9s o franc\u00e9s? Ambos son idiomas formales y ampliamente utilizados en el pa\u00eds. Y, por supuesto, el usuario podr\u00eda pertenecer a una minor\u00eda de habla \u00e1rabe, o ser un visitante de habla espa\u00f1ola.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"usar-maxmind-para-la-geolocalizacion\"><\/span>Usar MaxMind para la geolocalizaci\u00f3n<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Para determinar el pa\u00eds del usuario a partir de la direcci\u00f3n IP de la solicitud, utilizaremos la API de MaxMind para PHP y la base de datos de geolocalizaci\u00f3n de MaxMind. MaxMind es una empresa que ofrece algunos productos relacionados con IP, y entre ellos hay dos que son de inter\u00e9s para nosotros aqu\u00ed.<\/p>\n<ul>\n<li><a href=\"https:\/\/www.maxmind.com\/en\/geoip-databases\">Las bases de datos GeoIP2<\/a>: estas son las bases de datos de geolocalizaci\u00f3n comerciales de MaxMind, son de baja latencia y requieren suscripci\u00f3n. Puede que quieras actualizar a estas si quieres bases de datos m\u00e1s actualizadas o r\u00e1pidas.<\/li>\n<li><a href=\"https:\/\/dev.maxmind.com\/geoip\/\">Las bases de datos GeoLite2<\/a>: estas son las bases de datos de geolocalizaci\u00f3n gratuitas de MaxMind, y aunque se suele decir que son menos precisas que sus contrapartidas comerciales, son m\u00e1s que suficientes si est\u00e1s empezando. Usaremos una base de datos GeoLite2 aqu\u00ed. Ten en cuenta que tendr\u00e1s que dar cr\u00e9dito a Maxmind en tu <a href=\"https:\/\/phrase.com\/es\/blog\/posts\/how-translate-web-page\/\">p\u00e1gina web p\u00fablica<\/a> y enlazar a su sitio si usas una de sus bases de datos gratuitas.<\/li>\n<\/ul>\n<p>Para instalar la base de datos, solo <a href=\"https:\/\/www.maxmind.com\/en\/geolite2\/signup\">reg\u00edstrate<\/a> para obtener una cuenta gratuita de MaxMind. Recibir\u00e1s un correo electr\u00f3nico con un enlace para iniciar sesi\u00f3n. Sigue el enlace e inicia sesi\u00f3n. Una vez que lo hagas, deber\u00edas llegar a la p\u00e1gina <em>Resumen de Cuenta<\/em>.<br \/>\n<img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-9673 size-large\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2020\/01\/detectinguserlocale202001-maxmind-download-link-1024x193.png\" alt=\"Descargar bases de datos de MaxMind | Phrase\" width=\"1024\" height=\"193\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2020\/01\/detectinguserlocale202001-maxmind-download-link-1024x193.png 1024w, https:\/\/phrase.com\/wp-content\/uploads\/2020\/01\/detectinguserlocale202001-maxmind-download-link-300x57.png 300w, https:\/\/phrase.com\/wp-content\/uploads\/2020\/01\/detectinguserlocale202001-maxmind-download-link-768x145.png 768w, https:\/\/phrase.com\/wp-content\/uploads\/2020\/01\/detectinguserlocale202001-maxmind-download-link.png 1474w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/p>\n<p style=\"text-align: center\"><em>Haz clic en el enlace Descargar bases de datos en la p\u00e1gina Resumen de cuenta<\/em><\/p>\n<p>Esto te llevar\u00e1 a una p\u00e1gina con la lista de bases de datos GeoLite2 gratuitas. Descarga la base de datos <em>binaria del pa\u00eds<\/em> de all\u00ed.<br \/>\n<img loading=\"lazy\" decoding=\"async\" class=\"aligncenter wp-image-9676 size-full\" src=\"https:\/\/phrase.com\/wp-content\/uploads\/2020\/01\/detectinguserlocale202001-maxmind-databases-1.png\" alt=\"Base de datos binaria de pa\u00edses de MaxMind | Phrase\" width=\"729\" height=\"752\" srcset=\"https:\/\/phrase.com\/wp-content\/uploads\/2020\/01\/detectinguserlocale202001-maxmind-databases-1.png 729w, https:\/\/phrase.com\/wp-content\/uploads\/2020\/01\/detectinguserlocale202001-maxmind-databases-1-291x300.png 291w\" sizes=\"auto, (max-width: 729px) 100vw, 729px\" \/><\/p>\n<p style=\"text-align: center\"><em>Queremos la base de datos binaria del pa\u00eds para nuestros prop\u00f3sitos<\/em><\/p>\n<p>Coloca el archivo que descargaste en alg\u00fan lugar de tu proyecto.<br \/>\nTambi\u00e9n vamos a necesitar la API de PHP de MaxMind para trabajar con la base de datos. Podemos instalar eso con <a href=\"https:\/\/getcomposer.org\/\">Composer<\/a>.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\">composer require geoip2\/geoip2:~2.0<\/pre>\n<h3><span class=\"ez-toc-section\" id=\"paquete-de-pais-a-configuracion-regional-de-peter-kahl\"><\/span>Paquete de pa\u00eds a configuraci\u00f3n regional de Peter Kahl<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Necesitaremos un paquete m\u00e1s antes de llegar a nuestro c\u00f3digo. Para determinar las <em>regiones<\/em> (localidades) o los idiomas de un pa\u00eds, utilizaremos el paquete <code>country-to-locale<\/code> de Peter Kahl. Podemos instalarlo usando Composer tambi\u00e9n.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"js\">composer require peterkahl\/country-to-locale<\/pre>\n<h3><span class=\"ez-toc-section\" id=\"clase-de-deteccion-de-ubicacion-por-direccion-ip\"><\/span>Clase de detecci\u00f3n de ubicaci\u00f3n por direcci\u00f3n IP<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Con nuestra configuraci\u00f3n en su lugar, podemos pasar a nuestra propia clase, <code>IpAddressLocaleDetector<\/code>.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"php\">&lt;?php\nrequire '..\/vendor\/autoload.php';\nuse GeoIp2\\Database\\Reader;\nuse peterkahl\\locale\\locale;\nclass IpAddressLocaleDetector\n{\n  const MAX_MIND_DB_FILEPATH =\n    __DIR__ . '\/GeoLite2-Country_20200121\/GeoLite2-Country.mmdb';\n  private static $maxMindDbReader;\n  public static function detect()\n  {\n    $ipAddress = static::getIpAddress();\n    try {\n      $record = static::getMaxMindDbReader()-&gt;country($ipAddress);\n      $locales = locale::country2locale($record-&gt;country-&gt;isoCode);\n      $normalizedLocales = str_replace('_', '-', $locales);\n      return explode(',', $normalizedLocales);\n    } catch (Exception $ex) {\n      return null;\n    }\n  }\n  private static function getIpAddress()\n  {\n    return $_SERVER['REMOTE_ADDR'];\n  }\n  private static function getMaxMindDbReader()\n  {\n    if (static::$maxMindDbReader == null) {\n      static::$maxMindDbReader = new Reader(static::MAX_MIND_DB_FILEPATH);\n    }\n    return static::$maxMindDbReader;\n  }\n}<\/pre>\n<p>Nuestra clase es relativamente sencilla. Al igual que <code>HttpAcceptLanguageHeaderLocaleDetector<\/code>, tiene un m\u00e9todo p\u00fablico, <code>detect()<\/code>, que hace lo siguiente:<\/p>\n<ol>\n<li>Obt\u00e9n la direcci\u00f3n IP de la petici\u00f3n desde la matriz global <code>$_SERVER<\/code>.<\/li>\n<li>Alimenta esta direcci\u00f3n IP al m\u00e9todo <code>country<\/code> de <code>Reader<\/code> de la base de datos MaxMind, que intenta geolocalizar un pa\u00eds en funci\u00f3n de la direcci\u00f3n IP.<\/li>\n<li>Utiliza <code>locale::country2locale()<\/code> de Peter Kahl para obtener los idiomas del pa\u00eds especificado.<\/li>\n<li>Normaliza los locales adquiridos de modo que <code>\"en_CA,ar_EG\"<\/code> se conviertan en <code>\"en-CA,ar-EG\"<\/code>.<\/li>\n<li>Devuelve las configuraciones regionales que normaliz\u00f3 como una matriz, por ejemplo <code>[\"en-CA\", \"ar-EG\"]<\/code>.<\/li>\n<\/ol>\n<p>\ud83d\udcd6 <em>Profundiza m\u00e1s \u00bb<\/em> El <code>Reader<\/code> de MaxMind tiene muchos m\u00e1s m\u00e9todos. Consulta la <a href=\"https:\/\/maxmind.github.io\/GeoIP2-php\/\">documentaci\u00f3n oficial de la API<\/a> si deseas profundizar un poco m\u00e1s en la informaci\u00f3n disponible en las bases de datos de MaxMind.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"servidor-deteccion-la-localidad-o-region-en-cascada\"><\/span>Servidor: Detecci\u00f3n la localidad o regi\u00f3n en cascada<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Dadas las dos estrategias de detecci\u00f3n del lado del servidor que cubrimos anteriormente, podemos escribir una peque\u00f1a funci\u00f3n <code>detect_user_locales()<\/code> que puede intentar primero la estrategia del encabezado HTTP.<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"php\">&lt;?php\nrequire '.\/HttpAcceptLanguageHeaderLocaleDetector.php';\nrequire '.\/IpAddressLocaleDetector.php';\nfunction detect_user_locales()\n{\n  $locales = HttpAcceptLanguageHeaderLocaleDetector::detect();\n  if (count($locales) == 0) {\n    $locales = IPAddressLocaleDetector::detect();\n  }\n  if (count($locales) == 0) {\n    \/\/ usar una configuraci\u00f3n regional predeterminada, ingl\u00e9s en este caso\n    $locales = ['en'];\n  }\n  return $locales;\n}<\/pre>\n<p>Si la detecci\u00f3n del encabezado HTTP falla, <code>detect_user_locales()<\/code> intentar\u00e1 la detecci\u00f3n de geolocalizaci\u00f3n IP. Si lo \u00faltimo no da resultado, la funci\u00f3n recurrir\u00e1 a una configuraci\u00f3n regional predeterminada.<br \/>\nSi se maneja con cuidado, detectar la configuraci\u00f3n regional del usuario puede ayudar a ofrecer una mejor experiencia de usuario en nuestras aplicaciones web. Afortunadamente, el objeto <code>navigator.languages<\/code> y el encabezado HTTP <code>Accept-Language<\/code> est\u00e1n disponibles para reducir las incertidumbres al momento de detectar la configuraci\u00f3n regional del usuario.<\/p>\n<p>Si t\u00fa y tu equipo est\u00e1is trabajando en una aplicaci\u00f3n web internacionalizada, echad un vistazo a Phrase, una plataforma de internacionalizaci\u00f3n profesional pensada para desarrolladores. Con una CLI y API flexibles, sincronizaci\u00f3n de traducciones con integraci\u00f3n de GitHub y Bitbucket, <a href=\"https:\/\/phrase.com\/es\/blog\/posts\/machine-translation\/\">traducciones OTA<\/a> y mucho m\u00e1s, Phrase se encarga de tu i18n, para que puedas concentrarte en la l\u00f3gica de tu negocio.<\/p>\n<p>Consulta todas las <a href=\"https:\/\/phrase.com\/es\/roles\/developers\/\">caracter\u00edsticas de Phrase para desarrolladores<\/a> y comprueba por ti mismo c\u00f3mo puede optimizar tus flujos de trabajo de localizaci\u00f3n de software.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Uno de los problemas m\u00e1s comunes en el desarrollo de aplicaciones web es detectar la localidad del usuario. Esto es la manera correcta de hacerlo.<\/p>\n","protected":false},"author":41,"featured_media":2612,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"post-refresh-updated","format":"standard","meta":{"_acf_changed":false,"_stopmodifiedupdate":false,"_modified_date":"","_searchwp_excluded":"","episode_type":"","audio_file":"","podmotor_file_id":"","podmotor_episode_id":"","cover_image":"","cover_image_id":"","duration":"","filesize":"","filesize_raw":"","date_recorded":"","explicit":"","block":"","itunes_episode_number":"","itunes_title":"","itunes_season_number":"","itunes_episode_type":"","footnotes":""},"categories":[2520],"class_list":["post-133298","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-localizacion-de-software-es"],"acf":[],"_links":{"self":[{"href":"https:\/\/phrase.com\/es\/wp-json\/wp\/v2\/posts\/133298","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/phrase.com\/es\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/phrase.com\/es\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/phrase.com\/es\/wp-json\/wp\/v2\/users\/41"}],"replies":[{"embeddable":true,"href":"https:\/\/phrase.com\/es\/wp-json\/wp\/v2\/comments?post=133298"}],"version-history":[{"count":4,"href":"https:\/\/phrase.com\/es\/wp-json\/wp\/v2\/posts\/133298\/revisions"}],"predecessor-version":[{"id":136299,"href":"https:\/\/phrase.com\/es\/wp-json\/wp\/v2\/posts\/133298\/revisions\/136299"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/phrase.com\/es\/wp-json\/wp\/v2\/media\/2612"}],"wp:attachment":[{"href":"https:\/\/phrase.com\/es\/wp-json\/wp\/v2\/media?parent=133298"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/phrase.com\/es\/wp-json\/wp\/v2\/categories?post=133298"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}