Skip to content

Categories:

yii-language-behavior – Multiple Languages For Yii

This behavior allows you to quickly and easily add multiple language support to your Yii application. It has been developed as part of a new Yii powered eCommerce platform: Suocommerce – Next Generation eCommerce™.

Download:

You may download the extension from here: yii-language-behavior.

Installation:

To install the extension, create a directory named behaviors within your application /protected/ directory and copy the file LanguageBehavior.php to this directory.  That’s it!

Using the Extension:

Database Tables, Models and CRUD

The first thing that is required is a language table that will store information about the languages you wish to use in your application.  The download includes the file example.sql which contains an ideal structure for this table, plus an example of two tables to handle translatable data.  The structure of our language table is as follows:


CREATE TABLE IF NOT EXISTS `language` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(96) NOT NULL,
`code` varchar(12) NOT NULL,
`image` varchar(96) DEFAULT NULL,
`sort_order` tinyint(3) DEFAULT NULL,
`default` tinyint(1) unsigned NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
KEY `idx_language_code` (`code`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=1 ;

You should create a model and CRUD for the language table and populate it with your preferred languages. The column default allows you to set a default language for the front end of your application.

Next we need at least two tables for translatable data.  The example.sql file contains two tables: category and category_lang which are structured as follows:


CREATE TABLE IF NOT EXISTS `category` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`category_image` varchar(96) DEFAULT NULL,
`sort_order` int(3) unsigned DEFAULT NULL,
`status` tinyint(1) unsigned NOT NULL DEFAULT '1',
PRIMARY KEY (`id`),
) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=1 ;

