Wording is not a developer job - part 2

last week I introduced a nice tool which allows to translate/change static text without the need of disturbing the technical team !

Now, I want to explain a bit more how developers need to architecture static text in order to make the system work fine.

This takes place at different levels :

  • model
  • controller
  • view

The levels are the MVC pattern which is implemented into the symfony framework. What I am going to explain is something I use on a dayly basis for my development, it does not mean it’s going to work with your projects. So think, and just take what you think is best for you.

The mgI18nPlugin uses the i18n functions from the symfony framework. So my first advice will be to load the i18n helper at the ApplicationConfiguration level

<?php
class frontendConfiguration extends sfApplicationConfiguration
{
    public function configure()
    {
        $this->loadHelpers(array(
            'I18N',
        ));
    }

So now, you can use the __() function anywhere. The state of art would be to use a dependency injection inside each level, so i18n can be more independent. (wait for symfony 2).

__($text, array('%name% => ‘Chuck Norris’), $catalogue)

Let’s have a look at this method. The function has 3 parameters:

  • text : the text to be translated. As it is not a developer job to do this wording job, I advice you to use some kind of underscore_representative_sentence which explains the meaning.
  • array | null : add dynamics values to the translated sentence
  • catalogue : add a context to the related text, so that same text can have different translation/wording.

The model

The model can hold types list, status list or other kind of data. Mostly this “data” is represented by a key-pair value. The key is a model constant and the value is a textual/human representation of this value.

Which would give in php:

<?php
abstract class PluginswBlogComment extends BaseswBlogComment
{
    const
        MODERATED_NONE = -1,
        MODERATED_OK   = 1,
        MODERATED_KO   = 0;

    public static function getModeratedList()
    {
        return array(
            self::MODERATED_NONE => __('option_moderated_none', null, 'swBlogCommentsAdmin'),
            self::MODERATED_OK   => __('option_moderated_ok', null, 'swBlogCommentsAdmin'),
            self::MODERATED_KO   => __('option_moderated_ko', null, 'swBlogCommentsAdmin'),
        );
    }

    public function getModerationName()
    {
        $list = self::getModeratedList();

        if(!array_key_exists($this->moderated, $list))
        {
            return '-';
        }

        return $list[$this->moderated];
    }
}

As you can see, constants are used in a static method, which returns a list of moderation options. The pair values are just automatically translated by the symfony i18n helper. The last method is just used to retrieve the textual value of the constant : used in list or as a show view.

The other important thing is the catalogue definition (the third parameter). The catalogue binds a value definition to a catalogue. So a translated ‘yes’ can have different ‘textual’ representation depending on the context. Selecting the correct name for the catalogue is not an easy part.

The other good thing with the model declaration, is that you can call the static method inside a form configuration.

<?php
abstract class PluginswBlogCommentForm extends BaseswBlogCommentForm
{
    public function setup()
    {
        parent::setup();

        // define moderation values
        $this->widgetSchema['moderated'] = new sfWidgetFormSelect(array(
            'choices' => swBlogComment::getModeratedList()
        ));

        $this->validatorSchema['moderated'] = new sfValidatorChoice(array(
            'choices' => array_keys(swBlogComment::getModeratedList())
        ));

        // keep only necessary fields
        swToolboxFormHelper::useOnly(array(
            'id',
            'moderated',
            'comment',
        ));
    }
}

With this declaration, translations will be automatically included into the form.

The controller

This part should not contain text as your logic is not set in this place, the controller just binds elements (form, data) with the view… nothing more… Except for one thing : the flash value. Most of the time application sends to the user a flash message to say that the action has been executed successfully or an error occurs.

<?php
public function executeUpdate($request)
{
    $this->forward404Unless($request->isMethod('post'));

    $this->form = $this->getswBlogCommentForm($request->getParameter('id'));

    $this->form->bind($request->getParameter('sw_blog_comment'));
    if ($this->form->isValid())
    {
        $sw_blog_comment = $this->form->save();

        // assign a validation flash message
        $this->getUser()->setFlash('notice-ok', __('notice_your_change_has_been_saved', null, 'swBlogCommentsAdmin'));

        $this->redirect('swBlogCommentsAdmin/index');
    }

    // assign a error flash message
    $this->getUser()->setFlash('notice-error', __('notice_an_error_occurred_while_saving', null, 'swBlogCommentsAdmin'));

    $this->setTemplate('edit');
}

The catalogue name is the module’s name. The same logic will be applied to the view.

The view

Inside a template you have different elements, for each you can prefix the element with its semantic meaning :

  • a : link_edit
  • p/div : message_header_form
  • label : label_moderated
  • h[1,…] : title_edit_blog
<?php $sw_blog_comment = $form->getObject() ?>
<?php if($form->isNew()): ?>
    <h2><?php echo sw_t(__('title_new_blog_comment', null, 'swBlogCommentsAdmin')) ?></h2>
<?php else: ?>
    <h2><?php echo sw_t(__('title_edit_blog_comment', null, 'swBlogCommentsAdmin')) ?></h2>
<?php endif; ?>

<?php echo __('message_header_form', null, 'swBlogCommentsAdmin') ?>
<form action="<?php echo url_for('swBlogCommentsAdmin/update'.(!$form->isNew() ? '?id='.$sw_blog_comment['id'] : '')) ?>" method="post" >
    <div class="sw-form-actions">
        <a href="<?php echo url_for('swBlogCommentsAdmin/index') ?>">
            <?php echo __('link_cancel', null, 'swBlogCommentsAdmin') ?>
        </a>
        <?php if (!$form->isNew()): ?>
            <?php echo link_to(
            __('link_delete', null, 'swBlogCommentsAdmin'),
            'swBlogCommentsAdmin/delete?id='.$sw_blog_comment['id'],
            array('post' => true, 'confirm' => __('message_are_you_sure', null, 'swBlogCommentsAdmin'),)
            ) ?>
        <?php endif; ?>
        <input type="submit" value="<?php echo __('btn_save', null, 'swBlogCommentsAdmin') ?>" />
    </div>
    <table>
        <?php echo $form ?>
    </table>
</form>
<?php echo __('message_footer_form', null, 'swBlogCommentsAdmin') ?>

La cerise sur le gateau ! (“Cherry on the top”)

There is one thing not explained in this article : label translation from the sfForm framework. Another note, sfForm label is something … well… badly polished : there is no way of marking a field as mandatory (the * stuff).

Let’s introduce something which is part of the swToolboxPlugin : swToolboxFormHelper::resetFormLabels. This static method resets the labels of a form. By default if no label is defined, labels are generated with the name of related fields. resetFormLabels force field name to be ‘label_field_name’.

This method also has a nice feature, you can pass a mandatory format which can be used to render the ‘*’ on required field.

<?php
abstract class PluginswBlogCommentForm extends BaseswBlogCommentForm
{
    public function setup()
    {
        [...]
        // keep only necessary fields
        swToolboxFormHelper::useOnly($this, array(
            'id',
            'moderated',
            'comment',
        ));

        // reset field labels to 'label_field_name'
        swToolboxFormHelper::resetFormLabels($this, array(
            'prefix'    => 'label_',
            'catalogue' => 'swBlogCommentAdmin',
            'mandatory_format' => '%s <sup>*</sup>',
        ));
    }
}

Conclusion

I hope this will help you to get your application fully translatable by using the example from this article and with the mgI18nPlugin.