«Back to blog home

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

The content of this field is kept private and will not be shown publicly.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Allowed HTML tags: <a> <em> <strong> <cite> <code> <ul> <ol> <li> <dl> <dt> <dd>
  • Lines and paragraphs break automatically.

More information about formatting options

rob Zienert

I was blessed with being born into a tech-savvy family: my dad being a vehicle engineer at Ford and my mom a database programmer — it only made sense that I put my genetic inheritance to good use, so here I am. Whether it be application architecture, code reviews, or any flavor of voodoo-techno-whizbang; its my job to make sure your application or web site functions technically.

Outside of the office, I have the pleasure of serving on the Program Advisory Committee for Full Sail University’s Web Design and Development Bachelor Program. But alas, not all I do is work, oh-no. I’m super passionate about fast, sexy cars (and bikes), painting, coffee, watching the History channel, and playing paintball. I also play a lot of Modern Warfare 2 on XBox (Gamertag: Pievendor).

Oh! How could I forget: My nickname around town is RZA, like the rapper, because I’m so convincingly street.

my Favorites

Paintball

Used to fly around the nation to shoot people. My couple jobs in web was for the paintball industry, too.

Painting

I might be a programmer by day, but I love digital painting--even if its always sad and emotive.

Radiohead

Constantly ridiculed by my fellow co-workers for how much I love Radiohead... but no one can touch them.

my Last·fm

  • The Fiancee
  • Fantasies
  • Alpinisms
  • Rock Action

my Flickr

  • BESTER FRIENDS!!!
  • BEST FRIENDS!!!
  • exquisitely quisite
  • exquisitely quisite