在PHP中检测浏览器语言

174

我使用以下PHP脚本作为我的网站的索引。

这个脚本应该根据浏览器的语言(自动检测)包含特定的页面。

这个脚本在所有浏览器上都不能很好地工作,所以它总是包含index_en.php来适配检测到的任何语言(问题的原因很可能是某些Accept-Language头没有被考虑进去)。

请问您能否建议一个更加稳健的解决方案?

<?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;
}
?>

6
PHP 5.3.0+ 版本新增了 locale_accept_from_http() 函数,它可以从 Accept-Language 头部中获取用户首选语言。您应该优先使用此方法而不是自行编写的方法。将结果与正则表达式列表进行匹配以确定页面语言。有关示例,请参见 PHP-I18N - caw
3
locale_accept_from_http() 的问题在于您可能不支持它返回的最佳结果,因此仍然需要自己解析标头,以找到次佳的结果。 - Xeoncross
接受的答案应该修改为考虑多种语言的答案之一。 - Pekka
include和require在PHP的编译时发生,因此基本上您包含所有的index*.php并仅显示一个-浪费资源。 - Michael
除了真正的问题之外,关于上面的代码,我不建议为每种语言使用不同的索引页面。用户希望在每种语言中看到相同的页面布局和内容。您只需要在一个index.php页面中加载适当的文本/图片即可。内容可以组织在一个XML文件中,例如[item]->[lang]部分- <div><?php echo $xml->item [0]-> lang [0];?></div>,或者在SQL等中。 - Hristo
16个回答

410

为什么不保持简单而清晰

<?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"; 

?>

11
荷兰语、希腊语和斯洛文尼亚语的语言代码都只有一个字母。建议使用如下方式进行展开:http://www.php.net/manual/tr/reserved.variables.server.php#90293 - trante
13
为什么你说它们是一个字母?荷兰语(nl)、希腊语(el)和斯洛文尼亚语(sl)都是两个字母。参考链接:http://msdn.microsoft.com/en-us/library/ms533052(v=vs.85).aspx - Peter K.
25
这段代码没有遍历整个列表。如果我的语言列表中pl是第一选择,而fr是第二选择会怎么样呢?我会得到英语而不是法语。 - Kos
34
这个缺乏检测优先级,也不兼容非两个字母的代码(http://www.iana.org/assignments/language-subtag-registry)。 - Áxel Costas Pena
4
除了两个字母的长度外,没有其他长度!打开您喜欢使用的浏览器并更改语言优先级,您会发现它。 - Gigala
显示剩余8条评论

83

Accept-Language 是一个加权值列表(参见 q 参数)。这意味着仅仅查看第一个语言并不意味着它也是最受欢迎的;事实上,q 值为0表示根本不可接受。

因此,不要只看第一种语言,而是解析接受的语言和可用的语言列表,并找到最佳匹配:

// 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);
如果findMatches返回一个空数组,则表示未找到匹配项,你可以回退到默认语言。

嗨,脚本之前一直正常运行,现在突然停止了。如果服务器上的SESSION关闭了,这个脚本就无法工作,这种情况可能发生吗? - GibboK
@GIbboK:不,这与会话无关。 - Gumbo
正确,但我更喜欢@diggersworld的解决方案...写得更少更好。 - lrkwz
有人能告诉我 q 的值是如何决定的吗?谢谢。 - Phantom007
@Phantom007 取决于个人偏好:0 = 我不想要这种语言,1 = 我总是想要这种语言。 - Skyost
四级嵌套的foreach - 我立即感到需要重构这个代码。:-\ - domsson

47

现有的回答有些冗长,所以我创建了这个更小、自动匹配的版本。

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;
}

并且这是一个示例用法:

//$_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
)*/

完整的源代码请点击此处查看


8
这太棒了,正是我今天某个项目所需要的。我只做了一个小修改,让函数可以接受默认语言,并在可用语言和HTTP_ACCEPT_LANGUAGEs之间没有匹配时回退到默认语言。 - Scott
8
好的,以下是您要翻译的内容:哦,我对原文进行了改动并在此处提供了概要:https://gist.github.com/humantorch/d255e39a8ab4ea2e7005(为了简单起见,我还将其合并到了一个文件中) - Scott
2
非常好的方法!也许你应该检查一下 $langs 是否已经包含了该语言的条目。我曾经遇到过这样的情况,我的首选语言是 en-US,第二个是 de,第三个是 en,但是你的方法总是给出 de,因为 en 的第一个值被第三个条目覆盖了。 - Peter Pint
1
如果没有找到匹配项,它还会产生一个PHP警告。优雅地处理这个问题会更好。 - Simon East
不按预期工作,我的浏览器首选语言是 ("en","ar","en-us"),但实际显示的是 ar 作为首选语言。 - shamaseen

