Yii Framework Forum: Multilingual models - Yii Framework Forum

Jump to content

  • (5 Pages)
  • +
  • 1
  • 2
  • 3
  • 4
  • 5
  • You cannot start a new topic
  • You cannot reply to this topic

Multilingual models Rate Topic: ***** 4 Votes

#41 User is offline   heal 

  • Junior Member
  • Pip
  • Yii
  • Group: Members
  • Posts: 33
  • Joined: 08-December 10

Posted 10 December 2011 - 08:51 AM

Hi! Thank you for that nice behavior. I start to use it now, but I can't figure out, how can I reate CRUD for create translations.
Can anybody help me out with this?

I've created inputs in my views like this:
<div class="row">
	<?php echo $form->labelEx($model,'title'); ?>
	<?php echo $form->textField($model,'title',array('maxlength'=>50)); ?>
	<?php echo $form->textField($model,'title_hu',array('maxlength'=>50)); ?>
	<?php echo $form->textField($model,'title_en',array('maxlength'=>50)); ?>
</div>


When I post the form, I got these errors:
Failed to set unsafe attribute "title_hu".
Failed to set unsafe attribute "title_en".

This post has been edited by heal: 10 December 2011 - 11:35 AM

0

#42 User is offline   guillemc 

  • Junior Member
  • Pip
  • Yii
  • Group: Members
  • Posts: 21
  • Joined: 14-October 09

Posted 12 December 2011 - 06:41 AM

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:

public function rules() {
	$rules = array(
		array('title', 'required'),
		array('title', 'length', 'max' => 80),
		array('content', 'safe'),			
		array('id, title', 'safe', 'on' => 'search'),
	);
	foreach (Yii::app()->params['translateLangs'] as $k => $v) {
		$rules[] = array('title_'.$k, 'length', 'max' => 80);
		$rules[] = array('content_'.$k, 'safe');
	}
	return $rules;
}



View Postheal, on 10 December 2011 - 08:51 AM, said:

Hi! Thank you for that nice behavior. I start to use it now, but I can't figure out, how can I reate CRUD for create translations.
Can anybody help me out with this?

I've created inputs in my views like this:
<div class="row">
	<?php echo $form->labelEx($model,'title'); ?>
	<?php echo $form->textField($model,'title',array('maxlength'=>50)); ?>
	<?php echo $form->textField($model,'title_hu',array('maxlength'=>50)); ?>
	<?php echo $form->textField($model,'title_en',array('maxlength'=>50)); ?>
</div>


When I post the form, I got these errors:
Failed to set unsafe attribute "title_hu".
Failed to set unsafe attribute "title_en".

1

#43 User is offline   heal 

  • Junior Member
  • Pip
  • Yii
  • Group: Members
  • Posts: 33
  • Joined: 08-December 10

Posted 12 December 2011 - 04:30 PM

View Postguillemc, on 12 December 2011 - 06:41 AM, said:

You should create rules for those attributes..


Thanks for the help, guillemc!
0

#44 User is offline   G_Gus 

  • Newbie
  • Yii
  • Group: Members
  • Posts: 12
  • Joined: 29-August 11

Posted 13 December 2011 - 06:46 AM

View Postguillemc, on 24 November 2011 - 12:17 PM, said:

public function __get($name) {
  try { return parent::__get($name); } 
  catch (CException $e) {
    if ($this->hasLangAttribute($name))	return $this->getLangAttribute($name);
    else throw $e;
  }							
}

public function __set($name, $value) {
  try { parent::__set($name, $value); } 
  catch (CException $e) {
    if ($this->hasLangAttribute($name)) $this->setLangAttribute($name, $value);
    else throw $e;
  }					
}


The reason is that models currently don't have a way to check if a missing attribute can be obtained through a behavior's magic getter/setter. This is also the main reason why I first wrote this as a CActiveRecord subclass instead of a behavior. But I prefer to have it as a behavior, even with this inconvenience.


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.

Thanks to you and to everyone involved!
0

#45 User is offline   guillemc 

  • Junior Member
  • Pip
  • Yii
  • Group: Members
  • Posts: 21
  • Joined: 14-October 09

Posted 13 December 2011 - 10:06 AM

Thanks, good suggestions!

Now, if there was a way to get rid of this getter and setter overloads, it would start looking less like a hack and more like a real behavior :)

View PostG_Gus, on 13 December 2011 - 06:46 AM, said:

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.

