Привет! В этой статье речь пойдет о применении библиотеки angular-translate для локализации приложения. Расскажем о возможностях этой библиотеки, опишем проблемы, которые могут возникнуть, и дадим советы по их решению (основываясь, конечно, на собственном опыте).
Введение
Для локализации, то есть перевода приложения на другой язык, существует концепция языковых ресурсов. Суть её проста: вместо того, чтобы заводить константу с текстом, мы проставляем в коде некоторый идентификатор, обычно называемый ключом перевода [translation key], который может использоваться как на этапе сборки приложения, так и во время его исполнения. Встречая ключ перевода, компоновщик приложения (или runtime) заменяет его на нужный текст, и пользователь видит надпись уже на нужном ему языке. Конечно, полностью проблема перевода этим не решается. Например, строки текста вполне могут иметь разную длину на разных языках, и тогда перевод может испортить пользовательский интерфейс, выдав обрезанные надписи. Кошмаром для разработчика может стать перевод на арабские языки, где текст читается справа налево. Тем не менее, такая технология имеет место быть и успешно применяется в ряде случаев.
В мире .Net стандартом работы с языковыми ресурсами являются файлы *.Resx. Специальная утилита resgen превращает их в файлы *.resources., после чего эти ресурсы встраиваются либо в основную сборку, либо во вспомогательные сборки, а затем могут быть использованы в приложении.
В веб-проектах изначально использовалась такая же технология, и такое решение было удачным, так как страницы генерировались в основном на сервере, где могли быть размещены языковые ресурсы. Однако со временем клиентская часть усложнялась, и проблем с ней становились всё больше. Решали их обычно двумя способами: либо делали альтернативные JS для разных языков (такой подход используется, например, в MS SharePoint), либо передавали локализованные данные в виде параметров в коде *.ASPX-страниц. Понятно, что второй подход больше подходит к небольшим блокам клиентского кода — таким, где сообщений и тестовых элементов относительно немного. Что же касается первого, то его недостаток в том, что локализация “размазывается” между файлами ресурсов и локальными версиями JavaScript.
В последнее время во многих приложениях UI стал генерироваться непосредственно на клиенте, необходимость в серверных ресурсах стала меньше. Вместе с тем, широкое распространение получили framework’и для разработки приложений, реализующие достаточно богатые модели программирования (такие как knockout.js, angular.js, backbone.js и т.д.). Каждая из моделей рекомендует собственную организацию проекта, поэтому, по-видимому, наиболее удобным будет, чтобы framework имел свою модель локализации.
Итак, у нас есть приложение, которое необходимо научить работать с другим языком. Конечно, мы понимаем, что в идеальном мире об этом стоит позаботиться ещё на стадии проектировании приложения, однако в реальности поддержка локализации или «локализуемость» — это то, чем часто жертвуют на первых этапах разработки в силу понятных причин.
Кстати говоря, MSDN рекомендует добавлять поддержку обеспечения работы в 3 шага:
- Глобализация — т.е. поддержка систем записи, используемых календарей, соглашения о формате даты и времени, соглашения о представлении числовых и денежных величин, а также правила сортировки.
- Локализуемость — т.е. отделения логики приложение от локализуемых ресурсов.
- Локализация — непосредственно перевод приложения на целевой язык.
При разработке приложения с использованием AngularJS есть несколько способов локализовать приложение. Самый простой — создать отдельные шаблоны страниц для всех поддерживаемых языков. Минусы подобного подхода очевидны: трудозатратность, дублирование кода, отсутствие модульности, несоответствие подходу SPA и т.д. Оптимальным решением будет некий универсальный способ для перевода страниц на основе ресурсов (словарей). Здесь можно пойти двумя путями: реализовать свой собственный сервис локализации или использовать готовые средства, такие как библиотека angular-translate. О ней расскажем подробнее.
Основные возможности библиотеки angular-translate
Библиотека angular-translate реализует солидный функционал для нужд локализации. Он представлен набором сервисов, фильтров и директив для локализации. Для работы библиотеки нужны JSON-файлы с ключами и переводами.
/* file: “~/fruits/en.json” */ /* file “~/fruits/ru.json” */
{ {
"APPLE": "Apple", "APPLE": "Яблоко",
"ORANGE": "Orange" "ORANGE": "Апельсин"
} }
Библиотека способна подгружать с сервера необходимые файлы по необходимости (так называемый lazy-loading).
Подключить angular-translate в проект можно через менеджер пакетов Bower. К сожалению, на момент написания статьи в стандартном репозитории Nuget пакета для angular-translate не было, поэтому пришлось вручную добавить библиотеку angular-translate.js и ссылку на нее, a также добавить модуль “pascalprecht.translate” в angualr как зависимость.
Основная служба angular-translate – это провайдер $translate. На этапе конфигурации (обратите внимание, что нужно использовать обращение $translateProvider, например) сервис позволяет регистрировать таблицы с встроенными в приложение переводами, асинхронные загрузчики (urlLoader, staticFilesLoader, partialLoader), а также выбирать хранилищe (cookie, local) для настроек локализации. На этапе выполнения сервис доступен как $translate, являясь одновременно и функцией, используемой для перевода, и “объектом”, содержащим функции для настройки перевода.
Перевод
В angular-translate перевод возможно осуществить тремя различными способами:
C использованием сервиса “$translate”:
$translate('APPLE').then(function (result) {
$scope.fruitName = result;
});
$translate не обеспечивает two-way binding по умолчанию, в результате чего, при смене языка в runtime, текст, переведённый с помощью $trnaslate, не изменится автоматически. Исправить это возможно, подписавшись на событие $translateChangeSucces в $rootScope:
$rootScope.$on('$translateChangeSuccess', function () {
$translate('HEADLINE').then(function (translation) {
$scope.headline = translation;
});
});
$trnaslate стандартно работает асинхронно, но поддерживает и синхронный вариант пере-вода с использованием встроенного метода «instant».
С использованием директивы “translate”:
<ANY translate>TRANSLATION_ID</ANY>
<ANY translate="TRANSLATION_ID"></ANY>
С использованием фильтра “… | translate”:
<p>{{'APPLE' | translate}}</p>
Загрузка словарей
В angular-translate существует несколько способов добавить необходимые словари (обратите внимание, что в словарях можно использовать namespace’ы, реализованные посредством вложенных json-объектов):
Встроенные в приложение словари
Размещение словарей непосредственно в коде и/или из json-файлов ресурсов в составе проекта. Подключается так:
var fruits = {
APPLE: 'Apple'
CITRIC: {
ORANGE: 'Orange'
}
};
$translateProvider
.translations('en', fruits)
.preferredLanguage('en');
Асинхронная загрузка
Позволяет загружать словари с сервера по необходимости. Реализуется стандартными загрузчиками (есть возможность определить свой загрузчик):
- urlLoader. Загружает словари по указанному url. Реализован как отдельный пакет в Bower “angular-translate-loader-url”. Подключается так:
$translateProvider.useUrlLoader('foo/bar.json'); $translateProvider.preferredLanguage('en');
В итоге, загрузчик из примера пошлёт следующий запрос:
foo/bar.json?lang=en.
- staticFilesLoader. Используется для загрузки нескольких файлов для локализации, имеющих заданный формат (prefix и suffix). Реализован как отдельный пакет в Bower. Подключаются так:
$translateProvider.useStaticFilesLoader({ prefix: 'locale-', suffix: '.json' }); $translateProvider.preferredLanguage('en');
- partialLoader. Используется для частичной загрузки данных локализации, структурированных на сервере по шаблону (например, “/l10n/{part}/{lang}” или “/l10n/{lang}/{part}). Он асинхронно загружает только указанные файлы (части) после вызова соответствующей функции (addPart). Идёт в составе пакета angular-translate-loader-partial. Пример:
$translatePartialLoaderProvider.addPart('fruits'); $translateProvider.useLoader('$translatePartialLoader', { urlTemplate: '/l10n/{part}/{lang}.json' }); $translateProvider.preferredLanguage('en');
Интерполируемые переменные
Angular-translate поддерживает наличие в json-словарях “интерполируемых переменных”, например:
{
"DELICIOUS_FRUIT": "{{fruit_name}} is delicious!"
}
Подобного вида строки могут в дальнейшем быть использованы следующим образом:
Сервисом:
$translate('DELICIOUS_FRUIT', { fruit_name: 'Apple' });
Фильтром:
{{ 'DELICIOUS_FRUIT' | translate:'{ fruit_name: “Apple” }' }}
или
{{ 'DELICIOUS_FRUIT' | translate: fruitData }}
где
$scope.fruitData = {
o fruit_name: 'Apple'
o };
Директивой:
<ANY translate="DELICIOUS_FRUIT " translate-values='{ fruit_name: "Ap-ple"}'></ANY>
<ANY translate="DELICIOUS_FRUIT "translate-values="{ fruit_name: fruitData.fruit_name }"></ANY>
<ANY translate="DELICIOUS_FRUIT "translate-values="{{fruitData}}"></ANY>
Языковой стек
Библиотека поддерживает создание стека поддерживаемых языков, которые добавляются в в порядке приоритета. Так, если выбранный язык по тем или иным причинам будет недоступен, приложение будет искать ключ далее по стеку по принципу first-fit. Пример:
$translateProvider
.translations('de', { /* ... */ })
.translations('en', { /* ... */ })
.translations('fr', { /* ... */ })
.fallbackLanguage(['en', 'fr']);
Здесь итерация поиска начнётся с en и до fr.
Группы родственных наречий (языков)
Поддерживается объединение родственных наречий в группы. Например:
.registerAvailableLanguageKeys(['en', 'de'], {
'en_US': 'en',
'en_UK': 'en',
'de_DE': 'de',
'de_CH': 'de'
})
Плюрализация
Есть поддержка плюрализации с использованием библиотеки MessageFormat. Ставится из пакета angular-translate-interpolation-messageformat (bower). Пример:
Полный список можно найти на официальной странице.
Проблемы перевода реального приложения и сопутствующие сложности
Мы разобрали различные способы локализации, а теперь давайте посмотрим, с какими проблемами можно столкнуться, добавляя поддержку локализации к реальному приложению.
Синхронизация локализации клиента и сервера
В нашем случае небольшая часть логики, зависящей от языка, всё же находилась на сервере, поэтому пeрвое, с чем мы столкнулись, — это необходимость синхронизовать выбранные языки на клиенте и сервере. На стороне сервера имелся приватный сервис, отвечающий за серверную локализацию, и API-контроллер, используемый клиентской стороной для синхронизации изменений в языке. Для стартовой синхронизации языка веб-приложения сервер выставляет соответствующую переменную в @ViewBag, которая посредством директивы AngularJS транслируется в соответствующую scope, где используется сервисом angular-translate с помощью $translate.use().
Локализация директив angular, содержащих template
Для перевода ключей, используемых в шаблонах директив, можно использовать фильтр “…| translate”. Однако существует определённая сложность: при простом добавлении фильтра two-way binding может быть недоступен, то есть директива не станет реагировать на переключение языка. Причина кроется в том, что событие $translateChangeSuccess не транслируется в scope директивы. Проблема может быть решена c использованием еще одной директивы, которая транслирует событие:
angular.module('app').directive('uiBroadcastTranslate', function ($rootScope) {
return {
link: function (scope) {
$rootScope.$on('$translateChangeSuccess', function () {
scope.$broadcast('uiBroadcastTranslateDirectiveEvent');
});
}
} });
Создание словарей с необязательными переменными
В процессе работы мы наткнулись на еще одну интересную возможность. Рассмотрим пример. Пусть в приложении есть строки вида “Single”, “Single choice”. Можно составлять локализованные словосочетания, локализуя каждую из строк отдельно. Однако можно создать строку словаря вида «SINGLE»: «Single {{choice}}». Тогда, если не задать значения параметра choice, {{'SINGLE' | translate}} будет интерполирован как просто “Single”.
Проблема повторной загрузки ресурсов при использовании вложенных контроллеров
Пусть в приложении используется partialLoader. Имеется несколько ng-controller-ов, связанных с scope, вложенными друг в друга.
shellCtrl [shellScope] < — workspaceCtrl [workspaceScope] < — toolCtrl [toolScoope]).
В каждом из них определена своя “часть” для локализации $translate.addPart(“ctrl[]Part”).
При таком подходе partialLoader загрузит:
- один модуль shellCtrlPart для контроллера shellCtrl;
- два модуля workspaceCtrlPart, shellCtrlPart для контроллера shellCtrl;
- и три модуля shellCtrlPart, workspaceCtrlPart, toolCtrlPart для контроллера toolCtrl.
Таким образом, модуль shellCtrlPart будет загружен трижды, а workspaceCtrlPart дважды, что не очень хорошо. Причина проблемы кроется в том, что в $http GET-запросе partialLoader не используется кэширование. Решить проблему можно, например, включением кэширования для загрузки языковых ресурсов:
$http({
method: 'GET',
url: this.parseUrl(urlTemplate, lang),
cache:true
});
Заключение
На наш взгляд, библиотека достаточно удобна. Если приложение не задумывалось изначально локализуемым, то поддержку можно добавить путем глубокого рефакторинга. В нашем случае объем текста был невелик, поэтому на поддержку локализуемости и на локализацию на другой язык [немецкий] ушло ~ 1,5 недели вместе с переводом.
Ссылки
1. blog.novanet.no/creating-multilingual-support-using-angularjs/
2. angular-translate.github.io/docs/#/api/pascalprecht.translate.$translateProvider
3. www.beabigrockstar.com/blog/share-translations-between-aspnet-mvc-and-angularjs
4. github.com/beabigrockstar/SharedTranslationsAspnetAngular
Автор: eastbanctech