Internationalizing Your ZF Application
Today has another pretty interesting blog post lined up. Coincidentally enough, it goes perfectly with my blog post from about two months ago on Internationalizing websites. While that was a more topical post and not so much on actual implementation, I thought I'd make one with specifics regarding Zend Framework. I managed to catch a glimpse of some chat in #zftalk today regarding how to get a URL like http://example.com/en/module/controller/action/params to work.
Apologies in advance about the html entities in the code. I think the syntax highlighter we use should be re-evaluated. :(
Well, fancy that, PRPL released a website with that functionality not long ago on Hospice of the Comforter using Zend Framework and Oakland (my personal library). I feel like this method could be enhanced, but since it already works in my personal library, I may as well use it!
We'll start off with routing. It was mentioned to possibly use Chain Routes, but for the sake of simplicity, I just have chosen to use a simple Rewrite, as seen below. Take note that routes aren't even necessary to get this done. If you want the lang to just hang out in the params, that's cool, too.
default.route = "/:lang/:module/:controller/:action/*" default.defaults.lang = "en" default.defaults.module = "default" default.defaults.controller = "index" default.defaults.action = "index"
It's a very simple route, in fact, it's the same as the typical modular default route, just with a lang param tacked on the beginning. In future projects, I don't know if I would've appended the language specifically to the beginning of the route for each URL. It seems to me that it'd be better served as an additional param and then stored in user session from there on or always put on the end. I have no strong opinion about this, so it's really up to your preference.
The next part is getting the application to automatically handle setting up your Zend_Locale and Zend_Translate objects. Since we don't know what the param is until after routeShutdown, we need a Controller Plugin. The Plugin is setup to already have Zend_Translate and Zend_Locale already in Zend_Registry, so since Zend_Locale already has a Zend_Application Resource created for it, I just made one for Zend_Translate, like so:
class Oakland_Application_Resource_Translate extends Zend_Application_Resource_ResourceAbstract
{
const DEFAULT_REGISTRY_KEY = 'Zend_Translate';
/**
* @var Zend_Translate
*/
protected $_translate;
/**
* Defined by Zend_Application_Resource_Resource
*
* @return Zend_Translate
*/
public function init()
{
return $this->getTranslate();
}
/**
* Retrieve translate object
*
* @return Zend_Translate
*/
public function getTranslate()
{
if (null === $this->_translate) {
$options = $this->getOptions();
if (!isset($options['data'])) {
throw new Zend_Application_Resource_Exception('No translation source data provided.');
}
if (Zend_Registry::isRegistered('translateCache')) {
Zend_Translate::setCache(Zend_Registry::get('translateCache'));
}
$adapter = isset($options['adapter']) ? $options['adapter'] : Zend_Translate::AN_ARRAY;
$locale = isset($options['locale']) ? $options['locale'] : null;
$translateOptions = isset($options['options']) ? $options['options'] : array();
$this->_translate = new Zend_Translate(
$adapter, $options['data'], $locale, $translateOptions
);
$key = (isset($options['registry_key']) && !is_numeric($options['registry_key']))
? $options['registry_key']
: self::DEFAULT_REGISTRY_KEY;
Zend_Registry::set($key, $this->_translate);
Zend_Form::setDefaultTranslator($this->_translate);
}
return $this->_translate;
}
}
If you're at all familiar with Zend_Application_Resource_Locale, you'll see a lot of similarities: I actually copied it because it looked so nice. Both of these resources set the objects into Zend_Registry with their key names as "Zend_Translate" and "Zend_Locale": Easy. The only thing that may require modification if you want caching is the Registry option for the translation cache; which I created another (irrelevant for this post) Application Resource to setup multiple cache objects.
Now we've got everything setup for our Controller Plugin I mentioned earlier. I named it Oakland_Controller_Plugin_Locale and originally I got the idea from another source, but I'm unable to find where I referenced unfortunately. Regardless, here it is:
class Oakland_Controller_Plugin_Locale extends Zend_Controller_Plugin_Abstract
{
/**
* Sets the application locale and translation based on the lang param, if
* one is not provided it defaults to english
*
* @todo Allow default locale to be set by the application config
*
* @param Zend_Controller_Request_Abstract $request
*/
public function routeShutdown(Zend_Controller_Request_Abstract $request)
{
$registry = Zend_Registry::getInstance();
$locale = $registry->get('Zend_Locale');
$translate = $registry->get('Zend_Translate');
// Find the lang param. If not set, assign false
$params = $this->getRequest()->getParams();
$localeParam = isset($params['lang']) ? $params['lang'] : false;
// If the lang param is false, we'll get whatever the default language is
if (false === $localeParam) {
$localeParam = $locale->getLanguage();
}
// As extra precaution, check if a language translation is available.
// If not, then assign the application default. It really should instead
// pull from the application.ini for a default language translation instead.
if (!$translate->isAvailable($localeParam)) {
$localeParam = 'en';
}
$locale->setLocale($localeParam);
$translate->setLocale($locale);
Zend_Form::setDefaultTranslator($translate);
setcookie('lang', $locale->getLanguage(), null, '/');
}
}
It's fairly self explanatory, but it checks for the lang param we setup in the route back at the beginning of the post. If it doesn't exist we assign it to false and can then get the system default language. The second conditional checks to verify whatever language it has been provided exists. I set it up to switch to english instead of throwing an exception so that if a visitor to the site accidentally is linked an invalid URL, it can still provide a seamless user experience. Sure, they may have to select a different language, but at least they'll be on the proper page, ready to switch languages without having to navigate back.
After that, the Zend_Locale object is setup and assigned to the Zend_Translate object. After that you're onto smooth sailing with a param-driven i18n website. If you have any ideas on how to make this better, I'm certainly all ears!








Post new comment