Джон Резиг об интернационализации JavaScript-приложений

в 12:19, , рубрики: express, i18n, javascript, node.js, Веб-разработка, интернационализация, интерфейсы, локализация интерфейса

Джон Резиг об интернационализации JavaScript приложенийНедавно мне пришлось заниматься интернационализацией веб-приложения на Node.js+Express, над которым я сейчас работаю, и, как мне кажется, получилось довольно неплохо (иностранные пользователи очень довольны, и я вижу заметный приток трафика из неанглоязычных стран). Стратегия интернационализации, которую я опишу, не слишком сильно завязана на Node и может подойти любому веб-приложению.

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

  • Полное равенство между языками. Один и тот же контент должен быть доступен всем.
  • Разные языковые версии должны содержаться в поддоменах. Использовать отдельные домены верхнего уровня для разных языков — чересчур дорого, да и не нужно. Кроме того, так удобнее переключаться между языками, редактируя адресную строку.
  • Никакого автоматического перевода. Нет ничего хуже, чем наткнуться на нечитаемую версию сайта из-за того, что сервер подставил кривой машинный перевод, руководствуясь вашим IP или языковыми настройками компьютера. Один и тот же URL всегда должен указывать на одну и ту же языковую версию.
  • Никаких автоматических редиректов на национальную версию сайта. Примерно по тем же причинам, что в предыдущем пункте — если я захожу на foo.com, не надо меня направлять на es.foo.com только потому что сервер решил, что я из Испании. Вместо этого лучше показать уведомление на (предположительно) моём родном языке и предложить вручную перейти на национальную версию.

В конце концов должна получиться примерно такая структура:

  • domain.com — основная (английская) версия;
  • ja.domain.com — японская версия;
  • XX.domain.com — другие языки.

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

  • domain.com/search?q=mountain
  • ja.domain.com/search?q=mountain

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

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

Джон Резиг об интернационализации JavaScript приложений Джон Резиг об интернационализации JavaScript приложений

Кроме того, можно использовать технику rel=«alternate» hreflang=«x», чтобы помочь поисковику Google лучше понять структуру моего сайта. Если я помещу эту строку в заголовок страницы, Google покажет локализованную версию в результатах поиска.

<link rel="alternate" hreflang="ja"
    href="http://ja.domain.com/" />

Серверная часть

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

Джон Резиг об интернационализации JavaScript приложений

Как оказалось, реализовать это не так уж легко. Самый простой способ — посмотреть в поле Accepted language заголовка запроса, но это работает только если ваша страница полностью динамическая и никогда не кешируется.

Если же нет, то нужно учитывать, как и где подключается кеш.

В моем случае я использовал Nginx перед несколькими серверами Node/Express. А значит, кешировалось всё, что выдавал сервер приложения на Node, включая и уведомления с предложением переключить язык.

В результате приходится управлять этим уведомлением на стороне клиента. И тут нас ждет ещё одно препятствие: на клиенте средствами DOM/JavaScript невозможно надёжно определить нужный язык.

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

Для этого я использовал модуль Nginx AcceptedLanguage. Я установил cookie с указанием нужного языка и передал его клиенту. Вот как выглядела конфигурация Nginx:

set_from_accept_language $lang en ja;
add_header Set-Cookie lang=$lang;

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

Таким образом Nginx продолжает агрессивно кешировать весь контент, а на клиенте корректно отображается сообщение с предложением посетить локализованную версию страницы.

Логика интернационализации

Я написал свой модуль Node.js для интернационализации. Он использует следующую стратегию:

  • Все переводы хранятся в файлах в формате JSON;
  • Эти файлы используются для перевода на лету в момент использования;
  • Перевод осуществляется общепринятым способом, через вызов __("Some string"). Если перевод существует, строка "Some string" заменяется на перевод, если нет, используется напрямую.

Так как ограниченный набор серверов обрабатывает множество запросов одновременно, логика интернационализации не может быть общей, она должна работать для каждого запроса отдельно. Я встречал решения, например i18n-node, которые предполагают, что сервер одновременно будет выдавать страницы только на одном языке. На практике это не работает, особенно в асинхронном мире Node.js.

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

Джон Резиг об интернационализации JavaScript приложений

Как минимум, надо убедиться, что информация о предпочтительном языке не теряется в время выполнения текущего запроса (в моём модуле это реализовано).

На практике это значит, что необходимо добавить свойство i18n к объекту request, точно так же, как обычно добавляется middleware в Express:

app.use(function(req, res, next) {
	req.i18n = new i18n(/* options... */);
	next();
});

Организация работы

Логика интернационализации ведёт себя по-разному в режиме разработки и в продакшене.

В режиме разработки:

  • Файлы переводов загружаются при каждм запросе;
  • Файлы переводов обновляются динамически после любых изменений;
  • Показываются предупреждения и отладочные сообщения.

В продакшене:

  • Все файлы переводов кешируются;
  • Файлы никогда не обновляются динамически;
  • Предупреждения и отладочные сообщения не показываются.

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

Организация кода

Строки, нуждающиеся в переводе, могут оказаться где угодно: в коде приложения, шаблонах и даже (не дай бог!) в стилях.

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

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

Единственное исключение в моём приложении, когда в коде содержатся переводимые строки — это код представлений Express, где я использую привязанный к запросу объект i18n:

module.exports = {
  index: function(req, res) {
    req.render("index", {
      title: req.i18n.__("My Site Title"),
      desc: req.i18n.__("My Site Description")
    });
  }
};

Я использую шаблонизатор swig, но техника перевода будет примерно одинаковой для любых шаблонизаторов:

{% extends "page.swig" %}

{% block content %}
<h1>{{ __("Welcome to:") }} {{ title }}</h1>
<p>{{ desc }}</p>
{% endblock %}

Строка, обёрнутая в вызов __(...) заменяется на перевод.

Собственно перевод

Пока что я сам перевожу своё приложение, не прибегая к помощи сторонних переводчиков. Приложение пока очень маленькое, там всего несколько десятков строк для перевода.

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

  • Используйте готовые переводы из открытых проектов. Есть множество открытых проектов и фреймворков, содержащих профессиональные переводы элементов интерфейса. К примеру, фреймворк Drupal содержит множество таких переводов в удобном формате. Я нашёл там несколько готовых фраз.
  • Присмотритесь к похожим многоязычным проектам. Если есть качественные примеры многоязычных сайтов, реализующих похожий на ваш функционал, вы можете брать перевод оттуда. Моё текущее приложение — поисковый движок, так что многие строки со страниц Google в точности соответствовали моим.
  • Google Translate. Да, да, я знаю о чём вы сейчас подумали, но я очень впечатлён прогрессом Google Translate в последнее время, особенно при переводе отдельных слов и выражений. Теперь там появились даже разные версии перевода с указанием их вероятности, что очень неплохо.

Джон Резиг об интернационализации JavaScript приложений

Выводы

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

Автор: ilya42

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js