Programming With Yii2: Localization With I18n

Final product image
What You’ll Be Creating

If you’re asking, “What’s Yii?” check out my earlier tutorial: Introduction to the Yii Framework, which reviews the benefits of  Yii and includes an overview of what’s new in Yii 2.0, released October 12th, 2014.

In this Programming With Yii2 series, I’m guiding readers in use of the newly upgraded Yii2 Framework for PHP. In part one, we set up Yii2 locally, built a Hello World application, set up a remote server and used Github to deploy our code. In part two, we learned about Yii’s implementation of its Model View Controller architecture and how to build web pages and forms that collect and validate data. In part three, we used Yii’s database and active record capabilities to automate code generation for a basic web application. And, in part four, we learned how to integrate user registration.

In this tutorial, I’m going to show you how to make use of Yii’s built-in I18n internationalization support to make your application ready for translation into a number of languages.

For these examples, we’ll continue to imagine we’re building a framework for posting simple status updates, e.g. our own mini-Twitter.

What’s I18n?

According to Wikipedia, I18n is a numeronym for Internationalization:

18 stands for the number of letters between the first i and last n in internationalization, a usage coined at DEC in the 1970s or 80s.

With I18n, all of the text strings displayed to the user from the application are replaced by function calls which can dynamically load translated strings for any language the user selects.

The Goals of Internationalization

When building a web application, it’s useful to think globally from the beginning. Does your app need to work in other languages for users from different countries? If so, then implementing I18n from the beginning will save you a lot of time and headaches later.

In our case, the Yii Framework provides built-in support for I18n so it’s relatively easy to build support in for I18n as you go along.

How I18n Works

I18n operates by replacing all references to text displayed to the user with function calls that provide translation when needed. 

For example, here’s what the attribute field names in the Status model look like before I18n:

public function attributeLabels()
{
    return [
      'id' => 'ID',
      'message' => 'Message',
      'permissions' => 'Permissions',
      'created_at' => 'Created At',
      'updated_at' => 'Updated At',        ];
}

Providing translated versions of the code would become very complicated. Non-technical translators would have to translate code in place, likely breaking syntax.

Here’s what the same code looks like with I18n:

public function attributeLabels()
{
    return [
      'id' => Yii::t('app', 'ID'),
      'message' => Yii::t('app', 'Message'),
      'permissions' => Yii::t('app', 'Permissions'),
      'created_at' => Yii::t('app', 'Created At'),
      'updated_at' => Yii::t('app', 'Updated At'),        ];
}

Yii:t() is a function call that checks what language is currently selected and displays the appropriate translated string. The 'app' parameter refers to a section of our application. Translations can be optionally organized according to various categories. But where do these translated strings appear?

The default language, in this case English, is written into the code, as shown above. Language resource files are lists of arrays of strings whose key is the default language text, e.g. 'Message' or 'Permissions'—and each file provides translated text values for their appropriate language.

Here’s an example of our completed Spanish translation file, language code “es”. The Yii:t() function uses this file to find the appropriate translation to display:

<?php
/**
* Message translations.
*
* This file is automatically generated by 'yii translate' command.
* It contains the localizable messages extracted from source code.
* You may modify this file by translating the extracted messages.
*
* Each array element represents the translation (value) of a message (key).
* If the value is empty, the message is considered as not translated.
* Messages that no longer need translation will have their translations
* enclosed between a pair of '@@' marks.
*
* Message string can be used with plural forms format. Check i18n section
* of the guide for details.
*
* NOTE: this file must be saved in UTF-8 encoding.
*/
return [
    'Get started with Yii' => 'Comience con Yii',
    'Heading' => 'título',
    'My Yii Application' => 'Mi aplicación Yii',
    'Yii Documentation' => 'Yii Documentación',
    'Yii Extensions' => 'Extensiones Yii',
    'Yii Forum' => 'Yii Foro',
    'Are you sure you want to delete this item?' => '¿Seguro que quieres borrar este artículo?',
    'Congratulations!' => '¡Enhorabuena!',
    'Create' => 'crear',
    'Create {modelClass}' => 'crear {modelClass}',
    'Created At' => 'Creado el',
    'Delete' => 'borrar',
    'ID' => 'identificación',
    'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.' => 'Lorem ipsum dolor sit amet, consectetur adipisicing elit , sed no tempor eiusmod ut labore et dolore incididunt magna aliqua . Ut enim ad minim veniam , nostrud quis ullamco exercitation nisi ut laboris aliquip commodo ex ea consequat . Duis Aute Irure dolor en reprehenderit en voluptate velita esse cillum dolore eu nulla fugiat pariatur .',
    'Message' => 'mensaje',
    'Permissions' => 'Permisos',
    'Reset' => 'reajustar',
    'Search' => 'búsqueda',
    'Statuses' => 'Los estados',
    'Update' => 'actualización',
    'Update {modelClass}: ' => 'actualización {modelClass} :',
    'Updated At' => 'Actualizado A',
    'You have successfully created your Yii-powered application.' => 'Ha creado su aplicación Yii con alimentación.',
];

