Как реализовать интернационализацию в React
ITVDN: курсы программирования
Видеокурсы по
программированию

Доступ более чем к 7700 видеоурокам от $19.99

Подписка
ITVDN logo
Видеокурсы по
программированию

Доступ более чем к 7700 видеоурокам от $19.99

Подписка

Об авторе

Юрий Думов работает архитектором программного обеспечения в SAP, имеет более 10 лет опыта в разработке веб и мобильных приложений.

Прежде всего, давайте введем некоторые сокращения. Internationalization - достаточно длинное слово, поэтому предлагаю его заменить в нашем контексте на “intl”.

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

  • Определение пользовательской локали
  • Перевод UI-элементов, заголовков, подсказок и прочего
  • Поддержка местных специфических сервисов, таких как даты, валюты и числа

На заметку: в этой статье я заострю ваше внимание лишь на front-end части. Мы разработаем несложное универсальное React-приложение с полной поддержкой интернационализации.

Для начала предлагаю воспользоваться моим репозиторием. Здесь у нас есть веб-сервер Express для серверного рендеринга, вебпак для построения клиентского JS, Babel для конвертации современного JavaScript в ES5. Также мы будем использовать npm-run, nodemon для запуска веб-сервера под средой разработчика и webpak-dev-server для ассетов.

Нашей точкой входа в серверное приложение будет служить server.js. Здесь мы загружаем Babel и babel-polyfill для создания прочего серверного кода в современном стандарте JavaScript. В качестве бизнес-логики сервера мы используем src/server.jsx. Тут мы устанавливаем Express-сервер для прослушки порта 3001. Для рендеринга мы используем очень простой компонент components/App.jsx, который также является частью точки входа в универсальное приложение.

Точкой входа в клиентский JS нам служит src/client.jsx. Тут мы фиксируем корневой компонент - component/App.jsx - для placeholder'а react-view в HTML-разметке, предоставляемой Express-сервером.

Таким образом, сейчас мы склонируем репозиторий, запустим npm-install и выполним nodemon и webpack-dev-server одновременно в двух консолях.

В первой консоли:

И во второй:

Наш веб-сервер должен быть доступен по localhost:3001. Откройте предпочитаемый вами браузер и убедитесь в этом сами!

Итак, мы готовы начать.

  1. Определение пользовательской локали

Существует два возможных решения этого вопроса. По какой-то причине большинство популярных веб-сайтов, включая Skype и NBA, используют гео-IP для определения местоположения пользователя, таким образом определяя его родной язык. Подобный подход не только является дорогим в плане реализации, но и еще не совсем точным. Сейчас очень много людей путешествует, по этой причине местоположение пользователя не является надежным ориентиром при выборе подходящего языка приложения. Вместо этого мы предпочтем второй способ решить данную проблему при помощи обработки на стороне сервера заголовка Accept-Language. Этот заголовок отправляется любым более-менее современным браузером.

 

  • -Language

Этот заголовок предоставляет информацию о возможных вариантах языка, принятого в качестве ответа. Каждый язык обладает своим «приоритетом», показывая, как часто пользователь может его использовать. По умолчанию уровень «приоритетности» равен 1. К примеру, «Accept-Language: da, en-gb;q=0.8, en;q=0.7» означает «я предпочитаю датский, но могу также принять британский или другие виды английского».

 

(Также стоит упомянуть, что сей подход так же несовершенен. К примеру, пользователь может посетить ваш веб-сайт из интернет-кафе или публичного ПК. Лучше всего разработать виджет, при помощи которого пользователь на интуитивном уровне сможет поменять язык сайта.)

 

Реализация определения локали пользователя

Вот пример кода веб-сервера Node.js. Мы используем пакет accept-language, что извлекает локали из http-заголовков и находит наиболее предпочтительные, исходя из поддерживаемых вашим веб-сайтом. Если таковые не были найдены, тогда сайт будет использовать свою дефолтную локаль.

 

Приступим к установке пакетов:

После чего в src/server.jsx у нас будет следующее:

Здесь мы импортируем accept-language и устанавливаем поддержку английских и русских локалей. Также мы реализовываем функцию detectLocale, что извлекает значение локали из куки. Если ни одна не была обнаружена, начинается обработка Accept-Language. Наконец, мы выбираем дефолтную локаль. После обработки запроса мы добавим заголовок Set-Cookie для обнаруженной локали в ответ. Это значение будет использовано для всех последующих запросов.

 

  1. Перевод UI-элементов, заголовков, подсказок и прочего

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

 

Во-первых, мы должны установить провайдер интернационализации. Для этого мы немного изменим src/server.jsx и src/client.jsx.

  • src/server.jsx:

        

  • здесь src/client.jsx:

Так, теперь IntlProvider-дочерний компонент получит доступ к функциям интернационализации. Давайте добавим переведенный текст в наше приложение и клавишу для изменения локали. У нас есть два способа, как это можно сделать: через FormattedMessage или через formatMessage – функцию. Разница в том, что компонент будет обернут в span-тэг, что хорошо для текстовых данных, но не хорошо для значений HTML-атрибутов, таких как alt и title. Давайте опробуем и их!

 

