Christer’s blog o’ fun

April 24, 2008

Translating Zend_Form error messages and more

Filed under: PHP, Technology — Tags: , , , , , — christer @ 4:36 pm

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!

5 Comments »

  1. Christer and His Quest For More Zend_Form-age…

    I finally found out why Christer had been so quiet all day: he’s obviously been writing the largest post seen in the history of blogs. His introduction to Translating Zend Form Error Messages is enormous and a giant of a beast, and will give a t…

    Trackback by Mats Lindh — April 24, 2008 @ 7:17 pm

  2. [...] of this post will show you how to enable this marvelous new decorator. I will use some code from my Translating Zend_Form error messages and more post, so if you haven’t read that yet you should catch up by doing so. [...]

    Pingback by Quick and dirty custom Zend_Form_Decorator_Label « Christer’s blog o’ fun — April 25, 2008 @ 5:04 pm

  3. [...] Translating Zend_Form error messages and more [...]

    Pingback by PHP Weekly Reader - April 27th 2008 : phpaddiction — April 30, 2008 @ 6:53 am

  4. great article, thanks!

    Comment by dan — May 20, 2008 @ 11:08 am

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

    thanks!!

    Comment by patrick — May 29, 2008 @ 1:48 pm

RSS feed for comments on this post. TrackBack URI

Leave a comment

Blog at WordPress.com.