Thanks to you and to everyone involved!

0

#46 User is offline   dianakwt 

  • Newbie
  • Yii
  • Group: Members
  • Posts: 3
  • Joined: 13-December 11

Posted 25 January 2012 - 07:20 AM

View Postheal, on 12 December 2011 - 04:30 PM, said:

Thanks for the help, guillemc!



Hi guillemc and heal!

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?

Thank you!!
0

#47 User is offline   dianakwt 

  • Newbie
  • Yii
  • Group: Members
  • Posts: 3
  • Joined: 13-December 11

Posted 26 January 2012 - 12:21 PM

Hi!

Now I get the fields in the forms,I understood that you have to edit the _form.php in orther to include the translate fields..

And the Add/Create works fine for me.

But the Edit/Update returns an error (I cannot even see the form):

Property "Posts.title_es" is not defined. //Posts is my Model and title_es the first field in PostLang

the error trace comes from:
return parent::__get($name);

public function __get($name) {
try { return parent::__get($name); } //this line
catch (CException $e) {
if ($this->hasLangAttribute($name)) return $this->getLangAttribute($name);
else throw $e;
}
}

So... i'm a bit lost in here, because I don't undestand why is looking for the file in the Posts model, instead of the PostsLang model...

Any help?


View Postdianakwt, on 25 January 2012 - 07:20 AM, said:

Hi guillemc and heal!

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?

Thank you!!

0

#48 User is offline   guillemc 

  • Junior Member
  • Pip
  • Yii
  • Group: Members
  • Posts: 21
  • Joined: 14-October 09

Posted 26 January 2012 - 01:37 PM

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);
    ...
  }



View Postdianakwt, on 26 January 2012 - 12:21 PM, said:

Hi!

Now I get the fields in the forms,I understood that you have to edit the _form.php in orther to include the translate fields..

And the Add/Create works fine for me.

But the Edit/Update returns an error (I cannot even see the form):

Property "Posts.title_es" is not defined. //Posts is my Model and title_es the first field in PostLang

the error trace comes from:
return parent::__get($name);

public function __get($name) {
try { return parent::__get($name); } //this line
catch (CException $e) {
if ($this->hasLangAttribute($name)) return $this->getLangAttribute($name);
else throw $e;
}
}

So... i'm a bit lost in here, because I don't undestand why is looking for the file in the Posts model, instead of the PostsLang model...

Any help?

0

#49 User is offline   dianakwt 

  • Newbie
  • Yii
  • Group: Members
  • Posts: 3
  • Joined: 13-December 11

Posted 27 January 2012 - 03:53 AM

Thanks a lot guillemc!!

I missed that completely! I'm not only a newbie on the forum, but also in yii developing... You did a very useful behavior!!

Now it's working perfectly.

Diana.


View Postguillemc, on 26 January 2012 - 01:37 PM, said:

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);
    ...
  }


1

#50 User is offline   mjamado 

  • Newbie
  • Yii
  • Group: Members
  • Posts: 1
  • Joined: 02-June 10

Posted 18 February 2012 - 10:43 PM

First of all, kudos to guillemc for making this incredibly useful behavior.

I have a sugestion, though, following on G_Gus footsteps; he sugested changing this...

$owner->tableName()


... into this:

array_pop(explode('.',$owner->tableName()))


But I'll go as far as changing this (already with his sugestion)...

str_replace(array('{{','}}'),'',array_pop(explode('.',$owner->tableName()))).'_id'


... into this:

$owner->getTableSchema()->primaryKey


Why? In my experience, more often than not, the foreign key is the primary key of the non-translated table. For me, it's every single time.
0

#51 User is offline   c@cba 

  • Junior Member
  • Pip
  • Yii
  • Group: Members
  • Posts: 43
  • Joined: 02-December 11

Posted 22 February 2012 - 07:14 AM

View Postguillemc, on 13 December 2011 - 10:06 AM, said:

Thanks, good suggestions!

Now, if there was a way to get rid of this getter and setter overloads, it would start looking less like a hack and more like a real behavior :)


Hi guillemc,

first:lots of thanks for this great behavior!
second: You're message above made me think, whereupon I tried the following, which works so far:

1. I created a file 'ActiveRecord.php' 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.

2. Then in my particular model file, say Section.php, 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'),
		);
	}



3. 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));


4. In the Controller, the massive assignment looks like this:
$model->setAttributes($_POST['Section']);
$model->translations = $_POST['Section']['translations'];
$model->save();


