Translating Zend_Form error messages and more

Update: Fixed some stuff based on an email from Matthew Weier O’Phinney. Also updated the downloadable file. Changes include:

  • Removing calls to setDisableLoadDefaultDecorators() since they are not needed
  • Fixed typo in a comment
  • Added DtDdWrapper to the display group to generate valid markup

Zend_Form supports i18n using the Zend_Translate component. Here is a tutorial on how to make your (Zend_) forms display labels, descriptions and error messages in different languages. Just to be nice I’ll make the example a bit more complex by creating my own custom form elements and a display group for the form buttons. This tutorial includes alot of PHP code, so don’t say I didn’t warn you. This time I have made a sample application that you can download and look at while going through the tutorial. Download this tar.gz file and unpack it and configure apache to use the www directory as document root. You will also need mod_rewrite to be enabled on the apache server. You also need to put Zend Framework in the lib directory. I omitted it to keep the file size to a minimum.

Now, lets get on with the show!

Lets start by defining our form. This time I’ll make a “contact me” form with the following elements:

  • Name (Custom text element)
  • Email (Custom email text element)
  • Content (Custom textarea element)
  • Submit button (Custom submit element)
  • Reset button (Custom reset element)

The form ContactForm will extend Zend_Form and the class looks like this:

<?php
require_once 'Zend/Form.php';

class ContactForm extends Zend_Form {
    public function init() {
        $this->setMethod('post')
             ->setAction('/')
             ->setName('contactForm');

        $name = new My_Form_Element_Text('name');
        $name->setLabel('formLabels_Name')
             ->setDescription('formDescriptions_Name')
             ->setRequired(true);

        $email = new My_Form_Element_Text_Email('email');
        $email->setLabel('formLabels_Email')
              ->setDescription('formDescriptions_Email')
              ->setRequired(true);

        $content = new My_Form_Element_Textarea('content');
        $content->setLabel('formLabels_Content')
                ->setDescription('formDescriptions_Content')
                ->setRequired(true)
                ->setAttribs(array('rows' => 6, 'cols' => 50));

        $submit = new My_Form_Element_Submit('submit');
        $submit->setLabel('formLabels_Submit');

        $reset = new My_Form_Element_Reset('reset');
        $reset->setLabel('formLabels_Reset');

        $this->addElements(array($name, $email, $content, $submit, $reset));
        $this->addDisplayGroup(array('submit', 'reset'), 'buttons');

        $group = $this->getDisplayGroup('buttons');
        $group->setDecorators(array(
            array('FormElements'),
            array('HtmlTag', array('tag' => 'div',
                                   'class' => 'formButtons')),
            array('DtDdWrapper'),
        ));
    }
}

The first thing you might notice is the weird labels and descriptions in the form elements. We will cover those later on when setting up the Zend_Translate object. You don’t have to pay much attention to that aspect yet.

A couple of other things worth noticing in this class is the custom elements:

  • My_Form_Element_Text
  • My_Form_Element_Text_Email
  • My_Form_Element_Textarea
  • My_Form_Element_Submit
  • My_Form_Element_Reset

This is not really necessary but since I’m such a nice guy I’ll do it anyways! :p

The last thing I do in the form is to create a display group for the buttons in the form. I do this so I can wrap them in a div container. Later on you’ll se how I remove all the default decorators on the buttons so I only get two input tags in the div container.

Now that we have our form class set up, lets have a look at the custom elements.

My_Form_Element_Text

<?php
require_once 'Zend/Form/Element/Text.php';

class My_Form_Element_Text extends Zend_Form_Element_Text {
    public function init() {
        $this->setDecorators(array(
             array('ViewHelper'),
             array('Description', array('escape' => false,
                                        'class' => 'fieldDescription')),
             array('Errors'),
             array('HtmlTag', array('tag' => 'dd')),
             array('Label', array('requiredSuffix' => ' *',
                                  'tag' => 'dt',
                                  'escape' => false))
        ));
    }
}

Here I set the decorators I want and the order I want them to appear in. For the description and the label decorators you can see that I set the ‘escape’ attribute to false. This is so I can include HTML code/entities in the labels and descriptions. I also add a custom css class to the description decorator.

My_Form_Element_Text_Email

<?php
require_once 'My/Form/Element/Text.php';

