PHP and Javascript Internationalization using Gettext and Poedit

The term “localization” is used as a synonym of “internationalization”, which is the most proper term. Sometimes, internationalization is shortened to “i18N” (meaning “i – eighteen letters -n”).

gettext is a GNU project for software application resources internationalization. It has been ported to many programming languages. One of them is php. php gettext can be used as php extension or as cgi (here the first case is mentioned).

Terms to be translated inside php source code handled as following:

echo gettext("text to be translated");

or

echo _("text to be translated");

or using custom keyword

echo user_keyword("text to be translated");

Usually, the english version (text to be translated) will be the “key” for the rest translations. If language translation in not exist, the “key” term will be used (text to be translated).

The most important gettext feature is that extracts terms parsing source code files, and updates them in case of modification (without change the already translated terms).

gettext creates the following file types

  • .pot (Portable Object Template) first extracted terms from source code
  • .po (Portable Object) actually, it is the .pot file, but it also includes the translations
  • .mo (Machine Object) this is the compiled version of .po file, which will be used by web server to display the translated terms

.po files can be modified using any text editor. But the most convenient gettext interface is poedit.

Additionally, as there is no web application without javascript, a very interesting issue is the translation of application resources inside javascript code. Several solutions are available (see below).

Install gettext to your system

There are three steps to configure your server or development workstation properly with gettext and php

  1. install the GNU gettext package
  2. install php gettext extension
  3. configure locales for gettext

As I use Archlinux as workstation and Debian on servers, below I give information for both of these distributions. However, there are detailed instructions for setup in any operating system in gettext manual and php website.

Install the GNU gettext package

It’s easy to setup gettext to your system. For example, in a Debian distro

apt-get install gettext

The Archlinux way

pacman -S gettext

Install php gettext extension

In a Debian distro

apt-get install php-gettext

The Archlinux way

nano /etc/php/php.ini

...
extension=gettext.so
...

Configure locales for gettext

This step is very important. If you miss this step, gettext WILL NOT WORK. You have to configure your system locales for each language you are going to use with gettext.

To list available locales in your system, use

locale -a

probably, you will get something like

C
en_US.utf8
POSIX

After installing more locales, the result will be different, for example:

C
de_DE.utf8
el_GR
el_GR.iso88597
el_GR.utf8
en_US
en_US.iso88591
en_US.utf8
greek
POSIX

So, to configure locales, follow these instructions:

In a Debian distro

dpkg-reconfigure locales

Afterwards, just follow the instructions. You have to select locales for each language you are going to use with gettext

The Archlinux way

Uncomment locales you want to use

nano /etc/locale.gen

afterwards

locale-gen

Details here.

Install poedit

poedit is just a gettext interface. Instead of poedit, command line interface could be used. Of course, poedit is more easy.

In a Debian distro

apt-get install poedit

The Archlinux way

pacman -S poedit

A common internationalization example with PHP and gettext

The following php statements are needed to initialize gettext inside a php script

putenv("LC_ALL=$locale");
setlocale(LC_ALL, $locale);
bindtextdomain('your_app_domain', '/path/to/source/files');
textdomain('your_app_domain');

In some systems you have to set

putenv("LANG=$locale");

or

putenv("LANGUAGE=$locale");

Test them in your system.

where $locale is the desired locale, for example: en_US.utf8 for English, de_DE.utf8 for German, el_GR.utf8 for Greek language etc

$locale must be defined at run time, depending your application logic. Usually, it is a session variable (defined at user login). In the following examples, $locale is defined from the URL, just for demo and testing purposes.

textdomain is test (gettext files will be created like test.po, test.mo etc).

Localization files will be created inside i18n folder. German and Greek translation will be created. Files structure is something like

There is a directory i18n/locale/LC_MESSAGES/ for each language. Each language translation files is called a “catalog”.

Default language in source code is English. As THERE IS NOT a folder i18n/en_US/LC_MESSAGES/, if locale is en_US (or any other except el_GR and de_DE), PHP will use application resources in source code files.

The following demo is a simple project, inside folder named “gettext” (the real path in this example is /srv/http/test/gettext/). To test various locales, use

http://localhost/test/gettext/test.php?locale=el_GR.utf8

So, create the test file

nano /srv/http/test/gettext/test.php

with the following contents

<?php
if (isset($_GET["locale"])) {
        $locale = $_GET["locale"];
        putenv("LC_ALL=$locale");
        setlocale(LC_ALL, $locale);
        bindtextdomain("test", "/srv/http/test/gettext/i18n");
        textdomain("test");
}
?>

