Библиотека React Intl предоставляет механизм для перевода текста на другие языки.


В данном "туториале" мы используем названную библиотеку для реализации интернационализации в проекте, написанном на React. Мы создадим простое приложение, позволяющее пользователю выбирать язык приложения.


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


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



Демо приложения можно посмотреть здесь.


Прим. пер.: мы с вами также добавим русскую локализацию.


Интернационализация и локализация


Как упоминалось ранее, React Intl позволяет настроить интернационализацию в приложении. Но что такое интернационализация?


Интернационализация — это процесс подготовки продукта — в нашем случае React-приложения — к использованию в разных локациях (языковых средах). Для обозначения интернационализации часто используется аббревиатура Intl или i18n.


Локализация (l10n) — перевод интернационализованного (или интернационализированного?) приложения с оригинального языка на другой.


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


К счастью, React Intl имеет встроенную поддержку указанных различий для ряда локализаций.


Настройка проекта


Начнем с клонирования (копирования) простого проекта — с содержимым на английском языке — из этого GitHub-репозитория. Мы добавим поддержку для 4 дополнительных языков: русского, немецкого, французского и японского.


Открываем терминал, переключаемся на директорию для сохранения проекта и выполняем следующую команду:


git clone https://github.com/Ibaslogic/i18n_react_intl_starter

Переходим в директорию проекта и выполняем команду для установки зависимостей:


cd i18n_react_intl_starter
yarn
# или
npm i

Убедитесь, что на вашей машине установлен Node.js.


После установки зависимостей, запускаем проект с помощью команды yarn start или npm start. Мы должны увидеть наше приложение в браузере по адресу http://localhost:3000.



Структура проекта выглядит так:


i18n_react_intl_starter
    +-- node_modules
    +-- public
    +-- src
    ¦    +-- components
    ¦    ¦      +-- App.js
    ¦    ¦      +-- Content.js
    ¦    ¦      +-- Footer.js
    ¦    ¦      L-- Header.js
    ¦    +-- index.css
    ¦    L-- index.js
    +-- .gitignore
    +-- package.json
    +-- README.md
    L-- yarn.lock

Отлично. Теперь внедрим React Intl в наше приложение.


Настройка React Intl


Останавливаем сервер для разработки и устанавливаем библиотеку:


yarn add react-intl
# или
npm i react-intl

Данная библиотека предоставляет все API и компоненты, необходимые для реализации интернационализации в приложении.


Обернем основной (корневой) компонент приложения (App) в компонент IntlProvider.


Что такое IntlProvider?


Как следует из названия, IntlProvider обеспечивает передачу важных настроек в субкомпоненты дерева компонентов (делает эти настройки доступными для дочерних компонентов, независимо от уровня их вложенности).


Субкомпоненты, предоставляемые React Intl, называются formatted. Они отвечают за перевод и форматирование во время выполнения (кода). Среди них можно назвать следующие наиболее часто используемые компоненты:



Чаще всего, для интернационализации React-приложений используется компонент FormattedMessage, позволяющий пользователям переводить и форматировать простые и сложные строки и сообщения.


Возвращаемся к нашему проекту. Открываем файл src/components/App.js и оборачиваем компонент App в IntlProvider.


Не забудьте импортировать IntlProvider из react-intl в начале файла:


// ...
import { IntlProvider } from 'react-intl'

const App = () => {
  return (
    <IntlProvider>
      <div>
        <Header />
        <Content />
        <Footer />
      </div>
    </IntlProvider>
  )
}
// ...

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


// ...

const App = () => {
  console.log(IntlProvider)
  // ...
}

Вот, что мы увидим в консоли:



Сосредоточимся на defaultProps — это настройки по умолчанию, используемые IntlProvider. Также мы получаем сообщение об ошибке, в котором говорится о необходимости определения локации. Прим. пер.: если в вашем случае, как и в моем, console.log() не сработает, попробуйте console.dir().


Передадим IntlProvider необходимые пропы:


return (
  <IntlProvider messages={{}} locale='en' defaultLocale='en'>
    <div>
      <Header />
      <Content />
      <Footer />
    </div>
  </IntlProvider>
)

После сохранения изменений, сообщение об ошибке исчезает.


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


Проп message принимает объект с набором переведенных строк, отображаемых на странице. Значение этого пропа также будет динамически меняться в зависимости от текущей локации.


Наконец, проп defaultLocale определяет локализацию по умолчанию и должен совпадать с "дефолтным" языком приложения.


