Zend Framework and Translation

Tags: zend framework, zend_form, zend_locale, zend_translate

Zend Framework и перевод Zend Framework и перевод
Zend Framework і переклад Zend Framework і переклад

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:
Zend Framework and Translation
If you don’t know how to setup a Zend Framework application you can have a look at the 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:
Hide code highlighting
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
/**
 * 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. Once you have the Zend_Locale 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.
Zend Framework and Translation
This is the fasted way to create a working translation mechanism.
The en.inc.php file looks like this:
Hide code highlighting
1
2
3
4
5
6
7
8
9
10
<?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:
Hide code highlighting
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?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:
Hide code highlighting
1
2
3
4
5
6
7
8
9
10
11
12
13
<?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 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 Zend_Loader, it’s not necessary to include any file):
Hide code highlighting
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<?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:
Hide code highlighting
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
/**
 * 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.

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:

Hide code highlighting
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
<?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:
Zend Framework and Translation
/translate/helper

Zend Framework and Translation
/translate/filter

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

Original: Zend Framework and Translation

Rating: 12345   << Please, rate this article


Related articles:
   Integrating FCKeditor with Zend_Form
   Automated Testing Using Zend Framework
   A caching pattern for models
   Two (or more) Zend Framework projects on a shared host
   Memcached in PHP Made Easy With Zend Framework


Comments of article:

 
RedFox [2010-02-12]
Hi, good article. Can you put a sample application? Thanx


 

Leave a comment:

Name


E-mail


Message