<html>
<head>
        <meta http-equiv="content-type" content="text/html; charset=utf-8">
</head>

<body>

<?php
echo _("message1");
echo '<br>';
echo _("message2");
echo '<br>';
echo _("message3");
?>

</body>
</html>

Start poedit

Create catalog (.po file) – (German in this exapmle)

Catalog settings

Source code location

Gettext Keywords

Save catalog

Extract terms to be translated

Perform the translation

Then press Save.

Repeat these steps for each language.

In case the catalog (.po file) already exists, use “Open catalog” menu option to open it. Poedit will suggest only modified or added terms. To do it manually, just press ‘UPDATE’ button and Poedit will extract new terms to translate (if any).

The result for each language is:

Locale not given (default is used)

German translation

Greek translation

Parser default preferences

Edit → Preferences → Parsers → PHP → Edit

Pass parameters inside translated text

You may use something like:

echo sprintf(_("Hello, my name is %s and I live in %s"), $name, $city);

See sprintf for syntax details.

Multiple catalogs

A php application (especially a large one) could use multiple catalogs (.po file). The default catalog is defined by textdomain. In case where translations from another catalog have to be used, then dgettext is the solution:

<?php
if (isset($_GET["locale"])) {
        $locale = $_GET["locale"];
        putenv("LC_ALL=$locale");
        setlocale(LC_ALL, $locale);
        bindtextdomain("module1", "/srv/http/test/gettext/i18n");
        bindtextdomain("module2", "/srv/http/test/gettext/i18n");
        textdomain("module1");
}
?>

<html>
<head>
        <meta http-equiv="content-type" content="text/html; charset=utf-8">
</head>

<body>

<?php
echo _("message1");
echo '<br>';
echo _("message2");
echo '<br>';
echo dgettext("module2", "message3");
?>

</body>
</html>

Shared catalog

In case of using multiple catalogs, a shared catalog (to keep common strings all over the application) would be useful to avoid translation duplicates.

The problem in this case is that poedit has to extract only strings of domain shared. So, custom keywords will be used with xgettext. In the following example, keyword _common is used with xgettext, while “_common” is a user function which uses dgettext to do the trick:

<?php
if (isset($_GET["locale"])) {
        $locale = $_GET["locale"];
        putenv("LC_ALL=$locale");
        setlocale(LC_ALL, $locale);
        bindtextdomain("module1", "/srv/http/test/gettext/i18n");
        bindtextdomain("shared", "/srv/http/test/gettext/i18n");
        textdomain("module1");
}

function _common($str) {
        return dgettext('shared', $str);
}
?>

<html>
<head>
        <meta http-equiv="content-type" content="text/html; charset=utf-8">
</head>

<body>

<?php
echo _("message1");
echo '<br>';
echo _("message2");
echo '<br>';
echo _common("shared message 1");
?>

</body>
</html>

The following parameters have to be set (-k –keyword=_common), in order xgettext to extract strings with keyword _common and to ignore default gettext keywords (_, gettext, dgettext).

Edit → Preferences → Parsers → PHP → Edit

Localize javascript code

Using AJAX call

My favorite method is to use an AJAX call to get all required data from PHP

I use jQuery in the following example. So, the javascript code would be

$(function() {
        $.ajax({
                url: 'ajax_get_vars.php',
                type: "POST",
                dataType: 'json',
                success: function(data) {
                        var var1 = data["var1"];

                // your code here

                }
        });
});

and the php ajax call (ajax_get_vars.php ) will be something like this

<?php
$a_vars = array();
$a_vars['var1'] = var1;
echo json_encode($a_vars);
?>

php inside javascript

You may use this code inside main php file

var var1 = "<?php echo gettext("Var1 text"); ?>";
var var2 = "<?php echo gettext("Var2 text"); ?>";
...

If this is an external php file, give it a name with .php extension (e.g. localize.js.php ) and call it as following

<script type="text/javascript" src="localize.js.php"></script>

jsgettext

There is a interesting project, jsgettext. You may want to give it a try.

hidden inputs

You may use hidden inputs in main php file

<input type="hidden" id="localize1" value="<?php echo 'foo' ?>">

and then get the value from javascript. This is an example, using jQuery

var var1 = $("#localize1").val();

Not so safe. Not recommended.

gettext caching issues

After translation modifications (as they cached), a web server restart is needed. A workaround, which will not break connections (in Apache), would be the following

apachectl graceful