31
官方处理此问题的方法是使用PECL HTTP库。与其他答案不同,它可以正确处理语言优先级(q值)、部分语言匹配,并返回最接近的匹配项,或者当没有匹配项时,它会回到您数组中的第一个语言。
PECL HTTP:
http://pecl.php.net/package/pecl_http
如何使用:
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);

1
我找到了一个可用的链接,因此更新了您的答案以包含它。 - Simon East
6
这三个链接似乎都失效了,并且它们似乎没有任何可以通过谷歌轻松找到的安装说明(而且根据它们网页上的信息,此功能已被弃用)。 - Brian Leishman

15
选定答案的问题在于用户可能将其首选语言设置为不在情况结构中的语言,但其中一个其他语言选择已设置。应该循环直到找到匹配项。
这是一个更好的超级简单的解决方案。浏览器按偏好顺序返回语言,因此这简化了问题。虽然语言标识符可以超过两个字符(例如 -“EN-US”),但通常前两个足以。在以下代码示例中,我正在寻找与我的程序熟知的语言列表匹配的项目。
$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;
    }
}

我希望您能在代码中轻松使用这个快速简单的解决方案。我已经在生产环境中使用了相当长的时间。


6
浏览器会按照偏好的顺序返回语言,但是你不应该依赖这一点。使用“q”值来确定优先级,这是规范要求的做法。 - Quentin

7

试试这个:

#########################################################
# 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);
}

嘿,你能解释一下应该使用[0-1]{0,1}.\d{0,4}来捕获q值的正则表达式吗?首先,我猜你是指\.而不是.,对吧?并且q不总是以0.1324或其他形式出现吗?那么写成0\.?\d{0,4}就足够了,不是吗?如果你有q=1.0,那么你可以进入else部分。 - Adam
这里放一个使用示例会很棒。 - Simon East
2
@SimonEast var_dump(getDefaultLanguage()); - jirarium

7

不幸的是,这个问题的回答都没有考虑一些有效的HTTP_ACCEPT_LANGUAGE,例如:

  • q=0.8,en-US;q=0.5,en;q=0.3: 将q优先级值放在第一位。
  • ZH-CN: 旧版浏览器错误地将整个语言代码大写。
  • *: 基本上是说“提供任何你拥有的语言”。

经过对成千上万个到达我的服务器的不同Accept-Languages进行全面测试,这是我的语言检测方法:

define('SUPPORTED_LANGUAGES', ['en', 'es']);

function detect_language($fallback='en') {
    foreach (preg_split('/[;,]/', $_SERVER['HTTP_ACCEPT_LANGUAGE']) as $sub) {
        if (substr($sub, 0, 2) == 'q=') continue;
        if (strpos($sub, '-') !== false) $sub = explode('-', $sub)[0];
        if (in_array(strtolower($sub), SUPPORTED_LANGUAGES)) return $sub;
    }
    return $fallback;
}

4
以下脚本是 Xeoncross 的代码的修改版本(感谢 Xeoncross),如果没有任何语言匹配支持的语言,或者找到一个匹配项,则会将默认语言设置为回退语言,并根据语言优先级使用新语言替换默认语言设置。
在这种情况下,用户的浏览器按优先顺序设置为西班牙语、荷兰语、美式英语和英语,而应用程序仅支持英语和荷兰语,没有区域变体,英语是默认语言。如果由于某种原因浏览器未正确排序值,则“HTTP_ACCEPT_LANGUAGE”字符串中的值的顺序不重要。
$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" 

4

简单易懂:

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

注意: 第一个语言代码是浏览器正在使用的语言,其余的是用户在浏览器中设置的其他语言。

有些语言有一个地区代码,例如 en-GB,而其他一些只有语言代码,例如 sk。

如果你只想要语言而不需要地区(例如 en, fr, es 等),可以使用:

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

3

在php-intl扩展中有一个方法:

 locale_accept_from_http($_SERVER['HTTP_ACCEPT_LANGUAGE'])

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接