Статьи

Как локализовать ваше приложение с помощью jQuery.I18n

За последний месяц я написал несколько статей об интернационализации, посвященных использованию I18n в приложениях Rails, а также различным решениям для JavaScript . Сегодняшний гость — библиотека jQuery.I18n, кратко освещенная в одном из предыдущих постов. Это решение поддерживает пол, грамматические формы, динамическое изменение языка, резервные цепочки и многое другое.

Мы собираемся подробно обсудить возможности jQuery.I18n, а также увидеть его в действии, создав демонстрационное приложение JavaScript. Вот краткий обзор тем, которые мы рассмотрим:

  • Основная информация о jQuery.I18n.
  • Настройка демо-приложения и загрузка необходимых файлов.
  • Процесс создания файлов перевода.
  • Загрузка файлов перевода и некоторые ошибки, которые необходимо учитывать.
  • Переключение локали.
  • Сохранение данных локали с использованием History API.
  • Отображение локализованных сообщений на основе data-атрибутов HTML5 .
  • Отображение локализованных сообщений с использованием пользовательского кода JavaScript.
  • Использование пола и информации о плюрализме в переводах.
  • Оптимизация кода, чтобы избежать повторения.
  • Добавляем «волшебные» слова.
  • Документирование ваших переводов.

Исходный код этой статьи доступен на GitHub .

Краткое примечание о демо-приложении

Чтобы увидеть конечный результат в действии, вам нужно настроить веб-сервер. Я собираюсь использовать Microsoft IIS , но есть множество других доступных решений, которые помогут вам начать работу за минуту:

О jQuery.I18n

Конечно, вы знаете, что такое Википедия , и, вероятно, использовали этот веб-сайт много раз, так как он знает все, начиная от физики и химии до популярных (и менее популярных) фильмов и компьютерных игр. Wikipedia поддерживается компанией под названием Wikimedia Foundation, которая также занимается такими проектами, как Wikinews , Wikivoyage и многими другими. Все эти проекты доступны по всему миру и доступны на разных языках, поэтому интернационализация абсолютно необходима для Викимедиа.

Поэтому эта компания создала популярное решение на основе jQuery для интернационализации приложений JavaScript и назвала его просто jQuery.I18n. Не очень причудливое имя, но поверьте мне, это больше, чем кажется на первый взгляд. Эта библиотека действительно удобна и мощна, поэтому давайте не будем тратить наше время и обсудим ее особенности сейчас. Чтобы сделать наше путешествие более интересным и полезным, мы собираемся применить изученные концепции на практике.

Файлы перевода

jQuery.I18n хранит переводы в простых файлах JSON. Если вы, как и я, фанат Rails, вы обнаружите, что этот процесс очень похож на хранение переводов в файлах YAML.

Фактические переводы в формате ключ-значение, например

{ "title": "Example Application" }

titleэто, конечно, ключ, тогда как Example Applicationэто его значение. Рекомендуется называть ключи строчными буквами, разделив их словами -. Документация jQuery.I18n также предлагает добавить к ключам префикс имени приложения:

{ "myapp-title": "Example Application" }

… но это не обязательно.

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

{
"@metadata": {
    "authors": [
        "Alice",
        "David"
    ],
    "last-updated": "2016-10-25",
    "locale": "en"
},
"title": "Example Application"
}

Обычно файлы перевода помещаются в каталог i18n . Переводы для разных языков часто хранятся отдельно в файлах, названных по имени языка (например, en.json , de.json , ru.json и т. Д.). Однако для простого приложения вы можете поместить все в один файл . В этом случае переводы следует размещать под ключом, названным в честь языка:

"en": {
"title": "Example Application"
},
"ru": {
title: "Тестовое приложение"
}

Более того, вы можете указать путь к файлу вместо объекта с переводами:

"en": {
"title": "Example Application"
},
"ru": "ru/ru.json"

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

  • JS / i18n / en.json
  • JS / i18n / ru.json

Заполните ваши файлы некоторым основным содержанием:

JS / i18n / en.json

{
  "@metadata": {
    "authors": [
      "Ilya"
    ],
    "last-updated": "2016-10-25",
    "locale": "en"
  },
  "app-title": "Example Application"
}

JS / i18n / ru.json

{
  "@metadata": {
    "authors": [
      "Ilya"
    ],
    "last-updated": "2016-10-25",
    "locale": "ru"
  },
  "app-title": "Тестовое приложение"
}

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

Кроме того, чтобы подготовиться к следующим шагам, локально клонируйте jQuery.I18n вместе с его зависимостями:

$ git clone https://github.com/wikimedia/jquery.i18n.git
$ cd jquery.i18n
$ git submodule update --init

Внутри каталога вашего проекта создайте папку js / lib / jquery.i18n и скопируйте содержимое папки src из клонированного проекта внутри (без каталога вложенных языков ). Затем внутри js / lib создайте еще один каталог CLDRPluralRuleParser . Скопируйте туда файл libs / CLDRPluralRuleParser / src / CLDRPluralRuleParser.js из клонированного проекта.

Наконец, внутри js создайте файл с именем global.js, в который мы поместим наш пользовательский код. В результате ваш проект должен иметь следующую структуру:

|-- js
|-- global.js
|-- i18n
|-- en.json
|-- ru.json
|-- lib
|-- CLDRPluralRuleParser
|-- CLDRPluralRuleParser.js
|-- jquery.i18n
|-- jquery.i18n.js
|-- jquery.i18n.language.js
|-- ...other files here

Теперь в корне проекта создайте простой HTML-файл с необходимыми библиотеками, подключенными в правильном порядке:

demo.html

<!DOCTYPE>
<html>
<head>
  <meta charset="utf-8">
  <title>Demo Application</title>
  <script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>

  <script src="js/lib/CLDRPluralRuleParser/CLDRPluralRuleParser.js"></script>
  <script src="js/lib/jquery.i18n/jquery.i18n.js"></script>
  <script src="js/lib/jquery.i18n/jquery.i18n.messagestore.js"></script>
  <script src="js/lib/jquery.i18n/jquery.i18n.fallbacks.js"></script>
  <script src="js/lib/jquery.i18n/jquery.i18n.language.js"></script>
  <script src="js/lib/jquery.i18n/jquery.i18n.parser.js"></script>
  <script src="js/lib/jquery.i18n/jquery.i18n.emitter.js"></script>
  <script src="js/lib/jquery.i18n/jquery.i18n.emitter.bidi.js"></script>

  <script src="js/global.js"></script>
</head>
<body>
</body>
</html>

Я использую jQuery 3 здесь, но вы можете выбрать версию 1 или 2 в зависимости от браузеров, которые вы хотите поддерживать (версия 3 работает только с современными браузерами, поэтому IE7 для вас не подходит).

Все идет нормально. Мы можем перейти к следующему разделу и загрузить наш перевод в приложение.

Загрузка переводов

Следующий важный шаг — это, конечно,  загрузка ваших переводов . В простейшем случае вы просто предоставили бы пары ключ-значение непосредственно в коде:

$.i18n().load( {
'en': {
  'app-title': 'Example Application'
}
} );

Или для конкретной локали:

$.i18n().load({
'app-title': 'Example Application'
}, 'en');

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

$.i18n().load( {
'en': 'dir/en.json'
} );

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

$.i18n().load( {
'en': {
  'app-title': 'Example Application'
}
} );

// ... some instructions

$.i18n().load( {
'en': {
  'another key': 'a value'
}
} );

полностью действителен Просто убедитесь, что вы не перезаписываете существующие ключи.

Давайте загрузим переводы в демонстрационное приложение:

JS / global.js

jQuery(function($) {
  $.i18n().load( {
    'en': './js/i18n/en.json',
    'ru': './js/i18n/ru.json'
  } );
});

