Статьи

Перевод приложений-стимулов с I18Следующая

В моей предыдущей статье я рассмотрел Stimulus — скромный JavaScript-фреймворк, созданный Basecamp. Сегодня я расскажу об интернационализации приложения Stimulus, так как фреймворк не предоставляет никаких инструментов I18n из коробки. Интернационализация является важным шагом, особенно когда ваше приложение используют люди со всего мира, поэтому базовое понимание того, как это сделать, может действительно пригодиться.

Конечно, вам решать, какое решение по интернационализации реализовать, будь то jQuery.I18n , Polyglot или какой-либо другой. В этом уроке я хотел бы показать вам популярную платформу I18n под названием I18next, которая имеет много интересных функций и предоставляет множество дополнительных сторонних плагинов для еще большего упрощения процесса разработки. Даже со всеми этими функциями I18next не является сложным инструментом, и вам не нужно изучать много документации, чтобы начать работу.

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

  • I18 следующая конфигурация
  • перевод файлов и их загрузка асинхронно
  • выполнение переводов и перевод всей страницы за один раз
  • работа с множественным числом и гендерной информацией
  • переключение между локалями и сохранение выбранной локали в параметре GET
  • настройка локали на основе предпочтений пользователя

Исходный код доступен в репозитории GitHub .

Для начала давайте клонируем проект Stimulus Starter и устанавливаем все зависимости с помощью менеджера пакетов Yarn :

1
2
3
git clone https://github.com/stimulusjs/stimulus-starter.git
cd stimulus-starter
yarn install

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

Кроме того, мы собираемся представить переключатель языка в верхней части страницы. Когда язык выбран, интерфейс должен быть переведен сразу же, без перезагрузки страницы. Кроме того, к URL должен быть добавлен параметр ?locale GET, указывающий, какая локаль в настоящее время используется. Конечно, если страница загружается с уже предоставленным параметром, правильный язык должен быть установлен автоматически.

Хорошо, давайте перейдем к визуализации наших пользователей. Добавьте следующую строку кода в файл public / index.html :

1
<div data-controller=»users» data-users-url=»/api/users/index.json»></div>

Здесь мы используем контроллер users и предоставляем URL для загрузки наших пользователей. В реальном приложении у нас, вероятно, будет серверный скрипт, который выбирает пользователей из базы данных и отвечает JSON. Для этого урока, однако, давайте просто поместим все необходимые данные в открытый файл / api / users / index.json :

01
02
03
04
05
06
07
08
09
10
11
12
[
  {
    «login»: «johndoe»,
    «photos_count»: «15»,
    «gender»: «male»
  },
  {
    «login»: «annsmith»,
    «photos_count»: «20»,
    «gender»: «female»
  }
]

Теперь создайте новый файл src / controllers / users_controller.js :

1
2
3
4
5
6
7
import { Controller } from «stimulus»
 
export default class extends Controller {
  connect() {
    this.loadUsers()
  }
}

Как только контроллер подключен к DOM, мы асинхронно загружаем наших пользователей с помощью метода loadUsers() :

1
2
3
4
5
6
7
loadUsers() {
   fetch(this.data.get(«url»))
   .then(response => response.text())
   .then(json => {
     this.renderUsers(json)
   })
 }

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

1
2
3
4
5
6
7
renderUsers(users) {
   let content = »
   JSON.parse(users).forEach((user) => {
     content += `<div>Login: ${user.login}<br>Has uploaded ${user.photos_count} photo(s)</div><hr>`
   })
   this.element.innerHTML = content
 }

renderUsers() , в свою очередь, анализирует JSON, создает новую строку со всем содержимым и, наконец, отображает это содержимое на странице ( this.element будет возвращать фактический узел DOM, к которому подключен контроллер, который является div в наш случай).

Теперь мы собираемся приступить к интеграции I18next в наше приложение. Добавьте в наш проект две библиотеки: сам I18next и плагин для включения асинхронной загрузки файлов перевода из серверной части :

1
yarn add i18next i18next-xhr-backend

Мы собираемся сохранить все, что связано с I18, в отдельном файле src / i18n / config.js , поэтому создайте его сейчас:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
import i18next from ‘i18next’
import I18nXHR from ‘i18next-xhr-backend’
 
const i18n = i18next.use(I18nXHR).init({
  fallbackLng: ‘en’,
  whitelist: [‘en’, ‘ru’],
  preload: [‘en’, ‘ru’],
  ns: ‘users’,
  defaultNS: ‘users’,
  fallbackNS: false,
  debug: true,
  backend: {
    loadPath: ‘/i18n/{{lng}}/{{ns}}.json’,
  }
}, function(err, t) {
  if (err) return console.error(err)
});
  
