Recently I was tasked with coming up with a system for redirecting users to the correct version of a translated site based on where they were accessing the site from. Having spent some time looking on Google and Drupal.org I did not find anything suitable and so decided to write my own which I am now sharing due to the lack of resources on this subject.
The problem
The client is a multi national business and wants one site where users can select their language, no problem. Actually the site also needs to look up the IP of the user and if their country matches the list of languages that we have set up multi lingual support for then we need to redirect that user silently to the correct page within the site.
I did find a couple of tutorials / demos on this subject but having tried a couple of them none of them actually worked properly so having gathered what I could from these other modules and solutions I decided to write a simple solution using a third party API from ipinfodb.com.
The code
The module code itself may not be the most elegant solution nor does it provide a user interface as for our needs this was pointless and would have been a waste of time but I would consider making this code more user friendly if there was any interest from the community in me doing so.
The Logic
Basically this module tests to see
- If you have a writable session.
- If you have already been redirected.
- Which country you are in.
- If the URL you requested already has a country code.
- Which languages are provided for on the site.
Having gathered this information about the user and their request we have to work out some more information about the content on the site like if the page they are requesting actually has a translation in to the required language.
The result is then to either redirect the user or not based on the results of these tests, their location and the availability of translated pages within the site, the desired default behaviour for users that are trying to access the site for a country that is not catered for was to redirect to the default site language so this has been written in as well.
More problems
As we needed to intercept the first request of every user whether the page was cached or not I have had to replace some of the basic native Drupal functions with functional equivalents to get the job done.
How to use this code
If you would like to give this code a try then you will need a Drupal 7 site with internationalisation set up. We opted to use the URL prefix method for differentiating between the languages available so the redirects are hard coded to go to domain.com/LANG_CODE/drupal-path.
You will also need to be relatively proficient a PHP as the code I am providing is in the My Module format as there are only a few functions that you may wish to add to any other custom module.
You will also need to register for a free API key from ipinfodb.com and replace the "#######APIKEY#######" in the code with your API key.
- /**
- * Implements hook_boot().
- */
- function mymodule_boot() {
- // an array of country codes to match to their language codes
- 'FR' => 'fr',
- 'CN' => 'zh-hans',
- 'BR' => 'pt',
- 'PT' => 'pt',
- 'US' => 'en',
- 'NZ' => 'nz',
- );
- // the default lang code, we redirect to this if the users code is not in the
- // array of defined options above
- $default_lang_code = 'en-gb';
- // test to make sure the session is writable
- $_SESSION['test'] = 'test';
- return;
- }
- else {
- }
- // if there is no country_code session then this is the user's first request
- $key = '#######APIKEY#######';
- $request = mymodule_do_curl(mymodule_get_ip_info_db_url($key, $_SERVER['REMOTE_ADDR']));
- }
- // if we haven't redirected them yet and they do have a country code session
- global $base_url;
- // load all active languages
- // remove the country code from the current get q and save to path URL
- // the current lang code
- $current_lang_code = $default_lang_code;
- $current_lang_code = $path[0];
- }
- // if the user does not have a redirect Session
- // and does have a country_code session and their country code matches
- // one of the pre defined languages and their country code is not the current
- // country code in the url then redirect to their correct language
- if (
- $countries[$_SESSION['country_code']] != $current_lang_code
- ) {
- $_SESSION['redirected'] = TRUE;
- // redirect to the correct language home page if this is a homepage request
- mymodule_redirect($base_url . '/' . $countries[$_SESSION['country_code']], 100);
- }
- // test to see if there is a translated path
- SELECT a.source, b.alias
- FROM url_alias a JOIN url_alias b
- ON a.source = b.source
- WHERE a.alias = :path
- AND b.language = :langcode
- // if there is a translated path
- $result = $result->fetchAssoc();
- // if there is no translation then get the current base path for the alias
- SELECT source
- FROM url_alias
- WHERE alias = :path
- $result = $result->fetchAssoc();
- }
- mymodule_redirect($base_url . '/' . $countries[$_SESSION['country_code']] . '/' . $result['alias'], 100);
- }
- mymodule_redirect($base_url . '/' . $countries[$_SESSION['country_code']] . '/' . $result['source'], 100);
- }
- }
- elseif (
- (
- ) &&
- $current_lang_code != $default_lang_code
- ) {
- $_SESSION['redirected'] = TRUE;
- // redirect to the correct language home page if this is a homepage request
- mymodule_redirect($base_url . '/' . $default_lang_code, 100);
- }
- // test to see if there is a translated path
- SELECT a.source, b.alias
- FROM url_alias a JOIN url_alias b
- ON a.source = b.source
- WHERE a.alias = :path
- AND b.language = :langcode
- // if there is a translated path
- $result = $result->fetchAssoc();
- // if there is no translation then get the current base path for the alias
- SELECT source
- FROM url_alias
- WHERE alias = :path
- $result = $result->fetchAssoc();
- }
- mymodule_redirect($base_url . '/' . $default_lang_code . '/' . $result['alias'], 100);
- }
- mymodule_redirect($base_url . '/' . $default_lang_code . '/' . $result['source'], 100);
- }
- }
- }
- }
- /**
- * Does a curl request from hook_boot
- *
- * @param str $url
- * location of resource to get
- *
- * @return string
- */
- function mymodule_do_curl($url) {
- global $s;
- $headers[] = "Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5";
- $headers[] = "Connection: keep-alive";
- $headers[] = "Keep-Alive: 115";
- $headers[] = "Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7";
- $headers[] = "Accept-Language: en-us,en;q=0.5";
- $headers[] = "Pragma: ";
- $ch = curl_init();
- curl_setopt($ch, CURLOPT_URL, $url);
- curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
- curl_setopt($ch, CURLOPT_USERAGENT,'Mozilla/5.0 (Windows NT 5.1; rv:2.0.1) Gecko/20100101 Firefox/4.0.1');
- curl_setopt($ch, CURLOPT_ENCODING, 'gzip,deflate');
- curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
- curl_setopt($ch, CURLOPT_COOKIEFILE, $cookie);
- curl_setopt($ch, CURLOPT_COOKIEJAR, $cookie);
- curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 30);
- $data=curl_exec($ch);
- curl_close($ch);
- return $data;
- }
- /**
- * Custom function to build the api url for the ip location lookup
- * @param $ipinfodb_key
- * @param $ip_address
- * @return string
- */
- function mymodule_get_ip_info_db_url($ipinfodb_key, $ip_address) {
- return "<a href="http://api.ipinfodb.com/v3/ip-city/ip_query.php?key=">http://api.ipinfodb.com/v3/ip-city/ip_query.php?key=</a>$ipinfodb_key&ip=$ip_address&format=json";
- }
- /**
- * Redirect function for hook boot
- * @param string $url
- * @param int $num
- *
- * @return void
- */
- function mymodule_redirect($url, $num = 301){
- 100 => "HTTP/1.1 100 Continue",
- 101 => "HTTP/1.1 101 Switching Protocols",
- 200 => "HTTP/1.1 200 OK",
- 201 => "HTTP/1.1 201 Created",
- 202 => "HTTP/1.1 202 Accepted",
- 203 => "HTTP/1.1 203 Non-Authoritative Information",
- 204 => "HTTP/1.1 204 No Content",
- 205 => "HTTP/1.1 205 Reset Content",
- 206 => "HTTP/1.1 206 Partial Content",
- 300 => "HTTP/1.1 300 Multiple Choices",
- 301 => "HTTP/1.1 301 Moved Permanently",
- 302 => "HTTP/1.1 302 Found",
- 303 => "HTTP/1.1 303 See Other",
- 304 => "HTTP/1.1 304 Not Modified",
- 305 => "HTTP/1.1 305 Use Proxy",
- 307 => "HTTP/1.1 307 Temporary Redirect",
- 400 => "HTTP/1.1 400 Bad Request",
- 401 => "HTTP/1.1 401 Unauthorized",
- 402 => "HTTP/1.1 402 Payment Required",
- 403 => "HTTP/1.1 403 Forbidden",
- 404 => "HTTP/1.1 404 Not Found",
- 405 => "HTTP/1.1 405 Method Not Allowed",
- 406 => "HTTP/1.1 406 Not Acceptable",
- 407 => "HTTP/1.1 407 Proxy Authentication Required",
- 408 => "HTTP/1.1 408 Request Time-out",
- 409 => "HTTP/1.1 409 Conflict",
- 410 => "HTTP/1.1 410 Gone",
- 411 => "HTTP/1.1 411 Length Required",
- 412 => "HTTP/1.1 412 Precondition Failed",
- 413 => "HTTP/1.1 413 Request Entity Too Large",
- 414 => "HTTP/1.1 414 Request-URI Too Large",
- 415 => "HTTP/1.1 415 Unsupported Media Type",
- 416 => "HTTP/1.1 416 Requested range not satisfiable",
- 417 => "HTTP/1.1 417 Expectation Failed",
- 500 => "HTTP/1.1 500 Internal Server Error",
- 501 => "HTTP/1.1 501 Not Implemented",
- 502 => "HTTP/1.1 502 Bad Gateway",
- 503 => "HTTP/1.1 503 Service Unavailable",
- 504 => "HTTP/1.1 504 Gateway Time-out"
- );
- }
Comments
so you auto redirect the user based on ip? isnt that bad for google crawlers that try to index your site?
So you are assuming a users language based on IP - good for UX?
I hardly leave a response, but i did some searching and wound up here Add new comment | Tigerfish Interactive.
And I actually do have 2 questions for you if you tend not to
mind. Could it be simply me or does it look like a few of these remarks
appear as if they are left by brain dead individuals?
:-P And, if you are posting at other social sites, I'd like to follow everything new you have to post. Would you make a list of the complete urls of your shared pages like your twitter feed, Facebook page or linkedin profile?
Here is my homepage staystrongrelationships.com
Thanks for putting this together. I'm looking to do something almost identical actually. I'm curious as to whether or not you looked into the Smart IP module (http://drupal.org/project/smart_ip/) and if so, why you decided not to use it. The main difference I see is that the Smart IP module doesn't necessarily use the Internationalization module, but I'm curious to hear your thoughts on this.
Nice article Tom, that is a very useful solution.
My only concern which I would look to resolve is the curl call each time to the remote service.
To resolve this you can download from another source monthly/weekly updates to the IP registry (see links below) import them into your DB and handle everything locally.
ftp://ftp.ripe.net/pub/stats/arin/delegated-arin-latest
ftp://ftp.ripe.net/pub/stats/apnic/delegated-apnic-latest
ftp://ftp.ripe.net/pub/stats/lacnic/delegated-lacnic-latest
ftp://ftp.ripe.net/pub/stats/afrinic/delegated-afrinic-latest
ftp://ftp.ripe.net/pub/stats/ripencc/delegated-ripencc-latest
Food for thought, regardless though, good job!
Its nice solution. But please do have a look at http://drupal.org/project/geo_redirect :-)
Great work. Thanks for share this work. Its a very nice to share. Thanks for share this information.