Вот src/components/App.jsx:

Отметьте, что атрибут id должен быть всегда уникальным для всего приложения в целом, так что было бы не лишним установить для себя некоторые правила именования сообщений. Я предпочитаю следовать формату «имяКомпонента.некийУникальныйИдентификатор». В качестве некой дефолтной локали будет использовано сообщение defaultMessage. Атрибут description предоставит некую информацию для переводчика.

 

Перезапустите nodemon и обновите страницу в браузере. Вы должны увидеть «Hello World». Но если вы открываете статью при помощи инструментов разработчика, вы увидите, что текст теперь внутри тэга span. В этом случае это не ошибка, но иногда мы предпочитаем просто получить текст, без никаких дополнительных тэгов. Для этого нам нужен прямой доступ к объекту интернационализации React Intl.

 

Давайте вернемся назад к src/components/App.jsx:

Нам нужно написать гораздо больше кода, чем раньше. Во-первых, мы используем injectIntl, который «упаковывает» наш компонент приложения и внедряет intl-объект. Чтобы получить переведенное сообщение, нам нужно вызвать formatMessage и передать в качестве параметра message. Этот message должен иметь свой уникальный идентификатор и атрибуты defineMesasges из React Intl.

 

Лучшее, что есть в React Intl, так это его экосистема. Давайте добавим babel-plugin-react-intl к нашему приложению и построим словарь трансляции. Мы передадим этот словарь переводчикам, которым не нужно никаких задатков программирования для выполнения своей работы.

  • .babelrc:

Перезапустите nodemon, и вы увидите, что папка build/messages была успешно создана в корневой директории проекта, вместе с некоторыми другими папками и файлами внутри. Теперь нам необходимо собрать эти файлы в один JSON. Для этого вы можете использовать мой скрипт. Сохраните его как scripts/translate.js.

 

Теперь нам нужно добавить новый скрипт в package.json:

Что же, попробуем!

В конце вы должны увидеть файл en.json в build/lang.

И все работает! Теперь пришло время для кое-чего поинтересней. На стороне сервера мы можем загрузить все переводы в память и, соответственно, выдавать их в зависимости от характера запроса. Однако на клиентской стороне этот подход неприемлем. Потому вместо этого мы будем один раз принимать json-файл со всеми переводами, а клиент автоматически определит, какой из текстов ему нужен.

 

Скопируем результирующий файл в public/assets.

На заметку: если вы пользуетесь Windows, симлинки для вас недоступны. Таким образом, вам нужно будет вручную копировать команды каждый раз, как только вы будете перестраивать ваши переводы.

  • public/assets/ru.json применим следующее:

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

Для сервера наш src/server.jsx должен выглядеть так:

Здесь мы делаем следующее:

  • Кэшируем сообщения и специфичный для данной локали JS для валют, DateTime, Number во время запуска приложения.
  • Расширяем метод renderHTML так, что мы можем вставить специфичный для данной локали JS прямо в разметку.
  • Предоставляем переведенные сообщения IntlProvider (все те сообщения теперь доступны в качестве дочерних компонентов).

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

Вот src/client.jsx:

Также мы должны затронуть src/server.jsx, чтобы Express предоставлял json с переводом на сторону клиента. Заметьте, что на продакшине вы, скорее всего, будете использовать что-то вроде nginx.

После инициализации JavaScript, client.jsx возьмет локаль из куки и запросит JSON с переводом. Во всем остальном наше приложение будет работать, как и раньше.

Пришло время проверить, как все будет работать в браузере. Откройте вкладку «Network» в панели разработчика и удостоверьтесь, что JSON с переводом был успешно получен нашим клиентом.

Подведя итог, давайте добавим небольшой виджет для изменения локали в src/components/LocaleButton.jsx:

И так же в src/components/App.jsx:

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

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

Плюрализация и шаблоны

В английском большинство слов могут принимать одну или две возможные формы: «одно яблоко», «много яблок». В других языках все намного сложнее. К примеру, русский имеет 4 различные формы. Надеемся, React сумеет справиться и с этим. Он также поддерживает шаблоны, так что вы можете предоставить переменные, которые могут быть подставлены в шаблон во время рендеринга. Вот как это работает.

В src/components/App.jsx у нас есть:

Тут мы определяем шаблон с переменной count. Мы напечатаем или «одно яблоко», если значение переменной равно 1, 21 и так далее, или «два яблока» в противном случае. Нам нужно передать все переменные в formatMessage.

Давайте перестроим наш файл переводов и добавим русский перевод для теста.

Вот наш public/assets/ru.json файл:

Теперь все случаи предусмотрены!

  1. Поддержка местных специфических сервисов, таких как даты, валюты и числа

Ваша информация будет представляться по-разному в зависимости от локали. К примеру, русская локаль нам покажет $500.00 и 12/10/2016.

 

  • Intl предоставляет React-компоненты для такого типа данных, которые автоматически обновляются каждые 10 секунд, если вы за это время не перезаписывали значения.
  • это в src/components/App.jsx:

Обновите браузер и проверьте страницу. Вам необходимо будет подождать 10 секунд, чтобы увидеть, как обновится FormattedRelative.

 

Круто, не правда ли? Что же, теперь мы можем столкнуться еще с одной проблемой – универсального рендеринга.

 

В среднем между формированием разметки и инициализацией JS проходит порядка 2-х секунд. Это значит, что все DateTime`сы, сгенерированные на странице, могут иметь разные значения на стороне клиента и сервера. Что, как следствие, разрушает универсальный рендеринг.

 

Вот src/server.jsx:

  • вот src/client.jsx:

Перезапустите nodemon и проблема почти исчезнет! Она может, правда, остаться в случае, если вы используете Date.now() вместо показаний, записанных в базе. Чтобы сделать пример более «жизненным», заменим в app.jsx Date.now() на последний таймштамп, что-то вроде 1480187019228.

 

(Впрочем, вы можете столкнутся с другой проблемой – когда сервер не в состоянии отрендерить DateTime в нормальном формате. Это потому, что 4 версия Node.js по умолчанию не поддерживала Intl.)

Звучит слишком хорошо, чтобы быть правдой, не так ли? Мы как истинные фронт-енд разработчики всегда были на стороже того, что касается браузеров и платформ. React Intl использует нативный браузерный API для обработки форматов DateTime и Number. И, несмотря на тот факт, что подобная функциональность была представлена в 2012 году, до сих пор есть современные браузеры, которые ее все еще не поддерживают. Даже Safari поддерживает ее только частично. Вот таблица с детальной информацией:

Это значит, что если вы желаете покрыть большинство браузеров, которые не поддерживают Intl API на нативном уровне, polyfill для вас просто незаменим. Хвала Всевышнему, существует Intl.js. С одной стороны, кажется, вот оно – идеальное решение, но, как показывает мой опыт, всегда существуют свои недостатки. Во-первых, вам нужно добавить Js-bundle, что несколько ресурсоемко. Также для уменьшения размера вам нужно будет передавать ваш Intl.Js только для браузеров, у которых нет своей нативной поддержки. Конечно, все эти техники уже давным-давно известны, и существует великое множество статей, посвященных им. Но все равно, Intl.js – не абсолютное решение, так как все же представления DateTime и Number на стороне сервера и на стороне клиента могут несколько отличаться, что, разумеется, влечет за собой ошибки при серверном рендеринге.

 

Наконец, я пришел к своему собственному решению, которое хоть и имеет свои недостатки, но мои запросы в большинстве случаев оно устраивает. Я реализовал очень поверхностный polyfill, что имеет лишь небольшую часть из требуемой функциональности. В то время, как в большинстве случаев подобное решение непригодное, оно лишь добавляет лишние 2 КБ к размеру JS-файла, так что даже нет необходимости реализовывать динамическую загрузку кода для устаревших браузеров, что значительно упрощает решение в целом.

 

И в заключение

Возможно, сейчас вас не покидает чувство, как будто здесь написано слишком сложное решение. Также, возможно, сейчас вы подумываете о том, чтобы реализовать все самим. Я бы не советовал этого делать. В конце концов, вы все равно придете к выводам, представленным в данной статье. Или, что хуже, зайдете в тупик одного решения и не сможете увидеть остальные. Вы можете подумать, что можно легко решить проблему с Intl API, используя вместо него Moment.js (я специально не рассматривал другие библиотеки, так как они либо неподдерживаемые, либо неиспользуемые). К счастью, я уже опробовал это, так что я могу сохранить вам много времени. Moment.js монолитен и очень тяжел, так что если для кого-то он и подойдет, то остальная масса пользователей будет неудовлетворенной результатом. Разработка собственного polyfill не звучит интригующе, так как вам наверняка придется выделить определенное время для борьбы с возникающими при этом багами. Подведя итог, могу лишь сказать, что не существует идеального решения касательно проблемы на данный момент: просто выберите то, что вам подойдет лучше всего.

 

Надеюсь, эта статья дала вам все, что нужно знать для создания интернационализируемого React front-end приложения. Теперь вы выучили, как определять локаль пользователя, сохранять ее в куки, писать виджет для изменения локали и многое другое! Также вы ознакомились с некоторыми возможными проблема и ловушками, в которые вы можете попасть в процессе написания приложения.

 

Удачи в разработке!

Автор перевода: Евгений Лукашук

Источник

СТАТЬИ ПО СХОЖЕЙ ТЕМАТИКЕ
ВИДЕО КУРСЫ ПО СХОЖЕЙ ТЕМАТИКЕ
КОММЕНТАРИИ И ОБСУЖДЕНИЯ

ПОДПИСКА НА ITVDN ВЫГОДА ДО 29.95$ НА ОБУЧЕНИЕ ПРЕСТИЖНЫМ ПРОФЕССИЯМ!

1 месяц19.99$
подписка

легкий старт в обучении

3 месяца49.99$
подписка

выгода от подписки до9.98$

6 месяцев89.99$
подписка

выгода от подписки до29.95$