Перед тем, как добавлять переведенные строки, поиграем с компонентами для форматирвоания.


Открываем файл src/components/Footer.js и добавляем следующие компоненты в инструкцию return:


// ...
import { FormattedDate, FormattedNumber, FormattedPlural } from 'react-intl'

const Footer = () => {
  // ...

  return (
    <div className='container mt'>
      {/* ... */}

      <FormattedDate value={Date.now()} />
      <br />
      <FormattedNumber value={2000} />
      <br />
      <FormattedPlural value={5} one='1 click' other='5 clicks' />
    </div>
  )
}

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


6/26/2021 // текущая дата
2,000
5 clicks

Если мы изменим значение пропа locale на de, то увидим "немецкое" форматирование даты и числа:


26.6.2021

2.000

React Intl автоматически форматирует даты и числа на основе выбранной локации с помощью компонентов для форматирования (FormattedDate и FormattedNumber, соответственно).


Этим компонентам передается проп value, принимающий значения для форматирования.


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


return (
  <div className='container mt'>
    {/* ... */}

    <FormattedDate
      value={Date.now()}
      year='numeric'
      month='long'
      day='2-digit'
    />
    <br />
    <FormattedNumber value={2000} style={`currency`} currency='USD' />
    <br />
    {/* ... */}
  </div>
)

В этом случае для английской локации мы получим такой результат:


Jun 26, 2021

$2,000.00

А для немецкой — такой:


26. Juni 2021
2.000,00 $

Символ денежной единицы можно менять с помощью currency='EUR' для немецкой локации или currency='RUB' — для русской.


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


Использование React Intl API


Другим способом форматирования значений является использование React Intl API. Несмотря на то, что более предпочтительным является использование методов компонентов, бывают ситуации, когда требуются методы, предоставляемые названным API, например, когда нужно перевести значения атрибутов placeholder, title или aria-label элемента.


Компоненты для форматирования используют этот API под капотом.


Посмотрим, как он работает. Импортируем хук useIntl() в Footer.js:


import {
  // ...
  useIntl
} from 'react-intl'

Если мы выведем useIntl() в консоль, то увидим все доступные функции для форматирования значений:


// ...

const Footer = () => {
  // ...
  const intl = useIntl()
  console.log(intl)

  // ...
}

Одной из таких функций является formatDate(). Применим ее:


// ...
const Footer = () => {
  // ...
  const intl = useIntl()
  return (
    <div className='container mt'>
      {/* <... */}
      <br />
      <input placeholder={intl.formatDate(Date.now())} />
    </div>
  )
}

Не забудьте поменять значение locale, чтобы увидеть, как это работает.


Функция formatDate() также принимает объект с настройками, позволяющими выполнять дополнительную "кастомизацию":


<input
  placeholder={intl.formatDate(Date.now(), {
    year: 'numeric',
    month: 'long',
    day: '2-digit'
  })}
/>

Возвращаемся к проекту.


Перевод текстовых строк


Создаем директорию i18n в директории src. В этой директории создаем 2 файла locales.js и messages.js. locales.js будет содержать поддерживаемые локации, а messages.js — соответствующие переводы.


Добавляем в файл i18n/locales.js следующее:


export const LOCALES = {
  ENGLISH: 'en-US',
  RUSSIAN: 'ru-RU',
  FRENCH: 'fr-FR',
  GERMAN: 'de-DE',
  JAPANESE: 'ja-JA'
}

Это необходимо для динамического определения локалей. Например, для доступа к en-US мы будем использовать [LOCALES.ENGLISH].


Как определяется язык?


Код en-US расшифровывается как язык-СТРАНА, мы можем указывать только язык без страны.


Перевод


Теперь добавляем в файл i18n/messages.js следующие переводы:


import { LOCALES } from './locales'

