Commits

Bogdan Savluk committed 7e74cf6

Added i18n features(Url Manager, new StaticPage and StaticData models and updated editors, ). Added GalleryInput - widget for adding gallery to StaticPage or StaticData, Gii templates for i18d crud and model generation

  • Participants
  • Parent commits 406a9a7

Comments (0)

Files changed (62)

 testapp/protected/extensions/tinymce=https://bitbucket.org/z_bodya/yii-tinymce
 testapp/protected/extensions/elFinder=https://bitbucket.org/z_bodya/yii-elfinder
 testapp/protected/extensions/fileimagearbehavior=https://bitbucket.org/z_bodya/fileimagearbehavior
-testapp/protected/extensions/galleryManager=https://bitbucket.org/z_bodya/gallerymanager
+testapp/protected/extensions/galleryManager=https://bitbucket.org/z_bodya/i18d-gallerymanager
 testapp/protected/extensions/image=https://bitbucket.org/z_bodya/yii-image
+testapp/protected/extensions/swiftMailer=https://bitbucket.org/z_bodya/yii-swiftmailer
+testapp/protected/extensions/multiselect=https://bitbucket.org/z_bodya/yii-multiselect
 
-857ad8e7a2802ad311de41efb5600459449606af testapp/protected/extensions/chosen
+bfa224d14fda76b07c13686912c08eea9d9483ba testapp/protected/extensions/chosen
 066a5130682dab82c4ba9e42c69c116f50b64da5 testapp/protected/extensions/coordinatepicker
-52b49f95c6d0de305ccbfdc0084e9e3681aca153 testapp/protected/extensions/elFinder
-18e1e9e14315ab83880ed6b04c7fa5deb3f10ad5 testapp/protected/extensions/fileimagearbehavior
-f6fbc9d6f67e33f56afcb47c5d302ab8b3388c39 testapp/protected/extensions/galleryManager
-6049c0b1922d5f3257985060953f13899a9f5ffc testapp/protected/extensions/image
+88afc47412c5a81e6fb6abbf8d3dba13a505a285 testapp/protected/extensions/elFinder
+d9974099c8d303cea477f639bf5b3a3fb18e11d8 testapp/protected/extensions/fileimagearbehavior
+b4596470c4338dbc82ff24ad9ff58ba1064fd704 testapp/protected/extensions/galleryManager
+db2062402cb7fb8c10a873390670a3278a935b03 testapp/protected/extensions/image
+8e5908e6510ab5d3990808cc144346379a9d99b7 testapp/protected/extensions/multiselect
+fd85dd090831bb0d540ad1306dcbdfa674a6206e testapp/protected/extensions/swiftMailer
 ba0119d80485a46a88ecc89ae3272f225e0f7bcb testapp/protected/extensions/tinymce

File testapp/assets/.empty

File contents unchanged.

File testapp/gallery/.empty

File contents unchanged.

File testapp/index.php

 <?php
 
 // change the following paths if necessary
-$yii=dirname(__FILE__).'/../../yii-1.1.10/framework/yii.php';
+$yii=dirname(__FILE__).'/../../yii-1.1.12/framework/yii.php';
 $config=dirname(__FILE__).'/protected/config/main.php';
 
 // remove the following lines when in production mode

File testapp/protected/components/Application.php

+<?php
+//todo: remove this class.
+class Application extends CWebApplication
+{
+    public function init()
+    {
+        parent::init();
+
+        // todo: warning! remove this handler on production(after messages translation complete).
+        //Yii::app()->messages->onMissingTranslation = array(new MissingTranslationEventHandler(), 'missingTranslation');
+        //Yii::app()->messages->forceTranslation = true; // allow ru->ru translation
+    }
+}

File testapp/protected/components/Controller.php

  */
 class Controller extends CController
 {
-	/**
-	 * @var string the default layout for the controller view. Defaults to '//layouts/column1',
-	 * meaning using a single column layout. See 'protected/views/layouts/column1.php'.
-	 */
-	public $layout='//layouts/column1';
-	/**
-	 * @var array context menu items. This property will be assigned to {@link CMenu::items}.
-	 */
-	public $menu=array();
-	/**
-	 * @var array the breadcrumbs of the current page. The value of this property will
-	 * be assigned to {@link CBreadcrumbs::links}. Please refer to {@link CBreadcrumbs::links}
-	 * for more details on how to specify this property.
-	 */
-	public $breadcrumbs=array();
+    /**
+     * @var string the default layout for the controller view. Defaults to '//layouts/column1',
+     * meaning using a single column layout. See 'protected/views/layouts/column1.php'.
+     */
+    public $layout = '//layouts/column1';
+    /**
+     * @var array context menu items. This property will be assigned to {@link CMenu::items}.
+     */
+    public $menu = array();
+    /**
+     * @var array the breadcrumbs of the current page. The value of this property will
+     * be assigned to {@link CBreadcrumbs::links}. Please refer to {@link CBreadcrumbs::links}
+     * for more details on how to specify this property.
+     */
+    public $breadcrumbs = array();
+
+
+    public $keywords = null;
+    public $description = null;
+    public $pageName;
+    private $_pageTitle;
+
+    public function setSEOParams($title = null, $keywords = null, $description = null)
+    {
+        if ($title) $this->pageTitle = $title;
+        if ($keywords) $this->keywords = $keywords;
+        if ($description) $this->description = $description;
+    }
+
+    /**
+     * @return string the page title. Defaults to the controller name and the action name.
+     */
+    public function getPageTitle()
+    {
+        if ($this->_pageTitle !== null)
+            return $this->_pageTitle;
+        else {
+            if (!empty($this->pageName))
+                $this->_pageTitle = $this->pageName . " | " . Yii::t('site', 'Авто лайф');
+            else
+                $this->_pageTitle = Yii::t('site', 'Авто лайф');
+            return $this->_pageTitle;
+        }
+    }
+
+    /**
+     * @param string $value the page title.
+     */
+    public function setPageTitle($value)
+    {
+        $this->_pageTitle = $value;
+    }
+
+
+    /**
+     * @param $section
+     * @param $key
+     * @return array
+     */
+    public function loadStaticPage($section, $key)
+    {
+        /** @var $page StaticPage */
+        $page = StaticPage::modelByPk($section, $key);
+        $this->pageName = $page->i18n->name;
+        $this->setSEOParams($page->i18n->title, $page->i18n->keywords, $page->i18n->description);
+        return $page->getDataAttributes() + $page->i18n->getDataAttributes();
+    }
 }

File testapp/protected/components/UrlManager.php

+<?php
+/**
+ * Url manager with automatic language param addition
+ */
+class UrlManager extends CUrlManager
+{
+    /**
+     * Language key in request to be set
+     * @var string
+     */
+    public $langKey = 'lang';
+
+    /**
+     * Constructs a URL, adding language param if it does not set.
+     * @param string $route the controller and the action (e.g. article/read)
+     * @param array $params list of GET parameters (name=>value). Both the name and value will be URL-encoded.
+     * If the name is '#', the corresponding value will be treated as an anchor
+     * and will be appended at the end of the URL.
+     * @param string $ampersand the token separating name-value pairs in the URL. Defaults to '&'.
+     * @return string the constructed URL
+     */
+    public function createUrl($route, $params = array(), $ampersand = '&')
+    {
+        return parent::createUrl(
+            $route,
+            array_merge(array($this->langKey => Yii::app()->language), $params),
+            $ampersand);
+    }
+
+    public function parseUrl($request)
+    {
+        $res = parent::parseUrl($request);
+        $app = Yii::app();
+
+        if (isset($_GET['lang'])) { // set language from `lang` param
+            if (count(array_intersect(array($_GET['lang']), Yii::app()->params['languages'])) == 0)
+                $app->request->redirect('/');
+            $app->language = $_GET['lang'];
+        } else {
+            $_GET['lang'] = Yii::app()->language;
+            // todo: set language by request headers
+            // $al =  $_SERVER["HTTP_ACCEPT_LANGUAGE"];
+        }
+        return $res;
+    }
+
+}