export { i18n as i18n }

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

  • use(I18nXHR) включает плагин i18next-xhr-backend.
  • fallbackLng говорит ему использовать английский как запасной язык .
  • whitelist позволяет устанавливать только английский и русский языки. Конечно, вы можете выбрать любой другой язык.
  • preload загрузка инструктирует файлы перевода preload с сервера, а не загружать их, когда выбран соответствующий язык.
  • ns означает «пространство имен» и принимает либо строку, либо массив. В этом примере у нас есть только одно пространство имен, но для более крупных приложений вы можете ввести другие пространства имен, такие как admin , cart , profile и т. Д. Для каждого пространства имен должен быть создан отдельный файл перевода.
  • defaultNS устанавливает users в качестве пространства имен по умолчанию.
  • fallbackNS отключает резервирование пространства имен.
  • debug позволяет отображать информацию об отладке в консоли браузера. В частности, в нем указано, какие файлы перевода загружены, какой язык выбран и т. Д. Возможно, вы захотите отключить этот параметр перед развертыванием приложения в рабочей среде.
  • backend предоставляет конфигурацию для плагина I18nXHR и указывает, откуда загружать переводы. Обратите внимание, что путь должен содержать заголовок локали, а файл должен быть назван в соответствии с пространством имен и иметь расширение .json.
  • function(err, t) — это обратный вызов, который запускается, когда I18next готов (или когда возникла ошибка).

Далее давайте создадим файлы перевода. Переводы на русский язык должны быть размещены в публичном файле / i18n / ru / users.json :

1
2
3
{
  «login»: «Логин»
}

login здесь — ключ перевода, а Логин — значение для отображения.

Английские переводы, в свою очередь, должны идти в публичный файл / i18n / en / users.json :

1
2
3
{
  «login»: «Login»
}

Чтобы убедиться, что I18next работает, вы можете добавить следующую строку кода к обратному вызову внутри файла i18n / config.js :

1
2
3
4
5
// config goes here…
function(err, t) {
  if (err) return console.error(err)
  console.log(i18n.t(‘login’))
}

Здесь мы используем метод с именем t что означает «перевод». Этот метод принимает ключ перевода и возвращает соответствующее значение.

Однако у нас может быть много частей пользовательского интерфейса, которые необходимо перевести, и сделать это с помощью метода t было бы довольно утомительно. Вместо этого я предлагаю вам использовать другой плагин под названием loc-i18next, который позволяет переводить сразу несколько элементов.

Установите плагин loc-i18next:

1
yarn add loc-i18next

Импортируйте его в начало файла src / i18n / config.js :

1
import locI18next from ‘loc-i18next’

Теперь предоставьте конфигурацию для самого плагина:

1
2
3
4
5
6
7
8
9
// other config
 
const loci18n = locI18next.init(i18n, {
  selectorAttr: ‘data-i18n’,
  optionsAttr: ‘data-i18n-options’,
  useOptionsAttr: true
});
 
export { loci18n as loci18n, i18n as i18n }

Здесь следует отметить пару вещей:

  • locI18next.init(i18n) создает новый экземпляр плагина на основе ранее определенного экземпляра I18next.
  • selectorAttr указывает, какой атрибут использовать для обнаружения элементов, требующих локализации. По сути, loc-i18next будет искать такие элементы и использовать значение атрибута data-i18n в качестве ключа перевода.
  • optionsAttr указывает, какой атрибут содержит дополнительные параметры перевода.
  • useOptionsAttr указывает плагину использовать дополнительные параметры.

Наши пользователи загружаются асинхронно, поэтому мы должны ждать, пока эта операция не будет выполнена, и только после этого выполнять локализацию. А пока давайте просто установим таймер, который должен ждать две секунды перед вызовом метода localize() — это, конечно, временный взлом.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
import { loci18n } from ‘../i18n/config’
  
 // other code…
  
 loadUsers() {
   fetch(this.data.get(«url»))
   .then(response => response.text())
   .then(json => {
     this.renderUsers(json)
     setTimeout(() => { // <—
       this.localize()
     }, ‘2000’)
   })
 }

Код самого метода localize() :

1
2
3
localize() {
   loci18n(‘.users’)
 }

Как видите, нам нужно только передать селектор плагину loc-i18next. Все элементы внутри (которые имеют установленный атрибут data-i18n ) будут локализованы автоматически.

Теперь renderUsers метод renderUsers . А пока давайте переведем только слово «Логин»:

1
2
3
4
5
6
7
renderUsers(users) {
   let content = »
   JSON.parse(users).forEach((user) => {
     content += `<div class=»users»>ID: ${user.id}<br><span data-i18n=»login»>
   })
   this.element.innerHTML = content
 }

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

Мы локализовали часть интерфейса, что действительно круто. Тем не менее, у каждого пользователя есть еще два поля: количество загруженных фотографий и пол. Поскольку мы не можем предсказать, сколько фотографий будет у каждого пользователя, слово «фотография» должно быть правильно умножено на основе заданного количества. Для этого нам потребуется атрибут data-i18n-options настроенный ранее. Чтобы обеспечить счет, data-i18n-options должен быть назначен со следующим объектом: { "count": YOUR_COUNT } .

Гендерная информация также должна быть принята во внимание. Слово «загруженный» на английском языке может применяться как к мужскому, так и к женскому, но на русском языке оно становится «загрузил» или «загрузила», поэтому нам снова нужны data-i18n-options , которые имеют { "context": "GENDER" } в качестве значения. Кстати, обратите внимание, что вы можете использовать этот контекст для решения других задач, а не только для предоставления гендерной информации.

1
2
3
4
5
6
7
renderUsers(users) {
   let content = »
   JSON.parse(users).forEach((user) => {
     content += `<div class=»users»><span data-i18n=»login»>
   })
   this.element.innerHTML = content
 }

Теперь обновите английские переводы:

1
2
3
4
5
6
{
  «login»: «Login»,
  «uploaded»: «Has uploaded»,
  «photos»: «one photo»,
  «photos_plural»: «{{count}} photos»
}

Здесь нет ничего сложного. Поскольку для английского языка мы не заботимся о гендерной информации (которая является контекстом), ключ перевода должен быть просто uploaded . Для обеспечения правильного множественного перевода мы используем ключи photos и photos_plural . Часть {{count}} является интерполяцией и будет заменена фактическим числом.

Что касается русского языка, то все сложнее:

1
2
3
4
5
6
7
8
{
  «login»: «Логин»,
  «uploaded_male»: «Загрузил уже»,
  «uploaded_female»: «Загрузила уже»,
  «photos_0»: «одну фотографию»,
  «photos_1»: «{{count}} фотографии»,
  «photos_2»: «{{count}} фотографий»
}

Прежде всего, обратите внимание, что у нас есть ключи uploaded_male и uploaded_female для двух возможных контекстов. Далее, правила плюрализации также более сложны в русском языке, чем в английском, поэтому мы должны предоставить не две, а три возможные фразы. I18next поддерживает множество языков из коробки, и этот небольшой инструмент может помочь вам понять, какие ключи множественного числа должны быть указаны для данного языка.

Мы завершили перевод нашего приложения, но пользователи должны иметь возможность переключаться между локалями. Поэтому добавьте новый компонент «переключатель языка» в файл public / index.html :

1
<ul data-controller=»languages» class=»language-switcher»></ul>

Создайте соответствующий контроллер в файле src / controllers / languages_controller.js :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
import { Controller } from «stimulus»
import { i18n, loci18n } from ‘../i18n/config’
 
export default class extends Controller {
    initialize() {
      let languages = [
        {title: ‘English’, code: ‘en’},
        {title: ‘Русский’, code: ‘ru’}
      ]
 
      this.element.innerHTML = languages.map((lang) => {
        return `<li data-action=»click->languages#switchLanguage»
        data-lang=»${lang.code}»>${lang.title}</li>`
      }).join(»)
    }
}

Здесь мы используем обратный вызов initialize() для отображения списка поддерживаемых языков. Каждый li имеет атрибут data-action который указывает, какой метод (в данном случае switchLanguage ) должен быть запущен при нажатии на элемент.

Теперь добавьте метод switchLanguage() :

1
2
3
switchLanguage(e) {
   this.currentLang = e.target.getAttribute(«data-lang»)
 }

Он просто получает цель события и получает значение атрибута data-lang .

Я также хотел бы добавить метод получения и установки для атрибута currentLang :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
get currentLang() {
   return this.data.get(«currentLang»)
 }
 
 set currentLang(lang) {
   if(i18n.language !== lang) {
     i18n.changeLanguage(lang)
   }
 
   if(this.currentLang !== lang) {
     this.data.set(«currentLang», lang)
     loci18n(‘body’)
     this.highlightCurrentLang()
   }
 }

Получатель очень прост — мы выбираем значение используемого в настоящее время языка и возвращаем его.

