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.

    Tip
    Propel 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
    

    Tip
    Or 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, add Criteria::INNER_JOIN as second parameter to joinWithI18n().

    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. Also, you can change the table name, the primary key name and the phpName of the i18n table by setting the i18n_table, i18n_pk_name and i18n_phpname parameters:

    <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="i18n_table" value="item_translation" />
        <parameter name="i18n_pk_name" value="TransId" />
        <parameter name="i18n_phpname" value="ItemTranslation" />
      </behavior>
    </table>
    

    Found a typo ? Something is wrong in this documentation ? Just fork and edit it !