Detectar el lenguaje del navegador en PHP

144

Utilizo el siguiente script PHP como índice para mi sitio web.

Este script debe incluir una página específica según el idioma del navegador (detectado automáticamente).

Este script no funciona bien con todos los navegadores, por lo que siempre incluye index_en.phpcualquier idioma detectado (la causa del problema es probablemente un problema con algún encabezado Accept-Language no considerado).

¿Podría sugerirme una solución más robusta?

<?php
// Open session var
session_start();
// views: 1 = first visit; >1 = second visit

// Detect language from user agent browser
function lixlpixel_get_env_var($Var)
{
     if(empty($GLOBALS[$Var]))
     {
         $GLOBALS[$Var]=(!empty($GLOBALS['_SERVER'][$Var]))?
         $GLOBALS['_SERVER'][$Var] : (!empty($GLOBALS['HTTP_SERVER_VARS'][$Var])) ? $GLOBALS['HTTP_SERVER_VARS'][$Var]:'';
     }
}

function lixlpixel_detect_lang()
{
     // Detect HTTP_ACCEPT_LANGUAGE & HTTP_USER_AGENT.
     lixlpixel_get_env_var('HTTP_ACCEPT_LANGUAGE');
     lixlpixel_get_env_var('HTTP_USER_AGENT');

     $_AL=strtolower($GLOBALS['HTTP_ACCEPT_LANGUAGE']);
     $_UA=strtolower($GLOBALS['HTTP_USER_AGENT']);

     // Try to detect Primary language if several languages are accepted.
     foreach($GLOBALS['_LANG'] as $K)
     {
         if(strpos($_AL, $K)===0)
         return $K;
     }

     // Try to detect any language if not yet detected.
     foreach($GLOBALS['_LANG'] as $K)
     {
         if(strpos($_AL, $K)!==false)
         return $K;
     }
     foreach($GLOBALS['_LANG'] as $K)
     {
         //if(preg_match("/[[( ]{$K}[;,_-)]/",$_UA)) // matching other letters (create an error for seo spyder)
         return $K;
     }

     // Return default language if language is not yet detected.
     return $GLOBALS['_DLANG'];
}

// Define default language.
$GLOBALS['_DLANG']='en';

// Define all available languages.
// WARNING: uncomment all available languages

$GLOBALS['_LANG'] = array(
'af', // afrikaans.
'ar', // arabic.
'bg', // bulgarian.
'ca', // catalan.
'cs', // czech.
'da', // danish.
'de', // german.
'el', // greek.
'en', // english.
'es', // spanish.
'et', // estonian.
'fi', // finnish.
'fr', // french.
'gl', // galician.
'he', // hebrew.
'hi', // hindi.
'hr', // croatian.
'hu', // hungarian.
'id', // indonesian.
'it', // italian.
'ja', // japanese.
'ko', // korean.
'ka', // georgian.
'lt', // lithuanian.
'lv', // latvian.
'ms', // malay.
'nl', // dutch.
'no', // norwegian.
'pl', // polish.
'pt', // portuguese.
'ro', // romanian.
'ru', // russian.
'sk', // slovak.
'sl', // slovenian.
'sq', // albanian.
'sr', // serbian.
'sv', // swedish.
'th', // thai.
'tr', // turkish.
'uk', // ukrainian.
'zh' // chinese.
);