Еще одним важным моментом является то, что loadфункция возвращает jQuery Promise , что упрощает выполнение некоторых действий только после успешной загрузки переводов, например:

  $.i18n().load( {
    'en': 'i18n/en.json',
    'ru': 'i18n/ru.json'
  } ).done( function() { console.log('done!') } );

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

Переключение локали

Какой языковой стандарт будет установлен по умолчанию при загрузке страницы? Прежде всего, языковой стандарт по умолчанию может быть предоставлен как опция, передаваемая $.i18n:

$.i18n( {
    locale: 'en'
} );

Также jQuery.I18n проверит значение langатрибута, установленного для htmlтега. Давайте добавим это сейчас:

demo.html

<html lang="en" dir="ltr">

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

Конечно, можно переключить выбранную локаль позже, изменив localeопцию:

$.i18n().locale = 'ru';

Давайте добавим ссылки для переключения локали:

demo.html

<ul class="switch-locale">
  <li><a href="#" data-locale="en">English</a></li>
  <li><a href="#" data-locale="ru">Русский</a></li>
</ul>

Обработайте событие click, предотвращая действие по умолчанию и переключая локаль на основе data-localeатрибута:

JS / global.js

jQuery(function($) {
// ...
  $.i18n().load( {
    'en': './js/i18n/en.json',
    'ru': './js/i18n/ru.json'
  } ).done(function() {
  $('.switch-locale').on('click', 'a', function(e) {
    e.preventDefault();
    $.i18n().locale = $(this).data('locale');
  });
});
});

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

Сохраняющаяся локаль

Еще одна вещь, которую мы собираемся сделать, — сохранить выбранную локаль, добавив ее к URL-адресу в виде параметра GET. Так, например, по этому URL http://localhost/demo.html?locale=ruбудет запрашиваться русская версия сайта. Это очень важно сделать, потому что, когда пользователь делится ссылкой, он ожидает, что другие люди увидят ту же версию сайта.

Однако для этого нам понадобятся две дополнительные библиотеки. Первый — это History API, который поддерживает HTML5 History / State API . Я собираюсь взять в комплекте версию html4 + html5 для jQuery, которую можно найти здесь .

Второе — это библиотека URL, которая делает работу с URL в JavaScript легкой. Возьмите уменьшенную версию здесь .

Подключите эти две библиотеки сейчас:

demo.html

<!-- ... -->
  <script src="js/lib/history/jquery.history.js"></script>
  <script src="js/lib/url/url.min.js"></script>
  <script src="js/global.js"></script>
<!-- ... -->

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

JS / global.js

var set_locale_to = function(locale) {
  if (locale)
    $.i18n().locale = locale;
};

Он должен вызываться после загрузки страницы и выполнения переводов. Сама локаль должна быть выбрана из ?localeпараметра:

JS / global.js

jQuery(function() {
  $.i18n().load( {
// ...
  } ).done(function() {
    set_locale_to(url('?locale'));
// ...
});
});

url('?locale')принимает значение localeпараметра GET. Кроме того, нам нужно прослушать statechangeсобытие (которое происходит, например, при pushStateвызове метода) и соответствующим образом изменить локаль:

JS / global.js

jQuery(function() {
  $.i18n().load( {
// ...
  } ).done(function() {
    set_locale_to(url('?locale'));
History.Adapter.bind(window, 'statechange', function(){
  set_locale_to(url('?locale'));
});
// ...
});
});

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

JS / global.js

var set_locale_to = function(locale) {
  if (locale)
    $.i18n().locale = locale;
};

jQuery(function() {
  $.i18n().load( {
    'en': './js/i18n/en.json',
    'ru': './js/i18n/ru.json'
  } ).done(function() {
    set_locale_to(url('?locale'));

    History.Adapter.bind(window, 'statechange', function(){
      set_locale_to(url('?locale'));
    });

    $('.switch-locale').on('click', 'a', function(e) {
      e.preventDefault();
      History.pushState(null, null, "?locale=" + $(this).data('locale'));
    });
  });
});

