Robert Speer Web Development: Symfony, PHP, Wordpress, Business Analysis

Internationalized (i18n) Admin Generator CRUD’s in Symfony 1.2.9 + Doctrine

I was having some trouble finding documentation on how to i18n generated CRUD’s, so once I figured (most) of it out I thought I’d share it

The Example Application

Content Block CRUD with French langage selected

Since I have to create a feature in my one of my current work projects to store random bits of content, like privacy policies and such, in multiple languages. I thought I’d double dip and use that for this example.  I’m calling the feature content blocks.  It will have a backend CRUD that will facilitate translations.  The UI I needed was to have the default language show up as well as one of the many languages this information would be translated into.  My app has the possibility of having more than 20 language options so putting them all in the CRUD at once was unreasonable.

The example to the left shows what the CRUD looks like when French is set as the user’s culture.  If the default language is chosen a second language form does not show up.

On the frontend I’m just going to do a simple data pull for this example.  Both the frontend and the backend app will have very simple language switchers  to demonstrate how that works.
Now that I now how this works it’s actually pretty darn simple, however figuring it out took longer than I’d like.  Hopefully this tutorial will save you some time.

I’m going to skip the application setup, if you don’t know how to do that I used the same steps that are in the Doctrine version of the Jobeet tutorial.

Download the example app zip file here, includes Symfony 1.2.9

Doctrine schema.yml

The database schema.yml was by a little tricky at first.  I did not realize that Doctrine handles I18n tables so much differently from Propel.  With Propel I would have defined a second table named content_block_i18n and put the translated fields there.  For Doctrine they simply go in under the actAs and I18n.  This is less typing and I suspect more intuitive for those who don’t already know Propel.

Also remember to put columns: before your field definitions, and leave out the connection at the top of the file.  Timestammable adds the created_at & updated_at fields.  Also notice that the data types are different from Propel’s.

I think I’m going to like these changes, but they are different so be careful if you are used to Propel.

content_block:
actAs:
Timestampable: ~
I18n:
fields: [short_title, title, extract, content]
columns:
weight: integer
active: boolean
short_title: string(50)
title: string
extract: string
content: string(4000)

Once you’re done: create your database, edit databases.yml, build-all, and clear you cache.  Details are in the Jobeet tutoral in Day 3: The Data Model.

Adding embedI18n() to the form class a.k.a: where the magic happens

This part took some serious research, I was just sure all I had to do was edit something in the generator.yml, but that turned out not to be the case.

I finally found  embedI18n() in the Forms in Action book in the i18n chapter under Propel Objects Internationalization.  It does use the much maligned sfContext, and if you know a better way write a comment.

You’ll want to generate forms(php symfony doctrine:generate-forms) & then the contend block CRUD (php symfony doctrine:generate-admin backend ContentBlock), as well as turn I18n on in the backend settings.yml.

This is in: lib/form/content_blockForm.class in the example application.

/**
* content_block form.
*
* @package form
* @subpackage content_block
*/
class content_blockForm extends Basecontent_blockForm
{
/**
* Form configuration settings
*
* @author Robert H. Speer
*/
public function configure()
{
$this->embedI18n(array(sfConfig::get('sf_default_culture', 'en'),
$this->getCurrentCulture())
);
}


/**
* pulls the current culture from the user object
*
* @return string
* @author Robert H. Speer
*
* Notes:
* RHS 10/2/09 - sfContext::getInstance() violates MVC but I don't know a way
* around it ATM.
*/
public function getCurrentCulture()
{
$culture = sfContext::getInstance()->getUser()->getCulture();

if (strlen($culture)>0) { // return user selected language
return $culture;
}else{ // return default culture, or defaults to english
return sfConfig::get('sf_default_culture', 'en');
}
}

}

A simple language switcher component

I’ve included a very simple language switching component in the example application.  Assuming you know how to write a component, it’s not a big deal.

The actual language setter is in both apps (i know wet is bad) under language_switcher/actions/action.class.php & looks like this:

/**
* changes the users culture and redirects them back the their previous page
*
* @author Robert H. Speer
*/
public function executeLanguage() {
$this->getUser()->setCulture($this->getRequestParameter('culture'));

$url = $this->getRequest()->getReferer() != '' ? $this->getRequest()->getReferer() : '@homepage';
$this->redirect($url);
}

It’s going to take the user’s selected culture set that to the user object, and then redirect causing a refresh.

How to get at that translated content

This is the easy part, you actually don’t have to do anything special to grab content in the language set in the user object, just get the object and call the getter.

Grab the object(s) with something like this, but preferably in the model layer instead of apps/frontend/homepage/actions/action.class.php:

/**
* Executes index action
*
* @param sfRequest $request A request object
*/
public function executeIndex(sfWebRequest $request)
{
$this->block = Doctrine::getTable('content_block')->createQuery('a')->execute();
}

Then in your template you can access all the fields just like if they were in the same table (this is really cool):

/**
* Very simple homepage for demo purposes only
*
* @author Robert H. Speer
*/
foreach ($block as $key=>$row)
{
echo 'id: '.$row->getId().'<br>';
echo 'weight: '.$row->getWeight().'<br>';
echo 'short title: '.$row->getShortTitle().'<br>';
echo 'title: '.$row->getTitle().'<br>';
echo 'extract: '.$row->getExtract().'<br>';
echo 'content: '.$row->getContent().'<br>';
echo 'created at: '.$row->getCreatedAt().'<br>';
echo 'updated at: '.$row->getUpdatedAt().'<br>';
echo 'lang: '.$row->getLang().'<br>';
echo '<hr>';
}

If you get the example app going on your own machine add a few records with some translations, then change the language with the language drop down and it will just work automagicaly.  For your own apps remember to turn I18n on in your applications settings.yml.

Reference Links

What I have not figured out yet

  • I have got the file upload widget to show up in the Admin generator but I it does not work automagicaly, like I think it should, I think I’m going to have to write the file handler myself.
  • Getting at the embedded fields in the generator.yml is elusive as well.

I’ll be working on both of these problems as soon as I get back to work, so hopefully I’ll have an update soon.  If you figure it out first please write a comment.

Disclaimer:

By the time I was done writing this tutorial I was very ready to not be at my computer anymore, there are going to be some grammatical mistakes and maybe some code ones as well.  I through this together on WAMP, on my home desktop, so you may have to change the slashes on your path, and update your apache conf &/or your .htaccess file to get it to work.  The application I’ve uploaded does work, but it is just a demo so don’t trust it too much ;)

Be Sociable, Share!
  • Hi, nice post.
    Could you throw some light on data-separation (in case of a multi-tenant database). Im looking out for a simple way to filter records based on a tenant_ID and I don’t want to write the code for every action of every module. Hope you will help. Thanks in advance

  • Hi!

    Have you found the way of uploading I18n files?

    Regards!