// Redirect to the correct location.
// Example Implementation aff var lang to name file
/*
echo 'The Language detected is: '.lixlpixel_detect_lang(); // For Demonstration
echo "<br />";    
*/
$lang_var = lixlpixel_detect_lang(); //insert lang var system in a new var for conditional statement
/*
echo "<br />";    

echo $lang_var; // print var for trace

echo "<br />";    
*/
// Insert the right page iacoording with the language in the browser
switch ($lang_var){
    case "fr":
        //echo "PAGE DE";
        include("index_fr.php");//include check session DE
        break;
    case "it":
        //echo "PAGE IT";
        include("index_it.php");
        break;
    case "en":
        //echo "PAGE EN";
        include("index_en.php");
        break;        
    default:
        //echo "PAGE EN - Setting Default";
        include("index_en.php");//include EN in all other cases of different lang detection
        break;
}
?>
GibboK
fuente
3
PHP 5.3.0+ viene con el locale_accept_from_http()que obtiene el idioma preferido del Accept-Languageencabezado. Siempre debe preferir este método a un método auto-escrito. Comprueba el resultado con una lista de expresiones regulares que intentas y determina el idioma de la página de esa manera. Ver PHP-I18N para un ejemplo.
graznar
2
El problema locale_accept_from_http()es que es posible que no admita el mejor resultado que devuelve, por lo que aún debe analizar el encabezado usted mismo para encontrar el siguiente mejor .
Xeoncross
La respuesta aceptada a esto debe cambiarse por una que tenga en cuenta varios idiomas.
Pekka
incluir y exigir se producen en el momento de la compilación de php, por lo que básicamente se incluye todo el índice * .php y se muestra solo uno - pérdida de recursos
Michael

Respuestas:

361

¿Por qué no lo mantienes simple y limpio?

<?php
    $lang = substr($_SERVER['HTTP_ACCEPT_LANGUAGE'], 0, 2);
    $acceptLang = ['fr', 'it', 'en']; 
    $lang = in_array($lang, $acceptLang) ? $lang : 'en';
    require_once "index_{$lang}.php"; 

?>
Pramendra Gupta
fuente
9
Los códigos de idioma holandés, griego y esloveno son una letra. Parece mejor explotar así: php.net/manual/tr/reserved.variables.server.php#90293
trante
10
@trante: ¿Por qué dices que son una letra? Holandés ( nl), griego ( el) y esloveno ( sl) parecen ser dos letras: msdn.microsoft.com/en-us/library/ms533052(v=vs.85).aspx
Peter K.
16
Este código no mira la lista completa. ¿Qué sucede si ples la primera prioridad y la frsegunda en mi lista de idiomas? Me gustaría aprender inglés en lugar de francés.
Kos
24
Esto carece de prioridades de detección y no es compatible con códigos diferentes de dos letras
Áxel Costas Pena
3
¡No hay otras longitudes que dos letras! Vaya a su navegador favorito y cambie la prioridad del idioma y lo verá.
Gigala
76

Accept-Language es una lista de valores ponderados (verparámetro q ). Eso significa que solo mirar el primer idioma no significa que también sea el más preferido; de hecho, unvalor q de 0 significa que no es aceptable en absoluto.

Entonces, en lugar de solo mirar el primer idioma, analice la lista de idiomas aceptados e idiomas disponibles y encuentre la mejor combinación:

// parse list of comma separated language tags and sort it by the quality value
function parseLanguageList($languageList) {
    if (is_null($languageList)) {
        if (!isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
            return array();
        }
        $languageList = $_SERVER['HTTP_ACCEPT_LANGUAGE'];
    }
    $languages = array();
    $languageRanges = explode(',', trim($languageList));
    foreach ($languageRanges as $languageRange) {
        if (preg_match('/(\*|[a-zA-Z0-9]{1,8}(?:-[a-zA-Z0-9]{1,8})*)(?:\s*;\s*q\s*=\s*(0(?:\.\d{0,3})|1(?:\.0{0,3})))?/', trim($languageRange), $match)) {
            if (!isset($match[2])) {
                $match[2] = '1.0';
            } else {
                $match[2] = (string) floatval($match[2]);
            }
            if (!isset($languages[$match[2]])) {
                $languages[$match[2]] = array();
            }
            $languages[$match[2]][] = strtolower($match[1]);
        }
    }
    krsort($languages);
    return $languages;
}