class My_Form_Element_Text_Email extends My_Form_Element_Text {
    public function init() {
        // Call up our parents init method as well
        parent::init();

        // Add a validator
        $this->addValidator('EmailAddress');
    }
}

This is simply a text element with a validator attached. Yes, I know this is completely overkill but it might help you understand more on how to make custom elements. The validator could of course be added to the element in the form class, but hey, you already know how to do that! If not, have a look at my previous post on Zend_Form!

My_Form_Element_Textarea

<?php
require_once 'Zend/Form/Element/Textarea.php';

class My_Form_Element_Textarea extends Zend_Form_Element_Textarea {
    public function init() {
        $this->setDecorators(array(
             array('ViewHelper'),
             array('Description', array('escape' => false,
                                        'class' => 'fieldDescription')),
             array('Errors'),
             array('HtmlTag', array('tag' => 'dd')),
             array('Label', array('requiredSuffix' => ' *',
                                  'tag' => 'dt',
                                  'escape' => false))
        ));
    }
}

Almost identical to the custom text element. Nothing new here, move on!

My_Form_Element_Submit

<?php
require_once 'Zend/Form/Element/Submit.php';

class My_Form_Element_Submit extends Zend_Form_Element_Submit {
    public function init() {
        $this->addDecorator('ViewHelper');
    }
}

Here I only add the ViewHelper decorator that simply renders the input tag and nothing more.

My_Form_Element_Reset

<?php
require_once 'Zend/Form/Element/Reset.php';

class My_Form_Element_Reset extends Zend_Form_Element_Reset {
    public function init() {
        $this->addDecorator('ViewHelper');
    }
}

Identical to the submit element.

Now that we have made the classes for all the custom elements, lets include our form in a controller action and render it within a view.

To make it easy I’ll put it in the index action of my index controller:

class IndexController extends Zend_Controller_Action {
    public function indexAction() {
        require_once 'ContactForm.php';
        $contactForm = new ContactForm();

        $request = $this->getRequest();

        if ($request->isPost() && $contactForm->isValid($_POST)) {
            // do some processing of the form
            // ...
            // ...
        }

        $this->view->contactForm = $contactForm;
    }
}

In the view you could then simply do:

<?= $this->contactForm->render() ?>

If you have followed the tutorial you will end up with a form looking something like this:

As you see the form looks great, except for the weird labels and descriptions. This is where Zend_Translate enters the show!

From the ZF docs:

Zend_Translate is Zend Framework’s solution for multilingual applications.

Just what we need!

The Zend_Translate component is adapter based like many other ZF components. All of the supported adapters are listed in the docs. For this tutorial I will use the Array adapter. That means that we will store our language in php arrays. I usually structure my applications the following way:

  • /application
  • /application/controllers
  • /application/views
  • /application/models
  • /application/forms
  • /lib/My
  • /lib/Zend
  • /www
  • /language
  • /etc

For this simple application I will add two languages: English and Norwegian. I will store each language in its own script inside the language directory. The files will look like this:

english.php

<?php
return array(
    'formLabels_Name' => 'Name',
    'formLabels_Email' => 'Email',
    'formLabels_Content' => 'Content',
    'formLabels_Submit' => 'Submit',
    'formLabels_Reset' => 'Reset',

    'formDescriptions_Name' => 'Your complete name',
    'formDescriptions_Email' => 'Your email address so I can contact you',
    'formDescriptions_Content' => 'Write something clever here',
);

norwegian.php

<?php
return array(
    'formLabels_Name' => 'Navn',
    'formLabels_Email' => 'Email',
    'formLabels_Content' => 'Innhold',
    'formLabels_Submit' => 'Send',
    'formLabels_Reset' => 'Nullstill',

    'formDescriptions_Name' => 'Skriv ditt fulle navn',
    'formDescriptions_Email' => 'Skriv din epostadresse her slik at jeg kan kontakte deg',
    'formDescriptions_Content' => 'Skriv noe morsomt til meg her',
);

Both the files simply returns an array. To store the content of these files in variables we simply do:

$english = require APP_ROOT . '/language/english.php';
$norwegian = require APP_ROOT . '/language/norwegian.php';

Since Zend_Translate lets you specify a filename as the source of a language you can easily skip the two lines above.

A good place to instantiate your translate object is in the bootstrapper (typically /www/index.php). The way to do this is as follows:

