Вступление
Большинство веб-разработчиков сталкивалось с задачей перевода веб-сайта на несколько языков. Миссия это достаточно простая, и решение, как правило, относится к рутине. Уверен, что многие согласятся с утверждением, что локализация – это скучная, некреативная часть проекта.
В этой статье я хотел бы вынести на обсуждение альтернативную модель перевода веб-сайтов. Если попытаться описать принцип в одном предложении, то это: CDN, который переводит контент между пользователем и оригинальным источником.
Необходимость в переводах
Сомневаюсь, что пользу от многоязычности ресурса стоит доказывать, но, тем не менее, посвящаю этому один маленький параграф.
Любой Интернет-сайт доступен трем миллиардам пользователей на планете по умолчанию – просто потому что ваш сайт в Интернете. Если вы что-то продаете на сайте, то простым добавлением языка вы фактически выходите на новый рынок. Как минимум, вы захотите иметь версию на местном языке (языке территории, где вы ведете бизнес) и английскую версию, ведь английский язык – это половина Интернет-контента по данным W3Techs.
Существующие способы
Файлы с переводами
Варианты имеются разные – от специальных форматов вроде GNU gettext, до простых текстовых файлов, которые ваш текущий framework может использовать. Результат примерно одинаковый: в момент вывода текста вызывается функция, которая проверит наличие перевода в словаре.
Пример на PHP:
// gettext:
echo _("Привет, мир!");
// Фреймворк Laravel 5
echo trans('common.hello_world');
Плюсы способа:
- Проверенный десятилетиями метод (читай привычный);
- Возможность достаточно просто отдавать отдельные файлы сторонним переводчикам;
- Маленькое влияние на код.
Минусы способа:
- Как правило отсутствует возможность внесения немедленных изменений;
- gettext-словари нужно компилировать, а конечные файлы коммиттить в репозиторий;
- Относительно неудобное и медленное управление текстами, чем больше проект — тем больше файлов и запутаннее иерархия;
- Нет стандартных механизмов для работы над переводами командой переводчиков;
- Как правило, используется две схемы параллельно – переводы для backend и переводы для frontend (JavaScript);
- Время от времени в текстах для переводов появляются HTML-тэги, потому что программистам было неудобно их выдергивать.
Переводы в базе данных
В отличии от первого способа, переводы хранятся в базе данных проекта, что позволяет вносить корректировки на ходу. Как правило, разработчики проекта делают для администраторов панель управления переводами, на что уходит дополнительное время.
Плюсы способа:
- Легче организовать работу команд;
- Возможность внесения немедленных изменений.
Минусы способа:
- Сложнее отдавать разделы на перевод сторонним переводчикам;
- Frontend тексты переводятся всё ещё отдельно от backend текстов;
- Время от времени в текстах для переводов появляются HTML-тэги, потому что программистам было неудобно их выдергивать.
Перевод на стороне пользователя через JavaScript
Сравнительно новый способ, предлагается сейчас несколькими западными стартапами. От вас требуется только добавить ссылку на внешний JavaScript-файл, который начнет подменять тексты в DOM на основе предоставленных (или утвержденных) заранее переводов.
Плюсы способа:
- Простая установка практически без необходимости программирования;
- Frontend и backend переводятся одновременно из одного репозитория переводов;
- В репозитории текстов не будет HTML-тэгов, потому что все тексты обрабатывались post factum из DOM.
Минусы способа:
- Поисковые системы не увидят дополнительные языки;
- Поделиться ссылкой в социальных сетях также будет невозможно;
- Дополнительная сетевая нагрузка (читай риски задержек) при открытии сайта.
CDN-переводчик
Собственно, то, что выносится на обсуждение в данной статье. А что если между пользователем и сайтам вставить “прослойку” – пограничный сервер, способный переводить web-контент? Сервисы вроде CloudFlare уже умеют минимально мутировать клиентские страницы — добавлять код Google Analytics, к примеру. Что если сделать шаг дальше и позволить пользователю подменять тексты и ссылки?
Поведение традиционного CDN:
- Клиент запрашивает адрес X;
- Если адрес X есть в кэше, то он немедленно возвращается из кэша;
- Если адреса X нет в кэше, то пограничный сервер делает запрос на оригинальный сайт, а затем возвращает ответ клиенту. В зависимости от заголовков в ответе оригинального сайта и правил, установленных на сайте, ресурс X теперь может быть помещен в кэш.
Поведение CDN-переводчика:
- Клиент запрашивает адрес X;
- Если адрес X есть в кэше, то он немедленно возвращается из кэша как есть;
- Если адреса X нет в кэше, то пограничный сервер делает запрос на оригинальный сайт, а затем применяет правила мутации – подменяет ссылки, заменяет переведенные тексты. В зависимости от заголовков в ответе оригинального сайта и правил, установленных на сайте, ресурс X может быть помещен в кэш.
Шаг 2b в деталях
Получив ответ от оригинального сайта, у пограничного сервера стоит задача, как его перевести. Предлагаемая тактика:
- Обратить внимание на заголовок Content-type. Если значение не входит в список поддерживаемых, то не пытаться трансформировать контент;
- Обратить внимание на размер ответа. Если размер выше установленной границы – не пытаться трансформировать контент;
- Начать парсинг и редактирование контента. Пример для HTML-страницы: пройтись по всем узлам DOM, у которых есть текстовый узел-потомок. Запросить в репозитории переведённый текст, передав как параметры исходный текст и контекст.
- Заменив необходимые куски контента, возвращаем результат пользователю. Если заголовки и правила позволяют, то кэшируем результат.
Репозиторий было бы логично реализовать как отдельностоящий RESTful API, а контекст было бы удобно задавать вроде URL:selector. К примеру, хотим переводить слово “Main page” как “Главная” в любом блоке любой страницы начинающейся на /news, получаем контекст “/news*:head”. Мир настолько привык к селекторам в стиле CSS/jQuery, что начать работать с таким синтаксисом сможет практически любой разработчик с ходу.
Так как пограничный сервер обращается за переводом в API репозитория, то совершенно логичным становится реализация SDK и пакетов под популярные языки и фреймворки. Владельцам веб-сайтов дается выбор – можно переводить контент через CDN, можно через наш класс в существующем коде.
Предположим, что у нас приложение на PHP и используется фреймворк Laravel. Реализовать legacy-поддержку тривиально – пере-объявляем функцию-помощник trans(), заменяем её своей реализацией, где поиск идёт не в локальных текстовых файлах, а в удалённом API. Чтобы избежать задержек при каждом запросе, используем кэш или отдельный процесс-proxy.
Подобным образом, можем менять содержимое объектов JavaScript, графические изображения и так далее.
Плюсы способа:
- Полная абстракция приложения и переводов – приложение вообще не знает о наличии других языковых версий. Программисты спокойно работают над основным продуктом;
- Backend и frontend-контент переводится одновременно, используя один репозиторий переводов;
- Можно достаточно просто переводить графические изображения;
- Очень просто запускать переведенные версии сайта на других (отдельных) доменах;
- Совместимость с любым существующим сервисом CDN. Можно выстраивать в цепочку;
- Совместимость с поисковыми системами и социальными сетями;
- В репозитории текстов не будет HTML-тэгов, потому что все тексты обрабатывались post factum из DOM;
- Легко организовать работу команд.
Минусы способа:
- Мне не удалось найти, но буду очень рад помощи в этом!
Видео YouTube
Чтобы доходчиво объяснить концепцию, я снял очень короткий видео-ролик, который показывает мой прототип такой системы переводов. Нарратив на английском, но я добавил русские субтитры.
Реализация
Я уже проверил реализуемость и практичность предложенного метода – написал примитивный вариант пограничного приложения на PHP и Lumen.
Мой метод, получающий от пользователя запрос и возвращающий переведенный ответ:
/**
* @param Request $request
* @param WebClientInterface $crawler
* @param MutatorInterface $mutator
* @param TranslatorInterface $translator
* @return Response
*/
public function show(Request $request, WebClientInterface $crawler, MutatorInterface $mutator, TranslatorInterface $translator)
{
$url = $request->client['origin'] . parse_url($request->url(), PHP_URL_PATH);
$response = $crawler->makeRequest($request->getMethod(), $url);
if ($response === false) abort(502);
$mutator->initWithWebRequest($response);
if ($response->isTranslatable()) $mutator->translateText($translator);
if ($response->isCacheable()) $mutator->cache(60);
$mutator->replaceLinks($request->client['origin'], $request->getSchemeAndHttpHost());
return (new Response($mutator->getBody(), $mutator->getStatusCode()))
->withHeaders($mutator->getHeaders());
}
Уверен, что многие начнут сомневаться в парадигме из-за нагрузки на процессор – ведь тот же nginx потому и не хочет никак мутировать содержимое ответов, что это очень негативно отразилось бы на производительности. Вообще, переводить вот так, post factum – это, безусловно, дороже с точки зрения ресурсов.
Мои аргументы здесь следующие. Мы наблюдаем постоянное удешевление IT-ресурсов в течение последних 5-10 лет, наступила эпоха серверов за 5 долларов – для многих сайтов не так уж и страшно немного повысить нагрузку. Во-вторых, если я все-таки займусь этим проектом, то оптимизация производительности будет одним из приоритетных направлений. Наверняка, можно найти много мест для улучшений!
Заключение
Индустрия всегда движется в сторону оптимизации, повышения комфорта и экономии средств. Считаю, что предложенный способ локализации веб-приложений вполне вероятно может стать основным через 5-10 лет.
Более того, у CDN, как у структуры, могут появляться всё новые и новые применения. CloudFlare предложил миру защиту от DDoS, Imgix делает адаптивные картинки на лету.
Автор: dusterio