File testapp/protected/config/main.php.dist

 
 // This is the main Web application configuration. Any writable
 // CWebApplication properties can be configured here.
+$langRegExp = '(en|ru)';
 return array(
     'basePath' => dirname(__FILE__) . DIRECTORY_SEPARATOR . '..',
     'name' => 'CMS Test',
+    'language' => 'ru',
 
     // preloading 'log' component
     'preload' => array('log'),
         'ext.galleryManager.*',
         'ext.galleryManager.models.*',
         'ext.chosen.*',
+        'ext.swiftMailer.*',
     ),
 
     'modules' => array(
 
     // application components
     'components' => array(
+        'swiftMailer'=>array(
+            'class'=>'ext.swiftMailer.YiiSwiftMailer'
+        ),
+        'assetManager' => array(
+            'linkAssets' => true,
+        ),
         'widgetFactory' => array(
             'class' => 'CWidgetFactory',
             'widgets' => array(
                         'connectorRoute'=>'admin/elfinder/connector',
                     )
                 ),
-                'GalleryManager'=>array(
+                'GalleryManager' => array(
                     'controllerRoute' => '/admin/gallery',
-                ) ,
-                'ServerFileInput'=>array(
+                ),
+                'ServerFileInput' => array(
                     'connectorRoute' => 'admin/elfinder/connector',
                 ),
                 'ElFinderWidget' => array(
-                    'connectorRoute'=>'admin/elfinder/connector',
+                    'connectorRoute' => 'admin/elfinder/connector',
                 ),
             )
         ),
         'urlManager' => array(
             'urlFormat' => 'path',
             'showScriptName' => false,
+            'class' => 'UrlManager',
             'rules' => array(
-                '<controller:\w+>/<id:\d+>' => '<controller>/view',
-                '<controller:\w+>/<action:\w+>/<id:\d+>' => '<controller>/<action>',
+                // admin module
+                'admin' => array('admin', 'params' => array('defaultParams' => 'ru')),
+                'admin/<controller:\w+>' => array('admin/<controller>', 'defaultParams' => array('lang' => 'ru')),
+                'admin/<controller:\w+>/<action:\w+>' => array('admin/<controller>/<action>', 'defaultParams' => array('lang' => 'ru')),
+
+                //site links
+                "<lang:$langRegExp>/" => 'site/index',
+                "<lang:$langRegExp>/error" => 'site/error',
+                "<lang:$langRegExp>/contact" => 'site/contact',
+                "<lang:$langRegExp>/reviews" => 'site/reviews',
+
+                //default routing rules
+                "<lang:$langRegExp>/<controller:\w+>/" => '<controller>',
+                "<lang:$langRegExp>/<controller:\w+>/<action:\w+>" => '<controller>/<action>',
+                '<controller:\w+>/' => '<controller>',
                 '<controller:\w+>/<action:\w+>' => '<controller>/<action>',
             ),
         ),
 
         'db' => array(
-            'connectionString' => 'mysql:host=localhost;dbname=testcms',
+            'connectionString' => 'mysql:host=localhost;dbname=i18dtestcms',
             'emulatePrepare' => true,
             'username' => 'root',
             'password' => '',
     // application-level parameters that can be accessed
     // using Yii::app()->params['paramName']
     'params' => array(
+        'languages' => array(
+            //  'uk',
+            'ru',
+            'en',
+        ),
+        'languageNames' => array(
+            //  'uk' => 'Українська',
+            'ru' => 'Русский',
+            'en' => 'English',
+        ),
         // this is used in contact page
         'adminEmail' => 'webmaster@example.com',
     ),

File testapp/protected/data/database.sql

 -- http://www.phpmyadmin.net
 --
 -- Host: 127.0.0.1
--- Generation Time: Jul 08, 2012 at 02:49 AM
+-- Generation Time: Aug 23, 2012 at 03:05 PM
 -- Server version: 5.5.19
--- PHP Version: 5.3.12
+-- PHP Version: 5.3.15
 
 SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO";
 SET time_zone = "+00:00";
 /*!40101 SET NAMES utf8 */;
 
 --
--- Database: `testcms`
+-- Database: `i18dtestcms`
 --
 
 -- --------------------------------------------------------
   `versions_data` text NOT NULL,
   `name` tinyint(1) NOT NULL DEFAULT '1',
   `description` tinyint(1) NOT NULL DEFAULT '1',
+  `link` tinyint(1) NOT NULL DEFAULT '0',
   PRIMARY KEY (`id`)
 ) ENGINE=InnoDB  DEFAULT CHARSET=utf8 AUTO_INCREMENT=3 ;
 
 -- Dumping data for table `gallery`
 --
 
-INSERT INTO `gallery` (`id`, `versions_data`, `name`, `description`) VALUES
-(1, 'N;', 0, 0),
-(2, 'a:2:{s:5:"small";a:1:{s:6:"resize";a:2:{i:0;i:200;i:1;N;}}s:6:"medium";a:1:{s:6:"resize";a:2:{i:0;i:800;i:1;N;}}}', 1, 1);
+INSERT INTO `gallery` (`id`, `versions_data`, `name`, `description`, `link`) VALUES
+(1, 'a:2:{s:5:"small";a:1:{s:6:"resize";a:2:{i:0;i:200;i:1;N;}}s:6:"medium";a:1:{s:6:"resize";a:2:{i:0;i:800;i:1;N;}}}', 1, 1, 0),
+(2, 'a:2:{s:5:"small";a:1:{s:6:"resize";a:2:{i:0;i:200;i:1;N;}}s:6:"medium";a:1:{s:6:"resize";a:2:{i:0;i:800;i:1;N;}}}', 0, 0, 0);
 
 -- --------------------------------------------------------
 
   `id` int(11) NOT NULL AUTO_INCREMENT,
   `gallery_id` int(11) NOT NULL,
   `rank` int(11) NOT NULL DEFAULT '0',
-  `name` varchar(512) NOT NULL,
-  `description` text NOT NULL,
   `file_name` varchar(128) NOT NULL,
   PRIMARY KEY (`id`),
-  KEY `fk_gallery_photo_gallery1` (`gallery_id`)
-) ENGINE=InnoDB  DEFAULT CHARSET=utf8 AUTO_INCREMENT=488 ;
+  KEY `fk_gallery_photo_gallery1_idx` (`gallery_id`)
+) ENGINE=InnoDB  DEFAULT CHARSET=utf8 AUTO_INCREMENT=527 ;
 
 -- --------------------------------------------------------
 
 --
--- Table structure for table `page`
+-- Table structure for table `gallery_photo_i18n`
 --
 
