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, т.е. сообщение на нужном языке.

Заключение

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

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

Успехов!

Содержание

  • bobpps

    Большое спасибо!

  • Oleg

    Прекрасные статьи для начинающих работать с AngularJS. Спасибо!

  • Денис

    Какой скрип или плагин на данном сайте для вывода комментариев ??

    • DISQUS. У меня есть статья на эту тему, правда написана она довольно давно, но большая часть информации остаётся актуальной.

      • Игорь

        Владимир, а вы проводите обучение по Yii?

        • Только в рамках этого блога. Т.е. если у вас есть вопросы по одной из моих статей о Yii, я отвечу.

  • vitalka

    Скачал и развернул Ваш пример. Импортировал из дампа базу.

    На Денвере работает, а вот на удаленом хостинке выдает ошибку: «CDbCommand не удалось исполнить SQL-запрос: SQLSTATE[42S02]: Base table or view not found: 1146 Table 'yii.AuthAssignment' doesn't exist. The SQL statement executed was: SELECT *FROM `AuthAssignment` WHERE userid=:userid».

    Неужели в работе с базой как то имеет значение регистр в названиях таблиц?
    Или нужно копать в иную сторону?

    • Похоже у Вас не созданы таблицы в которых хранятся права пользователей (их использует библиотека RBAC).
      Импортируйте в базу файл
      framework/web/auth/schema-mysql.sql

      • vitalka

        Владимир, спасибо за Ваш ответ и цикл статей.
        Моя ошибка возникла видимо из-за того, что названия таблиц в MySql под Unix-ом регистрозависимы.
        В дампе базы из Вашего примера имеем «CREATE TABLE `authassignment` ( …», а в рекомендованным в Вашем ответе(выше) файле имеем «create table `AuthAssignment`…».

        Можно ли дамп базы из примера переделать из MySql под Unix-ом ?
        Либо лучше в php-коде переписать модель?
        Спасибо.

        • Странно, у меня приложение работает и под Win, и под Linux и я не сталкивался с этой проблемой.

          Возможно, регистр изменился при создании дампа.

          В любом случае, желательно использовать framework/web/auth/schema-mysql.sql, т.к. библиотека RBAC ориентирована именно на него.

        • vitalka

          Я тоже раньше с таким не сталкивался. 🙂
          Как оказалось проблемка в параметре lower_case_table_names MySQL-ля. Вот тут http://dev.mysql.com/doc/refman/5.1/en/identifier-case-sensitivity.html рекомендуют «Use lower_case_table_names=1 on all systems», а у моего хостера выставлено lower_case_table_names=0. Увы …

        • Сейчас вполне приемлемые цены на VPS. Правда в этом случае все настраивать нужно самостоятельно 🙂

  • Очень толковое описание ! Автору +100 в карму !

  • cpentyc

    Спасибо за уроки. не собираетесь сделать серию уроков по и yii Node.js

    • Нет. Node.js — интересная платформа, но пока для себя я не вижу особых преимуществ в переходе на неё.

  • Guest

    Подскажите начинающему, Не могу открыть «Обьекты», постоянно перебрасывает в раздел «Логин»

  • Іван

    Подскажите начинающему, почему я не могу перейти в раздел «Обьекты», сразу перебрасывает в site/login

    • Похоже действительно проблема с сессиями.

      1) Создайте новый файл (в корне приложения) info.php и добавьте в него
      phpinfo();

      2) Откройте в браузере your_site.com/info.php
      Вы увидите страницу с настройками php, найдите параметр session.save_path. У него должно быть значение вроде /var/lib/php/session

      3) Если это тестовый сервер, установите права на эту папку 777.

      4) Если предыдущие шаги не помогли, то можно указать папку для сессий в конфиге Yii (файл protected/config/main.php).

      'components'=>array(
      'session'=>array(
      'savePath'=>'path/to/new/tmp',
      ),
      )

      В этом случае также нужно убедиться, что папка существует и доступна для записи.

      • Іван

        PHP Version 5.3.13session.save_path/tmp/tmp
        Если в конфиге прописать то же путь — ничего не изменится, а если другой, то ругается что нет такой папки, даже если папка существует и на нее имеют полные права Everyone.
        PS залито с архива на Denwer, единственное что менял это имя пользователя бд в main и console

        • Честно говоря, Denwer'ом очень давно не пользовался.
          Как вы задаёте путь к существующей папке?
          «d:/folder_name» так? или как-то иначе?

          Как вариант, можете попробовать WampServer http://www.wampserver.com/ru/ вместо Denwer. Когда я им пользовался, проблем не было. Да и версию PHP желательно использовать по новее. Например, для Yii2 нужно PHP 5.4.

        • Іван

          Wamp-ом тож не вышло, помог XAMPP. Извините за дурацкие вопросы 🙂

        • Всё нормально 🙂 Теперь вы можете сравнить настройки сессий и разобраться почему возникла проблема.

  • Eugene Hype

    спасибо! информативно