I18n Behavior
The i18n
behavior provides internationalization to any ActiveRecord object. Using this behavior, you can separate text data from the other data, and keep several translations of the text data for a single object. Applications supporting several languages always use the i18n
behavior.
Basic Usage
In the schema.xml
, use the <behavior>
tag to add the i18n
behavior to a table. In the <parameters>
tag, list the columns that need internationalization as the i18n_columns
parameter:
<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>
Rebuild your model, insert the table creation sql again, and you’re ready to go. The internationalized columns have now been moved to a new translation table called item_i18n
; this new table contains a locale
column, and shares a many-to-one relationship with the item
table. In fact, everything happens as if you had defined the following schema:
<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" />
</table>
<table name="item_i18n">
<column name="id" type="integer" required="true" primaryKey="true" />
<column name="locale" type="varchar" size="5" required="true" primaryKey="true" />
<column name="name" type="varchar" required="true" />
<column name="description" type="longvarchar" />
<foreign-key foreignTable="item" onDelete="setnull" onUpdate="cascade">
<reference local="id" foreign="id" />
</foreign-key>
</table>
In addition, the ActiveRecord Item class now provides integrated translation capabilities.
<?php
$item = new Item();
$item->setPrice('12.99');
// add an English translation
$item->setLocale('en_US');
$item->setName('Microwave oven');
// same as
$itemI18n = new ItemI18n();
$itemI18n->setLocale('en_US');
$itemI18n->setName('Microwave oven');
$item->addItemI18n($itemI18n);
// add a French translation
$item->setLocale('fr_FR');
$item->setName('Four micro-ondes');
// save the item and its translations
$item->save();
// retrieve text and non-text translations directly from the main object
echo $item->getPrice(); // 12.99
$item->setLocale('en_US');
echo $item->getName(); // Microwave oven
$item->setLocale('fr_FR');
echo $item->getName(); // Four micro-ondes
Getter an setter methods for internationalized columns still exist on the main object ; they are just proxy methods to the current translation object, using the same signature and phpDoc for better IDE integration.
TipPropel uses the locale concept to identify translations. A locale is a string composed of a language code and a territory code (such as ‘en_US’ and ‘fr_FR’). This allows different translations for two countries using the same language (such as ‘fr_FR’ and ‘fr_CA’);
Dealing With Locale And Translations
If you prefer to deal with real translation objects, the behavior generates a getTranslation()
method on the !ActiveRecord class, which returns a translation object with the required locale.
<?php
$item = new Item();
$item->setPrice('12.99');
// get the English translation
$t1 = $item->getTranslation('en_US');
// same as
$t1 = new ItemI18n();
$t1->setLocale('en_US');
$item->addItemI18n($t1);
$t1->setName('Microwave oven');
// get the French translation
$t2 = $item->getTranslation('fr_FR');
$t2->setName('Four micro-ondes');
// these translation objects are already related to the main item
// and therefore get saved together with it
$item->save(); // already saves the two translations
TipOr course, if a translation already exists for a given locale,
getTranslation()
returns the existing translation and not a new one.
You can remove a translation using removeTranslation()
and a locale:
<?php
$item = ItemQuery::create()->findPk(1);
// remove the French translation
$item->removeTranslation('fr_FR');
Querying For Objects With Translations
If you need to display a list, the following code will issue n+1 SQL queries, n being the number of items:
<?php
$items = ItemQuery::create()->find(); // one query to retrieve all items
$locale = 'en_US';
foreach ($items as $item) {
echo $item->getPrice();
$item->setLocale($locale);
echo $item->getName(); // one query to retrieve the English translation
}
Fortunately, the behavior adds methods to the Query class, allowing you to hydrate both the Item
objects and the related ItemI18n
objects for the given locale:
<?php
$items = ItemQuery::create()
->joinWithI18n('en_US')
->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.
Tip
joinWithI18n()
adds a left join with two conditions. That means that the query returns all items, including those with no translation. If you need to return only objects having translations, addCriteria::INNER_JOIN
as second parameter tojoinWithI18n()
.
If you need to search items using a condition on a translation, use the generated useI18nQuery()
as you would with any useXXXQuery()
method:
<?php
$items = ItemQuery::create()
->useI18nQuery('en_US') // tests the condition on the English translation
->filterByName('Microwave oven')
->endUse()
->find();
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 tutorial:
<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.
Parameters
If you don’t specify a locale when dealing with a translatable object, Propel uses the default English locale ‘en_US’. This default can be overridden in the schema using the default_locale
parameter:
<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" />
<parameter name="default_locale" value="fr_FR" />
</behavior>
</table>
You can change the name of the locale column added by the behavior by setting the locale_column
parameter and the length of the created datatype by setting the locale_length
parameter. Also, you can change the table name and the phpName of the i18n table by setting the i18n_table
and i18n_phpname
parameters and you can set the name of created primary key column of the i18n table by setting the i18n_pk_column
parameter:
<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" />
<parameter name="locale_column" value="language" />
<parameter name="locale_length" value="6" />
<parameter name="i18n_table" value="item_translation" />
<parameter name="i18n_phpname" value="ItemTranslation" />
<parameter name="i18n_pk_column" value="i18n_id" />
</behavior>
</table>