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

Я достаточно много времени потратил на поиски подходящей библиотеки для этой цели. А когда я ее все таки нашел, долго не мог понять как правильно ей пользоваться, ибо нормальную документацию разработчики написать поленились. Собственно сама библиотека — i18next. Она предоставляет много возможностей, но в данной статье будут рассмотрены две из них.

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

Мы будем использовать 2 языка — английский и русский. Каждый из них хранится в файле en.json и ru.json соответственно. Выглядеть словарь может следующим образом:

en.json
{
  "en": {
    "translation": {
      "test_message": "This is a test message"
    }
  }
}


ru.json
{
  "ru": {
    "translation": {
      "test_message": "Это тестовое сообщение"
    }
  }
}


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

componentWillMount() {
  this.setLanguage('en');
}


Функция setLanguage() может выглядеть следующим образом:

setLanguage(language) {
  i18next.init({
    lng: language,
    resources: require(`json!./${language}.json`)
  });

  this.props.actions.changeLanguage(i18next);
}


В параметре мы передаем название языка, а дальше используем его для инициализации библиотеки. Важный момент, внутри функции инициализации значение ключа lng должно соответствовать названию языка внутри словаря (в данном случае lng == en, как и первый объект внутри файла en.json). В resources должен быть передан путь к файлу словаря. Я использовал json-loader, поэтому функция выглядит таким образом. Также, следует не забыть обновить стейт компонента, иначе язык изменится, а рендер не вызовется и текст не поменяется.

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

render() {
  return (
    <div>
      <div>
        <Button bsStyle="primary" onClick={this.setLanguage.bind(this, 'en')}>English</Button>
        <Button bsStyle="primary" onClick={this.setLanguage.bind(this, 'ru')}>Русский</Button>
      </div>
      <div>{i18next.t('test_message')}</div>
    </div>
  )
}


Сверху расположены 2 кнопки для каждого из языков, при нажатии на которые и будет происходить смена языка. Важно использовать bind, иначе функция setLanguage будет вызываться бесконечно и приведет к зацикливанию. После рендера, вместо странной конструкции i18next.t('test_message') пользователю будет выдан корректный перевод, а именно This is a test message либо Это тестовое сообщение, в зависимости от установленного языка.

По сути, это все. Вот так незамысловато работает интернационализация приложения с помощью библиотеки i18next. Теперь же, как и было обещано, рассмотрим еще одну полезную функцию библиотеки — плюрализацию.

2. Плюрализация. Если вкратце, этот термин означает, что в зависимости от количественного признака окончание слова будет меняться. Например, в английском языке к слову в конце прибавляется s (one car -> five cars). А вот в русском ситуация несколько хуже, ибо для каждого слова есть 3 различных варианта окончания, например: 1 банан, 2 банана, 5 бананов. Конечно, внутри приложения можно сделать что-то вроде «Количество бананов: 2», но смотрится это не так эффектно. А с помощью i18next эту ситуацию можно исправить очень просто!

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

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

en.json
  "amount_of_bananas": "{{count}} banana",
  "amount_of_bananas_plural": "{{count}} bananas" 


При рендере компонента будет выбран вариант в зависимости от цифры, которая будет передана как count. Т.е. если внутри компонента мы передадим 1, то будет выбран первый вариант, иначе будет выбран второй вариант. Как библиотека понимает, какой вариант выбрать? Второй ключ оканчивается на _plural, который и помогает библиотеке с выбором. Для каждого языка есть свои окончания, найти их можно в таблице, ссылку на которую я оставил выше. Например, для русского языка словарь будет выглядеть следующим образом:

  "amount_of_bananas_0": "{{count}} банан",
  "amount_of_bananas_1": "{{count}} банана",
  "amount_of_bananas_2": "{{count}} бананов"


Т.е. в компоненте при передаче 1 будет отдано «1 банан», при передаче 2 — «2 банана» и при передаче 5 — «5 бананов». Очень удобно, что мы сами задаем перевод слова для того или иного количества.

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

<div>{i18next.t('amount_of_bananas', {count: 5})}</div>
, где на выходе человек получит «5 banans» или же «5 бананов»

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

На этом все, спасибо за внимание!

Ссылка на репозиторий с примером — https://github.com/theWaR13/i18next-example
Поделиться с друзьями
-->