-CREATE TABLE IF NOT EXISTS `page` (
-  `id` int(11) NOT NULL AUTO_INCREMENT,
-  `link` varchar(64) DEFAULT NULL,
-  `name` varchar(256) DEFAULT NULL,
-  `content` longtext,
-  `title` varchar(512) DEFAULT NULL,
+CREATE TABLE IF NOT EXISTS `gallery_photo_i18n` (
+  `id` int(11) NOT NULL,
+  `lang` varchar(5) NOT NULL,
+  `name` varchar(512) DEFAULT NULL,
   `description` text,
-  `keywords` text,
-  `projects_list` varchar(128) DEFAULT NULL,
-  PRIMARY KEY (`id`),
-  UNIQUE KEY `link_UNIQUE` (`link`)
-) ENGINE=InnoDB  DEFAULT CHARSET=utf8 AUTO_INCREMENT=2 ;
-
---
--- Dumping data for table `page`
---
-
-INSERT INTO `page` (`id`, `link`, `name`, `content`, `title`, `description`, `keywords`, `projects_list`) VALUES
-(1, 'test1', 'Тестовая страница', '<p>Test content</p>', '', '', '', '1,2');
+  `link` varchar(512) DEFAULT NULL,
+  PRIMARY KEY (`id`,`lang`),
+  KEY `fk_gallery_photo_i18n_gallery_photo1_idx` (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 
 -- --------------------------------------------------------
 
 --
 
 CREATE TABLE IF NOT EXISTS `static_data` (
-  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `section` varchar(64) NOT NULL,
+  `key` varchar(64) NOT NULL,
   `data` longtext,
-  PRIMARY KEY (`id`)
-) ENGINE=InnoDB  DEFAULT CHARSET=utf8 AUTO_INCREMENT=101 ;
+  PRIMARY KEY (`section`,`key`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+-- --------------------------------------------------------
 
 --
--- Dumping data for table `static_data`
+-- Table structure for table `static_data_i18n`
 --
 
-INSERT INTO `static_data` (`id`, `data`) VALUES
-(1, 'a:3:{s:5:"item1";s:8:"Line One";s:5:"item2";s:1:"1";s:5:"item3";s:16:"<p>Rich Text</p>";}'),
-(100, 'a:2:{s:5:"phone";s:11:"999-999-333";s:10:"adminEmail";s:17:"email@example.com";}');
+CREATE TABLE IF NOT EXISTS `static_data_i18n` (
+  `section` varchar(64) NOT NULL,
+  `key` varchar(64) NOT NULL,
+  `lang` varchar(5) NOT NULL,
+  `data` longtext,
+  PRIMARY KEY (`section`,`key`,`lang`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 
 -- --------------------------------------------------------
 
 CREATE TABLE IF NOT EXISTS `static_page` (
   `section` varchar(64) NOT NULL,
   `key` varchar(64) NOT NULL,
+  `data` longtext,
+  PRIMARY KEY (`section`,`key`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+-- --------------------------------------------------------
+
+--
+-- Table structure for table `static_page_i18n`
+--
+
+CREATE TABLE IF NOT EXISTS `static_page_i18n` (
+  `section` varchar(64) NOT NULL,
+  `key` varchar(64) NOT NULL,
+  `lang` varchar(5) NOT NULL,
   `name` varchar(256) DEFAULT NULL,
   `data` longtext,
   `title` varchar(512) DEFAULT NULL,
   `description` text,
   `keywords` text,
-  PRIMARY KEY (`section`,`key`)
+  PRIMARY KEY (`section`,`key`,`lang`),
+  KEY `fk_static_page_i18n_static_page1_idx` (`section`,`key`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 
 --
 ALTER TABLE `gallery_photo`
   ADD CONSTRAINT `fk_gallery_photo_gallery1` FOREIGN KEY (`gallery_id`) REFERENCES `gallery` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION;
 
+--
+-- Constraints for table `gallery_photo_i18n`
+--
+ALTER TABLE `gallery_photo_i18n`
+  ADD CONSTRAINT `fk_gallery_photo_i18n_gallery_photo1` FOREIGN KEY (`id`) REFERENCES `gallery_photo` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
+
+--
+-- Constraints for table `static_data_i18n`
+--
+ALTER TABLE `static_data_i18n`
+  ADD CONSTRAINT `fk_static_data_i18n_static_data1` FOREIGN KEY (`section`) REFERENCES `static_data` (`section`) ON DELETE CASCADE ON UPDATE CASCADE;
+
+--
+-- Constraints for table `static_page_i18n`
+--
+ALTER TABLE `static_page_i18n`
+  ADD CONSTRAINT `fk_static_page_i18n_static_page1` FOREIGN KEY (`section`, `key`) REFERENCES `static_page` (`section`, `key`) ON DELETE CASCADE ON UPDATE CASCADE;
+
 /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
 /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
 /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;

File testapp/protected/models/Page.php

-<?php
-
-/**
- * This is the model class for table "page".
- *
- * The followings are the available columns in table 'page':
- * @property integer $id
- * @property string $link
- * @property string $name
- * @property string $content
- * @property string $title
- * @property string $description
- * @property string $keywords
- * @property string $projects_list
- */
-class Page extends CActiveRecord
-{
-    /**
-     * Returns the static model of the specified AR class.
-     * @param string $className active record class name.
-     * @return Page the static model class
-     */
-    public static function model($className = __CLASS__)
-    {
-        return parent::model($className);
-    }
-
-    /**
-     * @return string the associated database table name
-     */
-    public function tableName()
-    {
-        return 'page';
-    }
-
-    /**
-     * @return array validation rules for model attributes.
-     */
-    public function rules()
-    {
-        // NOTE: you should only define rules for those attributes that
-        // will receive user inputs.
-        return array(
-            array('link', 'length', 'max' => 64),
-            array('name', 'length', 'max' => 256),
-            array('title', 'length', 'max' => 512),
-            array('projects_list', 'length', 'max' => 128),
-            array('content, description, keywords, projectIds', 'safe'),
-            // The following rule is used by search().
-            // Please remove those attributes that should not be searched.
-            array('id, link, name, content, title, description, keywords, projects_list', 'safe', 'on' => 'search'),
-        );
-    }
-
-    /**
-     * @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(
-        );
-    }
-
-    /**
-     * @return array customized attribute labels (name=>label)
-     */
-    public function attributeLabels()
-    {
-        return array(
-            'id' => 'ID',
-            'link' => 'Псевдоним в адресной строке',
-            'name' => 'Название страницы',
-            'content' => 'Содержимое',
-            'title' => 'Заголовок',
-            'description' => 'Описание',
-            'keywords' => 'Ключевые слова',
-            'projects_list' => 'Связаные проекты',
-            'projectIds' => 'Связаные проекты',
-        );
-    }
-
-    /**
-     * Retrieves a list of models based on the current search/filter conditions.
-     * @return CActiveDataProvider the data provider that can return the models based on the search/filter conditions.
-     */
-    public function search()
-    {
-        // Warning: Please modify the following code to remove attributes that
-        // should not be searched.
-
-        $criteria = new CDbCriteria;
-
-        $criteria->compare('id', $this->id);
-        $criteria->compare('link', $this->link, true);
-        $criteria->compare('name', $this->name, true);
-        $criteria->compare('content', $this->content, true);
-        $criteria->compare('title', $this->title, true);
-        $criteria->compare('description', $this->description, true);
-        $criteria->compare('keywords', $this->keywords, true);
-        $criteria->compare('projects_list', $this->projects_list, true);
-
-        return new CActiveDataProvider($this, array(
-            'criteria' => $criteria,
-        ));
-    }
-
-    public function getProjectIds()
-    {
-        return explode(',', $this->projects_list);
-    }
-
-    public function setProjectIds($value)
-    {
-        $this->projects_list = implode(',', $value);
-    }
-}

File testapp/protected/models/StaticData.php

  * This is the model class for table "static_data".
  *
  * The followings are the available columns in table 'static_data':
- * @property integer $id
+ * @property string $section
+ * @property string $key
  * @property string $data
+ *
+ * The followings are the available model relations:
+ * @property StaticDataI18n[] $i18ns
+ * @property StaticDataI18n $i18n
  */
 class StaticData extends CActiveRecord
 {
         // NOTE: you should only define rules for those attributes that
         // will receive user inputs.
         return array(
+            array('section, key', 'required'),
+            array('section, key', 'length', 'max' => 64),
             array('data', 'safe'),
             // The following rule is used by search().
             // Please remove those attributes that should not be searched.
-            array('id, data', 'safe', 'on' => 'search'),
+            array('section, key, data', 'safe', 'on' => 'search'),
         );
     }
 
         // NOTE: you may need to adjust the relation name and the related
         // class name for the relations automatically generated below.
         return array(
+            'i18ns' => array(self::HAS_MANY, 'StaticDataI18n', array('section', 'key'), 'index' => 'lang'),
+            'i18n' => array(self::HAS_ONE, 'StaticDataI18n', array('section', 'key'), 'condition' => 'lang=\'' . Yii::app()->language . '\''),
         );
     }
 
     public function attributeLabels()
     {
         return array(
-            'id' => 'ID',
+            'section' => 'Section',
+            'key' => 'Key',
             'data' => 'Data',
         );
     }
 
         $criteria = new CDbCriteria;
 
-        $criteria->compare('id', $this->id);
+        $criteria->compare('section', $this->section, true);
+        $criteria->compare('key', $this->key, true);
         $criteria->compare('data', $this->data, true);
 
         return new CActiveDataProvider($this, array(

File testapp/protected/models/StaticDataI18n.php

+<?php
+
+/**
+ * This is the model class for table "static_data_i18n".
+ *
+ * The followings are the available columns in table 'static_data_i18n':
+ * @property string $section
+ * @property string $key
+ * @property string $lang
+ * @property string $data
+ *
+ * The followings are the available model relations:
+ * @property StaticData $staticData
+ */
+class StaticDataI18n extends CActiveRecord
+{
+    /**
+     * Returns the static model of the specified AR class.
+     * @param string $className active record class name.
+     * @return StaticDataI18n the static model class
+     */
+    public static function model($className = __CLASS__)
+    {
+        return parent::model($className);
+    }
+
+    /**
+     * @return string the associated database table name
+     */
+    public function tableName()
+    {
+        return 'static_data_i18n';
+    }
+
+    /**
+     * @return array validation rules for model attributes.
+     */
+    public function rules()
+    {
+        // NOTE: you should only define rules for those attributes that
+        // will receive user inputs.
+        return array(
+            array('data', 'safe'),
+            // The following rule is used by search().
+            // Please remove those attributes that should not be searched.
+            array('section, key, lang, data', 'safe', 'on' => 'search'),
+        );
+    }
+
+    /**
+     * @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(
+            'staticData' => array(self::BELONGS_TO, 'StaticData', array('section', 'key')),
+        );
+    }
+
+    /**
+     * @return array customized attribute labels (name=>label)
+     */
+    public function attributeLabels()
+    {
+        return array(
+            'section' => 'Section',
+            'key' => 'Key',
+            'lang' => 'Lang',
+            'data' => 'Data',
+        );
+    }
+
+    /**
+     * Retrieves a list of models based on the current search/filter conditions.
+     * @return CActiveDataProvider the data provider that can return the models based on the search/filter conditions.
+     */
+    public function search()
+    {
+        // Warning: Please modify the following code to remove attributes that
+        // should not be searched.
+
+        $criteria = new CDbCriteria;
+
+        $criteria->compare('section', $this->section, true);
+        $criteria->compare('key', $this->key, true);
+        $criteria->compare('lang', $this->lang, true);
+        $criteria->compare('data', $this->data, true);
+
+        return new CActiveDataProvider($this, array(
+            'criteria' => $criteria,
+        ));
+    }
+
+    private $_data = null;
+
+    /**
+     * @return array
+     */
+    public function getDataAttributes()
+    {
+        if (!isset($this->_data)) {
+            $this->_data = unserialize($this->data);
+        }
+        if (!is_array($this->_data)) $this->_data = array();
+        return $this->_data;
+    }
+
+    /**
+     * @param array $data
+     */
+    public function setDataAttributes($data)
+    {
+        $this->_data = $data;
+        $this->data = serialize($data);
+    }
+}

File testapp/protected/models/StaticPage.php

  * The followings are the available columns in table 'static_page':
  * @property string $section
  * @property string $key
- * //property string $link
- * @property string $name
  * @property string $data
- * @property string $title
- * @property string $description
- * @property string $keywords
- * @property string $last_change
- * @property double $priority
- * @property string $change_freq
+ *
+ * The followings are the available model relations:
+ * @property StaticPageI18n[] $i18ns
+ * @property StaticPageI18n $i18n
  */
 class StaticPage extends CActiveRecord
 {
         // NOTE: you should only define rules for those attributes that
         // will receive user inputs.
         return array(
-            array('section, key' /*link*/, 'required'),
-            //array('priority', 'numerical'),
-            array('section, key' /*link*/, 'length', 'max' => 64),
-            array('name', 'length', 'max' => 256),
-            array('title', 'length', 'max' => 512),
-            array('data, description, keywords' /* last_change, change_freq */, 'safe'),
+            array('section, key', 'required'),
+            array('section, key', 'length', 'max' => 64),
+            array('data', 'safe'),
             // The following rule is used by search().
             // Please remove those attributes that should not be searched.
-            array('section, key, name, data, title, description, keywords' /*last_change, priority, change_freq, link*/, 'safe', 'on' => 'search'),
+            array('section, key, data', 'safe', 'on' => 'search'),
         );
     }
 
         // NOTE: you may need to adjust the relation name and the related
         // class name for the relations automatically generated below.
         return array(
+            'i18ns' => array(self::HAS_MANY, 'StaticPageI18n', array('section', 'key'), 'index' => 'lang'),
+            'i18n' => array(self::HAS_ONE, 'StaticPageI18n', array('section', 'key'), 'condition' => 'lang=\'' . Yii::app()->language . '\''),
         );
     }
 
     public function attributeLabels()
     {
         return array(
-            'section' => 'Раздел сайта куда относится страница',
-            'key' => 'Уникальный ключ страницы в разделе',
-            // 'link' => 'Плевдоным страницы',
-            'name' => 'Название',
+            'section' => 'Section',
+            'key' => 'Key',
             'data' => 'Data',
-            'title' => 'Заголовок',
-            'description' => 'Описание',
-            'keywords' => 'Ключевые слова',
-            // 'last_change' => 'Дата последнего редактирования',
-            // 'priority' => 'Относительный приоритет страницы',
-            // 'change_freq' => 'Частота редактырования',
         );
     }
 
 
         $criteria->compare('section', $this->section, true);
         $criteria->compare('key', $this->key, true);
-        // $criteria->compare('link', $this->link, true);
-        $criteria->compare('name', $this->name, true);
         $criteria->compare('data', $this->data, true);
 
-        $criteria->compare('title', $this->title, true);
-        $criteria->compare('description', $this->description, true);
-        $criteria->compare('keywords', $this->keywords, true);
-        //  $criteria->compare('last_change', $this->last_change, true);
-        //  $criteria->compare('priority', $this->priority);
-        //  $criteria->compare('change_freq', $this->change_freq, true);
-
         return new CActiveDataProvider($this, array(
             'criteria' => $criteria,
         ));
         $this->_data = $data;
         $this->data = serialize($data);
     }
+
+    /**
+     * @static
+     * @param $section
+     * @param $key
+     * @return StaticPage
+     */
+    public static function modelByPk($section, $key)
+    {
+        return self::model()->with('i18n')->findByPk(array('section' => $section, 'key' => $key));
+    }
 }

File testapp/protected/models/StaticPageI18n.php

+<?php
+
+/**
+ * This is the model class for table "static_page_i18n".
+ *
+ * The followings are the available columns in table 'static_page_i18n':
+ * @property string $section
+ * @property string $key
+ * @property string $lang
+ * @property string $name
+ * @property string $data
+ * @property string $title
+ * @property string $description
+ * @property string $keywords
+ *
+ * The followings are the available model relations:
+ * @property StaticPage $staticPage
+ */
+class StaticPageI18n extends CActiveRecord
+{
+    /**
+     * Returns the static model of the specified AR class.
+     * @param string $className active record class name.
+     * @return StaticPageI18n the static model class
+     */
+    public static function model($className = __CLASS__)
+    {
+        return parent::model($className);
+    }
+
+    /**
+     * @return string the associated database table name
+     */
+    public function tableName()
+    {
+        return 'static_page_i18n';
+    }
+
+    /**
+     * @return array validation rules for model attributes.
+     */
+    public function rules()
+    {
+        // NOTE: you should only define rules for those attributes that
+        // will receive user inputs.
+        return array(
+            array('name', 'length', 'max' => 256),
+            array('title', 'length', 'max' => 512),
+            array('data, description, keywords', 'safe'),
+            // The following rule is used by search().
+            // Please remove those attributes that should not be searched.
+            array('section, key, lang, name, data, title, description, keywords', 'safe', 'on' => 'search'),
+        );
+    }
+
+    /**
+     * @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(
+            'staticPage' => array(self::BELONGS_TO, 'StaticPage', 'key'),
+        );
+    }
+
+    /**
+     * @return array customized attribute labels (name=>label)
+     */
+    public function attributeLabels()
+    {
+        return array(
+            'section' => 'Section',
+            'key' => 'Key',
+            'lang' => 'Lang',
+            'name' => 'Название страницы',
+            'data' => 'Data',
+            'title' => 'Заголовок',
+            'description' => 'Описание',
+            'keywords' => 'Ключевые слова',
+        );
+    }
+
+    /**
+     * Retrieves a list of models based on the current search/filter conditions.
+     * @return CActiveDataProvider the data provider that can return the models based on the search/filter conditions.
+     */
+    public function search()
+    {
+        // Warning: Please modify the following code to remove attributes that
+        // should not be searched.
+
+        $criteria = new CDbCriteria;
+
+        $criteria->compare('section', $this->section, true);
+        $criteria->compare('key', $this->key, true);
+        $criteria->compare('lang', $this->lang, true);
+        $criteria->compare('name', $this->name, true);
+        $criteria->compare('data', $this->data, true);
+        $criteria->compare('title', $this->title, true);
+        $criteria->compare('description', $this->description, true);
+        $criteria->compare('keywords', $this->keywords, true);
+
+        return new CActiveDataProvider($this, array(
+            'criteria' => $criteria,
+        ));
+    }
+
+    private $_data = null;
+
+    /**
+     * @return array
+     */
+    public function getDataAttributes()
+    {
+        if (!isset($this->_data)) {
+            $this->_data = unserialize($this->data);
+        }
+        if (!is_array($this->_data)) $this->_data = array();
+        return $this->_data;
+    }
+
+    /**
+     * @param array $data
+     */
+    public function setDataAttributes($data)
+    {
+        $this->_data = $data;
+        $this->data = serialize($data);
+    }
+}

File testapp/protected/modules/admin/components/DataModelI18n.php

+<?php
+/**
+ * Custom model based on configuration passed into constructor
+ *
+ * @author Bogdan Savluk <savluk.bogdan@gmail.com>
+ */
+class DataModelI18n extends DataModel
+{
+
+}

File testapp/protected/modules/admin/components/I18dCrudController.php

+<?php
+
+class I18dCrudController extends AdminController
+{
+    public $actionsMenu;
+    public $layout = 'crud_layout';
+    public $defaultAction = 'admin';
+
+    public $labels = array(
+        'admin' => 'Управление',
+        'create' => 'Добавить',
+        'update' => 'Редактировать',
+    );
+
+    public $modelName;
+    public $i18ModelName;
+
+    public function init()
+    {
+        parent::init();
+        if (!isset($this->i18ModelName))
+            $this->i18ModelName = $this->modelName . 'I18n';
+    }
+
+    /**
+     * Creates a new model.
+     * If creation is successful, the browser will be redirected to the 'view' page.
+     */
+    public function actionCreate()
+    {
+        $model = new $this->modelName;
+        $i18nModels = array();
+        foreach (Yii::app()->params['languages'] as $lang) {
+            $i18nModel = new $this->i18ModelName;
+            $i18nModel->lang = $lang;
+            $i18nModels[$lang] = $i18nModel;
+        }
+        // Uncomment the following line if AJAX validation is needed
+        // $this->performAjaxValidation($model);
+
+        if (isset($_POST[$this->modelName]) || isset($_POST[$this->i18ModelName])) {
+
+            if (isset($_POST[$this->modelName]))
+                $model->attributes = $_POST[$this->modelName];
+            $i18nValidation = true;
+            if (isset($_POST[$this->i18ModelName])) {
+                foreach ($_POST[$this->i18ModelName] as $lang => $post) {
+                    $i18nModels[$lang]->attributes = $post;
+                    $i18nValidation = $i18nValidation && $i18nModels[$lang]->validate();
+                }
+            }
+            if ($model->validate() && $i18nValidation) {
+                $model->i18ns = $i18nModels;
+                $model->save(false);
+                foreach ($i18nModels as $i18nModel) {
+                    $i18nModel->id = $model->id;
+                    $i18nModel->save();
+                }
+                $this->redirect(array('admin'));
+            }
+
+        }
+
+        $this->render('create', array(
+            'model' => $model,
+            'i18nModels' => $i18nModels,
+        ));
+    }
+
+    /**
+     * Updates a particular model.
+     * If update is successful, the browser will be redirected to the 'view' page.
+     * @param integer $id the ID of the model to be updated
+     */
+    public function actionUpdate($id)
+    {
+        $model = $this->loadModel($id);
+        $i18nModels = array();
+        foreach (Yii::app()->params['languages'] as $lang) {
+            if (!isset($model->i18ns[$lang])) {
+                $i18nModel = new $this->i18ModelName;
+                $i18nModel->id = $model->id;
+                $i18nModel->lang = $lang;
+                $i18nModels[$lang] = $i18nModel;
+            } else {
+                $i18nModels[$lang] = $model->i18ns[$lang];
+            }
+        }
+
+        if (isset($_POST[$this->modelName]) || isset($_POST[$this->i18ModelName])) {
+
+            if (isset($_POST[$this->modelName]))
+                $model->attributes = $_POST[$this->modelName];
+            $i18nValidation = true;
+            if (isset($_POST[$this->i18ModelName])) {
+                foreach ($_POST[$this->i18ModelName] as $lang => $post) {
+                    $i18nModels[$lang]->attributes = $post;
+                    $i18nValidation = $i18nValidation && $i18nModels[$lang]->validate();
+                }
+            }
+            if ($model->validate() && $i18nValidation) {
+                $model->i18ns = $i18nModels;
+                $model->save(false);
+                foreach ($i18nModels as $i18nModel) {
+                    $i18nModel->save();
+                }
+                $this->redirect(array('admin'));
+            }
+
+        }
+
+        $this->render('update', array(
+            'model' => $model,
+            'i18nModels' => $i18nModels,
+
+        ));
+    }
+
+    /**
+     * Deletes a particular model.
+     * If deletion is successful, the browser will be redirected to the 'admin' page.
+     * @param integer $id the ID of the model to be deleted
+     */
+    public function actionDelete($id)
+    {
+        if (Yii::app()->request->isPostRequest) {
+            // we only allow deletion via POST request
+            $this->loadModel($id)->delete();
+
+            // if AJAX request (triggered by deletion via admin grid view), we should not redirect the browser
+            if (!isset($_GET['ajax']))
+                $this->redirect(isset($_POST['returnUrl']) ? $_POST['returnUrl'] : array('admin'));
+        }
+        else
+            throw new CHttpException(400, 'Invalid request. Please do not repeat this request again.');
+    }
+
+    /**
+     * Manages all models.
+     */
+    public function actionAdmin()
+    {
+        /** @var $model CActiveRecord */
+        $model = new $this->modelName('search');
+        $model->unsetAttributes(); // clear any default values
+        foreach ($model->getAttributes() as $k => $v) {
+            if (isset($_GET[$k])) {
+                $model->$k = $_GET[$k];
+            }
+        }
+        if (isset($_GET[$this->modelName]))
+            $model->attributes = $_GET[$this->modelName];
+
+        $this->render('admin', array(
+            'model' => $model,
+        ));
+    }
+
+    /**
+     * Returns the data model based on the primary key given in the GET variable.
+     * If the data model is not found, an HTTP exception will be raised.
+     * @param integer the ID of the model to be loaded
+     */
+    public function loadModel($id)
+    {
+        $class = $this->modelName;
+        $model = call_user_func_array(array(&$class, 'model'), array());
+
+
+        $model = $model->findByPk($id);
+        if ($model === null)
+            throw new CHttpException(404, 'The requested page does not exist.');
+        return $model;
+    }
+}

File testapp/protected/modules/admin/components/I18dRcrudController.php

+<?php
+
+class I18dRcrudController extends I18dCrudController
+{
+    public $actionsMenu;
+    public $layout = 'crud_layout';
+    public $defaultAction = 'admin';
+
+    public $labels = array(
+        'admin' => 'Управление',
+        'create' => 'Добавить',
+        'update' => 'Редактировать',
+        'order' => 'Упорядочить',
+    );
+
+    public $modelName;
+    public $i18ModelName;
+
+
+    public function actionOrder()
+    {
+        if (Yii::app()->request->isPostRequest && isset($_POST['Order'])) {
+            if ($_POST['Order'] != 'none') {
+                $models = explode(',', $_POST['Order']);
+                for ($i = 0; $i < sizeof($models); $i++) {
+                    if ($model = $this->loadModel($models[$i])) {
+                        $model->rank = $i;
+                        $model->save();
+                    }
+                }
+            }
+            $this->redirect(array('admin'));
+        }
+        else {
+            $dataProvider = new CActiveDataProvider(
+                $this->modelName,
+                array(
+                    'pagination' => false,
+                    'criteria' => array(
+                        'order' => '`rank` ASC, id DESC',
+                    ),
+                ));
+            $this->render(
+                'order',
+                array(
+                    'dataProvider' => $dataProvider,
+                ));
+        }
+    }
+}

File testapp/protected/modules/admin/components/StaticDataEditor.php

 <?php
-/**
- * Action for editing StaticData models.
+/** Usage example
+ *   class PageController extends AdminController
+ *   {
+ *       public function actions()
+ *       {
+ *           return array(
+ *               'index' => array(
+ *                   'class' => 'StaticDataEditor',
+ *
+ *                   'submitLabel' => 'Сохранить',
+ *                   'section' => 'root',
+ *                   'key' => 'index',
+ *                   'view' => 'static_editor',
+ *                   'model' => array(
+ *                       'schema' => array(
+ *                           'item1' => array(
+ *                               'label' => 'Item #1',
+ *                               'default' => null,
+ *                           ),
+ *                       ),
+ *                       'rules' => array(),
+ *                   ),
+ *                   'form' => array(
+ *                       array(
+ *                           'type' => 'simpleInput',
+ *                           'name' => 'textField',
+ *                           'attribute' => 'item1',
+ *                           'options' => array(),
+ *                       ),
+ *                   ),
+ *                   'i18nModel' => array(
+ *                       'schema' => array(
+ *                           'item1' => array(
+ *                               'label' => 'Item #1',
+ *                               'default' => null,
+ *                           ),
+ *                       ),
+ *                       'rules' => array(),
+ *                   ),
+ *                   'i18nForm' => array(
+ *                       array(
+ *                           'type' => 'simpleInput',
+ *                           'name' => 'textField',
+ *                           'attribute' => 'item1',
+ *                           'options' => array(),
+ *                       ),
+ *                   ),
+ *               ),
+ *           );
+ *       }
+ *   }
  */
 class StaticDataEditor extends CAction
 {
-    public $storageModel = 'StaticData';
+    public $section;
     public $key;
+
     public $model;
     public $form;
+
+    public $i18nModel;
+    public $i18nForm;
+
     public $redirectUrl = null;
     public $view = null;
     public $submitLabel = 'Save';
 
     public function run()
     {
-        $model = new DataModel($this->model);
-        $dataModelClass = $this->storageModel;
-        $dataModel = CActiveRecord::model($dataModelClass)->findByPk($this->key);
-        $model->setAttributes($dataModel->getDataAttributes(), false);
-        if (isset($_POST['DataModel'])) {
-            $model->setAttributes($_POST['DataModel']);
-            if ($model->validate()) {
-                $dataModel->setDataAttributes($model->attributes);
-                $dataModel->save();
+        /**
+         * @var StaticData $model
+         */
+        $model = StaticData::model()->findByPk(array(
+                'section' => $this->section,
+                'key' => $this->key)
+        );
+
+        /**
+         * @var StaticDataI18n[] $i18nModels
+         */
+        $criteria = new CDbCriteria();
+        $criteria->compare('`section`', $this->section);
+        $criteria->compare('`key`', $this->key);
+
+        $criteria->index = 'lang';
+
+        $i18nModels = StaticDataI18n::model()->findAll($criteria);
+
+        if ($model === null) {
+            $model = new StaticData();
+            $model->key = $this->key;
+            $model->section = $this->section;
+            $model->setDataAttributes(array());
+        }
+        foreach (Yii::app()->params['languages'] as $lang) {
+            if (!isset($i18nModels[$lang])) {
+                $i18nModel = new StaticDataI18n();
+                $i18nModel->key = $this->key;
+                $i18nModel->section = $this->section;
+                $i18nModel->lang = $lang;
+                $i18nModel->setDataAttributes(array());
+                $i18nModels[$lang] = $i18nModel;
+            }
+        }
+
+        $dataModel = new DataModel($this->model);
+        $dataModel->setAttributes($model->getDataAttributes(), false);
+        /** @var $i18nDataModels DataModelI18n[] */
+        $i18nDataModels = array();
+        foreach (Yii::app()->params['languages'] as $lang) {
+            $i18nDataModels[$lang] = new DataModelI18n($this->i18nModel);
+            $i18nDataModels[$lang]->setAttributes($i18nModels[$lang]->getDataAttributes(), false);
+        }
+        if (isset($_POST['DataModel']) || isset($_POST['DataModelI18n'])) {
+            if (isset($_POST['DataModel']))
+                $dataModel->setAttributes($_POST['DataModel']);
+
+            if (isset($_POST['DataModelI18n'])) {
+                foreach ($_POST['DataModelI18n'] as $lang => $post) {
+                    if (isset($i18nDataModels[$lang]))
+                        $i18nDataModels[$lang]->setAttributes($post);
+                }
+            }
+            $i18nValidation = true;
+            foreach ($i18nDataModels as $i18nDataModel) {
+                $i18nValidation = $i18nValidation && $i18nDataModel->validate();
+            }
+
+            if ($dataModel->validate() && $i18nValidation) {
+                $model->setDataAttributes($dataModel->getAttributes());
+                $model->save(false);
+                foreach (Yii::app()->params['languages'] as $lang) {
+                    $i18nModels[$lang]->setDataAttributes($i18nDataModels[$lang]->getAttributes());
+                    $i18nModels[$lang]->save(false);
+                }
                 //redirect to index
                 if (isset($this->redirectUrl)) {
                     $this->getController()->redirect($this->redirectUrl);
                 }
             }
+
         }
         //render
 
-        $content = $this->renderForm($model, true);
+        $content = $this->renderForm($dataModel, $i18nDataModels, true);
         if (isset($this->view)) {
             $this->getController()->render($this->view, array('content' => $content));
         } else {
         }
     }
 
-    private function renderForm($model, $processOutput = false)
+    /**
+     * @param DataModel $dataModel
+     * @param DataModelI18n[] $i18nDataModels
+     * @param bool $processOutput
+     * @return null|string
+     */
+    private function renderForm($dataModel, $i18nDataModels, $processOutput = false)
     {
         $out = null;
         if ($processOutput) {
             ob_start();
         }
+        $form = $this->getController()->beginWidget('BHorizontalForm', array());
 
-        $form = $this->getController()->beginWidget('BHorizontalForm', array());
-        $this->getController()->beginWidget('FormGenerator', array(
+        $formConfig = array();
+        if (!empty($this->form))
+            $formConfig[] = array(
+                'type' => 'widget',
+                'class' => 'FormWidget',
+                'config' => array(
+                    'form' => $form,
+                    'model' => $dataModel,
+                    'config' => $this->form,
+                ),
+            );
+        if (!empty($this->i18nForm)) {
+            $tabs = array();
+            foreach (Yii::app()->params['languages'] as $lang) {
+                $tabs[] = array(
+                    'label' => Yii::app()->params['languageNames'][$lang],
+                    'className' => 'FormWidget',
+                    'properties' => array(
+                        'form' => $form,
+                        'suffix' => $lang,
+                        'model' => $i18nDataModels[$lang],
+                        'config' => $this->i18nForm,
+                    ),
+                );
+            }
+            //CVarDumper::dump($tabs,10,true);
+            $formConfig[] = array(
+                'type' => 'widget',
+                'class' => 'WidgetTabs',
+                'config' => array(
+                    'tabs' => $tabs,
+                ),
+            );
+        }
+        $this->getController()->widget('FormWidget', array(
             'form' => $form,
-            'model' => $model,
-            'config' => $this->form,
+            'model' => $dataModel,
+            'config' => $formConfig,
         ));
-        $this->getController()->endWidget();
+
         echo '<div class="form-actions"><button type="submit" class="btn btn-large btn-primary">',
         $this->submitLabel,
         '</button></div>';

File testapp/protected/modules/admin/components/StaticPageEditor.php

  *                           'options' => array(),
  *                       ),
  *                   ),
+ *                   'i18nModel' => array(
+ *                       'schema' => array(
+ *                           'item1' => array(
+ *                               'label' => 'Item #1',
+ *                               'default' => null,
+ *                           ),
+ *                       ),
+ *                       'rules' => array(),
+ *                   ),
+ *                   'i18nForm' => array(
+ *                       array(
+ *                           'type' => 'simpleInput',
+ *                           'name' => 'textField',
+ *                           'attribute' => 'item1',
+ *                           'options' => array(),
+ *                       ),
+ *                   ),
  *               ),
  *           );
  *       }
     public $model;
     public $form;
 
+    public $i18nModel;
+    public $i18nForm;
+
     public $redirectUrl = null;
     public $view = null;
     public $submitLabel = 'Save';
                 'section' => $this->section,
                 'key' => $this->key)
         );
+
+        /**
+         * @var StaticPageI18n[] $i18nModels
+         */
+        $criteria = new CDbCriteria();
+        $criteria->compare('`section`', $this->section);
+        $criteria->compare('`key`', $this->key);
+
+        $criteria->index = 'lang';
+
+        $i18nModels = StaticPageI18n::model()->findAll($criteria);
+
         if ($model === null) {
             $model = new StaticPage();
             $model->key = $this->key;
             $model->section = $this->section;
             $model->setDataAttributes(array());
         }
+        foreach (Yii::app()->params['languages'] as $lang) {
+            if (!isset($i18nModels[$lang])) {
+                $i18nModel = new StaticPageI18n();
+                $i18nModel->key = $this->key;
+                $i18nModel->section = $this->section;
+                $i18nModel->lang = $lang;
+                $i18nModel->setDataAttributes(array());
+                $i18nModels[$lang] = $i18nModel;
+            }
+        }
+
         $dataModel = new DataModel($this->model);
-
         $dataModel->setAttributes($model->getDataAttributes(), false);
-        if (isset($_POST['DataModel']) || isset($_POST['StaticPage'])) {
+        /** @var $i18nDataModels DataModelI18n[] */
+        $i18nDataModels = array();
+        foreach (Yii::app()->params['languages'] as $lang) {
+            $i18nDataModels[$lang] = new DataModelI18n($this->i18nModel);
+            $i18nDataModels[$lang]->setAttributes($i18nModels[$lang]->getDataAttributes(), false);
+        }
+        if (isset($_POST['DataModel'])
+            || isset($_POST['StaticPage'])
+            || isset($_POST['DataModelI18n'])
+            || isset($_POST['StaticPageI18n'])
+        ) {
             if (isset($_POST['StaticPage']))
                 $model->setAttributes($_POST['StaticPage']);
             if (isset($_POST['DataModel']))
                 $dataModel->setAttributes($_POST['DataModel']);
-            if ($model->validate() && $dataModel->validate()) {
+
+            if (isset($_POST['StaticPageI18n'])) {
+                foreach ($_POST['StaticPageI18n'] as $lang => $post) {
+                    if (isset($i18nModels[$lang]))
+                        $i18nModels[$lang]->setAttributes($post);
+                }
+            }
+            $model->setAttributes($_POST['StaticPageI18n']);
+            if (isset($_POST['DataModelI18n'])) {
+                foreach ($_POST['DataModelI18n'] as $lang => $post) {
+                    if (isset($i18nDataModels[$lang]))
+                        $i18nDataModels[$lang]->setAttributes($post);
+                }
+            }
+            $i18nValidation = true;
+            foreach ($i18nDataModels as $i18nDataModel) {
+                $i18nValidation = $i18nValidation && $i18nDataModel->validate();
+            }
+            foreach ($i18nModels as $i18nModel) {
+                $i18nValidation = $i18nValidation && $i18nModel->validate();
+            }
+
+            if ($model->validate() && $dataModel->validate() && $i18nValidation) {
                 $model->setDataAttributes($dataModel->getAttributes());
-                if ($model->save(false)) {
-                    //redirect to index
-                    if (isset($this->redirectUrl)) {
-                        $this->getController()->redirect($this->redirectUrl);
-                    }
+                $model->save(false);
+                foreach (Yii::app()->params['languages'] as $lang) {
+                    $i18nModels[$lang]->setDataAttributes($i18nDataModels[$lang]->getAttributes());
+                    $i18nModels[$lang]->save(false);
+                }
+                //redirect to index
+                if (isset($this->redirectUrl)) {
+                    $this->getController()->redirect($this->redirectUrl);
                 }
             }
 
         }
         //render
 
-        $content = $this->renderForm($model, $dataModel, true);
+        $content = $this->renderForm($model, $dataModel, $i18nModels, $i18nDataModels, true);
         if (isset($this->view)) {
             $this->getController()->render($this->view, array('content' => $content));
         } else {
     /**
      * @param StaticPage $model
      * @param DataModel $dataModel
+     * @param StaticPageI18n[] $i18nModels
+     * @param DataModelI18n[] $i18nDataModels
      * @param bool $processOutput
      * @return null|string
      */
-    private function renderForm($model, $dataModel, $processOutput = false)
+    private function renderForm($model, $dataModel, $i18nModels, $i18nDataModels, $processOutput = false)
     {
         $out = null;
         if ($processOutput) {
         }
         $form = $this->getController()->beginWidget('BHorizontalForm', array());
 
-        $formConfig = array(
-            array(
+        $formConfig = array();
+        if (!empty($this->form))
+            $formConfig[] = array(
+                'type' => 'widget',
+                'class' => 'FormWidget',
+                'config' => array(
+                    'form' => $form,
+                    'model' => $dataModel,
+                    'config' => $this->form,
+                ),
+            );
+        $tabs = array();
+        foreach (Yii::app()->params['languages'] as $lang) {
+            $tabForm = array(
+                array(
+                    'type' => 'simpleInput',
+                    'name' => 'textField',
+                    'attribute' => 'name',
+                    'options' => array('class' => 'input-xxlarge'),
+                ),
+            );
+
+            if (!empty($this->i18nForm))
+                $tabForm[] = array(
+                    'type' => 'fieldset',
+                    'legend' => 'Содержимое',
+                    'config' => array(
+                        array(
+                            'type' => 'widget',
+                            'class' => 'FormWidget',
+                            'config' => array(
+                                'form' => $form,
+                                'suffix' => $lang,
+                                'model' => $i18nDataModels[$lang],
+                                'config' => $this->i18nForm,
+                            ),
+                        )
+                    ),
+                );
+            $tabForm[] = array(
                 'type' => 'fieldset',
-                'legend' => 'Параметры',
+                'legend' => 'Параметры для поисковой оптимизации',
                 'config' => array(
                     array(
                         'type' => 'simpleInput',
                         'name' => 'textField',
-                        'attribute' => 'name',
+                        'attribute' => 'title',
                         'options' => array('class' => 'input-xxlarge'),
                     ),
-                    //  array(
-                    //      'type' => 'simpleInput',
-                    //      'name' => 'textField',
-                    //      'attribute' => 'link',
-                    //      'options' => array('class' => 'input-xxlarge'),
-                    //  ),
+                    array(
+                        'type' => 'simpleInput',
+                        'name' => 'textArea',
+                        'attribute' => 'description',
+                        'options' => array('class' => 'input-xxlarge'),
+                    ),
+                    array(
+                        'type' => 'simpleInput',
+                        'name' => 'textArea',
+                        'attribute' => 'keywords',
+                        'options' => array('class' => 'input-xxlarge'),
+                    ),
                 ),
+            );
+            $tabs[] = array(
+                'label' => Yii::app()->params['languageNames'][$lang],
+                'className' => 'FormWidget',
+                'properties' => array(
+                    'form' => $form,
+                    'model' => $i18nModels[$lang],
+                    'suffix' => $lang,
+                    'config' => $tabForm,
+                )
+            );
+        }
+        //CVarDumper::dump($tabs,10,true);
+        $formConfig[] = array(
+            'type' => 'widget',
+            'class' => 'WidgetTabs',
+            'config' => array(
+                'tabs' => $tabs,
             ),
         );
-        if (!empty($this->form))
-            $formConfig[] = array(
-                'type' => 'fieldset',
-                'legend' => 'Содержимое',
-                'config' => array(
-                    array(
-                        'type' => 'widget',
-                        'class' => 'FormGenerator',
-                        'config' => array(
-                            'form' => $form,
-                            'model' => $dataModel,
-                            'config' => $this->form,
-                        ),
-                    )
-                ),
-            );
-        $formConfig[] = array(
-            'type' => 'fieldset',
-            'legend' => 'Параметры для поисковой оптимизации',
-            'config' => array(
-                array(
-                    'type' => 'simpleInput',
-                    'name' => 'textField',
-                    'attribute' => 'title',
-                    'options' => array('class' => 'input-xxlarge'),
-                ),
-                array(
-                    'type' => 'simpleInput',
-                    'name' => 'textArea',
-                    'attribute' => 'description',
-                    'options' => array('class' => 'input-xxlarge'),
-                ),
-                array(
-                    'type' => 'simpleInput',
-                    'name' => 'textArea',
-                    'attribute' => 'keywords',
-                    'options' => array('class' => 'input-xxlarge'),
-                ),
-
-                //'last_change',
-                //'priority',
-                //'change_freq',
-            ),
-        );
-        $this->getController()->widget('FormGenerator', array(
+        $this->getController()->widget('FormWidget', array(
             'form' => $form,
             'model' => $model,
             'config' => $formConfig,

File testapp/protected/modules/admin/components/widgets/FormGenerator.php

-<?php
-/**
- * Widget to render form by config in BHorizontalForm.
- */
-class FormGenerator extends CWidget
-{