Сеттер более сложный. Прежде всего, мы используем метод changeLanguage если changeLanguage установленный язык не равен выбранному. Кроме того, мы сохраняем вновь выбранный языковой стандарт в атрибуте data-current-lang (доступ к которому осуществляется через средство получения), локализуем тело HTML-страницы с помощью плагина loc-i18next и, наконец, выделяем используемый в настоящее время языковой стандарт.

Давайте кодируем highlightCurrentLang() :

1
2
3
4
5
highlightCurrentLang() {
   this.switcherTargets.forEach((el, i) => {
     el.classList.toggle(«current», this.currentLang === el.getAttribute(«data-lang»))
   })
 }

Здесь мы перебираем массив переключателей локали и сравниваем значения их атрибутов data-lang со значением используемой в данный момент локали. Если значения совпадают, переключателю назначается current класс CSS, в противном случае этот класс удаляется.

Чтобы заставить конструкцию this.switcherTargets работать, нам нужно определить цели Stimulus следующим образом:

1
static targets = [ «switcher» ]

Также добавьте атрибуты data-target со значениями switcher для li :

1
2
3
4
5
6
7
8
initialize() {
     // …
     this.element.innerHTML = languages.map((lang) => {
       return `<li data-action=»click->languages#switchLanguage»
       data-target=»languages.switcher» data-lang=»${lang.code}»>${lang.title}</li>`
     }).join(»)
     // …
 }

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
initialize() {
   i18n.on(‘loaded’, (loaded) => { // <—
     let languages = [
       {title: ‘English’, code: ‘en’},
       {title: ‘Русский’, code: ‘ru’}
     ]
 
     this.element.innerHTML = languages.map((lang) => {
       return `<li data-action=»click->languages#switchLanguage»
       data-target=»languages.switcher» data-lang=»${lang.code}»>${lang.title}</li>`
     }).join(»)
 
     this.currentLang = i18n.language
   })
 }

Наконец, не забудьте удалить setTimeout из loadUsers() :

1
2
3
4
5
6
7
8
loadUsers() {
   fetch(this.data.get(«url»))
   .then(response => response.text())
   .then(json => {
     this.renderUsers(json)
     this.localize()
   })
 }

После переключения языкового стандарта я хотел бы добавить параметр GET ?lang к URL-адресу, содержащему код выбранного языка. Добавление параметра GET без перезагрузки страницы может быть легко сделано с помощью History API :

01
02
03
04
05
06
07
08
09
10
11
12
set currentLang(lang) {
   if(i18n.language !== lang) {
     i18n.changeLanguage(lang)
     window.history.pushState(null, null, `?lang=${lang}`) // <—
   }
 
   if(this.currentLang !== lang) {
     this.data.set(«currentLang», lang)
     loci18n(‘body’)
     this.highlightCurrentLang()
   }
 }

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

1
yarn add i18next-browser-languagedetector

Импортируйте LanguageDetector из файла i18n / config.js :

1
import LngDetector from ‘i18next-browser-languagedetector’

Теперь настройте конфигурацию:

1
2
3
4
5
6
7
8
9
const i18n = i18next.use(I18nXHR).use(LngDetector).init({ // <—
  // other options go here…
  detection: {
    order: [‘querystring’, ‘navigator’, ‘htmlTag’],
    lookupQuerystring: ‘lang’,
  }
}, function(err, t) {
  if (err) return console.error(err)
});

Опция order перечисляет все методы (отсортированные по их важности), которые плагин должен попробовать, чтобы «угадать» предпочтительный языковой стандарт:

  • querystring означает проверку параметра GET, содержащего код локали.
  • lookupQuerystring устанавливает имя используемого параметра GET, что в нашем случае является lang .
  • navigator означает получение данных локали по запросу пользователя.
  • htmlTag включает выбор предпочтительной локали из атрибута lang тега html .

В этой статье мы взглянули на I18next — популярное решение для простого перевода приложений JavaScript. Вы узнали, как интегрировать I18next со структурой Stimulus, настроить его и загружать файлы перевода в асинхронном режиме. Кроме того, вы видели, как переключаться между локалями и устанавливать язык по умолчанию на основе предпочтений пользователя.

I18next имеет некоторые дополнительные параметры конфигурации и множество плагинов , поэтому обязательно изучите его официальную документацию, чтобы узнать больше. Также обратите внимание, что Stimulus не заставляет вас использовать конкретное решение для локализации, поэтому вы также можете попробовать использовать что-то вроде jQuery.I18n или Polyglot .

Это все на сегодня! Спасибо, что читаете вместе и до следующего раза.