// Create the object and add a language
$translate = new Zend_Translate('Array', APP_ROOT . '/language/english.php', 'en_US');
// Add another translation
$translate->addTranslation(APP_ROOT . '/language/norwegian.php', 'nb_NO');
// Set nb_NO as default translation
$translate->setLocale('nb_NO');

In the first line we instantiate the $translate object and specify the English language file. The last argument is the locale of the language. A list of supported locales can be found in the Zend_Locale class.

Then we add a Norwegian translation and last we set the Norwegian translation as default. This should probably be specified in a ini file and maybe let the users viewing the site choose for themselves, but for now we’ll just set it in the bootstrapper.

Zend_Translate also supports automatic language detection by parsing the content of $_SERVER[‘HTTP_ACCEPT_LANGUAGE’] but I won’t go deeper into that here. Read more about it in the docs. For now we’ll settle on serving everyone a Norwegian version of the form. Everybody understands Norwegian right?

Now, let’s reload our page containing the form and see what happens!

Nothing! Well … what did you expect?! Zend_Form does not know anything about the global $translate object. What you need to do is store the $translate object in Zend_Registry with a given key as stated in the docs:

Zend_Registry::set('Zend_Translate', $translate);

And thats about it! If you reload the page containing the form you will see that the labels and descriptions have changed to Norwegian! Success! Now lets submit the empty form and see what happens!

Oops … the error messages are clearly still in English. And how do we fix that? We simply need to add the translation keys associated with the different error messages to the language we want. And where do we find them you ask? In the validator classes of course!

Since we have set the elements as required the NotEmpty validator is automatically added to the elements. Along with that we need to translate the EmailAddress validator. If we take a look in those classes we find the following message keys:

// Zend_Validate_NotEmpty
const IS_EMPTY = 'isEmpty';

// Zend_Validate_EmailAddress
const INVALID            = 'emailAddressInvalid';
const INVALID_HOSTNAME   = 'emailAddressInvalidHostname';
const INVALID_MX_RECORD  = 'emailAddressInvalidMxRecord';
const DOT_ATOM           = 'emailAddressDotAtom';
const QUOTED_STRING      = 'emailAddressQuotedString';
const INVALID_LOCAL_PART = 'emailAddressInvalidLocalPart';

All validators are built like this; with messages keys and a message template that holds the english translation of the error message.

The EmailAddress validator also does some validation of the hostname using Zend_Validate_Hostname so there are probably more message keys that needs to be translated but now that you know how it works I don’t have to do it for you. :)

So, if we want to translate the single message found in Zend_Validate_NotEmpty we add a key => value pair in the array in our norwegian.php file:

'isEmpty' => 'Felter mangler innhold.',

Now, if we leave the form elements empty and submit the form the Norwegian error message should appear.

And that’s about it I guess. Hopefully you have learned a thing or two about how to enable i18n in your Zend_Form’s! :) If you leave a comment it will make my day!

Advertisements
This entry was posted in PHP, Technology and tagged , , , , , . Bookmark the permalink.