CREATE TABLE IF NOT EXISTS `category_lang` (
`category_id` int(11) unsigned NOT NULL,
`language_id` int(11) unsigned NOT NULL,
`category_name` varchar(96) NOT NULL,
`category_description` text,
PRIMARY KEY (`category_id`,`language_id`),
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

We also need foreign keys, in particular to make sure that data is deleted from the category_lang table whenever a category record is deleted:

ALTER TABLE `category_lang`
ADD CONSTRAINT `key_category_description_category_id` FOREIGN KEY (`category_id`) REFERENCES `category` (`id`) ON DELETE CASCADE ON UPDATE NO ACTION,
ADD CONSTRAINT `key_category_description_language_id` FOREIGN KEY (`language_id`) REFERENCES `language` (`id`) ON DELETE RESTRICT ON UPDATE NO ACTION;

The important things to note with the category_lang table, which stores our translation strings are:

  1. A composite primary key comprised of category_id, which references the id column of our category table and language_id which references the id column of our language table.  These two columns are essential and can be named as you please.
  2. The category_name and category_description columns, both of which contain translation strings. You can have as many of these as you require and name them as you please.

You should generate a model for each of these tables and generate CRUD for the primary table, which in this case is for the Category class.

Configuring The Behavior

The behavior must be configured for each model pair you wish to use it with.  In this example, we will use our Category class file. First we must declare as public properties each attribute of our CategoryLang class that contain translation strings, plus a property  for model validation errors from our CategoryLang class. This attribute must always be named translationError. Within our Category class:

public $categoryName;
public $categoryDescription;
public $translationError;

Next we configure the behavior itself. Again in our Category class, we place the following:

	public function behaviors() {
		return array(
			'LanguageBehavior' => array(
				'class' => 'application.behaviors.LanguageBehavior',
				'translationClass' => 'CategoryLang',
				'translationForeignKey' => 'category_id',
				'languageColumn' => 'language_id',
				'languageRelation' => 'categoryLang',
				'translationColumns' => array('category_name', 'category_description'),
				'languages' => Yii::app()->params['languages'],
			),
		);
	}

The above is relatively straight forward. The attribute class defines the path to the LanguageBehavior class we installed. The attribute translationClass defines our translation class, which in this case is CategoryLang.  The attribute translationForeignKey refers to the foreign key column in our category_lang table that points to the id column of our category table.  The attribute languageColumn refers to the foreign key that points to the id column of our language table.  The attribute languageRelation refers to whatever we wish to name our relation between the category and  CategoryLang tables. The attribute transationColumns refers to an array containing all of our category_lang table  columns which contain translation strings.

With that the behavior is ready to use, however we need to set languages within the behavior. For testing I have been running two languages, English and Spanish (see example.sql). Languages need to  be set in the application configuration using a suitable configuration behavior. I use the following code within beginRequest():

	if(!isset(Yii::app()->params['languages'])) {
        $languages = Language::model()->findAll();
        $language_array = array();
        foreach($languages as $val) {
            $language_array[] = array('id' => $val->id,
                                      'code' => $val->code,
                                      'name' => $val->name,
                                      'image' => $val->image);
        }
        Yii::app()->params['languages'] = $language_array;
    }

We also need to modify the file _form.php that was created when generating CRUD for our category table. Below is an example from this file modified for the category_name and category_description attributes:

<?php foreach (Yii::app()->params['languages'] as $val) :
    ?>
<fieldset>
    <legend><?php echo $val['name']; ?></legend>
    <div class="row">
	<?php $class=''; (isset($model->translationError['category_name'][$val['id']]) ? $class='error':''); ?>
    <?php echo $form->labelEx($model,'category_name_', array('class' => $class)); ?>
    <?php echo $form->textField($model,'category_name[' . $val['id'] . ']',array('size'=>60,'maxlength'=>255, 'class' => $class)); ?>
    <?php echo (isset($model->translationError['category_name'][$val['id']]) ? '<div class="errorMessage">' . $model->translationError['category_name'][$val['id']] . '</div>':''); ?>
    </div>
     <div class="row">
    <?php echo $form->labelEx($model,'category_description'); ?>
    <?php echo $form->textArea($model,'category_description[' . $val['id'] . ']'); ?>
    <?php echo $form->error($model,'category_description'); ?>
    </div>
</fieldset>
<?php endforeach; ?>

At this point you should be able to create and update records for our two classes. For the front end however we need to set a session variable for the default or selected language, so that only a single language will be displayed on the front end. For this we must set Yii::app()->session['language_id']. This must be unset whenever we are working in the back end, otherwise you will only see data for one language. Exactly how this is accomplished for the front end I will leave to the reader. In the front end, translation attributes can be accessed like this:

$category = Category::model()->findByPk($id);
echo $category->categoryLang->category_name;

You can see above that we use categoryLang which is what we set as the name for our relation.

How It Works:

The LanguageBehavior class contains only four methods, which to me is beautiful in its simplicity. I am a strong believer in the KISS principle and always look to do things in the simplest way possible.

At the top of the class file we see six public properties. We set these properties when configuring the behavior within our class. Using properties in this way allows the behavior to be used for any pair of classes, no matter how their attributes are named or how many translation columns there are in a table.

The first method that we see is attach(), which is a method of the IBehavior interface. We use this to conditionally invoke a HAS_ONE relation between our two classes, so that in our front end when we retrieve records for the primary model, the relation data is also returned for the selected language in a single query:

		if(isset(Yii::app()->session['language_id'])) {
			$class = CActiveRecord::HAS_ONE;
			$owner->getMetaData()->relations[$this->languageRelation] = new $class($this->languageRelation, $this->translationClass, $this->translationForeignKey, array('condition' => $this->language_column . ' = ' . Yii::app()->session['language_id']));
		}

Even though in reality the relation between the tables is HAS_MANY, we can use a HAS_ONE relation by limiting it with the language_id condition.

The second method afterFind() is is used to find all translation records and to populate our declared translationColumns property within our primary class. This is used for the back end and is invoked whenever Yii::app()->session['language_id'] is not set.

The third method afterValidate() is invoked whenever our primary model attributes are validated. It first populates the translationClass properties, then validates those properties against the rules of the class. If errors are found, it first populates the translationError property we declared, then adds those errors to the primary class errors.

The fourth method afterSave() saves the translationClass data after the primary class data is saved. The translationClass attributes are populated by looping through the translationColumns properties.

I hope that many will find this extension as useful as I do. A happy new year to all!

Posted in PHP, Yii Framework.


8 Responses

Stay in touch with the conversation, subscribe to the RSS feed for comments on this post.

  1. ali says

    I receive the following error :
    Declaration of LanguageBehavior::afterValidate() should be compatible with that of CModelBehavior::afterValidate()

    When i comment afterValidate() method everything works fine but the category_lang table record has null value for category_name and category_description

    • Matti says

      Its due to changes with your newer PHP version. Please download the latest version which addresses this issue.

  2. Daniel Pettersson says

    This seems like the answer to my problems. I’m gonna test this out for sure.

    Look like you have done a great job!

  3. Michael says

    Hi Matt, I did a search for suocommerce, but couldn’t find anything. What’s the status on that project?

    • Matti says

      Hello Michael,

      Sorry its taken so long to reply! Suocommerce is in development, nothing out in the wild as yet.

      Matti

  4. Alex says

    Hey, this is one awesome piece of code. It helped me a lot. But here’s my problem. Somehow, I got to a point where I gotta make a longer SQL, and that SQL includes 2 columns, both reference 2 tables which have the language behavior set. So when I make this big JOINED SQL, I get WHERE id_language = 1 AND id_language = 1 … 2 times, which fails, being ambiguous. Any way I can integrate an alias in there ? Thanks.

    • Matti says

      I would be inclined to use DAO rather than ActiveRecord – I only use AR for simple queries, since its performance declines rapidly with more complex queries. If you need a data provider, just use CSqlDataProvider.

      • Alex says

        Hey, I was using CDbcriteria’s “WITH” option to generate my SQL. I threw that away and wrote my SQL with JOINS by hand, and set the language_id column with the session variable and with an alias before it. It works now.

        Thank you very much, your behavior si absolutely awesome, it showed me a way to build my multi language website. I also combined this with a language URL’s behavior and it works great. Again, millions of thanks.



Some HTML is OK

or, reply to this post via trackback.


− four = zero