- PVSM.RU - https://www.pvsm.ru -
Недавно мне пришлось заниматься интернационализацией веб-приложения на Node.js+Express, над которым я сейчас работаю, и, как мне кажется, получилось довольно неплохо (иностранные пользователи очень довольны, и я вижу заметный приток трафика из неанглоязычных стран). Стратегия интернационализации, которую я опишу, не слишком сильно завязана на Node и может подойти любому веб-приложению.
Мне часто приходилось пользоваться многоязычными сайтами или заходить на англоязычные сайты из разных стран мира, так что я хорошо представлял, каким требованиям должна удовлетворять интернационализация:
В конце концов должна получиться примерно такая структура:
Равенство между языками и использование поддоменов дают возможность переключиться между языками любой страницы просто добавив национальный поддомен:
Обе страницы должны работать и выглядеть одинаково за исключением того, что вторая будет на японском языке.
В шапке страницы я легко могу поставить ссылку на такую же страницу на другом языке:
Кроме того, можно использовать технику rel=«alternate» hreflang=«x» [1], чтобы помочь поисковику Google лучше понять структуру моего сайта. Если я помещу эту строку в заголовок страницы, Google покажет локализованную версию в результатах поиска.
<link rel="alternate" hreflang="ja"
href="http://ja.domain.com/" />
Очень важно помочь пользователю найти нужный ему контент, особенно если отказаться от автоматического перевода или редиректа. Ссылки на альтернативные языки в шапке сайта — это уже хорошо, но ещё лучше вывести вверху страницы уведомление, приглашающее пользователя переключиться на его родной язык (и здесь попытка автоматически угадать язык вполне уместна):
Как оказалось, реализовать это не так уж легко. Самый простой способ — посмотреть в поле Accepted language [2] заголовка запроса, но это работает только если ваша страница полностью динамическая и никогда не кешируется.
Если же нет, то нужно учитывать, как и где подключается кеш.
В моем случае я использовал Nginx перед несколькими серверами Node/Express. А значит, кешировалось всё, что выдавал сервер приложения на Node, включая и уведомления с предложением переключить язык.
В результате приходится управлять этим уведомлением на стороне клиента. И тут нас ждет ещё одно препятствие: на клиенте средствами DOM/JavaScript невозможно надёжно определить нужный язык.
Поэтому нам нужно, чтобы сервер дополнительно сообщил приложению о желательном языке.
Для этого я использовал модуль Nginx AcceptedLanguage [3]. Я установил cookie с указанием нужного языка и передал его клиенту. Вот как выглядела конфигурация Nginx:
set_from_accept_language $lang en ja;
add_header Set-Cookie lang=$lang;
Теперь всё, что мне оставалось сделать в клиентской части — это прочитать cookie и отобразить уведомление, если желаемый язык не совпадал с языком страницы.
Таким образом Nginx продолжает агрессивно кешировать весь контент, а на клиенте корректно отображается сообщение с предложением посетить локализованную версию страницы.
Я написал свой модуль Node.js [4] для интернационализации. Он использует следующую стратегию:
__("Some string")
. Если перевод существует, строка "Some string"
заменяется на перевод, если нет, используется напрямую.Так как ограниченный набор серверов обрабатывает множество запросов одновременно, логика интернационализации не может быть общей, она должна работать для каждого запроса отдельно. Я встречал решения, например i18n-node [5], которые предполагают, что сервер одновременно будет выдавать страницы только на одном языке. На практике это не работает, особенно в асинхронном мире Node.js.
Если пришедший запрос устанавливает предпочитаемый язык для общего объекта интернационализации, это может привести к тому, что сервер выдаст в ответ на другой запрос некорректную языковую версию:
Как минимум, надо убедиться, что информация о предпочтительном языке не теряется в время выполнения текущего запроса (в моём модуле это реализовано).
На практике это значит, что необходимо добавить свойство i18n
к объекту request
, точно так же, как обычно добавляется middleware в Express:
app.use(function(req, res, next) {
req.i18n = new i18n(/* options... */);
next();
});
Логика интернационализации ведёт себя по-разному в режиме разработки и в продакшене.
В режиме разработки:
В продакшене:
Два ключевых отличия — динамическая загрузка и обновление переводов. Во время разработки полезно обновлять их при каждом изменении, чтобы видеть результат своих действий, и не забыть перевести ни одной строки. В продакшене стоит считать их статическими файлами — дёргать диск при каждом запросе глупо.
Строки, нуждающиеся в переводе, могут оказаться где угодно: в коде приложения, шаблонах и даже (не дай бог!) в стилях.
В моём приложении я сразу позаботился о том, чтобы в JavaScript и CSS не было никаких текстовых строк. Если бы мне понадобилось динамически генерировать в приложении какой-то текст, я бы сделал это через шаблоны, используя что-то вроде моего микро-шаблонизатора [6].
Я считаю очень важным избегать попадания любых строк, подлежащих переводу, в файлы 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 [7], но техника перевода будет примерно одинаковой для любых шаблонизаторов:
{% extends "page.swig" %}
{% block content %}
<h1>{{ __("Welcome to:") }} {{ title }}</h1>
<p>{{ desc }}</p>
{% endblock %}
Строка, обёрнутая в вызов __(...)
заменяется на перевод.
Пока что я сам перевожу своё приложение, не прибегая к помощи сторонних переводчиков. Приложение пока очень маленькое, там всего несколько десятков строк для перевода.
Я могу поделиться несколькими хитростями, которые могут помочь подольше продержаться без привлечения профессионального переводчика:
Я работаю над интернационализацией сайта всего пару недель, так что наверняка многое ещё поменяется, когда сайт вырастет. Тем не менее, перевод уже дал ощутимые результаты, посещений стало заметно больше, во многом благодаря тому, что Google проиндексировал локализованные страницы. Мой модуль интернационализации служит мне хорошим подспорьем, и надеюсь, сможет облегчить работу по переводу и другим.
Автор: ilya42
Источник [9]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/javascript/25053
Ссылки в тексте:
[1] rel=«alternate» hreflang=«x»: http://support.google.com/webmasters/bin/answer.py?hl=en&answer=189077
[2] Accepted language: http://www.w3.org/International/questions/qa-accept-lang-locales
[3] AcceptedLanguage: http://wiki.nginx.org/AcceptLanguageModule
[4] модуль Node.js: http://ejohn.org/blog/i18n-module-for-node-and-express-js/
[5] i18n-node: https://github.com/mashpie/i18n-node
[6] моего микро-шаблонизатора: http://ejohn.org/blog/javascript-micro-templating/
[7] swig: http://paularmstrong.github.com/swig/
[8] Drupal содержит: http://localize.drupal.org/translate/languages
[9] Источник: http://habrahabr.ru/post/166127/
Нажмите здесь для печати.