Detect Browser Language in PHP
Asked Answered
A

16

174

I use the following PHP script as index for my website.

This script should include a specific page depending on the browser's language (automatically detected).

This script does not work well with all browsers, so it always includes index_en.php for any detected language (the cause of the problem is most probably an issue with some Accept-Language header not being considered).

Could you please suggest me a more robust solution?

<?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;
}
?>
Alteration answered 22/9, 2010 at 14:54 Comment(5)
PHP 5.3.0+ comes with locale_accept_from_http() which gets the preferred language from the Accept-Language header. You should always prefer this method to a self-written method. Check the result against a list of regular expressions that you try and determine the page language that way. See PHP-I18N for an example.Falk
The problem with locale_accept_from_http() is that you may not support the best result it returns so you still have the parse the header yourself to find the next-best.Prismatic
The accepted answer to this should be changed to one of those that take multiple languages into account.Reposeful
include and require's are happen at the compile time of php so basically you include all the index*.php and show only one - waste of ressourcesInjun
Besides from the real question and regarding the code above, I'd not recommend using different index pages for every language. An user would be happy to see same page layout and content in each language. You only have to load appropriate texts/pics inside one index.php page. The content can be organized e.g. in one XML file like [item]->[lang] sections - <div><?php echo $xml->item[0]->lang[0]; ?></div>, or in SQL, etc.Ambit
O
410

why dont you keep it simple and clean

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

?>
Override answered 22/9, 2010 at 15:5 Comment(13)
Just a questions in case SESSION on the server are disable script could work? thanksAlteration
Dutch, Greek and Slovenian's language codes are one letter. It seems better to explode like this: php.net/manual/tr/reserved.variables.server.php#90293Hilton
@trante: Why do you say they are one letter? Dutch (nl), Greek (el) and Slovenian (sl) all appear to be two letter: msdn.microsoft.com/en-us/library/ms533052(v=vs.85).aspxFredella
This code doesn't look at the whole list. What if pl is first priority and fr is second in my language list? I'd get English instead of French.Supportive
This lacks of detecting priorities, and is'nt compatible with codes different from two lettersCohen
There are no other lengths than two letters! Go in your favorite browser and change the language priority and you will see it.Camisole
@Camisole Yes there is other length than two letters, even if your favorite browser doesn't support them. They can length until 8 chars, the official list is administrated by IANA: iana.org/assignments/language-subtag-registry/…Seaweed
@Seaweed Right - there are - but header's Accept-Language primary-tag always consists of two letter language abbreviations defined in ISO-639.Lassalle
Only the large languages have a two-letter code, most languages don't, see: en.wikipedia.org/wiki/List_of_ISO_639-2_codesWebbed
dyeager.org/blog/2008/10/… i like this oneAntibody
@budwiser rfc2616 says that Accept-Language could have until 8 chars (obviously). My firefox let me define languages that have more than two letters. There are very few collisions possible for it/en/fr but it is a bad practice not to recommend.Seaweed
also there is a simpler and much shorter way to get the same result $lang = substr($_SERVER['HTTP_ACCEPT_LANGUAGE'],0,2); require("path/to/langs/lang.$lang.php");Moncton
This is a bad approach because the user may have their first choice set as a language that's not in the case structure, but one of their other choices are. You should loop until you find a match. In addition, it's best to explode the languages which are delimited by commas. See answer below.Jardine
D
83

Accept-Language is a list of weighted values (see q parameter). That means just looking at the first language does not mean it’s also the most preferred; in fact, a q value of 0 means not acceptable at all.

So instead of just looking at the first language, parse the list of accepted languages and available languages and find the best match:

// 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);

If findMatches returns an empty array, no match was found and you can fall back on the default language.

Downtown answered 22/9, 2010 at 16:27 Comment(6)
Hi, script was working fine and now stop. could be possible that if SESSION on the server are turn off this script wont work?Alteration
@GIbboK: No, this is independent of sessions.Downtown
Correct but I prefer @diggersworld solution ... better write less codeNatascha
Can someone please tell me who how is the value of q decided? ThanksPictish
@Pictish Depends of the preference : 0 = I don't want this language, 1 = I always want this language.Congruity
Four levels of nested foreach - I immediately feel the need to refactor this code. :-\Balsamic
P
47

The existing answers are a little too verbose so I created this smaller, auto-matching version.

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

And the sample usage:

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

Full gist source here

Prismatic answered 9/9, 2014 at 16:26 Comment(5)
This is brilliant and exactly what I needed for a particular project today. The only addition I made is to allow the function to accept a default language and fall back to that if there's no match between available languages and HTTP_ACCEPT_LANGUAGEs.Nankeen
Oh, a gist with my changes is here: gist.github.com/humantorch/d255e39a8ab4ea2e7005 (I also combined it into one file for simplicity)Nankeen
Very nice method! Maybe you should check if $langs already contains an entry for the language. happened to me that perferred language was en-US, 2nd de and 3rd en, your method always gave me de, cause the first value of en was overwritten by the 3rd entryMiss
It also produces a PHP warning if no matches are found. Would be nice to handle this gracefully.Oscular
not working as expected, my preferred language in the browser is ("en","ar","en-us") what is happening is that is shows ar is the preferred language :\Flatfoot
G
31

The official way to handle this is using the PECL HTTP library. Unlike some answers here, this correctly handles the language priorities (q-values), partial language matches and will return the closest match, or when there are no matches it falls back to the first language in your array.

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

