Workaround for gettext caching issue in PHP

gettext is the GNU internationalization and localization (i18n) library, which can be used in almost any programming language. And, of course, it can be used under PHP too, to internationalize your web applications.

I will not go into gettext usage details, there are enough resources available for that.

Official PHP gettext documentation:
http://www.php.net/manual/en/book.gettext.php
A nice “gettext introduction” and well-done article:
http://mel.melaxis.com/devblog/2005/08/06/localizing-php-web-sites-using-gettext/
Some benchmarks:
http://mel.melaxis.com/devblog/2006/04/10/benchmarking-php-localization-is-gettext-fast-enough/

And now let’s get back to what I actually wanted to talk about. It’s a well known problem that new or updated translated strings don’t appear immediately on your PHP page. Or to say the truth – they don’t come up at all. This happens only if you are running PHP as a module (mod_php, …) inside your webserver (Apache, etc), it does not affect php scripts run as CGI.

Why does it happen? The short answer is – because of caching.
The first time you are initializing a text domain (loading a compiled binary .MO file), the file is loaded and cached in memory. Performance-wise, this is a very good thing, the translation string lookups run very fast and not needing to check the filesystem again and again helps a lot with the speed.
The disadvantage is that if you update the .MO file or even delete it, gettext will not catch the change, it would still run with the file from the cache. Very irritating, especially in a development environment.

The recommended and most well-known solution is to restart the web server – this will restart the PHP module with its gettext extension and the cache will be cleared. Unfortunately restarting the web server is not always something that can be done easily, or we might not even have the privileges to do it if we’re on a shared hosting.

I came up with a workaround, that does not require fiddling at all with the web server, it’s all done through PHP. Basically, what we do, is make sure that we are always using the last updated .MO file by using copies of it. We check for modifications by checking the file modification time using filemtime and then we make sure that’s the one we’re using by binding to a unique text domain and a filename computed from the file modification time. If that file exists (from a previous run), then that’s all. But if it doesn’t exists, then create it by copying the original .MO file. So, what we are actually doing is activating a new unique translation domain for each and every time the .MO file is modified.

What you need to consider:

  • you need write permissions in the locale folder
  • if you update the .MO file many time, then a lot of garbage temporary .MO files are generated – you need to remember to clear the folder once a month or something like that
  • is meant to be used in a development environment only, the filesystem checks are not benchmarked (even though PHP filestat caching may help here) and may be too expensive on a production server.

Finally, the example code is:
{code type=php}
// settings you may want to change
$locale = “en_US”; // the locale you want
$locales_root = “locales”; // locales directory
$domain = “default”; // the domain you’re using, this is the .PO/.MO file name without the extension

// activate the locale setting
setlocale(LC_ALL, $locale);
setlocale(LC_TIME, $locale);
putenv(“LANG=$locale”);

// path to the .MO file that we should monitor
$filename = “$locales_root/$locale/LC_MESSAGES/$domain.mo”;
$mtime = filemtime($filename); // check its modification time
// our new unique .MO file
$filename_new = “$locales_root/$locale/LC_MESSAGES/{$domain}_{$mtime}.mo”;

if (!file_exists($filename_new)) { // check if we have created it before
// if not, create it now, by copying the original
copy($filename,$filename_new);
}
// compute the new domain name
$domain_new = “{$domain}_{$mtime}”;
// bind it
bindtextdomain($domain_new,$locales_root);
// then activate it
textdomain($domain_new);

// all done
{/code}