// compare two parsed arrays of language tags and find the matches
function findMatches($accepted, $available) {
    $matches = array();
    $any = false;
    foreach ($accepted as $acceptedQuality => $acceptedValues) {
        $acceptedQuality = floatval($acceptedQuality);
        if ($acceptedQuality === 0.0) continue;
        foreach ($available as $availableQuality => $availableValues) {
            $availableQuality = floatval($availableQuality);
            if ($availableQuality === 0.0) continue;
            foreach ($acceptedValues as $acceptedValue) {
                if ($acceptedValue === '*') {
                    $any = true;
                }
                foreach ($availableValues as $availableValue) {
                    $matchingGrade = matchLanguage($acceptedValue, $availableValue);
                    if ($matchingGrade > 0) {
                        $q = (string) ($acceptedQuality * $availableQuality * $matchingGrade);
                        if (!isset($matches[$q])) {
                            $matches[$q] = array();
                        }
                        if (!in_array($availableValue, $matches[$q])) {
                            $matches[$q][] = $availableValue;
                        }
                    }
                }
            }
        }
    }
    if (count($matches) === 0 && $any) {
        $matches = $available;
    }
    krsort($matches);
    return $matches;
}

// compare two language tags and distinguish the degree of matching
function matchLanguage($a, $b) {
    $a = explode('-', $a);
    $b = explode('-', $b);
    for ($i=0, $n=min(count($a), count($b)); $i<$n; $i++) {
        if ($a[$i] !== $b[$i]) break;
    }
    return $i === 0 ? 0 : (float) $i / count($a);
}

$accepted = parseLanguageList($_SERVER['HTTP_ACCEPT_LANGUAGE']);
var_dump($accepted);
$available = parseLanguageList('en, fr, it');
var_dump($available);
$matches = findMatches($accepted, $available);
var_dump($matches);

Si findMatchesdevuelve una matriz vacía, no se encontró ninguna coincidencia y puede recurrir al idioma predeterminado.

Gumbo
fuente
Hola, el script funcionaba bien y ahora para. ¿podría ser posible que si SESSION en el servidor se apaga, este script no funcione?
GibboK
@GIbboK: No, esto es independiente de las sesiones.
Gumbo
Correcto pero prefiero la solución @diggersworld ... mejor escribir menos código
lrkwz
¿Puede alguien decirme quién es el valor de qdecidir? Gracias
Phantom007
@ Phantom007 Depende de la preferencia: 0 = No quiero este idioma, 1 = Siempre quiero este idioma.
Skyost
43

Las respuestas existentes son demasiado detalladas, así que creé esta versión más pequeña y de coincidencia automática.

function prefered_language(array $available_languages, $http_accept_language) {

    $available_languages = array_flip($available_languages);

    $langs;
    preg_match_all('~([\w-]+)(?:[^,\d]+([\d.]+))?~', strtolower($http_accept_language), $matches, PREG_SET_ORDER);
    foreach($matches as $match) {

        list($a, $b) = explode('-', $match[1]) + array('', '');
        $value = isset($match[2]) ? (float) $match[2] : 1.0;

        if(isset($available_languages[$match[1]])) {
            $langs[$match[1]] = $value;
            continue;
        }

        if(isset($available_languages[$a])) {
            $langs[$a] = $value - 0.1;
        }

    }
    arsort($langs);

    return $langs;
}

Y el uso de la muestra:

//$_SERVER["HTTP_ACCEPT_LANGUAGE"] = 'en-us,en;q=0.8,es-cl;q=0.5,zh-cn;q=0.3';

// Languages we support
$available_languages = array("en", "zh-cn", "es");

$langs = prefered_language($available_languages, $_SERVER["HTTP_ACCEPT_LANGUAGE"]);

/* Result
Array
(
    [en] => 0.8
    [es] => 0.4
    [zh-cn] => 0.3
)*/

Fuente completa aquí

