Thursday, March 30, 2006

Simple multi-language web site

There are many ways to direct users of a multi lingual web site to the pages
in their language. But the quick search I made didn't return solutions which
I liked, so I did my own. I'm sure the same method has been used many times,
but it's sometimes faster to re-invent a little wheel than to go through zillions
of Google search results.

I had to work on a small site with about 20 pages, in which every language
is in it's own subdirectory (www.example.com/fr/, www.example.com/en/, etc.)
and the file names remain the same.

This simple structure made it pretty easy, and the solution makes it trivial
to add another language.

(For a lot of background about languages and a different solution involving the standard "content language negotiation", see Dan's Web Tips: Languages).

The way I choose works like this:

  • One line in every page calls a PHP script to display the language menu.
  • A cookie holds the visitor's preferred language.
  • Javascript updates the cookie when the visitor switches languages.
  • The main index page redirects visitors to the correct language, using:
    1. either the cookie set in a previous visit,
    2. or the browsers preferred language,
    3. or a pre-configured default language.
  • Finally, a few CSS rules take care of the menu's look

The redirector script

I used a little Perl script on www.example.com/index.cgi. It first looks for
a cookie with the user's preferred language. If there is no cookie (usually
because this is a first-time visit), it uses the HTTP_ACCEPT_LANGUAGE
header sent by the browser to find the best guess. If that doesn't work either,
it uses a default language. Finally, it redirects the user to the selected language,
while also setting the cookie for the next visit.

I probably could have done the index page in PHP, but I didn't know PHP at
all when I started so it was easier for me in Perl. (If someone feels like sharing
a PHP translation, you are welcome to post it here as a comment).

#!/usr/bin/perl

# Redirect to correct language version of the site.
# Use cookie if present. Otherwise, parse the HTTP_ACCEPT_LANGUAGE header

# Author: Milivoj Ivkovic "mi\x40alma.ch"

use CGI qw(:cgi);
my $VERSION = 0.3;

my %available_languages = (
fr => 'fr/',
de => 'de/',
en => 'en/',
default => 'fr/',
);
my $wanted_language = 'default';

my $q = new CGI;

# try cookie
my $cookie_lang = $q->cookie("lang");
if ( exists $available_languages{$cookie_lang} ) {
$wanted_language = $cookie_lang;
}
else { # try HTTP_ACCEPT_LANGUAGE
# Get languages from HTTP_ACCEPT_LANGUAGE, (ignore local variants like en_US, en_GB, etc.).
# Don't bother removing duplicates
my @langs = map { substr($_, 0, 2) } split(/,/, $ENV{HTTP_ACCEPT_LANGUAGE});
foreach my $l (@langs) {
if (exists $available_languages{$l}) {
$wanted_language = $l;
last;
}
}
}

# if nothing worked, we still have the default in $wanted_language which was set at the beginning

# fix URL
my $url = $q->url(-path_info=>1);

# (some servers give us a url ending with 2 "/", so we need "/+" in the
# regex below instead of just "/".
# Otherwise we may end up with "http://example.com//en/" which doesn't look nice.)
$url =~ s|/+[^/]*$|/$available_languages{$wanted_language}|;

# redirect, and also set the cookie for next time
print $q->redirect ( -uri => $url,
-cookie => cookie( -name=>"lang",
-value=>$wanted_language,
-path=>"/",
-expires=>"+1y",
),
);

The javascript setLang function

Anytime the user switches the language, this small javascript function is called
to update a cookie, so the preference is saved between sessions.

function setLang(l) {
// Called from links which switch the language.
// Sets cookie with "lang=fr" or other language code, valid for 1 year, on all the site
var expire = new Date();
expire.setTime(expire.getTime() + 3600000*24*365);
document.cookie = 'lang='+l+";expires="+expire.toGMTString()+";path=/";
}

The language menu

Since all the pages happened to be in PHP, I did the language menu in PHP,
and added an include() to every page.

<?php include("../common/lang-selector.php") ?>

The PHP lang-selector.php script

This is the PHP script which displays the language menu.

<!-- php lang selector -->
<div id="lang-selector">
<?php
$langs = array(
"fr" => "Français",
"de" => "Deutsch",
"en" => "English",
);
$self=$_SERVER['PHP_SELF'];
$pattern = "{^/[^\/]+/}i";
$links = array();
foreach ($langs as $l => $lang) {
$is_current = ! ( strpos($self, "/$l/") === false );
$url = preg_replace($pattern, "/$l/", $self);
$link = "<a href=\"$url\" class=\"lang-"
. ($is_current ? "current\">" : "other\" onclick=\"setLang('$l')\">")
. " $lang </a> ";
array_push($links, $link);
}
echo " " . implode(" | ", $links) . "\n";
?>
</div>
<!-- end php lang selector -->