14 thoughts on “Workaround for gettext caching issue in PHP

  1. Interesting workaround. I started using it at my local development machine, works nice so far. But i have to note, that there is a small mistake in your code. You can’t use
    $domain_new = “$domain-$mtime”;
    , since it doesn’t work because of the minus, don’t know why exactly (i use windows on my local development machine, maybe it works on Linux, dunno). I’ve replaced it with:
    $domain = “{$domain}_{$mtime}”;
    , and now it works. Also, $filename_new has to be accordingly, to this:
    $filename_new = “$path/$locale/LC_MESSAGES/{$domain}_{$mtime}.mo”;

  2. Hello! Very helpful thanks! 🙂

    In addition you could use this if statement:

    if (!file_exists($filename_new)) {
    $dir = scandir(dirname($filename));
    foreach ($dir as $file) {
    if (in_array($file, array(‘.’,’..’, “{$domain}.po”, “{$domain}.mo”))) continue;
    unlink(dirname($filename).DIRECTORY_SEPARATOR.$file);
    }
    copy($filename, $filename_new);
    }

    It will remove the old generated files 😉

  3. Nice script
    Here is Update to clear old files by it self.
    You will need to create new file in level with the po file , call it “oldfile.txt”.

  4. // settings you may want to change
    $locale = “de_DE”; // the locale you want

    if (isSet($_GET[“locale”])) $locale = $_GET[“locale”];

    $locales_root = “locale”; // locales directory
    $domain = “messages”; // the domain you’re using, this is the .PO/.MO file name without the extension

    // activate the locale setting
    setlocale(LC_ALL, $locale);
    setlocale(LC_TIME, $locale);
    putenv(“LC_ALL=$locale”);
    // path to the .MO file that we should monitor
    $filename = “$locales_root/$locale/LC_MESSAGES/$domain.mo”;
    $mtime = filemtime($filename); // check its modification time
    // our new unique .MO file
    $filename_new = “$locales_root/$locale/LC_MESSAGES/{$domain}_{$mtime}.mo”;
    $filestrlen =strlen($filename_new); //needed to read the file correctly

    if (!file_exists($filename_new)) { // check if we have created it before

    //check of old file exist
    $myFile = “$locales_root/$locale/LC_MESSAGES/oldfile.txt”;
    $fh = fopen($myFile, ‘r’);
    $theData = fread($fh, $filestrlen);
    fclose($fh);
    if(($theData!=””)&&($theData!=”$filename_new”)) //check if we have old file
    {
    //delete old file
    unlink(“$theData”);

    //Clear text file (oldfile.txt)
    $myFile = “$locales_root/$locale/LC_MESSAGES/oldfile.txt”;
    $fh = fopen($myFile, ‘w’) or die(“can’t open file”);
    $stringData = “”;
    fwrite($fh, $stringData);
    fclose($fh);
    }
    // // check if we have created it before if not, create it now, by copying the original
    copy($filename,$filename_new);

    //store as listed befor (old for next time to delete)
    //update file
    $myFile = “$locales_root/$locale/LC_MESSAGES/oldfile.txt”;
    $fh = fopen($myFile, ‘w’) or die(“can’t open file”);
    $stringData = “$filename_new”;
    fwrite($fh, $stringData);
    fclose($fh);
    //
    //
    }
    // compute the new domain name
    $domain_new = “{$domain}_{$mtime}”;
    // bind it
    bindtextdomain(“$domain_new”, “./$locales_root”);
    // then activate it
    textdomain(“$domain_new”);
    // all done

  5. Woah! I’m really digging the template/theme of this site.
    It’s simple, yet effective. A lot of times it’s hard
    to get that “perfect balance” between user friendliness and visual appearance.

    I must say that you’ve done a fantastic job with this.
    In addition, the blog loads extremely quick for me on Chrome.
    Excellent Blog!

  6. This solution is very interesting. I think it can be put in some class and automate domain change, to avoid garbage storage I suggest a modification to panosru code, because he was erasing all files in folder (even .po).

    if (!file_exists($filename_new)) { // check if we have created it before
    // if not, create it now, by copying the original
    $dir = glob(dirname($filename).’/’.$domain.’_*’); // we get all (1) copies from same domain
    foreach ($dir as $file) {
    unlink($file); // empty trash
    }
    copy($filename,$filename_new);
    }

    We can now store many .mo and .po in same language folder. In my code I put with two “_” to avoid possible confusions (if someone call their domain with _)

  7. That is no “solution”. You try to fix a problem (caching in ext-gettext) by creating another one (flooding PHP’s internal cache with no longer used domains).

    One is to restart apache2 (if mpm-prefork) or corresponding process (e.g. php-fpm). But that surely is not an option for development. Still also not flooding PHP’s internal cache.

    Any ideas would be warmly welcome.

Leave a Reply

Your email address will not be published. Required fields are marked *