How to use:
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);
Gales answered 11/7, 2012 at 10:15 Comment(2)
I found a working link, so updated your answer to include it.Oscular
All three of these links appear to be dead, and they don't seem to have any easily Googleable install instructions (also this function is deprecated according to their page for it)Joke
J
15

The problem with the selected answer above is that the user may have their first choice set as a language that's not in the case structure, but one of their other language choices are set. You should loop until you find a match.

This is a super simple solution that works better. Browsers return the languages in order of preference, so that simplifies the problem. While the language designator can be more than two characters (e.g. - "EN-US"), typically the first two are sufficient. In the following code example I'm looking for a match from a list of known languages my program is aware of.

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

I hope you find this a quick and simple solution that you can easily use in your code. I've been using this in production for quite a while.

Jardine answered 11/10, 2014 at 18:28 Comment(1)
"Browsers return the languages in order of preference" — They might do, but you should not depend on that. Use q values to determine preference, that's what the spec says you should do.Muniments
Z
8

Unfortunately, none of the answers to this question takes into account some valid HTTP_ACCEPT_LANGUAGE such as:

  • q=0.8,en-US;q=0.5,en;q=0.3: having the q priority value at first place.
  • ZH-CN: old browsers that capitalise (wrongly) the whole langcode.
  • *: that basically say "serve whatever language you have".

After a comprehensive test with thousands of different Accept-Languages that reached my server, this is my language detection method:

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;
}
Zingale answered 19/10, 2020 at 9:43 Comment(0)
S
7

Try this one:

#########################################################
# 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);
}
Stigma answered 18/5, 2015 at 10:24 Comment(3)
Hey could you explain the regex that should catch the q value with [0-1]{0,1}.\d{0,4} ? First I guess you mean \. instead of . right? And isn't q always of the form 0.1324 or something? Wouldn't it be then sufficient to write 0\.?\d{0,4}? If you have q=1.0 then you can go in the else part.Morel
Would be great to see a usage example here.Oscular
@SimonEast var_dump( getDefaultLanguage());Waterage
R
4

The following script is a modified version of Xeoncross's code (thank you for that Xeoncross) that falls-back to a default language setting if no languages match the supported ones, or if a match is found it replaces the default language setting with a new one according to the language priority.

In this scenario the user's browser is set in order of priority to Spanish, Dutch, US English and English and the application supports English and Dutch only with no regional variations and English is the default language. The order of the values in the "HTTP_ACCEPT_LANGUAGE" string is not important if for some reason the browser does not order the values correctly.

$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" 
Rinderpest answered 2/10, 2014 at 21:1 Comment(0)
S
4

Quick and simple:

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

NOTE: The first language code is what is being used by the browser, the rest are other languages the user has setup in the browser.

Some languages have a region code, eg. en-GB, others just have the language code, eg. sk.

If you just want the language and not the region (eg. en, fr, es, etc.), you can use:

$language =substr($_SERVER['HTTP_ACCEPT_LANGUAGE'], 0, 2);
Stevestevedore answered 17/6, 2020 at 23:48 Comment(0)
T
3

There is a method in php-intl extension:

 locale_accept_from_http($_SERVER['HTTP_ACCEPT_LANGUAGE'])
Thrippence answered 11/2, 2021 at 9:20 Comment(3)
It's not present by default in my PHP 7.4 installation. How do I enable it?Photomural
Yes, you need to install php-intl extension on your computerThrippence
In particular, since 7.4 is not the latest PHP, I need to install "php7.4-intl".Photomural
D
2

I think the cleanest way is this!

 <?php
  $lang = substr($_SERVER['HTTP_ACCEPT_LANGUAGE'], 0, 2);
  $supportedLanguages=['en','fr','gr'];
  if(!in_array($lang,$supportedLanguages)){
     $lang='en';
  }
    require("index_".$lang.".php");
Duenas answered 12/5, 2016 at 18:27 Comment(1)
This doesn't account for language priorities within the header.Oscular
V
1

FOR LARAVEL USERS, here's a single line of code that returns a very clean collection (or array) of preferred languages:

$langs = Str::of($_SERVER['HTTP_ACCEPT_LANGUAGE'])
        ->explode(',')
        ->transform(fn($lang) => Str::substr($lang, 0, 2))
        ->unique();
Vi answered 14/12, 2021 at 9:21 Comment(0)
D
1

Since PHP 5.3.0 there is a Locale class bundled with the php-intl extension which has a method for this:

echo Locale::acceptFromHttp($_SERVER['HTTP_ACCEPT_LANGUAGE']);

or procedural style:

locale_accept_from_http($_SERVER['HTTP_ACCEPT_LANGUAGE']);

https://www.php.net/manual/en/locale.acceptfromhttp.php

Drumm answered 4/3, 2022 at 17:39 Comment(0)
O
0

All of the above with fallback to 'en':

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

...or with default language fallback and known language array:

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

One Line:

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

Examples:

// 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'
Oleaster answered 26/3, 2015 at 0:48 Comment(0)
P
0

Try,

$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');
}

Thanks to

Prayer answered 15/7, 2019 at 13:57 Comment(0)
H
-1

I've got this one, which sets a cookie. And as you can see, it first checks if the language is posted by the user. Because browser language not always tells about the user.

<?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/');
        } 
?>
His answered 20/1, 2012 at 9:54 Comment(2)
I guess you can't send headers when you already echoed stuff?Protestation
I think the indention behind this post makes sense, which is to provide the user with a way to switch the language, and remembering this decision. Language detection should only be done once to best guess the first selection.Notify

© 2022 - 2024 — McMap. All rights reserved.