The finishing touch: CSS

The look of the language menu is done with CSS. This is what I used for that
particular site:

#lang-selector {
font-size: x-small;
text-align: left;
white-space: nowrap;
padding-bottom: 1em;
color: #CCCCCC;
}

a.lang-current,
a.lang-current:visited,
a.lang-current:hover
{
color: #CCCCCC;
text-decoration: underline;
}

a.lang-other,
a.lang-other:visited
{
color: #333333;
text-decoration: none;
}

a.lang-other:hover
{
text-decoration: underline;
background: #F7EBDB;
}

@media print {
#lang-selector {
display: none;
}
}

By the way, the site for which this was first set up is http://lesouffledudesert.com/.

When we added English to the site, the steps were:

  • Copy the /fr directory to a new /en directory
  • Add "en" => "English", to lang-selector.php
  • Add en => 'en/', to index.cgi
  • And of course, translate all pages, which was the real work and where
    this little multi-language system doesn't help...

Labels: , , , , ,

13 Comments:

Anonymous Anonymous said...

Hi Again,

I was wondering if this could be made to work for a structure with multiple levels of subdirectory.

Figured it out, here's how:

File the line:
$pattern = "{^.*/}i";

Now delete the asterix, so it looks like:
$pattern = "{^./}i";

Voila, now it uses the entire source URL and replaces the the string you've set up.

Cheers,
John

05 June, 2007 19:22  
Anonymous Anonymous said...

Indeed, my original regex didn't work with additional subdirectories. Thanks "jnesb2121" for pointing it out.

In the PHP lang-selector.php script, I replaced the line

  $pattern = "{^.*/}i";

with

  $pattern = "{^/[^\/]+/}i";

and the line

  $url = preg_replace($pattern, "../$l/", $self);

with

  $url = preg_replace($pattern, "/$l/", $self);

The links are now absolute instead of relative.

08 June, 2007 10:29  
Anonymous Anonymous said...

Do you have a zip file with a default working little sample?

I am not good with PHP and a very small sample to see your structure will help greatly.

Thank you.

04 June, 2008 04:41  
Anonymous Anonymous said...

Hi
Very nice implementation.
What would you change in php lang selector if all php pages were in one directory and only the name changes. (example).
index.php
indexFR.php
indexDE.php

It would help me very much if you would answer, as I'm not so good in php.
Best Regards
George

05 December, 2008 12:39  
Anonymous Anonymous said...

BUMP...

Would like to use this, but with indexEN, indexFR and so on were in the root folder.

Thanks!

11 December, 2008 12:51  
Anonymous Anonymous said...

index.cgi doesn't work on my ISP - cgi scrpts only work in cgi-bin. Is there a redirect page in php or html that will refer to the script.cgi in the bin?

15 December, 2008 16:01  
Anonymous Anonymous said...

Worked it out!
1) Save the redirector script as redirect.cgi and save in the cgi-bin.
2) Change:
my %available_languages = (
fr => 'fr/',
de => 'de/',
en => 'en/',
default => 'fr/',
TO:
my %available_languages = (
fr => '../fr/',
de => '../de/',
en => '../en/',
default => '../en/',
3)Make a new file in notepad called index.php. The only content of this file should be:
Removed by program - it is a straightforward redirect one line script. Email me for it.
Upload and Voila! It works!
Very many thankks for the original brilliant script.

15 December, 2008 19:33  
Anonymous Anonymous said...

very nice script indeed!, but where does the setlang function go? on each page? What triggers the setlang javascript function?

03 January, 2009 22:08  
Anonymous Anonymous said...

Nice work, simply brilliant script.

how do you display flag gifs instead of text. Example:

$langs = array(
"fr" => "fr.gif",

Thanks in advance

03 January, 2009 22:58  
Anonymous Anonymous said...

I tried"openbracketimg src="images/flag_france.jpg" alt="France Flag" border="0" /closebracket" instead of "French" but it didn't work. It stopped the page executing!

10 January, 2009 13:32  
Anonymous Anonymous said...

I tried"openbracketimg src="images/flag_france.jpg" alt="France Flag" border="0" /closebracket" instead of "French" but it didn't work. It stopped the page executing!

10 January, 2009 13:33  
Anonymous Anonymous said...

Cracked it! You have to change the double quotes " for single quotes '
e.g:
"nl" => "openbracketimg src='../images/flag_netherlands_sm.jpg' alt='France Flag' /closebracket",

The word 'bracket' stands for a < or >

10 January, 2009 18:04  
Anonymous Anonymous said...

Please can anyone tell me how to lay out the $langs = array( as a table? That is with say three rows or three countries across. Tis would be great if using flags. Thanks!

10 January, 2009 18:17  

Post a Comment

<< Home