export const messages = {
  [LOCALES.ENGLISH]: {
    learn_to: `Hello, let's learn how to use React-Intl`,
    price_display:
      'How {n, number, ::currency/USD} is displayed in your selected language',
    number_display:
      'This is how {n, number} is formatted in the selected locale',
    start_today: 'Start Today: {d, date}',
    // меню
    about_project: 'About the project',
    contact_us: 'Contact us'
  },
  [LOCALES.RUSSIAN]: {
    learn_to: 'Привет, научимся использовать React-Intl',
    price_display:
      'Как {n, number, ::currency/RUB} отображается на выбранном языке',
    number_display:
      'Вот как {n, number} форматируется на основе выбранной локации',
    start_today: 'Начни сегодня: {d, date}',
    // меню
    about_project: 'О проекте',
    contact_us: 'Свяжитесь с нами'
  },
  [LOCALES.FRENCH]: {
    learn_to: 'Bonjour, apprenons a utiliser React-Intl',
    price_display: `Comment {n, number, ::currency/USD} $ s'affiche dans la langue selectionnee`,
    number_display:
      'Voici comment {n, number} sont formates dans les parametres regionaux selectionnes ',
    start_today: `Commencez aujourd'hui: {d, date}`,
    // меню
    about_project: 'A propos du projet',
    contact_us: 'Contactez-nous'
  },
  [LOCALES.GERMAN]: {
    learn_to: 'Hallo, lass uns lernen, wie man React-Intl benutzt',
    price_display:
      'Wie {n, number, ::currency/USD} in Ihrer ausgewahlten Sprache angezeigt wird',
    number_display:
      'Auf diese Weise werden {n, number} im ausgewahlten Gebietsschema formatiert',
    start_today: 'Beginnen Sie heute: {d, date}',
    // меню
    about_project: 'Uber das Projekt',
    contact_us: 'Kontaktiere uns'
  },
  [LOCALES.JAPANESE]: {
    learn_to: '??????React-Intl???????????',
    price_display:
      '???????{n, number, ::currency/USD}????????????',
    number_display:
      '?????????????{n, number}???????????????',
    start_today: '???????:{d, date}',
    // меню
    about_project: '??????????',
    contact_us: '??????'
  }
}

Данный файл состоит из исходного текста приложения и переводов для поддерживаемых локаций. Ключи объектов — это уникальные идентификаторы, которые могут называться как угодно. Мы будем использовать их для внедрения соответствующих строк в приложение.


Пока сосредоточимся на простых строках без аргументов в фигурных скобках, которые называются заменителями (placeholders).


Передадим эти данные в проп message компонента IntlProvider.


Доступ к объекту с английским переводом можно получить так:


messages[LOCALES.ENGLISH]

Передадим этот объект провайдеру. Позже мы реализуем логику динамического добавления переведенных сообщений на основе выбранной пользователем локации.


В файле components/App.js импортируем LOCALES и messages:


import { LOCALES } from '../i18n/locales'
import { messages } from '../i18n/messages'

Затем обновляем IntlProvider:


// ...
const App = () => {
  const locale = LOCALES.ENGLISH

  return (
    <IntlProvider
      messages={messages[locale]}
      locale={locale}
      defaultLocale={LOCALES.ENGLISH}
    >
      ...
    </IntlProvider>
  )
}
// ...

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


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


Начнем с трансформации строки Hello, let’s learn how to use React-Intl.


Открываем файл components/Content.js и импортируем FormattedMessage:


import { FormattedMessage } from 'react-intl'

Затем заменяем строку в компоненте:


// ...

const Content = () => {
  return (
    <div className='container hero'>
      <h1>
        <FormattedMessage id='learn_to' />
      </h1>
      {/* ... */}
    </div>
  )
}
// ...

Для того, чтобы проверить работоспособность компонента, вручную поменяем значение locale в App.js на ja-JA:


const locale = LOCALES.JAPANESE

Сохраняем изменения и переходим в браузер:



FormattedMessage принимает проп id, значение которого должно совпадать с ключом соответствующего объекта из файла с переводами. Значение id должно быть уникальным в пределах локализации.


Использование аргументов


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


Ранее мы изучили, как форматировать эти типы значений с помощью FormattedDate и FormattedNumber. Но здесь мы будем использовать FormattedMessage, поскольку интересующие нас значения являются частью строки.


Как видно в файле с переводами, мы заменяем эти типы значений с помощью шаблона (паттерна) { key, type, format }.


Например, у нас имеется такая строка:


price_display:
  'How {n, number, ::currency/USD} is displayed in your selected language',

Первый элемент (n) — это ключ (key), который используется для поиска соответствующих данных. Второй элемент (type) — это тип данных для преобразования, соответствующий определенной локации.


Третий аргумент (format) — который является опциональным — позволяет определять дополнительную информацию о типе значения.


В данном случае мы указали, что заменитель для number должен быть форматирован как денежная единица — доллары США (USD). Список поддерживаемых денежных единиц можно найти здесь.


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


<FormattedMessage id='price_display' values={{ n: 59.99 }} />