Xeoncross
fuente
66
Esto es brillante y exactamente lo que necesitaba para un proyecto en particular hoy. La única adición que hice es permitir que la función acepte un idioma predeterminado y recurrir a eso si no hay coincidencia entre los idiomas disponibles y HTTP_ACCEPT_LANGUAGEs.
Scott
77
Ah, una idea general de mis cambios está aquí: gist.github.com/humantorch/d255e39a8ab4ea2e7005 (también lo combiné en un solo archivo para simplificar)
Scott
2
Muy buen método! Tal vez debería verificar si $ langs ya contiene una entrada para el idioma. me sucedió que el lenguaje preferido era en-EE. UU., 2do y 3er en, su método siempre me dio de, porque el primer valor de en fue sobrescrito por la 3ra entrada
Peter Pint
También produce una advertencia de PHP si no se encuentran coincidencias. Sería bueno manejar esto con gracia.
Simon East
26

La forma oficial de manejar esto es usando la biblioteca PECL HTTP . A diferencia de algunas respuestas aquí, esto maneja correctamente las prioridades del idioma (valores q), coincidencias parciales del idioma y devolverá la coincidencia más cercana, o cuando no hay coincidencias, vuelve al primer idioma de su matriz.

PECL HTTP:
http://pecl.php.net/package/pecl_http

Cómo usar:
http://php.net/manual/fa/function.http-negotiate-language.php

$supportedLanguages = [
    'en-US', // first one is the default/fallback
    'fr',
    'fr-FR',
    'de',
    'de-DE',
    'de-AT',
    'de-CH',
];

// Returns the negotiated language 
// or the default language (i.e. first array entry) if none match.
$language = http_negotiate_language($supportedLanguages, $result);
diggersworld
fuente
1
Encontré un enlace que funciona, así que actualicé su respuesta para incluirlo.
Simon East
Los tres enlaces parecen estar muertos y no parecen tener instrucciones de instalación fáciles de usar en Google (también esta función está en desuso según su página)
Brian Leishman
11

El problema con la respuesta seleccionada anteriormente es que el usuario puede tener su primera opción configurada como un idioma que no está en la estructura del caso, pero se establece una de sus otras opciones de idioma. Debes recorrer hasta encontrar una coincidencia.

Esta es una solución súper simple que funciona mejor. Los navegadores devuelven los idiomas en orden de preferencia, lo que simplifica el problema. Si bien el designador de lenguaje puede tener más de dos caracteres (por ejemplo, "EN-US"), normalmente los dos primeros son suficientes. En el siguiente ejemplo de código, estoy buscando una coincidencia de una lista de idiomas conocidos que mi programa conoce.

$known_langs = array('en','fr','de','es');
$user_pref_langs = explode(',', $_SERVER['HTTP_ACCEPT_LANGUAGE']);

foreach($user_pref_langs as $idx => $lang) {
    $lang = substr($lang, 0, 2);
    if (in_array($lang, $known_langs)) {
        echo "Preferred language is $lang";
        break;
    }
}

Espero que encuentre esta solución rápida y simple que pueda usar fácilmente en su código. He estado usando esto en producción durante bastante tiempo.

Darryl
fuente
3
"Los navegadores devuelven los idiomas en orden de preferencia": pueden hacerlo, pero no debe depender de eso. Use qvalores para determinar la preferencia, eso es lo que la especificación dice que debe hacer.
Quentin
7

Prueba este:

#########################################################
# Copyright © 2008 Darrin Yeager                        #
# https://www.dyeager.org/                               #
# Licensed under BSD license.                           #
#   https://www.dyeager.org/downloads/license-bsd.txt    #
#########################################################

function getDefaultLanguage() {
   if (isset($_SERVER["HTTP_ACCEPT_LANGUAGE"]))
      return parseDefaultLanguage($_SERVER["HTTP_ACCEPT_LANGUAGE"]);
   else
      return parseDefaultLanguage(NULL);
   }