17 Responses to Translating Zend_Form error messages and more

  1. Pingback: Mats Lindh

  2. Pingback: Quick and dirty custom Zend_Form_Decorator_Label « Christer’s blog o’ fun

  3. Pingback: PHP Weekly Reader - April 27th 2008 : phpaddiction

  4. dan says:

    great article, thanks!

  5. patrick says:

    thanks so much!
    i had trouble implementing the reset element and getting it all multilangual..

    thanks!!

  6. Tim Fletcher says:

    A great tutorial. Thanks!

  7. daniel says:

    great, thank you a lot

  8. Olagato says:

    Great tutorial.
    what about legends? I cant translate them.

  9. Willem Luijk says:

    Hello Christer,

    I took today to research this article but pitty enough i cant download the tar.gz. Can you have a look?

  10. christer says:

    @willem: I’m sorry about that. The server that the file is on is offline at the moment. It won’t be back up for at least three more weeks and I don’t have the file anywhere else. Sorry about the trouble!

  11. Anil says:

    Thanks. how should i know that all errors goes by translator.. damn..

  12. Christer, this is a very good piece of article you wrote!

    I must say, even though Zend_Form looks into the Zend_Registry after an index called “Zend_Translate” in order to establish the default translator, not everyone might have set it that way.

    A better way to tell the form which translator to use is:

    $form->setDefaultTranslator($translate);

    $translate should be appointed the Zend_Translate object in a way the developer is used to.

  13. Cialis says:

    3So6BA Excellent article, I will take note. Many thanks for the story!

  14. aruna says:

    This is a very good article. but I came across one problem when I have used
    following line to translate error msg.

    ‘isEmpty’ => ‘any lang used for translation ‘,

    i have en.csv & fr.csv file i wrote above line in fr.csv file but when i execute program, on Zend form error msg is displayed in French language only though i provided en for English language in url…
    and one more problem- what if after clicking on submit button?? I have used Zend_Session_Namespace() to remember same language on next page but its not working.. :(

    i hav en.csv & fr.csv file in /opt/lampp/htdocs/newproject/application/configs/lang folder
    file contains following code

    en.csv
    Home;Home
    Hello;Hello
    ***********
    fr.csv

    Home;Acceuil
    Hello;bonjour
    *************
    IndexController.php

    class IndexController extends Zend_Controller_Action
    {

    public function init()
    {

    }

    public function indexAction()
    {

    $frm=new Form_regForm();
    $db=new Model_User();
    if($this->_request->isPost())
    {
    if($frm->isValid($_POST))
    {
    $frmdata=$this->_request->getPost();
    $data=$this->getRequest();
    $db->add($data);
    $this->_redirect(‘/index/userdt/’);

    }
    }
    $this->view->form = $frm;

    }

    public function dojodemAction()
    {

    }

    public function userdtAction()
    {
    $db1=new Model_User();
    $data=$db1->selectdata();
    $this->view->data=$data;
    }

    public function deleteAction()
    {
    $db=new Model_User();
    $request_id=$this->getRequest();
    $db->deleterec($request_id);
    $this->_forward(‘/userdt’);

    }

    public function updateAction()
    {

    $frm=new Form_regForm();

    $frm->removeElement(‘passwd’);

    $db=new Model_User();
    $id_update=$this->getRequest();
    $update_arr=$db->updaterec($id_update);

    $frm->setDefaults($update_arr);

    if($this->_request->isPost())
    {

    if($frm->isValid($_POST))
    {

    extract($_POST);

    $db->up_data($id,$name,$gender,$language,$address,$role);
    $this->_forward(‘/userdt’);
    }
    }

    $this->view->form = $frm;
    }

    }

    **************
    index.phtml

    Welcome to translate(‘Home’); ?>
    form; ?>
    ************
    userdt.phtml

    getBaseUrl(); ?>

    welcome to : translate(‘Home’); ?>

    <a href="”>Add New User
    User Details

    ID
    NAME
    GENDER
    LANGUAGES
    ADDRESS
    ROLE
    ACTION

    <?php
    //echo "

    ";
    		//echo $this->data;
    		foreach($this->data as $key)
    		{
    	 ?>
    	
    		
    		
    		
    		
    		
    		
    		<a href="">Edit | <a href="">Delete
    	
    	
    
    
    
    
    ***********
    regForm.php
    
    class Form_regForm extends Zend_Form
    {
    	public function init()
    	{
    
    		$this->setMethod('POST');
    		$this->setAttrib('id','frm');
    		
    		$id=$this->createElement('hidden','id');
    		$this->addElement($id);
    		$registry = Zend_Registry::getInstance();
    		$username=$this->createElement('text','name');
    		$username->setLabel($this->getView()->translate('USERNAME'));
    		$username->addValidator('Regex', false, array('/^[A-Za-z]*$/'));
    		
    		$username->addValidator('StringLength',false,array(1,10));
    		$username->setRequired('true');
    		
    		$this->addElement($username);
    		
    		$password=$this->createElement('password','passwd');
    		$password->setLabel('PASSWORD:')
    			->setRequired(true);
    			
    		$this->addElement($password);
    
    		$gender=$this->createElement('radio','gender');
    		$gender->setLabel('GENDER:')
    			->setRequired(true)
    			->addMultiOptions(array(
    						'male'=>'Male',
    						'female'=>'Female'
    						));
    		$this->addElement($gender);
    		
    
    		$language=$this->createElement('multiCheckbox','language');
    		$language->setLabel('LANGUAGES KNOWN:')
    			->setRequired(true)
    			->addMultiOptions(array(
    					'english'=>'English',
    					'hindi'=>'Hindi',
    					'marathi'=>'Marathi'
    					 ));
    		$this->addElement($language);
    		
    		$address=$this->createElement('textarea','address');
    		$address->setLabel('ADDRESS:')
    			->setRequired(true)
    			->setAttrib('rows',3)
    			->setAttrib('cols',15);
    		$this->addElement($address);
    		
    		$role=$this->createElement('select','role');
    		$role->setLabel('Role:')
    			->setRequired(true)
    			->addMultiOptions(array('Select','user'=>'User','admin'=>'Admin'));
    		$this->addElement($role);
    		
    		$submit=$this->createElement('submit','submit');
    			$submit->setLabel('Save');
    		$this->addElement($submit);
    			
    		
    
    			
    			
    	}
    	
    }
    
    ***********
    
    application.ini
    
    resources.frontController.plugins.LangSelector = "ZC_Controller_Plugin_LangSelector"
    
    autoloaderNamespaces.zc = "ZC_"
    
    resources.translate.data = "/opt/lampp/htdocs/newproject/application/configs/lang"
    resources.translate.adapter = "csv"
    resources.translate.locale = "en"
    
    resources.translate.options.disableNotices = false
    resources.translate.options.logUntranslated = true
    
    ****************
    bootstrap.php
    
    APPLICATION_PATH,
    					'namespace'=>'',
    					'resourceTypes'=>array(
    							'form'=>array(
    								'path'=>'forms/',
    								'namespace'=>'Form_'
    								),
    							'model'=>array(
    								'path' => 'models/',
    								'namespace' => 'Model_'
    								),
    					
    							),
    					)
    			);
    		return $resource;
    	}
    
    	protected function _initRoutes()
    	{
    		$frontController=Zend_Controller_Front::getInstance();
    		$router=$frontController->getRouter();
    		$router->setGlobalParam('lang','en');
    		$router->removeDefaultRoutes();
    		$router->addRoute(
    				'lang',
    				new Zend_Controller_Router_Route('/:controller/:action/:lang', array('lang' => ':lang'))
    				);
    
    
    	}
    
    
    	protected function _initTranslate() 
    	{
    		// We use the Swedish locale as an example
    		$locale = new Zend_Locale('en_US');
    		Zend_Registry::set('Zend_Locale', $locale);
    		
    		// Create Session block and save the locale
    		$session = new Zend_Session_Namespace('session');
    
    		if(isset($session->lang))
    			$langLocale=$session->lang;
    		else
    			$langLocale=$locale;
    		$translate = new Zend_Translate('csv',"/opt/lampp/htdocs/newproject/application/configs/lang/", $langLocale,
         			array('disableNotices' => true));
    
    		$registry = Zend_Registry::getInstance();
      		$registry->set('Zend_Translate', $translate);
    
    
    
    		
    	}
    	
    
    }
    
    ***********
    and plugin in folder 
    
    /opt/lampp/htdocs/newproject/library/ZC/Controller/Plugin/
    
    LangSelector.php
    
    class ZC_Controller_Plugin_LangSelector extends Zend_Controller_Plugin_Abstract
    {
    
    	public function preDispatch(Zend_Controller_Request_Abstract $request)
    	{
    		$registry = Zend_Registry::getInstance();
    		$translate = $registry->get('Zend_Translate');
    		 $lang=$request->getParam('lang','');
    		
    		if($lang != 'en' && $lang != 'fr')
    		$request->setParam('lang','en');
    		 $lang=$request->getParam('lang') ;
    		if($lang=='en')
    		
    		$locale='en_US';
    		else
    		
    		 $locale='fr_ca';
    		$zl=new Zend_Locale();
    		$zl->setLocale($locale);
    		$registry->set('Zend_Locale' , $zl);
    		$translate->setLocale($locale);
    		$registry->set('Zend_Translate' , $translate);
    	}
    		
    }
    
    
    I am not getting where I am wrong???
  15. aruna says:

    the second problem which i have mentioned is bcoz of my fault…can any one help me out to solve this problem…

  16. Dave says:

    Let’s say we have several fields in our form such as Firstname, Lastname etc.

    If we want to have a different custom error message for each… for example “Please enter your firstname” and “Give me your lastname sucker” or something unique for each, how is this possible since the translation file only has one mapping from ‘isEmpty’ to “some custom empty field phrase” ?

    Many thanks…

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s