Даже если localeпараметр отсутствует, у нас все еще есть lang="en"набор для htmlтега.

Отображение переводов с помощью API данных

Самый простой способ отобразить локализованный контент — использовать data-атрибуты. Все, что вам нужно сделать, это предоставить data-i18nатрибут и установить ключ перевода в качестве его значения. Например, давайте отобразим название нашего сайта:

demo.html

<h1 data-i18n="app-title"></h1>

app-titleКак вы помните, он был определен внутри файла en.json как "app-title": "Example Application"и в ru.json как "app-title": "Тестовое приложение".

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

JS / global.js

var set_locale_to = function(locale) {
  if (locale) {
    $.i18n().locale = locale;
  }
  $('body').i18n();
};
// ...

Чтобы предоставить запасной текст, который будет отображаться во время загрузки переводов, просто поместите его в тег:

demo.html

<h1 data-i18n="app-title">Example Application</h1>

Теперь перезагрузите страницу и наблюдайте за результатом!

Параметры, пол и плюрализация в переводах

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

$.i18n('some-key');

Таким образом, мы можем, например, отобразить приветственное сообщение (с резервным текстом):

demo.html

<body>
<!-- ... -->
<h2 id="welcome">Welcome</h2>
</body>

Добавить переводы:

en.json

"welcome": "Welcome!"

ru.json

"welcome": "Добро пожаловать!"

и настроить код:

global.js

var set_locale_to = function(locale) {
// ...
$('#welcome').text($.i18n('welcome'));
};

Довольно мило, но было бы лучше отображать имя пользователя. Для этого нам нужно добавить параметр внутри сообщения. Параметры в jQuery.I18n в настоящее время названы $1, $2, и $3т.д.:

en.json

"welcome": "Welcome, $1!"

ru.json

"welcome": "Добро пожаловать, $1!"

Чтобы установить значение параметра, просто передайте другой аргумент $.i18n:

global.js

var set_locale_to = function(locale) {
// ...
$('#welcome').text($.i18n('welcome', 'John'));
};

Если у вас есть два параметра, вы, конечно, передадите два аргумента методу, и первый из них будет присвоен $1переменной, а второй — переменной $2.

А как насчет гендерной информации ? Предположим, например, что мы хотим отобразить несколько электронных писем от разных людей с заголовком «Вы получили новое письмо от кого-то. Он говорит:». Прежде всего, добавьте новую разметку:

demo.html

<!-- ... -->
<div id="letter-1">
  <p><em></em></p>
  <p>letter's text...</p>
</div>

<div id="letter-2">
  <p><em></em></p>
  <p>letter's text...</p>
</div>

Теперь переводы:

en.json

"letter": "A letter from $1! {{GENDER:$2|He|She}} says:"

ru.json

"letter": "Письмо от $1! {{GENDER:$2|Он|Она}} говорит:"

Так GENDER:действует как переключатель. Он получает параметр и выбирает один из двух вариантов: первый выбирается для мужского пола, а второй — для женского. Чтобы иметь возможность использовать это сообщение, укажите maleили femaleв качестве второго аргумента:

global.js

var set_locale_to = function(locale) {
// ...
  $('#letter-1').find('p > em').text($.i18n('letter', 'Ann', 'female'));
  $('#letter-2').find('p > em').text($.i18n('letter', 'Rick', 'male'));
}

Также давайте посмотрим, сколько букв у пользователя. Для этого нам понадобится переключатель PLURAL : .

demo.html

<p id="letters"></p>

Сообщения:

en.json

"letters": "You have $1 {{PLURAL:$1|letter|letters}}"

ru.json

"letters": "У вас $1 {{PLURAL:$1|письмо|писем|письма}}"

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

global.js

var set_locale_to = function(locale) {
// ...
$('#letters').text($.i18n('letters', 5));
};

Вся информация о поле и множественном числе хранится в файле jquery.i18n.language.js .

Обновление кода

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

В качестве первого шага измените разметку:

demo.html

