Multilingual models
#1
Posted 09 October 2009 - 12:42 PM
Let's say I have a table product and product_trans, in product I store all locale aware attributes like price, sizes etc, while in product_trans I store name, description etc.
It may be a solution to create a MultilingualActiveRecord class which extends CActiveRecord and are able to assemble a localized version of the Product instance based on a provided locale.
Anyone doing this already? Maybe this is such a general feature that it could be supported in the framework?
Any other ideas to accomplish the same result?
#2
Posted 09 October 2009 - 02:21 PM
#4
Posted 10 October 2009 - 02:08 AM
/**
* @return array relational rules.
*/
public function relations()
{
// NOTE: you may need to adjust the relation name and the related
// class name for the relations automatically generated below.
return array(
'en' => array(self::HAS_ONE,'productTrans','productId','condition'=>"??.`language`='en'",'alias'=>'ProductEN'),
'ru' => array(self::HAS_ONE,'productTrans','productId','condition'=>"??.`language`='ru'",'alias'=>'ProductRU'),
'es' => array(self::HAS_ONE,'productTrans','productId','condition'=>"??.`language`='es'",'alias'=>'ProductES'),
);
}Then if you need products with spanish translation, you do
$models=Product::model()->with('es')->findAll();
and your title now is
$models[0]->es->title
Could anyone think of a better solution?
#5
Posted 10 October 2009 - 05:47 AM
If I'm thinking loud, I would like to write something like:
$products = Product::model()->local('es')->findAll();or:
$products = Product::model()->localize('es')->findAll();When defining a multilingual active record model you'll have to specify the table name and which attributes that are locale aware.
In the product-table all attributes are present. In product_trans-table all locale aware attributes is added. Let's say my default language is English (en) and that my data in the product table is in English. Then this query should try to find translated attributes in Spanish (es) and if no translation is found it should fall back to the default language and use the default attribute values from the product-table.
I think this could be a killer feature
#6
Posted 10 October 2009 - 07:32 AM
#7
Posted 10 October 2009 - 08:58 AM
I'll post here the Model and ActiveRecordBehavior class (should give you a quick idea of what's going on). Check attachement for a small test app (and the other classes of course).
I don't use any additional db-tables. There are "multilanguage"-fields that have to be suffixed with a language-code (eg "title_en"). If one of these special fields are NULL, it means the content is not available in the defined language. If that's the case, a fallback-language may be used.
You can manually override the fallback-setting which is defined in the Model by doing for example:
Model::model()->fallback(false)->findAll();
The supported languages are defined in the app-config (I18n app-component class).
Well the whole thing is not complete of course, but it's something at least. Check test-app to see live (make sure to import database-dump). Use /index.php?language=it&fallback=1 to see the fallback-functionality in action.
This works by the way:
$postItem = Post::model()->findAll(); echo $postItem[0]['title'];
So there's no need to mess around with any language codes when doing queries.
class Post extends I18nActiveRecord
{
public $title;
public $text;
public static function model($className=__CLASS__)
{
return parent::model($className);
}
public function behaviors()
{
return array(
'I18n' => array(
'class' => 'I18nActiveRecordBehavior',
'fallback' => false,
'fields' => array('title', 'text'),
),
);
}
}class I18nActiveRecordBehavior extends CActiveRecordBehavior
{
public $fallback;
public $fields;
private function _getFallback()
{
if (null !== ($fallback = $this->owner->getFallback()))
{
return $fallback;
}
return $this->fallback;
}
private function _setFallback()
{
if (null === $this->owner->getFallback())
{
$this->owner->setFallback($this->fallback);
}
}
public function beforeFind()
{
$this->_setFallback();
foreach ($this->fields as $field)
{
$this->owner->dbCriteria->mergeWith(array(
'condition' => $this->_getCondition($field),
));
}
}
public function afterFind()
{
foreach ($this->fields as $field)
{
if (true === $this->_getFallback() && null === $this->owner->fallbackActive)
{
if (null === $this->owner->__get("{$field}_" . Yii::app()->i18n->activeLanguage['code']))
{
$this->owner->fallbackActive = true;
}
else
{
$this->owner->fallbackActive = false;
}
}
$languageCode = (true === $this->owner->fallbackActive) ? Yii::app()->i18n->fallbackLanguage['code'] : Yii::app()->i18n->activeLanguage['code'];
$this->owner->$field = $this->owner->__get("{$field}_{$languageCode}");
}
}
private function _getCondition($field)
{
if (true === $this->_getFallback() && Yii::app()->i18n->activeLanguage['code'] !== Yii::app()->i18n->fallbackLanguage['code'])
{
return sprintf("{$field}_%s IS NOT NULL OR {$field}_%s IS NOT NULL", Yii::app()->i18n->activeLanguage['code'], Yii::app()->i18n->fallbackLanguage['code']);
}
else
{
return sprintf("{$field}_%s IS NOT NULL", Yii::app()->i18n->activeLanguage['code']);
}
}
}
Attached File(s)
-
test.zip (4.18K)
Number of downloads: 75
#8
Posted 11 October 2009 - 12:41 AM
#9
Posted 11 October 2009 - 06:25 AM
#10
Posted 11 October 2009 - 07:22 AM
There is a join condition available including translated content, and it's possible to specify a defaultScope.
I think it's possible to create a base model that use a language parameter in named scope call.
This line in __call() seems to be the one to change. Some parameter binding semantic has to be added.
$this->getDbCriteria()->mergeWith($scopes[$name]);
The call would look something like
MyModel::model()->translated('language')->...
Edit:
Just recognized the guide section about parameterized named scopes: http://www.yiiframew...ide/database.ar
No need to extend CActiveRecord then. Just define a method named by the scope.
/Tommy
#11
Posted 04 November 2009 - 02:25 PM
Suppose you have a table (and model) Post, with fields id, title, content. The title and content fields store values in the primary or default language of the application.
Then you have a table (and model) PostLang, with fields id, postId, lang, title, content, which stores title and content in additional languages.
By adding the LocalizedBehavior to the Post model, you can do:
$posts = Post::model()->localized('es')->findAll($criteria);
What this will do is overwrite the value of the title and content attributes with their localized version. In case these values are empty it will leave the original ones. If the requested language is the same as the primary language, it will have no effect. You can also call it without the language parameter:
$posts = Post::model()->localized()->findAll($criteria);
And it will use the language currently set in the application.
It works fine, but maybe the implementation could be improved. What I'm doing internally is creating a relation, applying the with() method, and using afterFind() to process the results. All comments are welcome!
<?php
class LocalizedBehavior extends CActiveRecordBehavior {
/**
* Name of the field that stores the language in the translations table
*/
public $langField = 'lang';
/**
* Name of the model/table that stores the translations.
* For 'Post', it defaults to 'PostLang'.
*/
public $langClassName;
/**
* Name of the foreign key column of the translations table.
* For 'Post', it defaults to 'postId'.
*/
public $langForeignKey;
public $skipFields;
public $primaryLang;
public $overwriteIfEmpty = false;
public function localized($lang=null) {
$obj = $this->Owner;
if (!$lang) $lang = Yii::app()->language;
if ($lang == $this->getPrimaryLang()) return $obj;
$class = CActiveRecord::HAS_MANY;
$obj->getMetaData()->relations['localized'] = new $class('localized', $this->getLangClassName(), $this->getLangForeignKey(),
array('index'=>$this->langField, 'condition'=>"??.".$this->langField."='".$lang."'"));
return $obj->with('localized');
}
public function afterFind($event) {
$obj = $this->Owner;
if (isset($obj->localized) && sizeof($obj->localized)>0) {
$row = current($obj->localized);
$skipFields = $this->getSkipFields();
foreach ($row->getAttributes() as $field=>$value) {
if (!in_array($field, $skipFields)) {
if ($value || $this->overwriteIfEmpty) $obj->$field = $value;
}
}
}
}
private function getPrimaryLang() {
if (!$this->primaryLang)
return Yii::app()->sourceLanguage;
return $this->primaryLang;
}
private function getLangForeignKey() {
if (!$this->langForeignKey)
return strtolower($this->Owner->tableName()).'Id';
return $this->langForeignKey;
}
private function getlangClassName() {
if (!$this->langClassName)
return $this->Owner->tableName().'Lang';
return $this->langClassName;
}
private function getSkipFields() {
if (!$this->skipFields)
return array('id', $this->getLangForeignKey(), $this->langField);
return $this->skipFields;
}
}
(Note: for some reason, camelcase in the class names LocalizedBehavior and CActiveRecordBehavior appears messed up)
#12
Posted 02 December 2009 - 02:07 PM
http://www.yiiframew...-active-record/
#13
Posted 01 March 2010 - 04:43 AM
guillemc, on 02 December 2009 - 02:07 PM, said:
http://www.yiiframew...-active-record/
Hi there,
I integrated your Extension yesterday and it's just Great.
Thanks for that.
But I had a Problem with the CActiveDataProvider that is used with the grid view widget.
So I made a smal addition to your Extension. Maybe you can add this class to your next release.
class MultilingualActiveDataProvider extends CActiveDataProvider
{
public $blnLocalize = true;
public $blnAll = false;
public function __construct($modelClass, $config=array(), $blnLocalize=true, $blnAll=false)
{
$this->blnLocalize = $blnLocalize;
$this->blnAll = $blnAll;
parent::__construct($modelClass, $config);
}
protected function fetchData()
{
$criteria=clone $this->getCriteria();
if(($pagination=$this->getPagination())!==false)
{
$pagination->setItemCount($this->getTotalItemCount(true));
$pagination->applyLimit($criteria);
}
if(($sort=$this->getSort())!==false)
$sort->applyOrder($criteria);
if($this->blnLocalize) {
if($this->blnAll) {
return CActiveRecord::model($this->modelClass)->multilingual()->findAll($criteria);
} else {
return CActiveRecord::model($this->modelClass)->localized()->findAll($criteria);
}
} else {
return CActiveRecord::model($this->modelClass)->findAll($criteria);
}
}
}
Now I can Use the DataProvider with his pagination with the Multilang extension
$dataProvider=new MultilingualActiveDataProvider('Post', array(
'criteria'=>array(
'condition'=>'status=1 AND tags LIKE :tags',
'params'=>array(':tags'=>$_GET['tags']),
'with'=>array('author'),
),
'pagination'=>array(
'pageSize'=>20,
),
), true, true);
hope you like it
#14
Posted 25 March 2010 - 11:43 AM
Its giving some issues with related models (like $model->related->localized()->field), but have to look in to that a bit further. Perhaps it can be defined in the relation.
If someone can create a behavior like this please let it know!
#15
Posted 15 June 2010 - 04:42 AM
#16
Posted 19 June 2010 - 10:16 PM
In FlexicaCMS, the approach is to enable multi-language for developed features so there should be minimal effort to turn a website with article management features originally developed in mono-language to multi-language.
Assume that I have written all my code with Article::model()->findAll(...). What I try to avoid is to search and replace them with Article::model()->with('vi')->findAll() or Article::model()->localized()->findAll().
Also, because in this context it's in a CMS, or a sandbox, using behavior is better than a sub-class ActiveRecord. Save the inheritance for a more business specific thing.
It's quite a long writing so allow me to post a link here.
http://flexicacms.co...ur-content.html
Created by Flexica team, Gia Han Online Solutions
#17
Posted 29 June 2010 - 05:31 AM
Hudson Nguyen, on 19 June 2010 - 10:16 PM, said:
In FlexicaCMS, the approach is to enable multi-language for developed features so there should be minimal effort to turn a website with article management features originally developed in mono-language to multi-language.
Assume that I have written all my code with Article::model()->findAll(...). What I try to avoid is to search and replace them with Article::model()->with('vi')->findAll() or Article::model()->localized()->findAll().
Also, because in this context it's in a CMS, or a sandbox, using behavior is better than a sub-class ActiveRecord. Save the inheritance for a more business specific thing.
It's quite a long writing so allow me to post a link here.
http://flexicacms.co...ur-content.html
#18
Posted 11 July 2010 - 10:43 PM
mech7, on 29 June 2010 - 05:31 AM, said:
Yes, it's open source mech7. you can download it from http://www.flexicacms.com.
Created by Flexica team, Gia Han Online Solutions
#19
Posted 23 July 2010 - 03:57 AM
guillemc, on 02 December 2009 - 02:07 PM, said:
http://www.yiiframew...-active-record/
umm is this working in 1.1.3 ?
the langClassName method uses the table name instead of the object name.. but even then when returning the proper classname with..
public function langClassName() {
return get_class($this).'Lang';
}then.. it wants to include ModelLangLang.. and if I remove the Lang string it seems to create a new instance of the model but it runs out of memory..
Also the foreign keys seems to be hardcoded depending on table name.. cannot get this value from schema data?
#20
Posted 01 September 2010 - 03:24 AM
mech7, on 23 July 2010 - 03:57 AM, said:
the langClassName method uses the table name instead of the object name.. but even then when returning the proper classname with..
public function langClassName() {
return get_class($this).'Lang';
}then.. it wants to include ModelLangLang.. and if I remove the Lang string it seems to create a new instance of the model but it runs out of memory..
Also the foreign keys seems to be hardcoded depending on table name.. cannot get this value from schema data?
Hi I'm having the same problems, please notify me if you'll find a way around this!

Help


