Как и ожидалось, id указывает на текущую локацию для перевода, а заменитель заменяется (извиняюсь за тавтологию) значением key.


Если мы применим эту логику к проекту, наш файл components/Content.js будет выглядеть так:


import { FormattedMessage } from 'react-intl'

const Content = () => {
  return (
    <div className='container hero'>
      <h1>
        <FormattedMessage id='learn_to' />
      </h1>
      <p>
        <FormattedMessage id='price_display' values={{ n: 59.99 }} />
      </p>
      <p>
        <FormattedMessage id='number_display' values={{ n: 2000 }} />
      </p>
      <p>
        <FormattedMessage id='start_today' values={{ d: new Date() }} />
      </p>
    </div>
  )
}

export default Content

Сохраняем изменения и меняем локацию в файле component/App.js:


const locale = LOCALES.JAPANESE

Наше приложение выглядит так:



Плюрализация


Ранее я упоминал, что компонент FormattedPlural используется для плюрализации текста. В этом разделе мы будем использовать FormattedMessage для плюрализации текстового сообщения.


В данный момент в нашем приложении отсутствует логика отображения правильного текста для единичного значения (1) счетчика (count).


Для реализации такой логики мы можем использовать паттерн, схожий с паттерном {key, type, format}, но вместо type и format используются plural и matches, соответственно.


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


click_count: 'You clicked {count, plural, one {# time} other {# times}}'

Категории one и other соответствуют единичной и множественной формам значения. Сами значения указываются в фигурных скобках после символа #, обозначающего количественное значение.


Добавляем id для других локаций:


// для русской
click_count:
      'Вы кликнули {count, plural, one {# раз} other {# раз(а)}}',
// для немецкой
click_count:
      'Sie haben {count, plural, one {# Mal} other {# Mal}} geklickt',
// для французской
click_count:
  'Vous avez clique {count, plural, one {# fois} other {# fois}}',
// для японской
click_count: '{count, plural, one {# ?} other {# ?}}????????',

Сохраняем изменения и редактируем файл components/Footer.js:


// до
<p>You clicked {count} times</p>

// после
<p>
  <FormattedMessage id="click_count" values={{ count }} />
</p>

Не забудьте импортировать FormattedMessage в начале файла.


Добавим перевод элементов меню. Импортируем FormattedMessage в файле components/Header.js:


import { FormattedMessage } from "react-intl'

Нам необходим доступ к соответствующим key файла с переводами. Обновляем массив menu:


const menu = [
  {
    key: 'about_project'
    // ...
  },
  {
    key: 'contact_us'
    // ...
  }
]

После этого мы можем использовать ключи для доступа к переводам в FormattedMessage.


Находим соответствующий код и обновляем его:


// до
{
  menu.map(({ title, path }) => (
    <li key={title}>
      <a href={path}>{title}</a>
    </li>
  ))
}

// после
{
  menu.map(({ title, path, key }) => (
    <li key={title}>
      <a href={path}>
        <FormattedMessage id={key} />
      </a>
    </li>
  ))
}

Для локализации текста кнопки и текста, предлагающего нажать на нее, добавим соответствующие ключи в файл i18n/message.js:


// для английского языка
click_button: 'Please click the button below',
click_here: 'click here',

// для русского языка
click_button: 'Пожалуйста, нажмите на кнопку ниже',
click_here: 'нажмите здесь',

// для французского языка
click_button: 'Veuillez cliquer sur le bouton ci-dessous',
click_here: 'Cliquez ici',

// и т.д.

После этого открываем components/Footer.js и заменяем текстовые строки компонентом FormattedMessage:


return (
  <div className='container mt'>
    {/* Здесь находится содержимое подвала */}
    <p>
      <FormattedMessage id='click_button' />
    </p>
    <button onClick={onChange}>
      <FormattedMessage id='click_here' />
    </button>
    {/* ... */}
  </div>
)

Меняем локацию в App.js, например, на французскую, и сохраняем изменения:



Добавление возможности выбора языка


Открываем файл components/Header.js и добавляем следующий код в инструкцию return:


const languages = [
  { name: 'English', code: LOCALES.ENGLISH },
  { name: 'Русский', code: LOCALES.RUSSIAN },
  { name: '???', code: LOCALES.JAPANESE },
  { name: 'Francais', code: LOCALES.FRENCH },
  { name: 'Deutsche', code: LOCALES.GERMAN }
]

Не забываем импортировать LOCALES:


import { LOCALES } from '../i18n/locales'

