yii-i18n-attribute-messages

Transparent attribute translation for ActiveRecords, leveraging Yii's built-in translation features
5 followers

Yii Extension: I18nAttributeMessages

Transparent attribute translation for ActiveRecords, leveraging Yii's built-in translation features to retrieve translated attribute contents.

All you'll need to do is to rename the fields from $book->title to $book->_title in your database. The included console command scans your database and configuration and creates a migration for all necessary renames.

The behavior then transparently turns $book->title into Yii:t('attributes.Book.title', $book->_title) and $book->title_de into Yii:t('attributes.Book.title', $book->_title, array(), null, 'de'), while providing transparent saving of translations simply by assigning and saving these attributes in the model (Note: CDbMessageSource only).

Features

  • Eases the translation of user-generated content in a project
  • Eases the creation of UI for translators to perform translation work
  • Works with any Yii-compatible message source when retrieving translations
  • Saving of translations when using CDbMessageSource
  • Console command automatically creates migrations for the necessary database changes
  • The source message is left in the model for Gii compatibility (generated models will have the correct validation rules and field order for the translated attributes)
  • Rigorous unit tests
  • Use with any number of attributes/languages without worrying about database restrictions on row size and/or column counts being exceeded

Limitations

Not ideal for translated attributes that are supposed to be native in the active records' database tables, such as translated foreign keys, or multilingual look-up/search columns. Use https://github.com/neam/yii-i18n-columns for those attributes instead.

Requirements

  • Yii 1.1 or above
  • Use of Yii console

Setup

Download and install

Ensure that you have the following in your composer.json:

"repositories":[
    {
        "type": "vcs",
        "url": "https://github.com/neam/yii-i18n-attribute-messages"
    },
    ...
],
"require":{
    "neam/yii-i18n-attribute-messages":"dev-master",
    ...
},

Then install through composer:

php composer.phar update neam/yii-i18n-attribute-messages

If you don't use composer, clone or download this project into /path/to/your/app/vendor/neam/yii-i18n-attribute-messages

Add Alias to both main.php and console.php

'aliases' => array(
    ...
    'vendor'  => dirname(__FILE__) . '/../../vendor',
    'i18n-attribute-messages' => 'vendor.neam.yii-i18n-attribute-messages',
    ...
),

Import the behavior in main.php

'import' => array(
    ...
    'i18n-attribute-messages.behaviors.I18nAttributeMessagesBehavior',
    ...
),

Reference the console command in console.php

'commandMap' => array(
    ...
    'i18n-attribute-messages'    => array(
        'class' => 'i18n-attribute-messages.commands.I18nAttributeMessagesCommand',
    ),
    ...
),

Configure models to be multilingual

1. Add the behavior to the models that you want multilingual

public function behaviors()
{
    return array(
        'i18n-attribute-messages' => array(
             'class' => 'I18nAttributeMessagesBehavior',

             /* The multilingual attributes */
             'translationAttributes' => array(
                  'title',
                  'slug',
                  'book_id',
                  //'etc',
             ),

            /* An array of allowed language/locale ids that are to be used as suffixes, such as title_en, title_de etc */
            //'languageSuffixes' => array_keys(Yii::app()->params["languages"]),

            /* Configure if you want to use another translation component for this behavior. Default is 'messages' */
            //'messageSourceComponent' => 'attributeMessages',

        ),
    );
}

2. Create migration from command line:

./yiic i18n-attribute-messages process

Run with --verbose to see more detailed output.

3. Apply the generated migration:

./yiic migrate

This will rename the fields that are defined in translationAttributes to _fieldname, which will be the placed that the source content is stored (the content that is to be translated).

Sample migration file:

<?php
class m131115_204413_i18n extends CDbMigration
{
    public function up()
    {
        $this->renameColumn('book', 'title', '_title');
        $this->renameColumn('book', 'slug', '_slug');
        $this->renameColumn('chapter', 'title', '_title');
        $this->renameColumn('chapter', 'slug', '_slug');
        $this->dropForeignKey('fk_chapter_book', 'chapter');
        $this->renameColumn('chapter', 'book_id', '_book_id');
        $this->addForeignKey('fk_chapter_book', 'chapter', '_book_id', 'book', 'id', 'NO ACTION', 'NO ACTION');
    }

