Managing Gettext Translations on Shared Hosting

Web & App Developer

If you’re working for a big company, chances there are that sooner or later your employers will start to target the global market. With this ambition will come the need to translate the company’s website into one or more languages. Even if you don’t work for a big company, you may have a new service to launch in your native language (assuming you’re not a native English speaker) to target your local market, and in English for the global one. As developers, our role isn’t to translate texts but to prepare the website to support translations. The most popular method to do that in PHP is via Gettext. It’s a great method because it allows to separate translations from the application, enabling the parallelization of the process. The problem with it, is that Apache caches the translations, so unless you can restart the engine, any update to a translation file won’t be seen. This fact is particularly annoying if you work on a shared hosting where you don’t have administrator permissions. In this article, I’ll describe this issue in detail and explain the solution I found to avoid it.

Note: If you aren’t familiar with the concepts of I18N, translations, and Gettext, I strongly encourage you to read this series before exploring this article further. It will provide you with more details than the brief overview you’ll find here, and will help you have a better comprehension of these topics.

Setting up the Environment

There a lot of ways to use translations in PHP. The simplest one I can recall is to have an associative array containing all the translated strings and then use the keys to retrieve the right. As you may guess, this solution doesn’t scale well and should be avoided unless you’re working on a very very small project (perhaps something not longer than 5 lines of code). For serious translations we can use Gettext. This approach enables us to have different files for every targeted language which helps in maintaining separation between the business logic, the presentation layer, and the translations (which we can see as an add-on of the presentation layer). With Gettext, we can parallelize the process because while we’re working on some features of the website, translators can still work on translations using software like Poedit.

Translations should be stored in a path having a fixed structure. First of all, we’ll have a root folder named to your taste (for example “languages”). Inside it, we have to create a folder for every targeted language whose name must comply to the ISO 3166 standard. So, valid names for an Italian translation can be “it_IT” (Italian of Italy), “it_CH” (Italian of Switzerland), “en_US” (English of USA), and so on. Within the folder having the language code, we must have a folder named “LC_MESSAGES” where, finally, we’ll store the translation files.

Poedit, analyzing the source of the website, extracts the strings to translate based on one or more patterns we set in the software. It saves the strings in a single file having .po (Portable Object) as its extention that this software (or one equivalent) will compile into binary .mo file. The latter is the format of interest for us and for the PHP’s gettext() function. The .mo file is the one we must place inside the “LC_MESSAGES” directory we created earlier.

A sample code that uses gettext() is the following (the code is commented to give you a quick grasp of what it does):

<?php
   // The name of the root folder containing the translation files
   $translationsPath = 'languages';
       // The language into which to translate
       $language = 'it_IT';
   // The name of the translation file (referred as domain in gettext)
   $domain = 'audero';

   // Instructs which language will be used for this session
   putenv("LANG=" . $language); 
       setlocale(LC_ALL, $language);

   // Sets the path for the current domain
   bindtextdomain($domain, $translationsPath);
   // Specifies the character encoding
   bind_textdomain_codeset($domain, 'UTF-8');

   // Choose domain
   textdomain($domain);

   // Call the gettext() function (it has an alias called _())
   echo gettext("HELLO_WORLD"); // equivalent to echo _("HELLO_WORLD");
?>

Once you save the previous code in a page and load it in your browser, if gettext() is able to find the translation file, you’ll see the translations you made on the screen.

So far, so good. The bad news is that once a translation is loaded, Apache caches it. Therefore, unless we can restart the engine, any update to a translation file won’t be seen. This is particularly annoying if we work on a shared hosting where we don’t have administrator permissions. How to solve this issue? Audero Shared Gettext to the rescue!

What’s Audero Shared Gettext

Audero Shared Gettext is a PHP library (actually is just a single class, but let me dream) that allows you to bypass the problem of the translations, loaded via the gettext() function, that are cached by Apache. The library employs a simple yet effective trick so that you’ll always have the most up-to-date translation in use. Audero Shared Gettext requires PHP version 5.3 or higher because it uses namespaces, and the presence of the structure described in the previous section. It has two main methods: updateTranslation() and deleteOldTranslations(). The former is the core of the library and the method that implements the trick. But what is this trick? Let’s see its code, to discover more. To fully understand it, it’s worth highlighting that the constructor of the class accepts the path where the translations are stored, the language into which to translate, and the name of the translation file (domain).

