Zend Framework and Translation

As multi-lingual web sites become more and more important, I
would like to show two possible ways how to translate static text snippets in
your application with Zend Framework. Zend Framework provides us with several
packages like Zend_Locale and Zend_Translate to make developer’s life easier –
but how to put these components together?

Creating Zend Framework Application

First of all we need a Zend Framework application. You can
either use your own existing one or create a new one directly with Zend Studio
for Eclipse:

id="Grafik 0" src="/images/articles/4513/image001.png" alt="capture_03262009_155933.gif">

If you don’t know how to setup a Zend Framework application
you can have a look at the href="http://framework.zend.com/docs/quickstart">Zend
Framework Quickstart
.

Zend_Locale and Zend_Translate

As mentioned before, we need instances of Zend_Locale and
Zend_Translate. I initialized both objects in the Initializer class which is
created by the Zend Framework project wizard:

/**
 * Initialize Locale and Translation
 *
 * @return void
 */
public function initLocale() {
    $localeValue = 'en';

    $locale = new Zend_Locale($localeValue);
    Zend_Registry::set('Zend_Locale', $locale);

    $translationFile = $this->_root . DIRECTORY_SEPARATOR . 'lang' . DIRECTORY_SEPARATOR . $localeValue . '.inc.php';

    $translate = new Zend_Translate('array', $translationFile, $localeValue);
    Zend_Registry::set('Zend_Translate', $translate);
}

The Initializer::initLocale() method is called by the
Initializer::_routeStartup() method. Of course you can implement this
functionality in a similar way also in the bootstrap file.

I’ve chosen the very easy way of initializing a Zend_Locale
object for this demo: I set the variable $localeValue directly in the method.
Of course this is not recommended! Maybe you can get the current user locale
from the session or let Zend_Locale choose itself ( href="http://framework.zend.com/manual/en/zend.locale.html#zend.locale.selection"> lang=EN-US>Selecting the Right Locale). Once you have the Zend_Loclale
object, you can provide the complete application with it by putting into the
Zend_Registry with key ‘Zend_Locale’. So it’s also possible for several ZF
components to find it there. In the next step we create the necessary
components for translating. Therefore we use Zend_Translate and the Array
Adapter. That means that we have to define a php file which returns a
translation array.

height=186 id="Grafik 1" src="/images/articles/4513/image003.png" alt=lang.gif>

This is the fasted way to create a working translation
mechanism. More about the Translation Adapters can be found here: lang=DE> href="http://framework.zend.com/manual/en/zend.translate.adapter.html"> lang=EN-US>Adapters for Zend_Translate.

The en.inc.php file looks like this:

<?php
return array(
	'Berlin' => 'Berlin',
	'Hamburg' => 'Hamburg',
	'München' => 'Munich',
	'Köln' => 'Cologne',
	'Stuttgart' => 'Stuttgart',
	'Hauptstadt' => 'capital',
	'Hafen' => 'harbor',
// ...

The array key is the translation key which is used in our
view template and the value is the translation.

Back to our Initializer::initLocale() method, we create the
Zend_Translate object with the adapter name, the translation file, and the
locale and put this object analog to the Zend_Locale object into the
Zend_Registry. For example Zend_Form will now use this information for
translating the Zend_Form_Element labels automatically.

This is nearly everything we have to prepare before we can
use translation in our view script.

Zend_View_Helper_Translate

Because we have put the Zend_Translate object into the
registry, we can now use in our view script the Translate View Helper. Let’s
have a look on a phtml example:

<?php
$this->headTitle('Translate with Filter');
$this->placeholder('title')->set($this->translate('Das höchste Gut und Uebel'));
?>

<h2>Cicero</h2>
<small><?= $this->translate('Absatz') ?> 1.10.32 - 1.10.33</small>
<p>
<?= $this->translate('Damit Ihr indess erkennt, woher dieser ganze Irrthum gekommen ist, und weshalb man die Lust anklagt und den Schmerz lobet, so will ich Euch Alles eröffnen und auseinander setzen, was jener Begründer der Wahrheit und gleichsam Baumeister des glücklichen Lebens selbst darüber gesagt hat.') ?>
<!-- … -->

You can see, that every single static text snippet is used
as a parameter in the translate method of the Zend_View object. A Zend_View_Helper_Translate
instance will be created by the view object automatically. It will use the
Zend_Translate object from the registry, translate the param string (string
must be a key of the translation array) and returns the translated string, which
is then echoed in the view script. Very easy, isn’t it?

<i18n> Approach

Maybe you now think: “Uhh, there is a lot of php code in the
view template”. Then we do have the same opinion. What do you think about this
template:

<?php
$this->headTitle('Translate with Filter');
$this->placeholder('title')->set('<i18n>Das höchste Gut und Uebel</i18n>');
?>

<h2>Cicero</h2>
<small><i18n>Absatz</i18n> 1.10.32 - 1.10.33</small>
<p>
<i18n>Damit Ihr indess erkennt, woher dieser ganze Irrthum gekommen ist, und weshalb man die Lust anklagt und den Schmerz lobet, so will ich Euch Alles eröffnen und auseinander setzen, was jener Begründer der Wahrheit und gleichsam Baumeister des glücklichen Lebens selbst darüber gesagt hat.</i18n>

I included every static text snippet in <i18n> tags.
In my opinion it’s more easy to read and it has more in common with HTML tags (maybe
someone can tell me how to add custom html tags to the HTML validator in
Eclipse – without implementing a new validator -, so that there are no warnings
because of invalid tags). I know, these are not valid HTML tags, but we will
filter them out later – with a Zend_Filter object. The Zend_View object has a
hook to set a Zend_Filter with which the rendered content will be filtered. So
we have to implement a new class, e.g. Zx_View_Filter_Translate. To follow the lang=DE> href="http://framework.zend.com/manual/en/coding-standard.naming-conventions.html"> lang=EN-US>naming conventions of Zend Framework you should
create a file library/Zx/View/Filter/Translate.php and implement the class in
there (Zx is my personal ZendFramework-Demo-Extension-Prefix, choose whatever
you want).

Let’s have a look at the code (If you use also the lang=DE> lang=EN-US>Zend_Loader, it’s not necessary to include any
file):

<?php
class Zx_View_Filter_Translate implements Zend_Filter_Interface
{

    /**
     * Starting delimiter for translation snippets in view
     *
     */
    const I18N_DELIMITER_START = '<i18n>';

    /**
     * Ending delimiter for translation snippets in view
     *
     */
    const I18N_DELIMITER_END = '</i18n>';

    /**
     * Filter the value for i18n Tags and translate
     *
     * @param string $value
     * @return string
     */
    public function filter($value)
    {
        $startDelimiterLength = strlen(self::I18N_DELIMITER_START);
        $endDelimiterLength = strlen(self::I18N_DELIMITER_END);

        $translator = Zend_Registry::get('Zend_Translate');

        $offset = 0;
        while (($posStart = strpos($value, self::I18N_DELIMITER_START, $offset)) !== false) {
            $offset = $posStart + $startDelimiterLength;
            if (($posEnd = strpos($value, self::I18N_DELIMITER_END, $offset)) === false) {
                throw new Zx_Exception("No ending tag after position [$offset] found!");
            }
            $translate = substr($value, $offset, $posEnd - $offset);

            $translate = $translator->_($translate);

            $offset = $posEnd + $endDelimiterLength;
            $value = substr_replace($value, $translate, $posStart, $offset - $posStart);
            $offset = $offset - $startDelimiterLength - $endDelimiterLength;
        }

        return $value;
    }
}

We only have to implement one method: Zx_View_Filter_Translate::filter().
Param $value contains the complete rendered text from the view. In a loop we
check every occurrence of a starting <i18n> tag and an ending
</i18n> tag. Then we extract the text in between the tags and translate
it with the Zend_Translate object (Get it from the registry). At the end the
translated text has to be placed in the origin text, but the <i18n> tags
are stripped out.

For performance reasons I haven’t used regular expression. I
agree, regular expressions would clean up the code, but string operations are
faster. This is my first approach, suggestions for improvement are welcome.

Now we have a filter which is currently not used. So, we
have to tell the view where to find it. My suggestion: use the Initializer::initView()
method. To get the view at this point of the application is a little bit
tricky:

/**
 * Initialize view
 *
 * @return void
 */
public function initView()
{
    // …

    $view = new Zend_View();
    $view->addFilterPath('Zx/View/Filter', 'Zx_View_Filter');
    $view->setFilter('Translate');

    $viewRenderer = Zend_Controller_Action_HelperBroker::getStaticHelper('ViewRenderer');

    $viewRenderer->setView($view);
}

Create a new Zend_View object, add a new filter path (the
path we used for or our Translate filter) and a new filter prefix
(‘Zx_View_Filter’, otherwise the default ‘Zend_View_Filter’ would have been
used for creating the class name). Additionally we have to set the filter name,
we would like to use. In the end we have to set this view object in the static
action helper ‘ViewRenderer’.

Now it’s time to check whether it works.

Extended <i18n> approach

With the Translate ViewHelper it’s also possible to translate a formatted string. For example:

<?= $this->translate('Hello %s, how are you?', array($name); ?>

Therefore one has to pass an array as an additional parameter. The array values are the replacements , which are used internally by the function vsprintf.

In order to get this functionality in the <i18n> approach I extended the Filter class:

<?php
class Zx_View_Filter_Translate implements Zend_Filter_Interface{
    const I18N_DELIMITER_START = '<i18n>';
    const I18N_DELIMITER_END = '</i18n>';
    /**
     * Attribute name
     * Value is used for replacing content with vsprintf
     */
    const REPLACEMENT_ATTR = 'replacement';
    /**
     * If there is more than one value to replace, delimite them with this string
     */
    const REPLACEMENT_ATTR_DELIMITER = ',';

    public function filter($value) {
        $startDelimiterLength = strlen(self::I18N_DELIMITER_START);
        $endDelimiterLength = strlen(self::I18N_DELIMITER_END);

        $translator = Zend_Registry::get('Zend_Translate');

        $delimiterStart = substr(self::I18N_DELIMITER_START, 0, -1);

        $offset = 0;
        while (($posStart = strpos($value, $delimiterStart, $offset)) !== false) {
            $offset = $posStart + $startDelimiterLength;

            // check for an tag ending '>'
            $posTagEnd = strpos($value, '>', $offset - 1);
            $formatValues = null;
            // if '<i18n' is not followed by char '>' directly, then we obviously have attributes in our tag
            if ($posTagEnd - $posStart + 1 > $startDelimiterLength) {
                $format = substr($value, $offset, $posTagEnd - $offset);
                $matches = array();
                // check for value of 'format' attribute and explode it into $formatValues
                preg_match('/' . self::REPLACEMENT_ATTR . '="([^"]*)"/', $format, $matches);
                if (isset($matches[1])) $formatValues = explode(self::REPLACEMENT_ATTR_DELIMITER, $matches[1]);
                $offset = $posTagEnd + 1;
            }

            if (($posEnd = strpos($value, self::I18N_DELIMITER_END, $offset)) === false) {
                throw new Zx_Exception("No ending tag after position [$offset] found!");
            }
            $translate = substr($value, $offset, $posEnd - $offset);

            $translate = $translator->_($translate);
            if (is_array($formatValues)) $translate = vsprintf($translate, $formatValues);

            $offset = $posEnd + $endDelimiterLength;
            $value = substr_replace($value, $translate, $posStart, $offset - $posStart);
            $offset = $offset - $startDelimiterLength - $endDelimiterLength;
        }

        return $value;
    }
}

Now it’s possible to use not only <i18n> but also

<i18n replacement="John,McClane">Dear Mr. %s %s,</i18n>

Instead of passing the replacements as an array, we can use an attribute ‘replacement’ and a comma separated list for the values.

Currently I cannot recommend this solution for production use, as this implementation is experimental. It worked for me in my little example, but I haven’t tested it yet in a production environment.

Performance

As I installed this little demo application on a Zend Server
4.0 beta (VMware, Ubuntu JeOS 8.04), it was very easy for me to do some performance
test with Zend Studio for Eclipse and the integrated Profiler.

Some words about my ‘testing environment’: I implemented a
TranslateController class with two actions – helperAction and filterAction.
Both actions do nothing, just render a view either with the ViewHelper or the
Filter. I also implemented a postDispatch() method, which loops 150 times and
renders the script of the called action. I think it’s easier to understand, if
you have a look at the code:

<?php
/**
 * TranslateController
 *
 * @author  $LastChangedBy: $
 * @version $Id: TranslateController.php 148 2009-03-27 09:48:37Z  $
 */
class TranslateController extends Zend_Controller_Action {

    const ITERATIONS = 150;
    private static $cnt = 0;
    private $_script;

    private $_currentActionName;

    /**
     * builds the view script name which is rendered in the
     * postDispatch()
     *
     */
    public function init() {
        $this->_currentActionName = $this->getRequest()
                                             ->getActionName();

        $this->_script = $this->getRequest()->getControllerName()
                      . DIRECTORY_SEPARATOR
                      . $this->_currentActionName
                      . '.phtml';
    }

    /**
     * Builds up an action chain with self::ITERATIONS iterations.
     * In each iteration the appropriate script is rendered.
     *
     */
    public function postDispatch() {
        if (++self::$cnt >= self::ITERATIONS) return;

        $this->view->render($this->_script);
        $this->_forward($this->_currentActionName);
    }

    /**
     * View uses the Translate ViewHelper for translating
     *
     */
    public function helperAction() {
    }

    /**
     * View uses filter for translating
     *
     */
    public function filterAction() {
    }
}

Calling both Requests (/translate/helper and
/translate/filter) in the Profiler gives us following results:

/translate/helper

height=121 id="Grafik 2" src="/images/articles/4513/image010.png"
alt="profiler_helper.gif">

/translate/filter

height=121 id="Grafik 3" src="/images/articles/4513/image011.png"
alt="profiler_filter.gif">

As we can see, the postDispatch() method of the filter call
needs approximately 20% less total time than the helper call. I tried this several
times and got always similar results.

Of course this is not a scientific measurement, but I think
it’s worth the look on the filter implementation.

Published: April 29th, 2009 at 1:20
Categories: Zend Framework
Tags:

7 comments to “Zend Framework and Translation”

Thanks a lot, very interesting stuff :)

Thanks! It’s very intresting idea. I’m try use this filter in my project.

Usualy i’m initialize Zend_Translate in my front controller plugin, but now we have Zend_App and i think using them – right idea.

v.veselinov@gmail.com
April 30th, 2009 at 6:57 am

That’s a really nice approach. I wouldn’t have thought of that! I’m gonna give it a try in my next project :)

how do you translate <?php print $this->translate(‘Hello %s, how are you?’,array($name); ?> to <i18n> approach?

Hey xaviersarrate,

I am also interessed in how that would translate. If you do find something out please post it in the comments!

Good article, but I would like to know how to translate URLs on-the-fly?
ie.

en: article/read/xxx
pt-BR: artigo/ler/xxx

I know we can create custom routes right on the bootstrap, but I would like to have it created like translating actions names on the fly, based on a translation lib.

I really like this way of translating and have implemented the class in the project I’m currently working on. Unfortunatly, I would like to use gettext / poEdit to do the actual translation.

How do I import the text into poEdit? Or is there some other tool to use for the translation?