Комментарии (36)


  1. ARad
    12.08.2016 17:57
    +1

    Почему amount_ofbananas0 соответствует 1 банан, а не 0 бананов? И что будет если передать 11 и 21?


    1. theWaR_13
      12.08.2016 17:58

      Действительно, нумерация внутри словаря идет несколько странно. Однако, один раз все настроив, про это можно забыть.
      Передавать можно любые цифры, конкретно 5 я взял для примера.


  1. ARad
    12.08.2016 19:47
    +1

    Вопрос про 11 и 21 был задан потому, что
    0, 10, 20 и т.д.бананов
    1 банан, 11 бананов, 21 банан
    2,3,4 банана, 12,13,14 бананов, 22,23,24 банана
    5 и далее как и ноль.


  1. michael_vostrikov
    12.08.2016 21:17

    Language Plural Rules:
    http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html


  1. MrCheater
    12.08.2016 22:40

    Есть ли возможность сразу загружать только один язык, а остальные подтягивать по мере необходимости?


    1. theWaR_13
      12.08.2016 22:42

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


      1. Skara
        13.08.2016 12:07
        +2

        подгрузка синхронная, значит вебпак (или что вы используете) уже вкомпилировал json словарь в бандл. Для более-менее большого приложения не годится. Решается легко, но в статье — не боевой вариант.


        1. yury-dymov
          13.08.2016 18:40

          Я у себя в проекте сделал на базе https://github.com/WebbyLab/itsquiz-wall, на мой взгляд очень неплохой вариант.

          json не вкомпиливается, работает изоморфно.

          Чтобы было совсем хорошо, надо добавить в языковые json хеш-суммы и ставить большой expire на веб-сервере



  1. Gugic
    13.08.2016 19:59

    А существуют ведь наверняка библиотеки интернационализации для js, которые не заставляют самостоятельно объявлять ключи, а используют в качестве ключа саму фразу на языке «по умолчанию»? (что-то вроде PHP-шного gettext).

    Мне такой подход кажется куда более удобным, особенно если имеется удобные инструменты для собственно процесса перевода.


    1. Gugic
      13.08.2016 20:06

      https://slexaxton.github.io/Jed/ — что-то нашлось. Приделать удобный псевдоним в глобальное пространство (window.t, например) и какую-нибудь очень простую вещь, которая будет ловить все фразы, туда попадающие и выдавать по каким уже есть перевод, а по каким еще нет — и можно почти не терять времени на поддержку версий для разных языков.


    1. vintage
      14.08.2016 13:35
      +1

      Косяки этого способа:


      1. Вы всегда грузите два языка.
      2. Исправление опечатки в тексте приводит к созданию нового ключа с последующей необходимостью повторного перевода.
      3. Нужно что-то решать с плюральными формами.


      1. Gugic
        14.08.2016 18:54

        1. С другой стороны я гружу только один язык, а не язык + ключ на каждую фразу, когда работаю с языком по умолчанию. А в способе из статьи это точно всегда будет язык + ключи.
        2. Да, но по идее это должно отслеживаться какой-то автоматикой.
        3. В вышеприведенном Jed это решено и выглядит так:

        i18n.translate("%d key doesnt exist").ifPlural( n, "%d keys dont exist" ).fetch();


        1. vintage
          14.08.2016 23:57

          1. Ключи обычно куда меньше текстов.
          2. В идеальном мире — да :-)
          3. Выглядит как жуткий костыль по сравнению c this.text( 'resultMessage' )


    1. megahertz
      16.08.2016 11:22

      Делал для первого ангуляра https://megahertz.github.io/mg-translate/


  1. vintage
    14.08.2016 13:31

    Как вы будете действовать, когда потребуется число сделать жирным шрифтом, а бананы — серым?


    1. theWaR_13
      14.08.2016 16:58

      Внутри словаря вы сами определяете, как будет выводиться строка, конкретно {{count}} отвечает за число. Таким образом, можно перед 18next.t('amount_of_bananas', {count: 5}) поставить 5 в спане, которая и будет выводиться.


      1. vintage
        14.08.2016 23:59

        Вы предлагаете зашивать разметку в локализацию?


        1. theWaR_13
          15.08.2016 09:00

          Нет. Внутри компонента, перед вызовом функции локализации будет стоять span с нужным числом (например, 5). Дальше идет функция перевода, куда вы эту 5 передаёте. А уже внутри словаря вы уберете {{count}}, и вторая пятерка (внутри функции) выводиться не будет.


          1. vintage
            15.08.2016 09:25
            +2

            Давайте более предметно. Вот у вас несколько вариантов текста:


            На вас ещё никто не подписан.

            На вас подписан 1 пользователь

            На вас подписаны 2 пользователя

            На вас подписаны 5 пользователей

            На вас подписаны 11 пользователей

            На вас подписаны 12 пользователей

            На вас подписаны 15 пользователей

            На вас подписан 21 пользователь

            На вас подписаны 22 пользователя

            На вас подписаны 25 пользователей

            Что вы напишете в коде и что будет в словаре?


  1. btd
    14.08.2016 21:53

    Может кому пригодится. Мы использовали очень долго babelfish. Мне не нравилось что он не дает компилировать сообщения (и таким образом довольно много места в бандле занимает). Я стал искать, чтобы во первых можно было компилировать, во вторых чтобы можно было использовать icu message format. Смотрел на format.js, там была какая то беда со сборкой (сейчас не знаю), хотя они и используют message format из icu. В конце концов, взял то что они использовали внутри messageformat.js. Надеюсь это кому то сэкономит время.


  1. nonlux
    14.08.2016 22:51

    После одного недавнего проекта осталось несколько мыслей по поводу перевода.
    1 Тексты это косвенные данные проекта. Не следует их засовывать в код через require. Это приведет к тому, что из-за изменения текстов придется пересобирать проект.
    Гораздо проще засунуть их в Глобал.
    2. Не надо привязывать Тексты жёстко к компонентам.
    Например, з я сделал так, добавил в компонент свойство text. Компонент знает только об схеме этого свойства, т.е о доступных ключах.
    Так же сделал простой декоратор @ text который из Глобал объекта тянет нужный ключ в компонент.


  1. pastor_sv
    15.08.2016 15:27
    +1

    использовал react-intl, все отлично и быстро делается

    vintage вот пример для вашего комментария выше.

    translates = {
      "message_subscribed_count" : "На вас {count, plural, =0 { ещё никто не подписан} one { подписан <b>#</b> пользователь} few { подписаны <b>#</b> пользователя} other { подписаны <b>#</b> пользователей}}."
    }
    
    <FormattedHTMLMessage id="message_subscribed_count" values={{count: COUNT}}/>
    


    1. vintage
      15.08.2016 18:21

      Заставляете переводчиков понимать теги и экранировать символы? А среда перевода понимает этот волшебный синтаксис или и его нужно знать бедным переводчикам?


      1. pastor_sv
        15.08.2016 18:34

        в админке есть просто конструктор, а переменную он просто не трогает. К примеру у него сначала есть конструкция «На вас $MESSAGE$» и дальше идет определение конструкций (=0, one, few, other), и потом уже MESSAGE собирается в {count, plural, ...}.

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


        1. vintage
          16.08.2016 00:13

              var lang = { 'ru' : {
                'result' : [
                  'На вас ещё никто не подписан.' ,
                  'На вас подписан 1 пользователь' ,
                  'На вас подписаны 2 пользователя' ,
                  'На вас подписаны 5 пользователей'
                 ]
              } }

              document.body.innerHTML += text( 'result' , 0 ) + '<br/>'
              document.body.innerHTML += text( 'result' , 1 ) + '<br/>'
              document.body.innerHTML += text( 'result' , 2 ) + '<br/>'
              document.body.innerHTML += text( 'result' , 5 ) + '<br/>'
              document.body.innerHTML += text( 'result' , 10 ) + '<br/>'
              document.body.innerHTML += text( 'result' , 11 ) + '<br/>'
              document.body.innerHTML += text( 'result' , 12 ) + '<br/>'
              document.body.innerHTML += text( 'result' , 15 ) + '<br/>'
              document.body.innerHTML += text( 'result' , 20 ) + '<br/>'
              document.body.innerHTML += text( 'result' , 21 ) + '<br/>'
              document.body.innerHTML += text( 'result' , 22 ) + '<br/>'
              document.body.innerHTML += text( 'result' , 25 ) + '<br/>'

          http://liveweave.com/c90hNl


          Для коллаборативного перевода уже давно есть специализированные сервисы так то.


          1. pastor_sv
            16.08.2016 10:22

            Отличный пример.
            Только как по мне, тяжело расширять такие переводы.

            Для того же примера: будем выводить не 'вас', а '$username$ ', и пусть username = nick1, и все поломается


            1. vintage
              16.08.2016 13:50

              Не сломается, там же будет плейсхолдер {username}.


          1. michael_vostrikov
            16.08.2016 10:38
            +1

            Извините, я не очень понял, а в чем преимущество такого решения, в связи с вашими комментариями про теги? Как вы будете действовать, когда потребуется число сделать жирным шрифтом, а надпись «пользователей» — серым?


            1. vintage
              16.08.2016 13:55
              -1

              Обычно требуется статичный текст приглушить, а вариативный выделить.


              1. michael_vostrikov
                16.08.2016 19:38
                +1

                Ну так как вы будете делать это в вашем варианте с result?


                1. vintage
                  17.08.2016 08:16

                  Я же уже привёл пример. Динамически добавляемые данные оборачиваются тегом.


                  1. michael_vostrikov
                    17.08.2016 12:19

                    В вашем примере только один тег <br/>, ни цвета ни болда там нет. У вас функция подстановки перевода сама магические спаны добавляет?


                    1. vintage
                      17.08.2016 13:52

                      Вы не умеете пользоваться css? Да, сама.


                      1. michael_vostrikov
                        17.08.2016 18:59

                        Ясно. А если надо 2 связанных параметра передать в сообщение и каждый выделить по-своему («Вы набрали 18 очков из 20 возможных»), у вас будет другая функция text2()?


                        1. vintage
                          17.08.2016 19:20

                          Зачем? Эту вполне можно расширить и на этот случай. Это пример, а не готовое решение, если это не очевидно.