    public function down()
    {
      $this->renameColumn('book', '_title', 'title');
      $this->renameColumn('book', '_slug', 'slug');
      $this->renameColumn('chapter', '_title', 'title');
      $this->renameColumn('chapter', '_slug', 'slug');
      $this->dropForeignKey('fk_chapter_book', 'chapter');
      $this->renameColumn('chapter', '_book_id', 'book_id');
      $this->addForeignKey('fk_chapter_book', 'chapter', 'book_id', 'book', 'id', 'NO ACTION', 'NO ACTION');
    }
}

4. Add save-support

Save-support is only enabled if you use CDbMessageSource. Configure your app to use it and make sure the following tables (Note: with auto-increment for SourceMessage) exists:

CREATE TABLE `SourceMessage` (
  `id` INT(11) NOT NULL AUTO_INCREMENT,
  `category` VARCHAR(32) NULL DEFAULT NULL,
  `message` TEXT NULL DEFAULT NULL,
  PRIMARY KEY (`id`))
COLLATE = utf8_bin;

CREATE TABLE `Message` (
  `id` INT(11) NOT NULL DEFAULT '0',
  `language` VARCHAR(16) NOT NULL DEFAULT '',
  `translation` TEXT NULL DEFAULT NULL,
  PRIMARY KEY (`id`, `language`),
  CONSTRAINT `FK_Message_SourceMessage`
    FOREIGN KEY (`id`)
    REFERENCES `SourceMessage` (`id`)
    ON DELETE CASCADE)
COLLATE = utf8_bin;

Hint: You can still keep CPhpMessageSource as the default messages component for your app, and configure CDbMessageSource to be used only for attribute messages.

Your application config should have two message source components configured:

'components' => array(
    ...
    // Static messages
    'messages' => array(
        'class' => 'CPhpMessageSource',
    ),
    // Attribute messages
    'attributeMessages' => array(
        'class' => 'CDbMessageSource',
    ),
    ...
),

And when configuring the behavior, set an appropriate 'messageSourceComponent' configuration option (see example configuration above).

5. Re-generate models

Use Gii as per the official documentation. To be able to save translations, you'll need to generate the models Message and SourceMessage as well.

Usage

Example usage with a Book model that has a multilingual title attribute.

All translations will be available through attribute suffix, ie $book->title_en for the english translation, $book->title_sv for the swedish translation. $book->title will be an alias for the currently selected language's translation.

Reading and saving translations

Fetching translations

 $book = Book::model()->findByPk(1);
 Yii::app()->language = 'en';
 echo $book->title; // Outputs 'The Alchemist'
 Yii::app()->language = 'sv';
 echo $book->title; // Outputs 'Alkemisten'
 echo $book->title_en; // Outputs 'The Alchemist'

Saving a single translation

 Yii::app()->language = 'sv';
 $book->title = 'Djävulen bär Prada';
 $book->save(); // Saves 'Djävulen bär Prada' as if it was assigned to Book.title_sv

Saving multiple translations

 $book->title_en = 'The Devil Wears Prada';
 $book->title_sv = 'Djävulen bär Prada';
 $book->save(); // Saves both translations

More examples

...can be found in tests/codeception/unit/BasicTest.php

Creating a UI for translators

Configuration

The default behavior when a translation is missing is to return the source message. When we construct a translation UI, we want the fields to be null until they have a translation.

'import' => array(
    ...
    'i18n-attribute-messages.components.MissingTranslationHandler',
    ...
),

'components' => array(
    ...
    'attributeMessages' => array(
        'class' => 'CDbMessageSource',
        'onMissingTranslation' => array('MissingTranslationHandler', 'returnNull'),
    ),
    ...
),

Creating an input to change the source language content of the field "title"