While this looks time-consuming, Yii provides scripts to automate the generation and organization of these file templates.

By separating the text from the code, we make it easier for non-technical multi-lingual experts to translate our applications for us—without breaking the code.

I18n also offers specialized functions for translating time, currency, plurals et al. I won’t go into the detail of these in this tutorial. 

Configuring I18n Support

Unfortunately, the Yii2 documentation for I18n is not yet very descriptive, and it was difficult to find working, step by step examples. Luckily for you, I’m going to walk you through what I’ve learned from scouring the docs and the web. I found The Code Ninja’s I18n example and the Yii2 Definitive Guide on I18n helpful, and Yii contributor Alexander Makarov offered me some assistance as well.

Generating the I18n Configuration File

We’re using the Yii2 basic application template for our demonstration application. This places our codebase below the /hello root directory. Yii’s configuration files in /hello/config/* are loaded whenever page requests are made. We’ll use Yii’s I18n message scripts to build out a configuration file for I18n in the common/config path.

From our codebase root, we’ll run the Yii message/config script:

 ./yii message/config @app/config/i18n.php

This generates the following file template which we can customize:

<?php

return [
    // string, required, root directory of all source files
    'sourcePath' => __DIR__,
    // array, required, list of language codes that the extracted messages
    // should be translated to. For example, ['zh-CN', 'de'].
    'languages' => ['de'],
    // string, the name of the function for translating messages.
    // Defaults to 'Yii::t'. This is used as a mark to find the messages to be
    // translated. You may use a string for single function name or an array for
    // multiple function names.
    'translator' => 'Yii::t',
    // boolean, whether to sort messages by keys when merging new messages
    // with the existing ones. Defaults to false, which means the new (untranslated)
    // messages will be separated from the old (translated) ones.
    'sort' => false,
    // boolean, whether to remove messages that no longer appear in the source code.
    // Defaults to false, which means each of these messages will be enclosed with a pair of '@@' marks.
    'removeUnused' => false,
    // array, list of patterns that specify which files/directories should NOT be processed.
    // If empty or not set, all files/directories will be processed.
    // A path matches a pattern if it contains the pattern string at its end. For example,
    // '/a/b' will match all files and directories ending with '/a/b';
    // the '*.svn' will match all files and directories whose name ends with '.svn'.
    // and the '.svn' will match all files and directories named exactly '.svn'.
    // Note, the '/' characters in a pattern matches both '/' and '\'.
    // See helpers/FileHelper::findFiles() description for more details on pattern matching rules.
    'only' => ['*.php'],
    // array, list of patterns that specify which files (not directories) should be processed.
    // If empty or not set, all files will be processed.
    // Please refer to "except" for details about the patterns.
    // If a file/directory matches both a pattern in "only" and "except", it will NOT be processed.
    'except' => [
        '.svn',
        '.git',
        '.gitignore',
        '.gitkeep',
        '.hgignore',
        '.hgkeep',
        '/messages',
    ],

    // 'php' output format is for saving messages to php files.
    'format' => 'php',
    // Root directory containing message translations.
    'messagePath' => __DIR__ . DIRECTORY_SEPARATOR . 'messages',
    // boolean, whether the message file should be overwritten with the merged messages
    'overwrite' => true,


    /*
    // 'db' output format is for saving messages to database.
    'format' => 'db',
    // Connection component to use. Optional.
    'db' => 'db',
    // Custom source message table. Optional.
    // 'sourceMessageTable' => '{{%source_message}}',
    // Custom name for translation message table. Optional.
    // 'messageTable' => '{{%message}}',
    */

    /*
    // 'po' output format is for saving messages to gettext po files.
    'format' => 'po',
    // Root directory containing message translations.
    'messagePath' => __DIR__ . DIRECTORY_SEPARATOR . 'messages',
    // Name of the file that will be used for translations.
    'catalog' => 'messages',
    // boolean, whether the message file should be overwritten with the merged messages
    'overwrite' => true,
    */
];

