I haven’t tried, but CActiveDataProvider takes a CDbCriteria object as a parameter. So, if you pass it a criteria object that selects also the related language tables (using “with” or “scopes”, what you prefer) then it all should work. What I mean is that you should pass CActiveDataProvider the same criteria that you pass to a findAll() call in order to get the translations.
The behavior I’m using expects the main model to have the main attributes defined, so that you can call $news->title, and get the title in the current language. But you could define these placeholder attributes directly in the model, instead of the database. So, in your News model, you’d define the attributes:
public $title;
public $intro;
public $content;
...
This would allow you to continue using $news->title and get the translated title, even if the title field doesn’t exist in the news table.
But doing this means you won’t have the fallback value. If the title translated in the current language does not exist you will get a blank title…
If I understand correctly, I could fill those attributes up with the current language first, check if they’re empty and use a fallback language when necessary. Is there any drawback to this method (except for the extra query)?
I guess it would be better to query both languages at the same time (current and fallback) - and have the code figure out which one to use - or using a query that will select the right values at once?
Something like:
SELECT o.id
, o.type
, o.code
, o.position
, ifnull(t.name,d.name) name
, ifnull(t.description,d.description) description
FROM base_material o
INNER JOIN base_material_i18n d
ON ( o.id=d.base_material_id)
LEFT OUTER JOIN base_material_i18n t
ON ( d.base_material_id=t.base_material_id AND t.localization_id='nl' )
WHERE d.localization_id='en'
I don’t think I follow you. You could make a query like that, but the purpose of all this was to avoid having to do it. Just by declaring relations and using a behavior you should be able to get the data in the right language.
What I was trying to say is that if you don’t want to have the name and description attributes on the main table, you don’t have to. You can declare them as public properties of your model, and let the behavior populate them with the right data coming from the translations table.
But I think you are right in that you could get current and fallback languages in the same query. I’d try to define a relation with the i18n table like this:
'i18n' => array(self::HAS_MANY, 'ModelLang', 'model_id', 'on'=>"i18n.lang_id='".Yii::app()->language."' OR i18n.lang_id='".Yii::app()->params['defaultLanguage']."'", 'index'=>'lang_id'),
Then, in the behavior’s afterFind() method, you would check for those translations, and set the main model attribute to either of the two (the current language if it exists, the fallback otherwise).
Sorry, I made a mistake in my example. In Section model, the “localized” scope should use the relation “i18nSection”, not “i18n”, since that’s the relation name we have defined in the model. (You can use whatever name you want for the relation, just make sure you use the same name in the scope).
//Section.php
public function localized() {
$this->getDbCriteria()->mergeWith(array(
'with'=>'i18nSection',
));
return $this;
}
Thanks for your fast reply i’ll try it. should i create two table for each model. like news for News model and news_lang for NewsLang model. sorry i’m new bie
You should create rules for those attributes, as if they were attributes from the model itself. Better if you use params instead of hardcoding them, something like:
You probably want to add a __isset magic overload as well:
public function __isset($name){
if (! parent::__isset($name)) {
return ($this->hasLangAttribute($name));
} else {
return true;
}
}
I haven’t tested it yet, but something along those lines will do.
I also have suggestions for better names for relName1 and relName2: localizedRelation and multilangRelation
As a final thought, the point where you create $this->langForeignKey from $owner->tableName() is broken if tableName returns a string containing the database name (e.g. ‘mydb.products’)
You can replace
$owner->tableName()
with
array_pop(explode('.',$owner->tableName()))
to achieve that extra compatibility.
But those are all small points. All in all, this multilang stuff is really useful.
I’m using the MultilingualBehaviour, and it’s working fine in an example I created. The user gets to choose in wich language wants to display the sections and posts related, and it gets the translation correctly.
But my question is how I change the Models and Controllers of the Posts and Section (in the example) to be able to introduce all the files in the diferent lenguages in the Add and Update Form?
Using the previous MultilingualActiveRecord class, by defining the localizedAttributes, languages, etc. it was correctly displayed all the language fields on the form view…
Can you show us the code in order to achieve that?
Hi, it’s looking in the Post model because that’s what we’re after, in “multilang” mode: to make appear the attributes of PostLang model as if they were attributes of Post, for easier handling (like title_es, title_de, etc).
Probably the problem is that you are not loading your model in multilang mode. In my controllers I use this modified loadModel():
public function loadModel($id, $ml=false) {
if ($ml) {
$model = News::model()->with('multilang')->findByPk((int) $id);
} else {
$model = News::model()->findByPk((int) $id);
}
if ($model === null)
throw new CHttpException(404, 'The requested page does not exist.');
return $model;
}
Then in my update action I load the model like this:
public function actionUpdate($id) {
$model = $this->loadModel($id, true);
...
}
second: You’re message above made me think, whereupon I tried the following, which works so far:
I created a file ‘[font=“Courier New”]ActiveRecord.php[/font]’ with the following content (all my models extend this base model class ActiveRecord):
class ActiveRecord extends CActiveRecord
{
/**
* @array $translations translations of all translatable attributes of the current model
* in all translation languages.
* To get the translation of $field in the language $lang use: $translations[$lang][$field]
* To add a translation to the array, use the addTranslation() method.
* Since getter/setter mothods are declared, you can access the property with YourModelClass::model()->translations
*/
public $translations = array();
public function addTranslation($lang, $field, $value) {
$this->translations[$lang][$field] = $value;
}
public function getTranslations($array) {
return $this->translations;
}
public function setTranslations($tarray) {
$this->translations = $tarray;
}
public function multilang() {
$this->getDbCriteria()->mergeWith(array('with' => 'multilang'));
return $this;
}
public function behaviors()
{
return array(
'translation' => array(
'class' => 'ext.behaviors.TranslationBehavior',
'sourceLanguage' => Yii::app()->sourceLanguage,
'language' => Yii::app()->language,
'translationLanguages' => Yii::app()->params->translationLanguages,
'translationRelation' => RES::lc(get_class($this)).'translation',
'multiLangRelation' => 'multilang',
'translationActive' => (Yii::app()->sourceLanguage==Yii::app()->language)?false:true,
)
);
}
I put this all here into the base file, since the code will always be the same for each model and it will be needed by almost all of my models (and if not, that’s no problem, they get ignored).
Note: RES::lc is just a helper function turning every letter into lower case.
Then in my particular model file, say [font="Courier New"]Section.php[/font], I add:
/**
* $translatableAttributes: array of the translatable attributes of this class (e.g. array('Name', 'Description')).
* Add translatable attribute names, or leave empty if there are none. Don't remove this declaration.
*/
public $translatableAttributes = array('Name', 'Description');
// Don't forget to add the new attribute $translations to the 'safe' rule in your (every) model:
public function rules() {
return array(
...
array('Name, Description, translations', 'safe', 'on'=>'search'),
);
}
In the form, the according textfields are added for example with:
// $lang is the current language in the loop of all translationLanguages
echo $form->textField($model,"translations[$lang][Name]", array('maxlength' => 100));
In the Controller, the massive assignment looks like this:
first of all thanks for the (I suppose, because I havn’t tried it yet) great work! has anyone tried to compile the above behaviours, components and changes to be made in the models and controllers and publish it as an extension?
I’m going to try to filter all this information and use it to suit my needs anyways, but I just wanted to get the idea out there.