Перебираем массив languages и формируем выпадающий список:


<div className='switcher'>
  {/* Выпадающий список для выбора языка */}
  Languages <select>
    {languages.map(({ name, code }) => (
      <option key={code} value={code}>
        {name}
      </option>
    ))}
  </select>
</div>

Добавляем парочку стилей в src/index.css:


.switcher select {
  width: 99px;
  height: 30px;
}

Сохраняем изменения. После этого на странице появляется выпадающий список, но выбор определенного элемента из этого списка не влияет на содержимое страницы. Давайте это исправим.


Для того, чтобы сделать элемент select упарвляемым, необходимо добавить к нему пропы value и onChange. Значением пропа value является текущая локация, а onChange — обработчик изменения локации.


Поскольку IntlProvider является родительским компонентом, App.js должен знать о текущей локации.


Мы можем передать нужные данные в дочерний компонент Header через пропы. Данный процесс называется "передачей пропов" (prop drilling).


Открываем components/App.js и добавляем в него локальное состояние для выбранного языка:


const locale = LOCALES.ENGLISH

const [currentLocale, setCurrentLocale] = useState(locale)

Не забудьте импортировать хук useState().


Теперь передадим локацию в компонент Header для ее использования в элементе select.


<Header currentLocale={currentLocale} />

Далее деструктурируем пропы в компоненте Header и устанавливаем значения для select:


const Header = ({ currentLocale }) => {
  // ...

  return (
    {/* ... */}
    <select onChange="" value={currentLocale}>
      {/* ... */}
    </select>
  )
}

Определяем обработчик выбора языка в App.js и также передаем его в Header в виде пропа:


// ...
const App = () => {
  // ...

  const hangeChange = ({ target: { value } }) => {
    setCurrentLocale(value)
  }

  return (
    {/* ... */}
    <div>
        <Header currentLocale={currentLocale} handleChange={handleChange} />
        {/* ... */}
    </div>
  )
}

Далее снова возвращаемся в Header, извлекаем обработчик из пропов и используем его для обработки выбора языка:


const Header = ({ currentLocale, handleChange }) => {
  // ...

  return (
    {/* ... */}
    <select onChange={handleChange} value={currentLocale}>
      {/* ... */}
    </select>
  )
}

Проверяем работоспособность приложения. Все работает. Круто!


Убедитесь, что определили ключ для текста Languages в файле с переводами.


Сохранение выбранного языка в локальном хранилище браузера


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


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


Открываем файл App.js и добавляем в инструкцию return следующий код:


function getInitialLocale() {
  // получаем сохраненные данные
  const savedLocale = localStorage.getItem('locale')
  return savedLocale || LOCALES.ENGLISH
}

Затем обновляем логику установки начального значения для локального состояния текущей локации:


const [currentLocale, setCurrentLocale] = useState(getInitialLocale())

Наконец, обновляем код обработчика выбора языка:


const handleChange = ({ target: { value } }) => {
  setCurrentLocale(value)
  // сохраняем локацию в хранилище
  localStorage.setItem('locale', value)
}

Прим. пер.: при работе с локальным хранилищем, рекомендуется использовать методы JSON.stringify() при записи данных и JSON.parse() при их извлечении, поскольку localStorage умеет работать только со строками. В данном случае, это не имеет значения, поскольку значением локации является строка, но при записи в хранилище объекта, например, он будет преобразован в [object Object] с помощью метода toString() и сохранен в таком виде, так что лучше взять за правило использовать связку localStorage.setItem(key, JSON.stringify(value)) и JSON.parse(localStorage.getItem(key)) для записи/извлечения данных из локального хранилища.


Эту строку можно удалить:


const locale = LOCALES.ENGLISH

Таким образом, при запуске приложения мы пытаемся получить сохраненную локацию из локального хранилища и установить ее в качестве текущей локации приложения. Если локация в хранилище отсутствует (например, при первом посещении страницы пользователем), то текущей локацией становится LOCALES.ENGLISH.


При изменении пользователем локации приложения, новое значение записывается в хранилище.


Заключение


В данном туториале мы рассмотрели практически все, что вам нужно знать о библиотеке React Intl. Мы научились использовать этй библиотеку в React-приложении. Мы также реализовали сохранение выбранной пользователем локации в локальном хранилище браузера.


Надеюсь, статья вам понравилась. Исходный код проекта находится здесь.




Купить VDS-сервер с быстрыми NVMе-дисками и посуточной оплатой можно у Маклауд.