Usable for Yii 1.x but not maintained anymore. Feel free to use the code to create a new version. I think it should be hosted on Github (multiple contributors, issues, pull requests...).
This behavior allow you to create multilingual models and to use them (almost) like normal models. For each model, translations have to be stored in a separate table of the database (ex: PostLang or ProductLang), which allow you to easily add or remove a language without modifying your database.
First example: by default translations of current language are inserted in the model as normal attributes.
// Assuming current language is english (in protected/config/main.php : 'sourceLanguage' => 'en')
$model = Post::model()->findByPk((int) $id);
echo $model->title; //echo "English title"
//Now let's imagine current language is french (in protected/config/main.php : 'sourceLanguage' => 'fr')
$model = Post::model()->findByPk((int) $id);
echo $model->title; //echo "Titre en Français"
$model = Post::model()->localized('en')->findByPk((int) $id);
echo $model->title; //echo "English title"
//Here current language is still french
Second example: if you use multilang() in a "find" query, every translation of the model are loaded as virtual attributes (title_en, title_fr, title_de, ...).
$model = Post::model()->multilang()->findByPk((int) $id);
echo $model->title_en; //echo "English title"
echo $model->title_fr; //echo "Titre en Français"
Requirements ¶
Yii 1.1 or above
Usage ¶
Here an example of base 'post' table :
~~~
[sql]
CREATE TABLE IF NOT EXISTS post
(
id
int(11) NOT NULL AUTO_INCREMENT,
title
varchar(255) NOT NULL,
content
TEXT NOT NULL,
created_at
datetime NOT NULL,
updated_at
datetime NOT NULL,
enabled
tinyint(1) NOT NULL DEFAULT '1',
PRIMARY KEY (id
)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
~~~
And his associated translation table (configured as default), assuming translated fields are 'title' and 'content':
~~~
[sql]
CREATE TABLE IF NOT EXISTS postLang
(
l_id
int(11) NOT NULL AUTO_INCREMENT,
post_id
int(11) NOT NULL,
lang_id
varchar(6) NOT NULL,
l_title
varchar(255) NOT NULL,
l_content
TEXT NOT NULL,
PRIMARY KEY (l_id
),
KEY post_id
(post_id
),
KEY lang_id
(lang_id
)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
ALTER TABLE postLang
ADD CONSTRAINT postlang_ibfk_1
FOREIGN KEY (post_id
) REFERENCES post
(id
) ON DELETE CASCADE ON UPDATE CASCADE;
~~~
Attach this behavior to the model (Post in the example). Everything that is commented is with default values.
public function behaviors() {
return array(
'ml' => array(
'class' => 'application.models.behaviors.MultilingualBehavior',
//'langClassName' => 'PostLang',
//'langTableName' => 'postLang',
//'langForeignKey' => 'post_id',
//'langField' => 'lang_id',
'localizedAttributes' => array('title', 'content'), //attributes of the model to be translated
//'localizedPrefix' => 'l_',
'languages' => Yii::app()->params['translatedLanguages'], // array of your translated languages. Example : array('fr' => 'Français', 'en' => 'English')
'defaultLanguage' => Yii::app()->params['defaultLanguage'], //your main language. Example : 'fr'
//'createScenario' => 'insert',
//'localizedRelation' => 'i18nPost',
//'multilangRelation' => 'multilangPost',
//'forceOverwrite' => false,
//'forceDelete' => true,
//'dynamicLangClass' => true, //Set to true if you don't want to create a 'PostLang.php' in your models folder
),
);
}
Look into the behavior source code and read the comments of each attribute to understand how to configure them and how to use the behavior.
In order to retrieve translated models by default, add this function in the model class:
public function defaultScope()
{
return $this->ml->localizedCriteria();
}
You also can modify the loadModel function of your controller to minimize the changes to make in your controller:
public function loadModel($id, $ml=false) {
if ($ml) {
$model = Post::model()->multilang()->findByPk((int) $id);
} else {
$model = Post::model()->findByPk((int) $id);
}
if ($model === null)
throw new CHttpException(404, 'The requested post does not exist.');
return $model;
}
and use it like this in the update action :
public function actionUpdate($id) {
$model = $this->loadModel($id, true);
...
}
Here is a very simple example for the form view :
<?php foreach (Yii::app()->params['translatedLanguages'] as $l => $lang) :
if($l === Yii::app()->params['defaultLanguage']) $suffix = '';
else $suffix = '_'.$l;
?>
<fieldset>
<legend><?php echo $lang; ?></legend>
<div class="row">
<?php echo $form->labelEx($model,'title'); ?>
<?php echo $form->textField($model,'title'.$suffix,array('size'=>60,'maxlength'=>255)); ?>
<?php echo $form->error($model,'title'.$suffix); ?>
</div>
<div class="row">
<?php echo $form->labelEx($model,'content'); ?>
<?php echo $form->textArea($model,'content'.$suffix); ?>
<?php echo $form->error($model,'content'.$suffix); ?>
</div>
</fieldset>
<?php endforeach; ?>
To enable search on translated fields, you can modify the search() function in the model like this :
public function search()
{
$criteria=new CDbCriteria;
//...
//here your criteria definition
//...
return new CActiveDataProvider($this, array(
'criteria'=>$this->ml->modifySearchCriteria($criteria),
//instead of
//'criteria'=>$criteria,
));
}
Warning: the modification of the search criteria is based on a simple str_replace so it may not work properly under certain circumstances.
It's also possible to retrieve languages translation of two or more related models in one query. Example for a Page model with a "articles" HAS_MANY relation :
$model = Page::model()->multilang()->with('articles', 'articles.multilangArticle')->findByPk((int) $id);
echo $model->articles[0]->content_en;
With this method it's possible to make multi model forms like it's explained here
History ¶
24/03/2012: First release
28/03/2012: It's now possible to modify language when retrieving data with the localized relation.
Example:
$model = Post::model()->localized('en')->findByPk((int) $id);
30/03/2012: Correction for the after save method.
26/04/2012 Modification of the rules definition for translated attributes:
if you set forceOverwrite to true, every rules defined in the model for the attributes to translate will be applied to the translations.
if you set forceOverwrite to false (default), every rules defined in the model for the attributes to translate will be applied to the translations except "required" rules that will only be applied to the default translation.
28/06/2012 ** Bug fix ** two2wyes found and fixed a bug that prevented translations to be correctly saved on attributes that only have a "required" rule and with the "forceOverwrite" option set to false. Thanks again to him. See the thread
Resources ¶
Authors ¶
Many thanks to guillemc who made the biggest part of the work on this behavior (see original thread).
Manupulation data with afterFind [error]
Excellent extension but when use afterFind() function in model class it give error "Property "Category.title_en" is not defined."
How to fix? Help me please!
Model:
#Events public function afterFind() { # Scenario for nested view of title if(self::model()->scenario === self::SCENARIO_NESTED_VIEW) { $sp = '|'; for($i=0;$i<$this->level;$i++) $sp .= '-'; $this->title = $sp . $this->title; foreach (Yii::app()->params['translatedLanguages'] as $l => $lang) { $a = "title_$l"; $this->$a = $sp . $this->$a; } } parent::afterFind(); } #Additional function public function nestedFindAll() { $model = Category::model(); $model->scenario = self::SCENARIO_NESTED_VIEW; return $model->findAll(array('order'=>'lft')); }
Controller:
Yii::app()->language='en'; $categories = Category::model()->nestedFindAll(); foreach($categories as $category) { echo $category->title . "<br />"; }
Re: Manupulation data with afterFind [error]
@eibrahimov: Have you tried to put "parent::afterFind()" at the beginning of your afterFind() function ?
You also have to modify the "nestedFindAll()" function like this I think:
public function nestedFindAll() { $model = Category::model(); $model->scenario = self::SCENARIO_NESTED_VIEW; return $model->multilang()->findAll(array('order'=>'lft')); }
rules problem
hi.
I have text attributes(textarea) for all language. When I write rules for 'text' attribute it work only for default language? How do this for all language? Please help me(
public function rules() { // NOTE: you should only define rules for those attributes that // will receive user inputs. return array( array('title, text, categories', 'required'), array('status, featured', 'numerical', 'integerOnly' => true), array('title, alias, metakey', 'length', 'max' => 255), array('text', 'unsafe'), array('metadesc', 'safe'), array('fileimg', 'file', 'types' => array('jpg', 'jpeg', 'gif', 'png'), 'allowEmpty' => true), array('modifierUser, creatorUser, id, title, text, alias, default_image, metakey, metadesc, created_by, created_at, modified_by, modified_at, status, featured', 'safe', 'on' => 'search'), ); }
Re: rules problem
@eibrahimov: Hi, I've updated the behavior file and added explanations in the history part of the page. I think it will help you. Long story short: you can now have every rules defined in your model applied on translations, even required ones.
If not translate
Hello. Help me please. How to do? When not found translate of item, not show default language. I want to check if not translated item redirect to home page!? Thanks before! ;)
Re: If not translate
@eibrahimov: Hi, I'm not sure I understand well your problem but you can try to set the "forceOverwrite" option of the behavior to false and then if there is no translation for one item it will be empty. And you will be able to test if it's empty and redirect to home if it is. Hope this help.
[SOLVED] admin view...
Hi.
First: thanks for this extension! Very useful.
Here's my problem: I'm trying to put into admin grid all translation for a particular element on a single row but I'm failing... it's saying that [value]_[lang] it's not existing.
Thx
I solved this prolbem.
In admin view I added multilang to dataprovider definition:
$this->widget('zii.widgets.grid.CGridView', array( 'id'=>'feature-grid', 'dataProvider'=>$model->multilang()->search(), 'filter'=>$model, 'columns'=>$arrColumns, )); ?>
$arrColumns contains non translated field plus every multilanguage field:
foreach (Yii::app()->params['translatedLanguages'] as $l => $lang) { $arrColumns[] = array( 'type'=>'raw', 'header'=>'Title in '.$lang, 'value'=>'$data->checkIfFieldIsPopulated("title_'.$l.'") ? CHtml::image(bu()."/css/imgs/common/action_check_sharp_thick.png","text available") : "-"', 'htmlOptions'=>array("style"=>"text-align:center"), ); [...] }
Actually this code show an "V" image to notify that a translated text is present.
Resulting admin table is something like this:
Id | Title | Italian | Italian | English | English | Spanish | Spanish | | title | descr. | title | descr. | title | descr. -------+--------+---------+---------+---------+---------+---------+--------- 1 | abc | v | v | v | v | v | v 2 | def | v | v | v | v | - | - 3 | ghi | v | v | v | v | v | v
...that means spanish translation for element 2 are missing.
Hope can help!
Re: admin view...
@sirfaber: Hi, I'm sorry but I can't help you with so little information. Maybe you can send me a private message on the forum (here) with some code and the complete error so I can see what's going on ;)
Another problem... original model relations
I have this 2 models:
Both are multilanguage.
In Product model I have:
public function relations() { return array( 'technicalDatas' => array(self::HAS_MANY, 'TechnicalData', 'product_id'), ); }
In "views/product/view.php" I'm trying to show all linked TechnicalData objects with all translated fields (actually this is an administrator page not for public).
My code is:
foreach ($model->technicalDatas as $techData) { $temp = $techData->multilang(); [...] }
The problem is that $temp doesn't have any
<field>_<lang>
attribute...I took a look into sql query log and I noticed that is retrived just "localized" data (ie my actual choosen language in the example is spanish):
~~~
[sql]
SELECT
technicalDatas
.id
ASt1_c0
,technicalDatas
.product_id
ASt1_c1
,technicalDatas
.description
ASt1_c2
,technicalDatas
.units
ASt1_c3
,technicalDatas
.value
ASt1_c4
,[...]
,
i18nTechnicalData
.l_id
ASt2_c0
,i18nTechnicalData
.technical_data_id
ASt2_c1
,i18nTechnicalData
.lang_id
ASt2_c2
,i18nTechnicalData
.l_description
ASt2_c3
,i18nTechnicalData
.l_units
ASt2_c4
,i18nTechnicalData
.l_value
ASt2_c5
,[...]
FROM
tbl_technical_data
technicalDatas
LEFT OUTER JOINtbl_technical_data_lang
i18nTechnicalData
ON (i18nTechnicalData
.technical_data_id
=technicalDatas
.id
) AND (i18nTechnicalData.lang_id='es') WHERE (technicalDatas
.product_id
=1)~~~
Any idea?
Thx
Re: Another problem... original model relations
@sirfaber: Hi! You should retrieve your product model like this (technicalDatas objects in product will have all translations inside) :
$product = Product::model()->multilang()->with('technicalDatas', 'technicalDatas.multilangTechnicalData')->findByPk((int) $id);
or if you want to keep the "foreach" to retrieve translations (but it will make more db queries for the same result) :
foreach ($model->technicalDatas as $techData) { $temp = TechnicalData::model()->multilang()->findByPk($techData->id); [...] }
Virtual attributes
For those like me who don't have the localized fields in the main table but only in the localized one ( if keeping this exemple Post don't have title and content ), then you have to add the fields ( with same name than PostLang) in your Model Class as public properties .
class Post extends CActiveRecord { public $l_title; public $l_content; ... }
By the way it's not a good practice to have duplicate fields on differents table, also not a good practive to use camelCase ( using underscore is better ) for table name as some Database Server Like Postgres are not case sensitive for table names.
Duplicated records
On the insert or update of a record, the record for the default language it's saved in "table" and in "table_lang"
Reading the documentation i thinked that the record for the main language it's saved only in the main table, not in table_lang, isn't it?
Where i wrong?
Thanks for this fantastic class!
find query with localized attribute
Hi!
In your examples you load a model usinf findByPk, but what if I need to load a model by a localized attribute (ex. url defined in db).
Page::model()->->find('url=:title', array( ':title'=>$_GET['title'], ));
In my default language it works ok, but in translated one, it should refer to l_url instead of url, and it throws a 404 error.
What do I have to do in that case?
Thanks!
Diana.
Bug with PostgreSQL 9.1
Hi, thanks for such a great extension. But I found this error when using PostgreSQL 9.1 :
CDbCommand failed to execute the SQL statement: SQLSTATE[42P01]: Undefined table: 7 ERROR: missing FROM-clause entry for table "i18nannualpurchase" LINE 1: ...nnual_purchase_id"="t"."annual_purchase_id") AND (i18nAnnual... ^. The SQL statement executed was: SELECT COUNT(DISTINCT "t"."annual_purchase_id") FROM "tbl_annual_purchase" "t" LEFT OUTER JOIN "tbl_annual_purchase_lang" "i18nAnnualPurchase" ON ("i18nAnnualPurchase"."annual_purchase_id"="t"."annual_purchase_id") AND (i18nAnnualPurchase.lang_id='en_us')
It is because there is no quotation before and after the localizedRelation. Fixed by changing line 259 into :
$owner->getMetaData()->relations[$this->localizedRelation] = new $class($this->localizedRelation, $this->langClassName, $this->langForeignKey, array('on' => '"'.$this->localizedRelation.'"' . "." . $this->langField . "='" . $lang . "'", 'index' => $this->langField));
Note : Adding quotation mark before and after $this->localizedRelation
Nice work, shame about the design
This is a really nice attempt at multi language support, however its fundamentally flawed in design.
The post table should not have the columns 'title' and 'content'. Instead, these columns should be in the postlang table. Sharing the same data type between the two tables is very bad design. What if I want to change my default language?
'postlang' should not have an auto_increment 'l_id' column. Instead, 'post_id' and 'lang_id' should be a composite primary key.
It should also include a languages table, otherwise where are you controlling available languages in the application? In this case 'lang_id' should be an integer referencing this table.
Re: Nice work, shame about the design
@Backslider: How about you developing a version of the extension without "flawed design" and giving it to the community?
Because if all you can make is write about others works to describe already known problems without trying to contribute to the solution, you're just another troll...
First problem, the translated columns in the main table: Yes you're right, but this is not easy to do with Yii models. Feel free to provide a solution (with some code).
Second problem, the primary key should be composite and not integer: I agree with you but as for the first problem, this is not easy to do with Yii models. Same thing, show us your code.
Third problem, make a language table: if you had read the doc and try the extension, you would already know that the languages are in the main config file of the application. So yes they are centralized without having to make additional queries to retrieve them. But it's also possible for anyone to store the languages in a database table because the languages are passed to the extension in the configuration.
I've got a good news for you and everyone else: the extension will be put on github so that everyone can contribute easily to it. I don't use Yii anymore so I won't be able to maintain it.
I will update the extension page to point to the github repository as soon as it will be available.
Re: you're just another troll
Fred, I think you are being somewhat precious. You essentially agree with my "criticism", yet wish to label me a troll? Those comments are designed solely to push development toward a better solution, not to offend, so sorry if they did. I even said it was "really nice". As far as I am concerned, my comments are a contribution - I am contributing my own knowledge.
Yii models do not have any problem with composite keys - its only if you try to generate CRUD that there is an issue. Since the postlang table does not require CRUD, there is no issue.
Clearly you are almost there with this extension and I really don't see why you chose to duplicate columns in the main table for the default language where a simple flag in postlang would suffice.
I think that a separate language table is more robust, its far simpler to add languages (particularly for the end user) and its easy to store language specific data. Its simple enough to retrieve this data on application startup and keep it in $_SESSION, so only one query.
Good that you are putting it on Github if you don't plan to work on it further. I'd be more than happy to fork and improve on it.
Re: Re: you're just another troll
@Backslider: If you don't want to be called troll, please avoid the words "shame" and "flawed". If it's not to offend, I don't see the point of using these words. Maybe they have another sense for you than for me because English is not my language. In that case, sorry for the misunderstanding.
As I've said before, ideas and contributions are always welcome. I see that you're a big poster on the Yii forum so I can imagine that you use the framework often and know a lot of things about it. Certainly a lot more than me because I don't really use Yii. The only "usage" I had is my small contribution to this extension and other small things I've made in order to make the framework usable by colleagues in my company. And it was 8 month ago. So you probably see technical solutions to these problems that I don't see.
The purpose of duplicating the translated attributes in the main table is not to store the default language values but to have attributes in the model with rules and everything. But, as I said, I'm not a fan of this solution. So if you think about another one, please share.
The default language is defined by the configuration of the behavior. I don't think that having a flag in the translations table is good because the flag would be duplicated for every translated entity and harder to modify that a simple string in the configuration. And if you choose to store your application's languages in a table, the default flag should be in that table.
For most of the application, storing the application's languages in the configuration is sufficient, that's why it's the solution presented in the doc. But I understand that there are use cases where you want to have a languages table specially when you want to be able to manage them using a backend (add/remove languages, store extra data like a flag image or even translate the language label). And of course, in that case, languages data should be cached to avoid extra queries, with sessions or even better the Yii cache system.
About the Github repo, I've been contacted by another member of the forum (thyandrecardoso) who offers to create it. I've accepted that and I'm waiting for his answer to update the page. But if you want to do it or help him, you can contact him to organize that.
Re. I don't think that having a flag in the translations table
Of course not, it should be in the languages table... that was just a brain fart :-)
I look forward to seeing it on Github.
Dynamically Setting Application Languages
This is an example of how I set languages in my applications. I have this in a behavior class which extends CBehavior within my components directory:
public function beginRequest() { if(!isset(Yii::app()->params['languages'])) { $languages = Language::model()->findAll(); $language_array = array(); foreach($languages as $val) { $language_array[$val->id] = $val->name; if($val->default == 1) Yii::app()->params['defaultLanguage'] = $val->id; } Yii::app()->params['languages'] = $language_array; } // user changes the language if(isset($_GET['language'])) { if(array_key_exists($_GET['language'], Yii::app()->params['languages'])) { Yii::app()->setLanguage($_GET['language']); } } }
New Extension Coming. Merry Christmas!
I have a behavior working, as I have described. That is, all translatable columns are now in their own language table, without any mixing of attributes with the main table.
I just need to complete validation, which can be done using beforeSave() within the behavior. Its possible to merge errors from two different models, so it should be simple enough. Once I have completed testing I will release it.
New Extension Uploaded
Taking up Fred's challenge, I have released a new language extension that behaves in the way I have described.
Validation was in fact best done using afterValidate().
http://www.yiiframework.com/extension/yii-language-behavior/
error
'db'=>array(
'connectionString' => 'mysql:host=localhost;dbname=rcsalg', 'emulatePrepare' => true, 'username' => 'root', 'password' => '', 'charset' => 'utf8', //'tablePrefix'=>'', 'tablePrefix'=>'x_' ),
i use tableprefix in my website
and i'm getting this error please any solution
The relation "i18nCategory" in active record class "Category" is specified with an invalid foreign key "category_id". There is no such column in the table "x_categorylang".
problem gridview
I have this 2 models
In UserMessage model I have:
public function relations() { return array( 'user' => array(self::BELONGS_TO, 'User', 'user_id'), 'sendUser' => array(self::BELONGS_TO, 'User', 'send_user_id'), ); } public function search() { .................... $criteria->with = array('user','sendUser'); $criteria->compare('user.username',$this->user_search,true); $criteria->compare('sendUser.username',$this->sendUser_search,true); ................... }
problem in gridview "UserMessage":
CDbCommand failed to execute the SQL statement: SQLSTATE[42000]: Syntax error or access violation: 1066 Not unique table/alias: 'i18nUser'. The SQL statement executed was: SELECT COUNT(DISTINCT `t`.`id`) FROM `user_message` `t` LEFT OUTER JOIN `user` `user` ON (`t`.`user_id`=`user`.`id`) LEFT OUTER JOIN `userLang` `i18nUser` ON (`i18nUser`.`user_id2`=`user`.`id`) AND (i18nUser.language='en') LEFT OUTER JOIN `user` `sendUser` ON (`t`.`send_user_id`=`sendUser`.`id`) LEFT OUTER JOIN `userLang` `i18nUser` ON (`i18nUser`.`user_id2`=`sendUser`.`id`) AND (i18nUser.language='en') WHERE (t.user_id=2)
Thank you
The table "{{pay_grades_lang}}" for active record class "PayGradesLang" cannot be found in the database.
i have two tables pay_grades,pay_grades_lang but when i create object
new PayGrade from any place this error is happen
The table "{{pay_grades_lang}}" for active record class "PayGradesLang" cannot be found in the database.
how can i fixed it ?
The table "{{postLand}}" for active record class "PostLang" cannot be found in the database.
@mahmoud khafagy
You can easily solve this issue by setting 'tablePrefix' => '', in your db connection setting in main.php
This can cause some trouble with other modules that use tbl_prefix
Hope it Help!
Nice
nice extension i was looking for this.
yii 2.0?
Any updates for yii2 porting?
I like this ext very much. And have to port it for my project https://digiscend.com
Re: Yii 2.0?
@tjin I'm not using Yii anymore so I won't be able to port this to Yii 2.
I think any motivated person should create a GitHub repository to manage the source of the new version.
If you have any questions, please ask in the forum instead.
Signup or Login in order to comment.