Blog: Propel Gets I18n Behavior, And Why It Matters

The Propel Team – 11 January 2011

Propel recently got yet another behavior: the Internationalization behavior, also named i18n behavior (the numeronym is a frequent abbreviation). It allows Propel model objects to get translations, and is useful in multilingual applications.

Not only is it intuitive and dead easy to setup, it also replaces the existing symfony i18n behavior without any change to the application code. And why the Symfony i18n behavior implementation has an important flaw, the new native i18n behavior does things the proper way.

Usage

Consider as an e-commerce website selling home appliances across the world. This website should keep the name and description of each item separated from the other details, and keep one version for each supported language.

Starting with Propel 1.6, this is possible by adding a simple <behavior> tag to the table that needs internationalization:

<table name="item">
  <column name="id" required="true" primaryKey="true" autoIncrement="true" type="INTEGER" />
  <column name="name" type="VARCHAR" required="true" />
  <column name="description" type="LONGVARCHAR" />
  <column name="price" type="FLOAT" />
  <column name="is_in_store" type="BOOLEAN" />
  <behavior name="i18n">
    <parameter name="i18n_columns" value="name, description" />
  </behavior>
</table>

In this example, the name and description columns are moved to a new table, called item_i18n, which shares a many-to-one relationship with Item - one Item has many Item translations. But all this happens in the background; for the end user, everything happens as if there were only one main Item object:

$item = new Item();
$item->setPrice('12.99');
$item->setName('Microwave oven');
$item->save();

This creates one record in the item table with the price, and another in the item_i18n table with the English (default language) translation for the name. Of course, you can add more translations:

$item->setLocale('fr_FR');
$item->setName('Four micro-ondes');
$item->setLocale('es_ES');
$item->setName('Microondas');
$item->save();

This works both for setting AND for getting internationalized columns:

$item->setLocale('en_EN');
echo $item->getName(); //'Microwave oven'
$item->setLocale('fr_FR');
echo $item->getName(); // 'Four micro-ondes'

Tip: The big advantage of Propel behaviors is that they use code generation. Even though it’s only a proxy method to the ItemI18n class, Item::getName() has all the phpDoc required to make your IDE happy.

Combined Hydration

This new behavior also adds special capabiliies to the Query objects. The most interesting allows you to execute less queries when you need to query for an Item and one of its translations - which is common to display a list of items in the locale of the user:

$items = ItemQuery::create()->find(); // one query to retrieve all items
$locale = 'en_EN';
foreach ($items as $item) {
  echo $item->getPrice();
  $item->setLocale($locale);
  echo $item->getName(); // one query to retrieve the English translation
}

This code snippet requires 1+n queries, n being the number of items. But just add one more method call to the query, and the SQL query count drops to 1:

$items = ItemQuery::create()
  ->joinWithI18n('en_EN')
  ->find(); // one query to retrieve both all items and their translations
foreach ($items as $item) {
  echo $item->getPrice();
  echo $item->getName(); // no additional query
}

In addition to hydrating translations, joinWithI18n() sets the correct locale on results, so you don’t need to call setLocale() for each result.

Symfony Compatibility

This behavior is entirely compatible with the i18n behavior for symfony. That means that it can generate setCulture() and getCulture() methods as aliases to setLocale() and getLocale(), provided that you add a locale_alias parameter. That also means that if you add the behavior to a table without translated columns, and that the translation table is present in the schema, the behavior recognizes them.

So the following schema is exactly equivalent to the first one in this article:

<table name="item">
  <column name="id" required="true" primaryKey="true" autoIncrement="true" type="INTEGER" />
  <column name="price" type="FLOAT" />
  <column name="is_in_store" type="BOOLEAN" />
  <behavior name="i18n">
    <parameter name="locale_alias" value="culture" />
  </behavior>
</table>
<table name="item_i18n">
  <column name="id" type="INTEGER" required="true" primaryKey="true" />
  <column name="name" type="VARCHAR" required="true" />
  <column name="description" type="LONGVARCHAR" />
</table>

Such a schema is almost similar to a schema built for symfony; that means that the Propel i18n behavior is a drop-in replacement for symfony’s i18n behavior, keeping BC but improving performance and usability.

Why It Matters

The SQL generated by the previous query looks like the following (in MySQL):

SELECT item.*, item_i18n.*
FROM item LEFT JOIN item_i18n ON (item.id = item_i18n.id AND item_i18n.locale = 'en_EN');

It does NOT generate the following query:

SELECT item.*, item_i18n.*
FROM item LEFT JOIN item_i18n ON (item.id = item_i18n.id)
WHERE item_i18n.locale = 'en_EN';

Can you see the difference? In the last SQL query, the LEFT JOIN actually behaves like an INNER JOIN because of the WHERE clause. That means that item records with no item_i18n translation won’t appear in the result. In the first query, even items with no translations are returned.

This difference is important for two reasons:

  • Propel couldn’t create joins with two conditions properly in version 1.5 and below. Only Propel 1.6 allows it (see What’s New In Propel 1.6?).
  • The previous i18n behavior implementation for symfony did it the wrong way, and applied the locale condition using WHERE instead of ON. That made results incomplete.

Tip: If you need to return only objects having translations, add Criteria::INNER_JOIN as second parameter to joinWithI18n().

Get It

Just like the recently added versionable behavior, the i18n behavior is thoroughly unit-tested and fully documented. It is ready to use in the Propel 1.6 branch, and your multilingual applications will love it.