Как интернационализировать ваше приложение AngularJS

Опубликовано: 2022-03-11

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

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

Соответственно, приложению AngularJS требуется доставка по запросу данных интернационализации (i18n) и локализации (l10n), которые должны быть доставлены клиенту для отображения в соответствующей локали. В отличие от традиционных приложений, отображаемых на стороне сервера, вы больше не можете полагаться на сервер для доставки уже локализованных страниц. Вы можете узнать о создании многоязычного PHP-приложения здесь.

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

Простое приложение AngularJS с поддержкой i18n

Чтобы клиент мог менять язык и локаль «на лету» в зависимости от предпочтений пользователя, вам потребуется принять ряд ключевых дизайнерских решений:

  • Как вы проектируете свое приложение, чтобы оно с самого начала не зависело от языка и локали?
  • Как вы структурируете данные i18n и l10n?
  • Как вы эффективно доставляете эти данные клиентам?
  • Как абстрагироваться от низкоуровневых деталей реализации, чтобы упростить рабочий процесс разработчика?

Ответив на эти вопросы как можно раньше, вы сможете избежать препятствий в процессе разработки в будущем. Каждая из этих проблем будет рассмотрена в этой статье; некоторые с помощью надежных библиотек AngularJS, другие с помощью определенных стратегий и подходов.

Библиотеки интернационализации для AngularJS

Существует ряд библиотек JavaScript, созданных специально для интернационализации приложений AngularJS.

angular-translate — это модуль AngularJS, предоставляющий фильтры и директивы, а также возможность асинхронной загрузки данных i18n. Он поддерживает множественное число через MessageFormat и спроектирован таким образом, чтобы его можно было легко расширять и настраивать.

Если вы используете angular-translate в своем проекте, вам могут пригодиться некоторые из следующих пакетов:

  • angular-sanitize : может использоваться для защиты от XSS-атак в переводах.
  • angular-translate-interpolation-messageformat : множественное число с поддержкой форматирования текста с учетом пола.
  • angular-translate-loader-partial : используется для доставки переведенных строк клиентам.

Для действительно динамичного опыта вы можете добавить angular-dynamic-locale к связке. Эта библиотека позволяет вам динамически изменять локаль, в том числе способ форматирования дат, чисел, валют и т. д.

Начало работы: установка соответствующих пакетов

Предполагая, что у вас уже есть готовый шаблон AngularJS, вы можете использовать NPM для установки пакетов интернационализации:

 npm i -S angular-translate angular-translate-interpolation-messageformat angular-translate-loader-partial angular-sanitize messageformat

После установки пакетов не забудьте добавить модули в качестве зависимостей вашего приложения:

 // /src/app/core/core.module.js app.module('app.core', ['pascalprecht.translate', ...]);

Обратите внимание, что имя модуля отличается от имени пакета.

Перевод вашей первой строки

Предположим, в вашем приложении есть панель инструментов с текстом и поле с текстом-заполнителем:

 <nav class="navbar navbar-default"> <div class="container-fluid"> <div class="navbar-header"> <a class="navbar-brand" href="#">Hello</a> </div> <div class="collapse navbar-collapse"> <form class="navbar-form navbar-left"> <div class="form-group"> <input type="text" class="form-control" ng-model="vm.query" placeholder="Search"> </div> ... </div> </div> </nav>

Представление выше содержит два фрагмента текста, которые вы можете интернационализировать: «Привет» и «Поиск». С точки зрения HTML, один отображается как внутренний текст тега привязки, а другой — как значение атрибута.

Чтобы интернационализировать их, вам придется заменить оба строковых литерала токенами, которые AngularJS затем может заменить фактически переведенными строками в зависимости от предпочтений пользователя при отображении страницы.

AngularJS может сделать это, используя ваши токены для поиска в предоставленных вами таблицах перевода. Модуль angular-translate ожидает, что эти таблицы перевода будут предоставлены как простые объекты JavaScript или как объекты JSON (при удаленной загрузке).

Вот пример того, как обычно выглядят эти таблицы перевода:

 // /src/app/toolbar/i18n/en.json { "TOOLBAR": { "HELLO": "Hello", "SEARCH": "Search" } } // /src/app/toolbar/i18n/tr.json { "TOOLBAR": { "HELLO": "Merhaba", "SEARCH": "Ara" } }

Чтобы интернационализировать представление панели инструментов сверху, вам нужно заменить строковые литералы токенами, которые AngularJS может использовать для поиска в таблице перевода:

 <!-- /src/app/toolbar/toolbar.html --> <a class="navbar-brand" href="#" translate="TOOLBAR.HELLO"></a> <!-- or --> <a class="navbar-brand" href="#">{{'TOOLBAR.HELLO' | translate}}</a>