function parseDefaultLanguage($http_accept, $deflang = "en") {
   if(isset($http_accept) && strlen($http_accept) > 1)  {
      # Split possible languages into array
      $x = explode(",",$http_accept);
      foreach ($x as $val) {
         #check for q-value and create associative array. No q-value means 1 by rule
         if(preg_match("/(.*);q=([0-1]{0,1}.\d{0,4})/i",$val,$matches))
            $lang[$matches[1]] = (float)$matches[2];
         else
            $lang[$val] = 1.0;
      }

      #return default language (highest q-value)
      $qval = 0.0;
      foreach ($lang as $key => $value) {
         if ($value > $qval) {
            $qval = (float)$value;
            $deflang = $key;
         }
      }
   }
   return strtolower($deflang);
}
usuario956584
fuente
Oye, ¿podrías explicar la expresión regular que debería capturar el valor q con [0-1]{0,1}.\d{0,4}? Primero, supongo que quieres decir en \.lugar de .¿verdad? ¿Y no es q siempre de la forma 0.1324o algo así? ¿No sería entonces suficiente escribir 0\.?\d{0,4}? Si es q=1.0así, puedes ir a la otra parte.
Adam
Sería genial ver un ejemplo de uso aquí.
Simon East
2
@SimonEast var_dump( getDefaultLanguage());
jirorio
4

El siguiente script es una versión modificada del código de Xeoncross (gracias por ese Xeoncross) que recurre a una configuración de idioma predeterminada si no hay idiomas que coincidan con los admitidos, o si se encuentra una coincidencia, reemplaza la configuración de idioma predeterminada por una nueva. según la prioridad del idioma

En este escenario, el navegador del usuario está configurado en orden de prioridad para español, holandés, inglés de EE. UU. E inglés, y la aplicación solo admite inglés y holandés sin variaciones regionales, y el inglés es el idioma predeterminado. El orden de los valores en la cadena "HTTP_ACCEPT_LANGUAGE" no es importante si por alguna razón el navegador no ordena los valores correctamente.

$supported_languages = array("en","nl");
$supported_languages = array_flip($supported_languages);
var_dump($supported_languages); // array(2) { ["en"]=> int(0) ["nl"]=> int(1) }

$http_accept_language = $_SERVER["HTTP_ACCEPT_LANGUAGE"]; // es,nl;q=0.8,en-us;q=0.5,en;q=0.3

preg_match_all('~([\w-]+)(?:[^,\d]+([\d.]+))?~', strtolower($http_accept_language), $matches, PREG_SET_ORDER);

$available_languages = array();

foreach ($matches as $match)
{
    list($language_code,$language_region) = explode('-', $match[1]) + array('', '');

    $priority = isset($match[2]) ? (float) $match[2] : 1.0;

    $available_languages[][$language_code] = $priority;
}

var_dump($available_languages);

/*
array(4) {
    [0]=>
    array(1) {
        ["es"]=>
        float(1)
    }
    [1]=>
    array(1) {
        ["nl"]=>
        float(0.8)
    }
    [2]=>
    array(1) {
        ["en"]=>
        float(0.5)
    }
    [3]=>
    array(1) {
        ["en"]=>
        float(0.3)
    }
}
*/

$default_priority = (float) 0;
$default_language_code = 'en';

foreach ($available_languages as $key => $value)
{
    $language_code = key($value);
    $priority = $value[$language_code];

    if ($priority > $default_priority && array_key_exists($language_code,$supported_languages))
    {
        $default_priority = $priority;
        $default_language_code = $language_code;

        var_dump($default_priority); // float(0.8)
        var_dump($default_language_code); // string(2) "nl"
    }
}

var_dump($default_language_code); // string(2) "nl" 
Noel Whitemore
fuente
1

