-
Multi-Lingual Support with Zend_Translate and PHPTAL
Posted on March 23rd, 2009 4 commentsI recently had to implement multi lingual support in a Zend Framework application to accomodate 4 languages in a social networking site: English, Spanish, Porteguese, and French Canadian. The site was fairly large comprising of a large number of heavily dynamic templates with a lot of user generated content. At first it seemed daunting and there were some worries that a lot of work needed to be done before the translations would be available. I’ll share some of the trials I went through figuring out how to leverage Zend_Translate and the even more annoying process of getting it conveniently usable with PHPTAL.
Translation tends to be a bit intimidating for some developers as it has an air of black magic around it. A lot of this can be blamed on a lack of understanding about how translations are handled. A common misconception was that the software behaved like a universal translator of sorts, the kind you would see in Star Trek. As super cool as that is, unfortunately technology available to us mere mortals has not reached that level of sophistication to a point where it’s practical or accurate to do that. Instead we must rely on predetermined translations on controlled static content provided by hired translators or our limited knowledge from our highschool language courses.
The Zend_Translate module, available with the Zend Framework, gives us a multitude of options for implementing web site translations. It supports adapters for most known translation mechanisms/formats and even some that I had never heard of until I had need for this service (array, csv, ini, gettext, tbx, tmf, qt, etc). The only real difference between these standards is the format in which the translations are stored and as a result how their translation stores are created. Some are binary based, others are code based, while others are xml based. Needless to say some are human readable and others require additional tools to manage so the needs of most developers and their clients should be met by one of these solutions. The details of the individual formats aren’t terribly important for the scope of this article since Zend_Translate provides a normalized interface to all of them. In this article I will demonstrate the use of the time tested gettext adapter.
Gettext relies on the use of .po and .mo files, .po files being a human readable structured text file mapping the translations, and .mo files being their compiled binary target format. The adapter reads the translations from the .po files. Each .po file contains the translations for a different language and is named in a way that allows the adapter to quickly reference it. I prefer to use locale strings to identify the different languages as the adapter requires no modifications to reference them (ie. en_US.po, es_US.po, pt_BR.po, and fr_CA.po). For languages that utilize non-ascii characters, it’s mandatory that the .mo file containing the translations be in UTF format. These files can be created by hand but you can save a lot of time using a utility called Poedit. This program if properly configured can scan your source code directly and identify strings that require translations and automatically generate your .mo/.po files for you. Depending on what language you code in and how you implement gettext mileage will vary on automating this process. I didn’t have much luck at this since I used a convention that was geared towards PHPTAL and unfortunately Poedit could not identify my translations by default. I am certain with enough tinkering I can find a devise a pattern that the tool can use but time is valuable so perhaps someone else can contribute their suggestions. I had to create my .po files by hand.
Here is an excerpt from my fr_CA.po:
msgid “”
msgstr “”
“Project-Id-Version: Super Duper Social Networking Site”
“POT-Creation-Date: ”
“PO-Revision-Date: 2009-02-26 15:02-0800″
“Last-Translator: Brandon <______@____.com>”
“Language-Team: eROI <_____@___.com>”
“MIME-Version: 1.0″
“Content-Type: text/plain; charset=utf-8″
“Content-Transfer-Encoding: 8bit”
“X-Poedit-Language: French”
“X-Poedit-Country: CANADA”msgid “Favorite this artist”
msgstr “Cet artiste dans mes Favoris”msgid “Join this group”
msgstr “Joindre ce groupe”msgid “Message the group”
msgstr “Envoyer un message au groupe”msgid “Post a bulletin”
msgstr “Afficher un bulletin”msgid “Post a new bulletin”
msgstr “Afficher un nouveau bulletin”Regardless of how this file is created, it must be compiled by Poedit into the .mo file . The binary file is then placed in a folder where the Zend_Translate adapter can find it. My directory structure is as follows:
- /app
- /config
- /lang
- en_US.po
- es_US.po
- fr_CA.po
- pt_BR.po
- /lib
- /log
- /plugins
- /public
- /tmp
Typically I handle application wide locale in one of my bootstrap subsystem files located in the lib folder. I have one such file called locale.php which looks something like this:
-
-
<?php
-
-
//Call this from any where in app to change locale
-
function set_application_locale($pLocale, $pSave=true){
-
$config = Zend_Registry::get(‘config’);
-
$locale_config = $config->locale;
-
$locale = new Zend_Locale($pLocale);
-
Zend_Locale::setDefault($pLocale);
-
Zend_Registry::set(‘locale’, $locale);
-
-
//Initialize translater, set to scan lang/ folder for .mo files
-
$translater = new Zend_Translate(‘gettext’, BASE_DIR . ‘lang’, $locale_config->default, array(’scan’ => Zend_Translate::LOCALE_FILENAME));
-
$translater->setLocale($pLocale);
-
Zend_Registry::set(‘Zend_Translate’, $translater);
-
-
if ($pSave)
-
save_application_locale($pLocale);
-
-
return $locale;
-
}
-
-
function save_application_locale($pLocale){
-
}
-
-
$config = Zend_Registry::get(‘config’);
-
-
//Turn off user notices for zend 1.7
-
Zend_Locale::$compatibilityMode = false;
-
-
$locale_config = $config->locale;
-
-
//Restore Locale if present in cookie otherwise set default from config.xml
-
$locale = set_application_locale($_COOKIE[‘locale’], false);
-
else
-
$locale = set_application_locale($locale_config->default);
-
-
date_default_timezone_set($config->locale->timezone);
-
-
?>
-
This file checks for a cookie named ‘locale’ for a locale string or a config xml file for the default value. Once it has this value we are able to set some other locale settings and instantiate our default translater. In the constructor for our Zend_Translate object we specify we would like to use the gettext adapter and some options that assist the adapter in locating the necessary .po files. The second parameter tells it to search for the .po files in the lang folder, and the scan parameter tells it to identify the .po files automatically by filename. By setting the Zend_Registry variable ‘Zend_Translate’, the translate() view helper used later can automatically retrieve the translater and use it. An added bonus here from creating a set_application_locale function is that we can switch languages on the fly from some facility such as a select box or link.
Next you must get familiar with the translate() view helper which is used in the template to define placeholders for strings that need to be translated. Using phtml, a demonstration of it’s use would look like this:
It’s important to note that the string we pass into this helper is not necessarily the string that will appear in the rendered html. It will be if no translation is available, however what is supposed to happen is the string is nothing more than an identifier that is mapped to a real translated string in the .po file. If you reference the example .mo file above, you will use the strings identified in the msgid rows when calling the helper. Again these can be a verbose human readable string in your (the developer) native language (‘Go Home’) or it can be a more cryptic identifier (‘header_menu_home_label’). As long as it matches the msgid identifier it doesn’t matter. Each .po file will contain this mapping with a different translation of the string.
So moving on, we’re not using phtml, we are using PHPTAL. If you are not familiar with how to implement PHPTAL with the Zend_View, refer to my older post on this subject (I am aware the examples are currently broken, I will fix them when time permits). With the stock implementation we can use the tal:content attribute with the php expression syntax like such:
-
-
-
<h1 tal:content="php:this.translate(‘Go Home’)"/>
-
-
Pretty clunky… there’s got to be a better way. There is fortunately a much more ‘TAL’ way of doing it. PHPTAL allows you to define your own expression prefixes that allow for some neat customization. Wouldn’t you rather do it this way instead:
-
-
-
<h1 tal:content="translate:Go Hom"/>
-
-
All we have to do to make this happen is define a TALES function somewhere in the bootstrap that is available in the templates:
-
-
-
function phptal_tales_translate($pSrc, $pNoThrow){
-
return ‘$ctx->this->translate(‘.PHPTAL_TalesInternal::string($src, $pNoThrow).‘)’;
-
}
-
-
I define this in a file called PHPTAL_custom.php which is required automatically by my PHPTAL view plugin class.
Now when we produce our html we can sprinkle this attributes throughout the templates on tags that will require translations such as p tags, h tags, or anything you want. Without the translations ahead of time these modified tags simply serve as placeholders that will show whatever string you pass in by default. When the translations become available, they will be diligently swapped out with the real thing when the locale is set. The only downside to this approach as I mentioned above is that I have not found a pattern that Poedit can use to spot these placeholders but it’s just a minor detail.
That’s all there is to it.
4 responses to “Multi-Lingual Support with Zend_Translate and PHPTAL”
-
Just to be clear, this isn’t related at all to CMSMS2.0 silk framework, is it?
http://blog.cmsmadesimple.org/2008/12/31/announcing-the-silk-framework/
-
Funny, my company used to use cmsms quite frequently and I was not aware that there was a sequel being released for it. In fact I suspected that development had ceased altogether. My experience with it left a lot to be desired when writing custom modules for it. I find it disappointing that such a lackluster product is using the same name for his framework, so to answer your question, no, they are not related in any way.
-
Why didn’t you use PHPTAL’s first-class support for translation? You could use PHPTAL->setTranslator() to use Zends system automatically and transparently within PHPTAL, without need to create custom modifier.
-
Great tip. I had mostly written off the i18n support in PHPTAL because i was focusing on template language agnostic translation but it seems you can have your cake and eat it too. Thanks for the info.
Leave a reply