<!-- ... -->
<h1 class="translate" data-args="app-title">Example Application</h1>
<h2 class="translate" data-args="welcome,John">Welcome</h2>

<p class="translate" data-args="letters,5"></p>

<div>
  <p><em class="translate" data-args="letter,Ann,female"></em></p>
  <p>letter's text...</p>
</div>

<div>
  <p><em class="translate" data-args="letter,Rick,male"></em></p>
  <p>letter's text...</p>
</div>

Теперь для каждого элемента .translateкласса нам нужно взять значение data-args, разделить его на массив и передать в $.i18nметод. Единственная проблема в том, что мы не знаем, сколько аргументов будет передано. Это не проблема, однако, с методом apply :

var set_locale_to = function(locale) {
// ...
  $('.translate').each(function() {
    var args = [], $this = $(this);
    if ($this.data('args'))
      args = $this.data('args').split(',');
    $this.html( $.i18n.apply(null, args) );
};

applyпринимает два аргумента: первый — это значение thisвнутри вызываемой функции, а второй — массив, представляющий все передаваемые аргументы.

Теперь вы можете перезагрузить страницу и посмотреть результат. Конечно, это решение не идеально, но вы можете расширить его по мере необходимости.

Волшебные слова

PLURAL:и GENDER:оба являются «волшебными» словами, которые поддерживает библиотека. Вы можете, однако, ввести новые волшебные слова по мере необходимости, расширив $.i18n.parser.emitter:

global.js

jQuery(function() {
  $.extend($.i18n.parser.emitter, {
    sitename: function() {
      return "Demo";
    }
});
});

Итак, имя sitenameволшебного слова и соответствующие функции возвращают его значение. Чтобы получить это значение, используйте шаблонный синтаксис (аналогичный тому, который используют Handlebars ): {{SITENAME}}или {{sitename}}. :не нужен, потому sitenameчто не принимает никаких аргументов.

Теперь вы можете использовать это волшебное слово в своих переводах:

en.json

"copyright": "{{SITENAME}}. All rights reserved."

ru.json

"copyright": "{{SITENAME}}. All rights reserved."

Разметка:

demo.html

<!-- ... -->
<footer class="translate" data-args="copyright"></footer>

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

global.js

// ...
  $.extend($.i18n.parser.emitter, {
    sitename: function() {
      return "Demo";
    },
    link: function (nodes) {
      return '<a href="' + nodes[1] + '">' + nodes[0] + '</a>';
    }
  } );

nodesэто массив аргументов. Используйте эти волшебные слова так же, как gender:илиplural:

en.json

"about": "{{link:About {{SITENAME}}|localhost/about?locale=en}}"

ru.json

"about": "{{link:{{SITENAME}}|localhost/about?locale=ru}}"

Обратите внимание, что магические слова могут быть вложенными, как показано в этом примере.

Документирование ваших переводов

Локализовать приложение сложно . Во многих случаях простого перевода сообщений недостаточно, так как вам необходимо знать их контекст. Поэтому переводы jQuery.i18n могут быть задокументированы . Обычно это делается внутри файла с именем qqq.json . Внутри вы предоставляете переводы ключей и их описания. Например:

i18n / qqq.json

{
  "@metadata": {
    "authors": [
      "Ilya"
    ]
  },
  "app-title": "App's title",
  "welcome": "Welcoming message. Should be friendly.",
  "letter": "Notification about letter from someone. Should be informal.",
  "letters": "Total letters count.",
  "copyright": "The name of the site (transliterated) and copyrights.",
  "about": "Link to About Us page. The URL should contain the proper locale."
}

Наличие такого файла действительно полезно.

Заключение

В этой статье мы увидели библиотеку jQuery.i18n от Фонда Викимедиа в действии. Мы обсудили все его основные функции и, надеюсь, к настоящему времени вы почувствуете себя более уверенно в его использовании. Конечно, есть и другие похожие (и не очень похожие) решения, так что вы можете попробовать их.

Я благодарю вас за то, что вы остались со мной и до скорой встречи!