¡Creo que la forma más limpia es esta!

 <?php
  $lang = substr($_SERVER['HTTP_ACCEPT_LANGUAGE'], 0, 2);
  $supportedLanguages=['en','fr','gr'];
  if(!in_array($lang,$supportedLanguages)){
     $lang='en';
  }
    require("index_".$lang.".php");
Mike Antoniadis
fuente
Esto no tiene en cuenta las prioridades de idioma dentro del encabezado.
Simon East
0

Todo lo anterior con respaldo a 'en':

$lang = substr(explode(',',$_SERVER['HTTP_ACCEPT_LANGUAGE'])[0],0,2)?:'en';

... o con una reserva de idioma predeterminada y una matriz de idiomas conocida:

function lang( $l = ['en'], $u ){
    return $l[
        array_keys(
            $l,
            substr(
                explode(
                    ',',
                    $u ?: $_SERVER['HTTP_ACCEPT_LANGUAGE']
                )[0],
                0,
                2
            )
        )[0]
    ] ?: $l[0];
}

Una línea:

function lang($l=['en'],$u){return $l[array_keys($l,substr(explode(',',$u?:$_SERVER['HTTP_ACCEPT_LANGUAGE'])[0],0,2))[0]]?:$l[0];}

Ejemplos:

// first known lang is always default
$_SERVER['HTTP_ACCEPT_LANGUAGE'] = 'en-us';
lang(['de']); // 'de'
lang(['de','en']); // 'en'

// manual set accept-language
lang(['de'],'en-us'); // 'de'
lang(['de'],'de-de, en-us'); // 'de'
lang(['en','fr'],'de-de, en-us'); // 'en'
lang(['en','fr'],'fr-fr, en-us'); // 'fr'
lang(['de','en'],'fr-fr, en-us'); // 'de'
Toby
fuente
0

Tratar,

$lang = substr($_SERVER['HTTP_ACCEPT_LANGUAGE'], 0,2);

if ($lang == 'tr') {
include_once('include/language/tr.php');
}elseif ($lang == 'en') {
include_once('include/language/en.php');
}elseif ($lang == 'de') {
include_once('include/language/de.php');
}elseif ($lang == 'fr') {
include_once('include/language/fr.php');
}else{
include_once('include/language/tr.php');
}

Gracias a

mrbengi
fuente
0

Rápido y simple:

$language = trim(substr( strtok(strtok($_SERVER['HTTP_ACCEPT_LANGUAGE'], ','), ';'), 0, 5));

NOTA: El código del primer idioma es el que usa el navegador, el resto son otros idiomas que el usuario ha configurado en el navegador.

Algunos idiomas tienen un código de región, por ejemplo. en-GB, otros solo tienen el código de idioma, por ejemplo. sk.

Si solo desea el idioma y no la región (por ejemplo, en, fr, es, etc.), puede usar:

$language =substr($_SERVER['HTTP_ACCEPT_LANGUAGE'], 0, 2);
Justin Levene
fuente
-1

Tengo este, que establece una cookie. Y como puede ver, primero verifica si el usuario publica el idioma. Porque el lenguaje del navegador no siempre informa sobre el usuario.

<?php   
    $lang = getenv("HTTP_ACCEPT_LANGUAGE");
    $set_lang = explode(',', $lang);
    if (isset($_POST['lang'])) 
        {
            $taal = $_POST['lang'];
            setcookie("lang", $taal);
            header('Location: /p/');
        }
    else 
        {
            setcookie("lang", $set_lang[0]);
            echo $set_lang[0];
            echo '<br>';
            echo $set_lang[1];
            header('Location: /p/');
        } 
?>
Matthijs
fuente
11
¿Supongo que no puedes enviar encabezados cuando ya has hecho eco?
2
Creo que la sangría detrás de esta publicación tiene sentido, que es proporcionar al usuario una forma de cambiar el idioma y recordar esta decisión. La detección de idioma solo debe hacerse una vez para adivinar mejor la primera selección.
danijar