5. In the Behavior file 'TranslationBehavior.php' I made the according changes:
public function afterFind($event) {
...
	} else if ($owner->hasRelated($this->multiLangRelation)) {
		$related = $owner->getRelated($this->multiLangRelation);
		foreach ($this->translationLanguages as $lang) {
			// change her -->
			foreach ($owner->translatableAttributes as $field) {
				$owner->addTranslation( $lang, $field, isset($related[$lang][$field])?$related[$lang][$field]:null );
			}
		}
	}
}
...
public function afterSave($event) {
...
	foreach ($this->translationLanguages as $lang) {
		if (!isset($rs[$lang])) {
			$obj = new $this->translationClassName;          
			$obj->{$this->langField} = $lang;
			$obj->{$this->translationForeignKey} = $ownerPk;          
		} else {
			$obj = $rs[$lang];
		}
		// change here -->
		foreach ($owner->translations[$lang] as $fieldname => $value) {
			$obj->$fieldname = $value;
		}
		$obj->save(true);
	}
...

0

#52 User is offline   int21hi 

  • Newbie
  • Yii
  • Group: Members
  • Posts: 1
  • Joined: 24-February 12

Posted 29 February 2012 - 07:25 AM

thanks guillemc, i figure out finally to run it up. :)

i have such situation

//Section.php
public function relations() {
  return array(
    'subsections' => array(self::HAS_MANY, 'Section', 'parent_id'),
    'i18nSection' => array(self::HAS_MANY, 'SectionLang', 'section_id', 'on'=>"i18nSection.lang_id='".Yii::app()->language."'", 'index'=>'lang_id'),
    'multilang' => array(self::HAS_MANY, 'SectionLang', 'section_id', 'index' => 'lang_id'),
  );
}


i made the changes for Section localized method

//Section.php
public function localized() {
  $this->getDbCriteria()->mergeWith(array(                      
    'with'=>array(
        'i18nSection'=>array(),
        'subsections'=>array('scopes'=>array('localized')),
    ),
  ));
  return $this;
}


and i got the error

Fatal error: Maximum function nesting level of '100' reached, aborting!


how should i act in this case, with relation to itself ? any suggestions, please.

This post has been edited by int21hi: 12 March 2012 - 05:41 AM

0

#53 User is offline   Trakina 

  • Newbie
  • Yii
  • Group: Members
  • Posts: 9
  • Joined: 19-March 12
  • Location:Portugal

Posted 22 March 2012 - 06:03 AM

Hey guys,
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.
0

#54 User is offline   fredpeaks 

  • Newbie
  • Yii
  • Group: Members
  • Posts: 15
  • Joined: 20-March 12

Posted 22 March 2012 - 09:47 AM

Hello everyone,

I've tried to make a compilation of everything in this thread and to make things work only with a behavior, without changing the model. So you don't have to overload getters or setters in the model. Only to attach the behavior and define the defaultScope.

And of course create a new table in the database to store translations.
The table has this form by default (example for a base table named "post") :
postLang: l_id,post_id,lang_id,[list of attributes to translate prefixed by 'l_'(configurable)]

The attributes to translate have to be also in the base table but with no prefix.

Here is the file : Attached File  MultilingualBehavior.php (9.79K)
Number of downloads: 19

To attach the behavior to the model, use this piece of code (everything that is commented is default values):

public function behaviors() {
    /*$table_name_chunks = explode('.', $this->tableName());
    $simple_table_name = str_replace(array('{{', '}}'), '', array_pop($table_name_chunks));*/
    return array(
        'ml' => array(
            'class' => 'application.components.MultilingualBehavior',
            /*
            'langClassName' => __CLASS__ . 'Lang', //example if model class name is 'Post' : 'PostLang'
            'langTableName' => $simple_table_name . 'Lang', //example if table name is 'post' : 'postLang'
            'langForeignKey' => $simple_table_name . '_id', //example if table name is 'post' : 'post_id'
            'langField' => 'lang_id',
            'localizedRelation' => 'i18n' . __CLASS__, //example if model class name is 'Post' : 'i18nPost'
            'multilangRelation' => 'multilang' . __CLASS__, //example if model class name is 'Post' : 'multilangPost'
            'dynamicLangClass' => true, //Set to true if you don't want to create a 'PostLang.php' in your models folder
            'localizedPrefix' => 'l_', //In the lang table, localized attributes have to be prefixed with this to avoid collisions in some queries and allow search on translations
            */
            'localizedAttributes' => array('slug', 'title'), //attributes of the model to be translated
            'languages' => Yii::app()->params['translatedLanguages'], // array of your translated languages. Example : array('fr' => 'Fran├žais', 'en' => 'English')
            'primaryLang' => Yii::app()->params['defaultLanguage'], //your main language. Example : 'fr'
        ),
    );
}


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 as guillemc suggested in a previous post :