/**
 * Create a mirror copy of the translation file
 *
 * @return string The name of the created translation file (referred as domain in gettext)
 *
 * @throws \Exception If the translation's file cannot be found
 */
public function updateTranslation()
{
    if (!self::translationExists()) {
        throw new \Exception('The translation file cannot be found in the given path.');
    }
    $originalTranslationPath = $this->getTranslationPath();
    $lastAccess = filemtime($originalTranslationPath);
    $newTranslationPath = str_replace(self::FILE_EXTENSION, $lastAccess . self::FILE_EXTENSION, $originalTranslationPath);

    if(!file_exists($newTranslationPath)) {
            copy($originalTranslationPath, $newTranslationPath);
    }

    return $this->domain . $lastAccess;
}

The first thing the method does is to test if the original, binary translation file exists (the .mo file). In case it doesn’t exist, the method throws an exception. Then, it calculates the complete path to the translation file based on the parameters given to the constructor, and the timestamp of the last modification of the file. After, it creates a new string concatenating the original domain to the previously calculated timestamp. Once done, and here is the trick, it creates a mirror copy of the translation file. The class is smart enough to avoid this copy if a file with such a name already exists. Fianlly, it returns the new name that we’ll employ in bindtextdomain(), bind_textdomain_codeset(), and textdomain(). Doing so, Apache will see the translation as if it isn’t related to the original one, avoiding the caching problem. As I said, simple but effective!

“Great Aurelio!”, you’re thinking, “but in this way my folders will be bloated by these replications.” Right. That’s why I created deleteOldTranslations(). It removes all the mirror copies but the last from the folder of the chosen translation.

Now that you know what Audero Shared Gettext is and what it can do for you, let’s see how to obtain it.

Installing Audero Shared Gettext

You can obtain “Audero Shared Gettext” via Composer adding the following lines to your
composer.json:

"require": {
    "audero/audero-shared-gettext": "1.0.*"
}

And then run the install command to resolve and download the dependencies:

php composer.phar install

Composer will install the library to your project’s vendor/audero directory.

In case you don’t want to use Composer (you should, really) you can obtain Audero Shared Gettext via Git running the command:

git clone https://github.com/AurelioDeRosa/Audero-Shared-Gettext.git

The last option you have is to visit the repository and download it as an archive.

How to use Audero Shared Gettext

Assuming you obtained Audero Shared Gettext using Composer, you can rely on the its autoloader to dynamically load the class. Then, you have to create an SharedGettext instance and call the method you need. You can use one of the previously cited methods as shown in the following example.

<?php
    // Include the Composer autoloader
    require_once 'vendor/autoload.php';

    $translationsPath = 'languages';
        $language = 'it_IT';
    $domain = 'audero';

    putenv('LC_ALL=' . $language);
        setlocale(LC_ALL, $language);

    try {
       $sharedGettext = new Audero\SharedGettext\SharedGettext($translationsPath, $language, $domain);
       // Create the mirror copy of the translation and return the new domain
       $newDomain = $sharedGettext->updateTranslation();

       // Sets the path for the current domain
       bindtextdomain($newDomain, $translationsPath);
       // Specifies the character encoding
       bind_textdomain_codeset($newDomain, 'UTF-8');

       // Choose domain
       textdomain($newDomain);
        } catch(\Exception $ex) {
       echo $ex->getMessage();
    }
?>

Conclusions

This article has introduced you to Audero Shared Gettext, a simple library (emh…class) that allows you to bypass the problem of the translations, loaded via the gettext() function, cached by Apache. Audero Shared Gettext has a wide compatibility because it requires that you have at least PHP 5.3 (released for a while now) because of its use of namespaces. Feel free to play with the demo and the files included in the repository, to submit Pull Requests and issues if you find them. I’ve released Audero Shared Gettext under the CC BY-NC 4.0 license, so it’s free to use.

Have you ever encountered this issue? How did you solve it? Don’t be shy and post your solutions in the comments!

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • Anthony

    us_US or en_US? :)

    • Aurelio De Rosa

      Ops! You’re right. I’m going to fix the typo.