Обратите внимание, что для внутреннего текста вы можете использовать либо директиву translate , либо фильтр translate . (Вы можете узнать больше о директиве translate здесь и о фильтрах translate здесь.)

С этими изменениями при отображении представления angular-translate автоматически вставит соответствующий перевод, соответствующий TOOLBAR.HELLO , в DOM на основе текущего языка.

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

 <!-- /src/app/toolbar/toolbar.html --> <input type="text" class="form-control" ng-model="vm.query" translate translate-attr-placeholder="TOOLBAR.SEARCH">

А что, если ваши токенизированные строки содержат переменные?

Чтобы обработать такие случаи, как «Привет, {{name}}.», вы можете выполнить замену переменных, используя тот же синтаксис интерполятора, который уже поддерживает AngularJS:

Таблица перевода:

 // /src/app/toolbar/i18n/en.json { "TOOLBAR": { "HELLO": "Hello, {{name}}." } }

Затем вы можете определить переменную несколькими способами. Вот некоторые из них:

 <!-- /src/app/toolbar/toolbar.html --> <a ... translate="TOOLBAR.HELLO" translate-values='{ name: vm.user.name }'></a> <!-- or --> <a ... translate="TOOLBAR.HELLO" translate-value-name='{{vm.user.name}}'></a> <!-- or --> <a ...>{{'TOOLBAR.HELLO | translate:'{ name: vm.user.name }'}}</a>

Что делать с плюрализацией и гендером

Множественное число — довольно сложная тема, когда дело доходит до i18n и l10n. В разных языках и культурах действуют разные правила того, как язык обрабатывает множественное число в различных ситуациях.

Из-за этих проблем разработчики программного обеспечения иногда просто не решают проблему (или, по крайней мере, не решают ее должным образом), в результате чего программное обеспечение выдает глупые предложения, подобные этим:

 He saw 1 person(s) on floor 1. She saw 1 person(s) on floor 3. Number of people seen on floor 2: 2.

К счастью, существует стандарт, как с этим справиться, и JavaScript-реализация стандарта доступна как MessageFormat.

С помощью MessageFormat вы можете заменить приведенные выше плохо структурированные предложения следующими:

 He saw 1 person on the 2nd floor. She saw 1 person on the 3rd floor. They saw 2 people on the 5th floor.

MessageFormat принимает выражения, подобные следующим:

 var message = [ '{GENDER, select, male{He} female{She} other{They}}', 'saw', '{COUNT, plural, =0{no one} one{1 person} other{# people}}', 'on the', '{FLOOR, selectordinal, one{#st} two{#nd} few{#rd} other{#th}}', 'floor.' ].join(' ');

Вы можете создать средство форматирования с указанным выше массивом и использовать его для генерации строк:

 var messageFormatter = new MessageFormat('en').compile(message); messageFormatter({ GENDER: 'male', COUNT: 1, FLOOR: 2 }) // 'He saw 1 person on the 2nd floor.' messageFormatter({ GENDER: 'female', COUNT: 1, FLOOR: 3 }) // 'She saw 1 person on the 3rd floor.' messageFormatter({ COUNT: 2, FLOOR: 5 }) // 'They saw 2 people on the 5th floor.'

Как вы можете использовать MessageFormat с angular-translate , чтобы использовать его полную функциональность в своих приложениях?

В конфигурации вашего приложения вы просто сообщаете angular-translate , что интерполяция формата сообщения доступна следующим образом:

 /src/app/core/core.config.js app.config(function ($translateProvider) { $translateProvider.addInterpolation('$translateMessageFormatInterpolation'); });

Вот как может выглядеть запись в таблице перевода:

 // /src/app/main/social/i18n/en.json { "SHARED": "{GENDER, select, male{He} female{She} other{They}} shared this." }

И в представлении:

 <!-- /src/app/main/social/social.html --> <div translate="SHARED" translate-values="{ GENDER: 'male' }" translate-interpolation="messageformat"></div> <div> {{ 'SHARED' | translate:"{ GENDER: 'male' }":'messageformat' }} </div>

Здесь вы должны явно указать, что интерполятор формата сообщения должен использоваться вместо интерполятора по умолчанию в AngularJS. Это связано с тем, что два интерполятора немного отличаются по своему синтаксису. Подробнее об этом можно прочитать здесь.

Предоставление таблиц перевода вашему приложению

Теперь, когда вы знаете, как AngularJS может искать переводы ваших токенов из таблиц перевода, как ваше приложение вообще узнает о таблицах перевода? Как вы сообщаете своему приложению, какую локаль/язык следует использовать?

Здесь вы узнаете о $translateProvider .

Вы можете предоставить таблицы перевода для каждой локали, которую вы хотите поддерживать, непосредственно в файле core.config.js вашего приложения следующим образом:

 // /src/app/core/core.config.js app.config(function ($translateProvider) { $translateProvider.addInterpolation('$translateMessageFormatInterpolation'); $translateProvider.translations('en', { TOOLBAR: { HELLO: 'Hello, {{name}}.' } }); $translateProvider.translations('tr', { TOOLBAR: { HELLO: 'Merhaba, {{name}}.' } }); $translateProvider.preferredLanguage('en'); });

Здесь вы предоставляете таблицы перевода как объекты JavaScript для английского (en) и турецкого (tr), при этом объявляя текущий язык как английский (en). Если пользователь хочет изменить язык, вы можете сделать это с помощью сервиса $translate:

 // /src/app/toolbar/toolbar.controller.js app.controller('ToolbarCtrl', function ($scope, $translate) { $scope.changeLanguage = function (languageKey) { $translate.use(languageKey); // Persist selection in cookie/local-storage/database/etc... }; });

Остается вопрос, какой язык следует использовать по умолчанию. Жесткое программирование исходного языка нашего приложения не всегда может быть приемлемым. В таких случаях альтернативой является попытка определить язык автоматически с помощью $translateProvider:

 // /src/app/core/core.config.js app.config(function ($translateProvider) { ... $translateProvider.determinePreferredLanguage(); });

determinePreferredLanguage ищет значения в window.navigator и выбирает интеллектуальное значение по умолчанию до тех пор, пока пользователь не предоставит четкий сигнал.

Ленивая загрузка таблиц перевода

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

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

Почему это так сложно? Приложения AngularJS организованы в модули. В сложном приложении может быть много модулей, каждый со своими собственными данными i18n. Поэтому следует избегать наивного подхода, такого как одновременная загрузка и предоставление всех данных i18n.

Что вам нужно, так это способ организовать ваши данные i18n по модулям. Это позволит вам загружать только то, что вам нужно, когда вам это нужно, и кэшировать то, что было загружено ранее, чтобы избежать повторной загрузки одних и тех же данных (по крайней мере, до тех пор, пока кэш не станет недействительным).

Здесь в игру вступает partialLoader .

Допустим, таблицы перевода вашего приложения имеют следующую структуру:

 /src/app/main/i18n/en.json /src/app/main/i18n/tr.json /src/app/toolbar/i18n/en.json /src/app/toolbar/i18n/tr.json

Вы можете настроить $translateProvider для использования partialLoader с шаблоном URL, который соответствует этой структуре:

 // /src/app/core/core.config.js app.config(function ($translateProvider) { ... $translateProvider.useLoader('$translatePartialLoader', { urlTemplate: '/src/app/{part}/i18n/{lang}.json' }); });

Как и следовало ожидать, «lang» заменяется кодом языка во время выполнения (например, «en» или «tr»). А "часть"? Как $translateProvider узнает, какую «часть» загружать?

Вы можете предоставить эту информацию внутри контроллеров с помощью $translatePartialLoader :

 // /src/app/main/main.controller.js app.controller('MainCtrl', function ($translatePartialLoader) { $translatePartialLoader.addPart('main'); }); // /src/app/toolbar/toolbar.config.js app.controller('ToolbarCtrl', function ($translatePartialLoader) { $translatePartialLoader.addPart('toolbar'); });

Теперь шаблон завершен, и данные i18n для данного представления загружаются при первом выполнении его контроллера, а это именно то, что вам нужно.

Кэширование: сокращение времени загрузки

Что с кэшированием?

Вы можете включить стандартный кеш в конфигурации приложения с помощью $translateProvider :

 // /src/app/core/core.config.js app.config(function ($translateProvider) { ... $translateProvider.useLoaderCache(true); // default is false });

Если вам нужно очистить кеш для данного языка, вы можете использовать $translate :

 $translate.refresh(languageKey); // omit languageKey to refresh all

Благодаря этим элементам ваше приложение полностью интернационализировано и поддерживает несколько языков.

Локализация чисел, валют и дат

В этом разделе вы узнаете, как можно использовать angular-dynamic-locale для поддержки форматирования элементов пользовательского интерфейса, таких как числа, валюты, даты и т. д., в приложении AngularJS.

Для этого вам потребуется установить еще два пакета:

 npm i -S angular-dynamic-locale angular-i18n

После установки пакетов вы можете добавить модуль в зависимости вашего приложения:

 // /src/app/core/core.module.js app.module('app.core', ['tmh.dynamicLocale', ...]);

Правила локали

Правила локали — это простые файлы JavaScript, содержащие спецификации того, как даты, числа, валюты и т. п. должны форматироваться компонентами, зависящими от службы $locale.

Список поддерживаемых в настоящее время локалей доступен здесь.

Вот фрагмент из angular-locale_en-us.js иллюстрирующий форматирование месяца и даты:

 ... "MONTH": [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" ], "SHORTDAY": [ "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" ], ...

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

По умолчанию angular-dynamic-locale ожидает, что файлы правил локали будут расположены в angular/i18n/angular-locale_{{locale}}.js . Если они расположены в другом месте, необходимо использовать tmhDynamicLocaleProvider для переопределения значения по умолчанию:

 // /src/app/core/core.config.js app.config(function (tmhDynamicLocaleProvider) { tmhDynamicLocaleProvider.localeLocationPattern( '/node_modules/angular-i18n/angular-locale_{{locale}}.js'); });

Кэширование автоматически обрабатывается службой tmhDynamicLocaleCache .

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

Для переключения между локалями angular-dynamic-locale предоставляет сервис tmhDynamicLocale :

 // /src/app/toolbar/toolbar.controller.js app.controller('ToolbarCtrl', function ($scope, tmhDynamicLocale) { $scope.changeLocale = function (localeKey) { tmhDynamicLocale.set(localeKey); // Persist selection in cookie/local-storage/database/etc... }; });

Генерация таблиц перевода с автоматическим переводом

Правила локали поставляются с пакетом angular-i18n , поэтому все, что вам нужно сделать, это сделать содержимое пакета доступным для вашего приложения по мере необходимости. Но как вы создаете файлы JSON для своих таблиц перевода? Не существует пакета, который вы могли бы загрузить и подключить к нашему приложению.

Одним из вариантов является использование программных API-интерфейсов перевода, особенно если строки в вашем приложении представляют собой простые литералы без переменных или выражений во множественном числе.

С Gulp и парой дополнительных пакетов запросить программный перевод для вашего приложения очень просто:

 import gulp from 'gulp'; import map from 'map-stream'; import rename from 'gulp-rename'; import traverse from 'traverse'; import transform from 'vinyl-transform'; import jsonFormat from 'gulp-json-format'; function translateTable(to) { return transform(() => { return map((data, done) => { const table = JSON.parse(data); const strings = []; traverse(table).forEach(function (value) { if (typeof value !== 'object') { strings.push(value); } }); Promise.all(strings.map((s) => getTranslation(s, to))) .then((translations) => { let index = 0; const translated = traverse(table).forEach(function (value) { if (typeof value !== 'object') { this.update(translations[index++]); } }); done(null, JSON.stringify(translated)); }) .catch(done); }); }); } function translate(to) { return gulp.src('src/app/**/i18n/en.json') .pipe(translateTable(to)) .pipe(jsonFormat(2)) .pipe(rename({ basename: to })) .pipe(gulp.dest('src/app')); } gulp.task('translate:tr', () => translate('tr')); This task assumes the following folder structure: /src/app/main/i18n/en.json /src/app/toolbar/i18n/en.json /src/app/navigation/i18n/en.json ...

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

Наконец, новая таблица перевода записывается как одноуровневая с английской таблицей перевода, что дает:

 /src/app/main/i18n/en.json /src/app/main/i18n/tr.json /src/app/toolbar/i18n/en.json /src/app/toolbar/i18n/tr.json /src/app/navigation/i18n/en.json /src/app/navigation/i18n/tr.json ...

Реализация getTranslation также проста:

 import bluebird from 'bluebird'; import MicrosoftTranslator from 'mstranslator'; bluebird.promisifyAll(MicrosoftTranslator.prototype); const Translator = new MicrosoftTranslator({ client_id: process.env.MICROSOFT_TRANSLATOR_CLIENT_ID, client_secret: process.env.MICROSOFT_TRANSLATOR_CLIENT_SECRET }, true); function getTranslation(string, to) { const text = string; const from = 'en'; return Translator.translateAsync({ text, from, to }); }

Здесь мы используем Microsoft Translate, но можно легко использовать другого поставщика, например Google Translate или Yandex Translate.

Хотя программные переводы удобны, у них есть несколько недостатков, в том числе:

  • Перевод роботов хорош для коротких строк, но даже в этом случае могут быть ловушки со словами, которые имеют разные значения в разных контекстах (например, «бассейн» может означать плавание или группировку).
  • API-интерфейсы могут быть не в состоянии обрабатывать строки с переменными или строки, зависящие от формата сообщения.

В этих и других случаях может потребоваться человеческий перевод; однако это тема для другого сообщения в блоге.

Интернационализация интерфейсов только выглядит пугающе

В этой статье вы узнали, как использовать эти пакеты для интернационализации и локализации приложений AngularJS.

angular-translate , angular-dynamic-locale и gulp — мощные инструменты для интернационализации приложения AngularJS, которые инкапсулируют болезненные низкоуровневые детали реализации.

Демонстрационное приложение, иллюстрирующее идеи, обсуждаемые в этом посте, можно найти в этом репозитории GitHub.