public function loadModel($id, $ml=false) {
    if ($ml) {
        $model = Post::model()->with('multilang')->findByPk((int) $id);
    } else {
        $model = Post::model()->findByPk((int) $id);
    }
    if ($model === null)
        throw new CHttpException(404, 'The requested page 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,'slug'); ?>
    <?php echo $form->textField($model,'slug'.$suffix,array('size'=>60,'maxlength'=>255)); ?>
    <?php echo $form->error($model,'slug'.$suffix); ?>
    </div>

    <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>
</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.

Hope this will help, it's my first contribution so be cool please :). And sorry for my poor english.
1

#55 User is offline   Trakina 

  • Newbie
  • Yii
  • Group: Members
  • Posts: 9
  • Joined: 19-March 12
  • Location:Portugal

Posted 22 March 2012 - 02:07 PM

@fredpeaks YOU FREAKIN' ROCK, that's a sweet compilation, man... Thanks a lot!
0

#56 User is offline   fredpeaks 

  • Newbie
  • Yii
  • Group: Members
  • Posts: 15
  • Joined: 20-March 12

Posted 22 March 2012 - 05:57 PM

Thanks Trakina!

I've edited my previous post and the behavior file with bug corrections and improvements. Mainly I've tried to enable search on translated fields and to do so I've changed a little the way the lang table has to be created in the database.
Now the translated attributes and the id have to be prefixed (with 'l_' by default, but configurable).
And I've added a "modifySearchCriteria($criteria)" function in the behavior to use in the "search()" function of the model.
Look at my previous post if you are interested.
1

#57 User is offline   Trakina 

  • Newbie
  • Yii
  • Group: Members
  • Posts: 9
  • Joined: 19-March 12
  • Location:Portugal

Posted 23 March 2012 - 11:11 AM

Hey there, @fredpeaks, I've been using your behavior and it works like a charm, I've also made a few changes of my own, namely including vars for a "created" and "modified" datetime fields. That was amazing work, man. Also thanks to @gillemc, I believe your work was based on his, right? And yes, this should be on the base setup of Yii sometime soon.

Attached File(s)


0

#58 User is offline   fredpeaks 

  • Newbie
  • Yii
  • Group: Members
  • Posts: 15
  • Joined: 20-March 12

Posted 23 March 2012 - 12:22 PM

Thanks again and of course thanks to guillemc who has made the biggest part of the work on this behavior.
About your modifications, I'm sorry but I don't think datetime fields management have to be included in this behavior because it's a different functionality. In my application, I've used the CTimestampBehavior (included in Yii) next to the MultilingualBehavior and it work fine. And maybe I'm gonna have multilinguals models where I won't have datetime fields for some reason. So I don't think we should mix both functionalities.

I'm going to contact guillemc and ask him if he's interested in publishing this behavior in the Yii extension repository because I think this is an important feature and it should be easily accessible to everyone.
0

#59 User is offline   Trakina 

  • Newbie
  • Yii
  • Group: Members
  • Posts: 9
  • Joined: 19-March 12
  • Location:Portugal

Posted 23 March 2012 - 12:30 PM

Ok, I wasn't aware of that functionality. Anyway, great work, you guys!
0

#60 User is offline   fredpeaks 

  • Newbie
  • Yii
  • Group: Members
  • Posts: 15
  • Joined: 20-March 12

Posted 24 March 2012 - 06:55 AM

A few bugs corrected, add a "multilang()" function to ease the use and a change made on the "multilangRelation" default name ("multilangPost" instead of "multilang") in order to be able to retrieve languages translation of two or more related models in one query.
Example for a Page 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.

Here is the new version : Attached File  MultilingualBehavior.php (9.79K)
Number of downloads: 19
0

Share this topic:


  • (5 Pages)
  • +
  • 1
  • 2
  • 3
  • 4
  • 5
  • You cannot start a new topic
  • You cannot reply to this topic

1 User(s) are reading this topic
0 members, 1 guests, 0 anonymous users