I’m customizing the file as follows. I move messagePath up to the top and customize sourcePath and messagePath. I am also specifying the languages I want my application to support besides English—in this case Spanish (es), German (de), Italian (it) and Japanese (ja). Here’s a list of all the I18n language codes.

<?php

return [
    // string, required, root directory of all source files
    'sourcePath' => __DIR__. DIRECTORY_SEPARATOR .'..',
    // Root directory containing message translations.
    'messagePath' => __DIR__ . DIRECTORY_SEPARATOR .'..'. DIRECTORY_SEPARATOR . 'messages',
    // array, required, list of language codes that the extracted messages
    // should be translated to. For example, ['zh-CN', 'de'].
    'languages' => ['de','es','it','ja'],
    // string, the name of the function for translating messages.
    // Defaults to 'Yii::t'. This is used as a mark to find the messages to be
    // translated. You may use a string for single function name or an array for
    // multiple function names.
    'translator' => 'Yii::t',

In the next step, we’ll run Yii’s extract script which will scan all the code in the sourcePath tree to generate default string files for all the labels used in our code. I’m customizing sourcePath to scan the entire code tree. I’m customizing messagePath to generate the resulting files in common/messages.

 ./yii message/extract @app/config/i18n.php

You’ll see Yii scanning all of your code files:

Extracting messages from /Users/Jeff/Sites/hello/views/layouts/main.php...
Extracting messages from /Users/Jeff/Sites/hello/views/site/about.php...
Extracting messages from /Users/Jeff/Sites/hello/views/site/contact.php...
Extracting messages from /Users/Jeff/Sites/hello/views/site/error.php...
Extracting messages from /Users/Jeff/Sites/hello/views/site/index.php...
Extracting messages from /Users/Jeff/Sites/hello/views/site/login.php...
Extracting messages from /Users/Jeff/Sites/hello/views/site/say.php...
Extracting messages from /Users/Jeff/Sites/hello/views/status/_form.php...
Extracting messages from /Users/Jeff/Sites/hello/views/status/_search.php...
Extracting messages from /Users/Jeff/Sites/hello/views/status/create.php...
Extracting messages from /Users/Jeff/Sites/hello/views/status/index.php...
Extracting messages from /Users/Jeff/Sites/hello/views/status/update.php...
Extracting messages from /Users/Jeff/Sites/hello/views/status/view.php...
Extracting messages from /Users/Jeff/Sites/hello/web/index-test.php...
Extracting messages from /Users/Jeff/Sites/hello/web/index.php...

When it completes, you’ll see something like this in your codebase:

Yii2 Localization with I18n Directory Paths

Activating I18n and Selecting a Language

In the common configuration file, /hello/config/web.php, we’re going to tell Yii about our new language support. I’ll make Spanish my default language:

<?php

$params = require(__DIR__ . '/params.php');

$config = [
    'id' => 'basic',
    'basePath' => dirname(__DIR__),
    'bootstrap' => ['log'],
    'language'=>'es', // spanish
    'components' => [

But there’s still more to do. We have to make our code I18n aware.

Using Yii’s Gii Code Generator With I18n

In part three of this series, we used Yii’s database and active record capabilities to automate code generation. But we did not activate I18n, so all of our code had text strings embedded. Let’s redo this.

We return to Gii, likely http://localhost:8888/hello/gii in your browser, and re-run the model and controller generators with I18n activated.

Here’s an example of generating the Meeting model code with I18n activated. Notice that we specify “app” for our Message Category. We’re placing all of our text strings in one app category file. 

Yii2 Gii Code Generator Status Model for I18n

Let’s do the same for the CRUD generation for controllers and views:

Yii2 Gii Code Generator for Status CRUD With I18n

If you browse the generated code in models, controllers and views you’ll see the text strings replaced with the Yii:t('app',...) function:

<?php

use yii\helpers\Html;
use yii\grid\GridView;

/* @var $this yii\web\View */
/* @var $searchModel app\models\StatusSearch */
/* @var $dataProvider yii\data\ActiveDataProvider */

$this->title = Yii::t('app', 'Statuses');
$this->params['breadcrumbs'][] = $this->title;
?>
<div class="status-index">

    <h1><?= Html::encode($this->title) ?></h1>
    <?php // echo $this->render('_search', ['model' => $searchModel]); ?>

    <p>
        <?= Html::a(Yii::t('app', 'Create {modelClass}', [
    'modelClass' => 'Status',
]), ['create'], ['class' => 'btn btn-success']) ?>
    </p>

    <?= GridView::widget([
        'dataProvider' => $dataProvider,
        'filterModel' => $searchModel,
        'columns' => [
            ['class' => 'yii\grid\SerialColumn'],

            'id',
            'message:ntext',
            'permissions',
            'created_at',
            'updated_at',

            ['class' => 'yii\grid\ActionColumn'],
        ],
    ]); ?>

</div>

Making Static Views I18n Ready

Because we generate a number of views in our application by hand or in HTML, we need to manually convert these to use I18n. For example, our navigation bar in /views/layouts/main.php and our home page in /views/site/index.php both need to be edited by hand. 

Here’s the navigation bar before I18n:

NavBar::begin([
                'brandLabel' => 'My Company',
                'brandUrl' => Yii::$app->homeUrl,
                'options' => [
                    'class' => 'navbar-inverse navbar-fixed-top',
                ],
            ]);
            $navItems=[
                ['label' => 'Home', 'url' => ['/site/index']],
                ['label' => 'Status', 'url' => ['/status/index']],
                ['label' => 'About', 'url' => ['/site/about']],
                ['label' => 'Contact', 'url' => ['/site/contact']]
              ];
              if (Yii::$app->user->isGuest) {
                array_push($navItems,['label' => 'Sign In', 'url' => ['/user/login']],['label' => 'Sign Up', 'url' => ['/user/register']]);
              } else {
                array_push($navItems,['label' => 'Logout (' . Yii::$app->user->identity->username . ')',
                    'url' => ['/site/logout'],
                    'linkOptions' => ['data-method' => 'post']]
                );
              }
            echo Nav::widget([
                'options' => ['class' => 'navbar-nav navbar-right'],
                'items' => $navItems,
            ]);
            NavBar::end();

Here’s the navigation bar after I18n:

NavBar::begin([
    'brandLabel' => Yii::t('app', 'My Company'),
    'brandUrl' => Yii::$app->homeUrl,
    'options' => [
        'class' => 'navbar-inverse navbar-fixed-top',
    ],
]);
$navItems=[
    ['label' =>  Yii::t('app', 'Home'), 'url' => ['/site/index']],
    ['label' => Yii::t('app','Status'), 'url' => ['/status/index']],
    ['label' => Yii::t('app','About'), 'url' => ['/site/about']],
    ['label' => Yii::t('app','Contact'), 'url' => ['/site/contact']]
  ];
  if (Yii::$app->user->isGuest) {
    array_push($navItems,['label' => Yii::t('app','Sign In'), 'url' => ['/user/login']],['label' => Yii::t('app','Sign Up'), 'url' => ['/user/register']]);
  } else {
    array_push($navItems,['label' => Yii::t('app','Logout').' (' . Yii::$app->user->identity->username . ')',
        'url' => ['/site/logout'],
        'linkOptions' => ['data-method' => 'post']]
    );
  }
echo Nav::widget([
    'options' => ['class' => 'navbar-nav navbar-right'],
    'items' => $navItems,
]);
NavBar::end();

Here’s a fragment of the home page content from index.php after I18n—much of the HTML has been replaced by PHP calls to Yii::t():

<div class="jumbotron">
        <h1><?= Yii::t('app','Congratulations!'); ?></h1>

        <p class="lead"><?= Yii::t('app','You have successfully created your Yii-powered application.'); ?></p>

        <p><a class="btn btn-lg btn-success" href="http://www.yiiframework.com"><?= Yii::t('app','Get started with Yii'); ?></a></p>
    </div>

    <div class="body-content">

        <div class="row">
            <div class="col-lg-4">
                <h2><?= Yii::t('app','Heading'); ?></h2>

                <p><?= Yii::t('app','Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.'); ?></p>

                <p><a class="btn btn-default" href="http://www.yiiframework.com/doc/"><?= Yii::t('app','Yii Documentation') ?> &raquo;</a></p>
            </div>

Translating Your Message Files

Take a look at our Spanish message file, /common/messages/es/frontend.php. It’s a long list of empty array values:

return [
    'About' => '',
    'Contact' => '',
    'Home' => '',
    'Logout' => '',
    'My Company' => '',
    'Sign In' => '',
    'Sign Up' => '',
    'Status' => '',
    ...

For the purposes of filling in our Spanish language translations for this tutorial, I’ll use Google Translate. Tricky, huh?

Yii2 I18n Using Google Translator to Fill Message Files

Then, we’ll do some cut and paste with those translations back into the message file. 

return [
    'About' => 'Acerca de',
    'Contact' => 'Contacto',
    'Home' => 'Home',
    'Logout' => 'Salir',
    'My Company' => 'Mi Empresa',
    'Sign In' => 'Entrar',
    'Sign Up' => 'Registrarse',
    'Status' => 'Estado',

When we visit the Application home page, you’ll see the Spanish version—nice, huh?

Yii2 I18n Our App Home Page in Spanish

Here’s the Create Status form:

Yii2 I18n Create a Status Update in Spanish Form

If I want to switch back to English, I just change the configuration file, /config/web.php, back to English:

<?php

$params = require(__DIR__ . '/params.php');

$config = [
    'id' => 'basic',
    'basePath' => dirname(__DIR__),
    'bootstrap' => ['log'],
    'language'=>'en', // back to English
    'components' => [

You’ll also notice as you proceed that replacing strings in JavaScript has its own complexities. I haven’t explored it myself, but the Yii 1.x JsTrans extension may provide a useful guideline for supporting this.

Going Further With I18n

Ultimately, we may want to translate our application into a number of languages. I’ve written a new tutorial called Using the Google Translate API to Localize Your I18n App (Tuts+) which automatically translates your application into a variety of languages. If it’s not published yet, it will be published soon (check my instructor page). Of course, this just provides base translations. You may want to hire professional translators to tune the files afterwards.

Some applications allow users to select their native language so that when they log in, the user interface automatically translates for them. In Yii, setting the $app->language variable does this:

\Yii::$app->language = 'es';

Other applications, like JScrambler.com below, leverage the URL path to switch languages. The user just clicks the language prefix they want, e.g. “FR”, and the app is automatically translated: 

JScrambler Dynamic Language Paths

Note: Read my recent introduction to JScrambler to find out more—it’s a pretty useful service.

Yii’s URL Manager can provide this type of functionality as well. I will probably implement these features in a future tutorial in this Yii2 series when I focus on Routing.

What’s Next?

I hope you’re excited about the power of I18n and the benefits of using the Yii Framework over vanilla PHP. Watch for upcoming tutorials in our Programming With Yii2 series.

If you’d like to know when the next Yii2 tutorial arrives, follow me @reifman on Twitter or check my instructor page. My instructor page will include all the articles from this series as soon as they are published. You can also email me at my Lookahead Consulting website.

Related Links

  • Yii Framework Website
  • Introduction to the Yii Framework (Tuts+)
  • Using the Google Translate API to Localize Your I18n App (Tuts+)
  • Building Your Startup With PHP: Localization With I18n (Tuts+)
  • Other Free and Open Source Yii Developer Examples by the Instructor