Personal Maps: локализация и интернационализация. Часть 10

Владимир | | AngularJS, JavaScript, PHP, Web разработка, Yii.

personal maps i18n

Приветствую! Это заключительная статья цикла о разработке web приложения с использованием фреймворков Yii и AngularJS. На данный момент у нас есть полностью работающее приложение, и остаётся добавить возможность перевода интерфейса на разные языки.

Примечание. Ссылки на все предыдущие статьи вы найдёте в конце этой страницы.

Вообще создание многоязычного интерфейса – задача довольно тривиальная. В большинство фреймворков (и Yii здесь не исключение) входят соответствующие библиотеки. Но в нашем случае ситуация немного сложнее из-за того, что приложение состоит из клиентской и серверной частей. При этом для обоих фреймворков (Yii и AngularJS) есть собственные средства для работы с переводами.

В принципе, можно работать с двумя библиотеками. Но обычно удобнее, собрать все переводы в одном месте, либо на клиенте, либо на сервере. Т.к. передача данных от сервера клиенту (браузеру) проще, то мы будем использовать библиотеку Yii в качестве основной. А при формировании главной страницы приложения, передадим переводы AngularJS.

Напоминаю. Вы можете посмотреть исходный код приложения на GitHub и поэкспериментировать с демо-версией.

Source Demo

Интернационализация серверной части (Yii)

В официальном руководстве есть подробная статья на эту тему, повторять её я не буду, а остановлюсь только на тех моментах, которые относятся к нашему приложению.

Прежде всего, необходимо выбрать тип источника сообщений. Yii поддерживает три типа таких источников:

  • обычные PHP массивы (CPhpMessageSource);
  • файлы формата GNU Gettext (CGettextMessageSource);
  • базу данных (CDbMessageSource).

Я остановился на первом варианте (PHP массивы), но принципиальной разницы нет.

Важно другое. Для передачи переводов клиентской части нам очень желательно передать сразу все переводы, иначе их придётся загружать AJAX запросами, а это занимает время и не лучшим образом скажется на внешнем виде приложения. Но класс CPhpMessageSource не позволяет получить все переводы сразу, точнее нужный нам метод loadMessages объявлен защищённым (protected).

Поэтому мы создадим компонент, который наследует CPhpMessageSource и будем использовать его в качестве источника сообщений.

protected/components/PhpMessageSource.php

class PhpMessageSource extends CPhpMessageSource {
    public function getAllMessages($category, $lang) {
        return $this->loadMessages($category, $lang);
    }
}

Мы объявили один метод getAllMessages, который просто вызывает loadMessages, т.е. возвращает массив переводов для указанного языка.

Подключаем наш компонент в config/main.php

return array(
	...
    'language'=>'ru',

	...
	// application components
	'components'=>array(
		...
        'messages'=>array(
            'class'=>'PhpMessageSource',
        ),
	),
	...
);

Файлы с переводами находятся в папке protected/messages.

messages/
	en/
		frontend.php
		...
	ru/
		frontend.php
		...

Сами переводы выглядят следующим образом:

return array(
    'CREATE_PLACE' => 'Create place',
    'UPDATE_PLACE' => 'Update place',
	...
);

return array(
    'CREATE_PLACE' => 'Создать объект',
    'UPDATE_PLACE' => 'Изменить объект',
	...
);

Интернационализация клиентской части (AngularJS)

Прежде всего, нам нужно передать переводы браузеру. Для этого в представление, которое создаёт главную страницу приложения (protected/views/places/index.php), добавим следующий код:

Yii::app()->clientScript->registerScriptFile(Yii::app()->baseUrl.'/js/angular-translate.min.js', CClientScript::POS_END);
Yii::app()->clientScript->registerScript(
    'langScript'
    , '
    var lang = "'.Yii::app()->getLanguage().'";
    var translations = '.CJSON::encode(Yii::app()->messages->getAllMessages('frontend', Yii::app()->getLanguage())).';'
    , CClientScript::POS_HEAD
);

В первой строке мы подключаем Angular translate. Это модуль, предназначенный для интернационализации приложений на Angular.

Затем мы создаём две JS переменные:

lang – содержит название языка;
translations – содержит массив с переводами.

Этих данных нам достаточно для того, чтобы настроить приложение public_html/js/app.js

Мы указываем модуль pascalprecht.translate в списке зависимостей приложения.

var app = angular.module('personalmaps', ['ui.bootstrap', 'pascalprecht.translate'])
    .value('lang', lang);

В результате через систему внедрения зависимостей (Dependency injection — DI) станет доступен сервис $translateProvider, которому мы передаём массив с переводами.

app.config(['$translateProvider', function($translateProvider) {
    // add translation table
    $translateProvider.translations(translations);
}]);

Также через DI будет доступна переменная lang. Вообще мы можем обойтись без неё, но я решил просто показать пример использования переменных. Т.е. получить к ней доступ можно, например, так:

app.controller('PlacesListController'
    , ['$scope', '$rootScope', 'Places', '$dialog', 'lang'
    , function($scope, $rootScope, Places, $dialog, lang) {

    $scope.curLang = lang;

	...
}]);

Возвращаемся к модулю Angular translate.

На официальном сайте предлагается загрузить все варианты переводов.

app.config(function ($translateProvider) {
  $translateProvider.translations('en', {
    TITLE: 'Hello',
	...
  });
  $translateProvider.translations('de', {
    TITLE: 'Hallo',
	...
  });
  $translateProvider.preferredLanguage('en');
});

Такой способ имеет смысл использовать, если вы хотите предоставить пользователю возможность переключать языки прямо из интерфейса приложения, т.е. выполнять следующий код.

$scope.changeLanguage = function (key) {
	$translate.uses(key);
};

Но в нашем приложении такая возможность не предусматривается. Язык приложения указывается в конфигурационном файле main.php, поэтому отправлять браузеру все переводы нет смысла. Мы просто один раз вызываем метод translations и передаём ему массив с переводами на выбранный язык.

$translateProvider.translations(translations);

Для использования переводов в модуль angular translate входит специальный фильтр – translate. Т.е. теперь в шаблонах мы можем написать что-то вроде (partials/list.html):

<span ng-show="isEmpty()">{{ 'NO_PLACES' | translate }}</span>

<div>
    <a href="places/index#/add" class="btn btn-success">{{ 'ADD_PLACE' | translate }}</a>
</div>

В фигурных скобках мы указываем имя сообщения (ключ в массиве translations) и через вертикальную черту – название фильтра. В результате мы получим значение из массива translations, т.е. сообщение на нужном языке.

Заключение

Этот цикл получился довольно объемным, и публикация растянулась на два месяца. Но мне кажется, что такой формат полезнее, чем отдельные статьи, потому что в них сложно рассмотреть взаимодействие нескольких технологий между собой. Даже в этом цикле многие вещи пришлось упростить, чтобы не перегружать код различными проверками и дополнительными функциями. Всё-таки основная цель заключалась в том, чтобы показать, как компоненты взаимодействуют между собой, а не создать приложение для продакшена (хотя приложение вполне можно использовать).

И отдельно хочу поблагодарить всех читателей, которые присылали вопросы и замечания. Вы помогли сделать этот цикл лучше.

Успехов!

Содержание