<div class="row">
    <?php echo $form->labelEx($model,'_title'); ?>
    <?php echo $form->textField($model,'_title'); ?>
    <?php echo $form->error($model,'_title'); ?>
</div>

Note: This field is generated automatically by Gii.

Creating an input to set/update the swedish translation of the field "title"

<div class="row">
    <?php echo $form->labelEx($model,'title_sv'); ?>
    <?php echo $form->textField($model,'title_sv'); ?>
    <?php echo $form->error($model,'title_sv'); ?>
</div>

Hint: You might want to display the source language content next to the translation field, like so:

<div class="row">
    <?php echo Yii::t('app', 'Content to translate'); ?>: <?php echo CHtml::encode($model->_title); ?>
</div>
<div class="row">
    <?php echo $form->labelEx($model,'title_sv'); ?>
    <?php echo $form->textField($model,'title_sv'); ?>
    <?php echo $form->error($model,'title_sv'); ?>
</div>

Also, don't forget to adjust the validation rules (safe, required, etc) for the virtual translation fields.

Creating an input to set/update the current app language's translation of the field "title"

<div class="row">
    <?php echo $form->labelEx($model,'title'); ?>
    <?php echo $form->textField($model,'title'); ?>
    <?php echo $form->error($model,'title'); ?>
</div>

More examples

Simply look at any other examples of form building in Yii. Since the translated attributes are ordinary model attributes, you may use core or third-party extensions that save to and read from model attributes for constructing your translation UI.

Changelog

0.1.0

  • Eases the translation of user-generated content in a project
  • Eases the creation of UI for translators to perform translation work
  • Works with any Yii-compatible message source when retrieving translations
  • Saving of translations when using CDbMessageSource
  • Console command automatically creates migrations for the necessary database changes
  • The source message is left in the model for Gii compatibility (generated models will have the correct validation rules and field order for the translated attributes)
  • Rigorous unit tests

0.0.0

Testing the extension

One-time preparations

Switch to the extension's root directory

cd vendor/neam/yii-i18n-attribute-messages

Create a database called yiam_test in your local mysql server installation. Create a user called yiam_test with yiam_test as the password and make sure that this user has access to the local database.

After this, you can run the following routine to test the extension:

Test the command

1. Set-up the test database

Load tests/db/unmodified.sql into the database.

2. Run the console command

tests/app/protected/yiic i18n-attribute-messages process

3. Apply the migration

tests/app/protected/yiic migrate

Test the behavior

Run the unit tests

php codecept.phar run unit

You should get output similar to:

Codeception PHP Testing Framework v1.6.2
Powered by PHPUnit 3.7.19 by Sebastian Bergmann.

Suite unit started
Trying to ensure empty db (BasicTest::ensureEmptyDb) - Ok
Trying to ensure known source language (BasicTest::ensureKnownSourceLanguage) - Ok
Trying to see behavior (BasicTest::seeBehavior) - Ok
Trying to interpret language suffix (BasicTest::interpretLanguageSuffix) - Ok
Trying to get (BasicTest::get) - Ok
Trying to set without suffix (BasicTest::setWithoutSuffix) - Ok
Trying to set with suffix (BasicTest::setWithSuffix) - Ok
Trying to save single with source message (BasicTest::saveSingleWithSourceMessage) - Ok
Trying to save single without source message (BasicTest::saveSingleWithoutSourceMessage) - Ok
Trying to fetch single without suffix (BasicTest::fetchSingleWithoutSuffix) - Ok
Trying to reuse previous translation (BasicTest::reusePreviousTranslation) - Ok
Trying to update existing (BasicTest::updateExisting) - Ok
Trying to further fallback behavior tests (BasicTest::furtherFallbackBehaviorTests) - Ok
Trying to test test suite (EmptyTest::testTestSuite) - Ok


Time: 0 seconds, Memory: 14.25Mb

OK (14 tests, 124 assertions)

Resources

Be the first person to leave a comment

Please to leave your comment.

Create extension