Вступление
Большинство веб-разработчиков сталкивалось с задачей перевода веб-сайта на несколько языков. Миссия это достаточно простая, и решение, как правило, относится к рутине. Уверен, что многие согласятся с утверждением, что локализация – это скучная, некреативная часть проекта.
В этой статье я хотел бы вынести на обсуждение альтернативную модель перевода веб-сайтов. Если попытаться описать принцип в одном предложении, то это: 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 делает адаптивные картинки на лету.
Комментарии (22)
zirf
14.03.2016 13:16К сожалению, все способы локализации, как их не автоматизируй, будут страдать одним недостатком — писать их придется на очень упрощенном языке, без идиом и специфических оборотов. Проблема в том, что значение отдельных слов зависит от контекста. То есть пойти дальше:
welcome
signup
и чего то подобного пока все равно трудно, поэтому задача сложности перевода и не выходит за немудреные файлы словарей или базы. Речь даже не о переводе, а подстановке понятий.
Отсюда возникает вторая мудреная грабля. Перевод, который даст сам API, кто сказал, что он будет заведомо качественным? А вот повлиять Вы на него уже просто так не сможете, без дополнительных затрат.dusterio
14.03.2016 13:22Вы совершенно правы, но тут проблема частично решается предлагаемым параметром 'контекст'. Если 'welcome' попалось внутри меню — пусть это будет "Приветствие", а если внутри параграфа — хочу "Добро пожаловать". Это, вероятно, не универсальное решение, но уже шаг вперед.
Перевод от API — за ним можно ставить онлайн-рынок переводчиков вроде Gengo, там качество контролируется, можно закреплять конкретных переводчиков, оспаривать тексты и так далее. Скорее всего, спустя месяц работы у большинства клиентов проблема отпадет — переводчики привыкнут к требованиям
michael_vostrikov
14.03.2016 14:55Запросить в репозитории переведённый текст, передав как параметры исходный текст и контекст.
контекст было бы удобно задавать вроде URL:selector
Навскидку такие минусы:
— Верстка поменялась — половина переводов не работает.
— Перевод фраз с переменными числами, разные словоформы для разных чисел — 2 files, 10 files / 2 файла, 10 файлов
— Локализация дат — месяц-день-год или день-месяц-год
— Перевод многозначных слов типа "on", "from" или "to". Можно каждое такое слово оборачивать в span с определенным классом, но это кажется не очень удобным решением.dusterio
14.03.2016 15:36Это ожидаемые вопросы и они решаемые ;-)
1) Если правильная конвенция по именам в CSS — верстка ничего ломать не будет. Если конвенции нет, то при смене дизайна или верстки достаточно в правилах адреса переписать.
2) Во-первых, это вполне себе решается в данной модели. Во-вторых, таких сценариев достаточно немного и они повторяются от сайта к сайту — вероятно, можно сделать какое-то общее решение.
3) Тоже самое, что в 2.
4) Если подобные слова используются где-то по отдельности, не как часть фразы – это может быть проблемой. Но думаю, что можно найти рациональное решение.michael_vostrikov
14.03.2016 16:47Таких сценариев больше, чем кажется. "Поле {fieldname} должно быть заполнено". Вместо {fieldname} может быть какое угодно название, при этом в верстке это просто одно строка текста. "Привет, {username}". "Привет" переводить надо, {username} нет. По пробелу разбивать?
Это может подойти для простых сайтов с парой страниц, но не более. Для сложных проще управлять переводами на своем сервере, чем на удаленном CDN.dusterio
14.03.2016 17:11Одно из рациональных решений, что приходит в голову – API получает N запросов на перевод строк, которые находятся в одном и том же месте и совпадают на процент X. Приложение понимает, что это, наверное, строка с переменной и предлагает пользователю быстро это подтвердить.
С другой стороны, этот пост-рендерный взгляд на ресурс позволяет нормально переводить тексты из БД, а не из словарей. Скажем, у нас онлайн-магазин, и мы хотим показывать "Матрёшка" как "Babushka" на английском сайте. Большинство разработчиков сейчас добавят дополнительное значение где-то в хранилище данных, таким образом далее усложняя локализацию – теперь у нас часть переводов в файлах, часть в базах.
А с точки зрения CDN – это обыкновенный текст, переводить и редактировать его можно вместе со всем остальным. Мне кажется, это достаточно удобно.
t1gor
15.03.2016 10:29минусы есть — вы отдаете переводы (а соответственно и ответственность за них) вендорам (читай — Api) и полностью теряете над ними контроль. А еще у проекта появляется еще одна зависимость. имхо
dusterio
15.03.2016 11:51Согласен, но так можно сказать про любой SaaS вообще. Плата за удобство и гибкость – как раз вынесение за рамки приложения, это как две стороны одной монеты.
Можно добавить на стороне CDN возможность выгрузки всех переводов в удобных форматах, чтобы уверить пользователя, что его не привязали. Захотел – сделал себе экспорт в текстовые файлы и ушёл.
Контроль, кстати, не полностью теряется. Предполагается, что клиент может через панель управления контролировать все переводы, оспаривать варианты, требовать повышения качества и так далее. Клиент всегда прав, CDN просто пытается сделать его жизнь проще.
nikitasius
Получается, у вас CloudFlare -> сервер переводчик -> главный сервер, и по сути переводчик проксирует и модифицирует контент для CloudFlare?
Как решается проблема 2 часового кеша? ведь запросив hello.php через CF из штатов я получу английский текст, а запросив из франции, если кто-то запросил (с местных pop 'ов) с английской локалью, я получу английский вариант, вместо французского. А чистить кеш по АПИ — у них лимит на колво запросов.
Для модификации контента на стороне nginx средствами nginx есть ngx_http_substitutions_filter_module, правда придется поиграть с location и переменными, чтобы логику реализовать. Я просто испольщую lang=ru или через cookies.
dusterio
Сервер-переводчик может стоять по обе стороны от CloudFlare, в общем-то.
Разные языковые версии привязаны либо к разным доменам (domain.es, domain.ru и так далее), либо URI (/es/news, /ru/news и так далее) — поэтому описанной проблемы с кэшем не будет. Кэш всегда использует полный адрес как ID объекта. Другой адрес — другой объект.
Указанный модуль для nginx работает несколько примитивно — он построчно считывает буфер и применяет regular expressions. Таким способом гибкий выбор места (селекторы) реализовать нельзя — текущая строка не знает о следующей строке. Ну и самое главное — сомневаюсь, что кто-то захочет 10 тысяч правил в конфигурацию NGINX добавлять ;-) Это решение слишком низкоуровневое
nikitasius
Если перед — тогда какой смысл от CF? Галочки ради? Если после — смотрите мой коммент. Так что стоять-то может, только безтолку.
Тогда какой вообще смысл от этой статьи?
Вы рассматриваете пачку способов, рассуждаете в JS
Затем решение с CDN, читатель настроен на разрешение недостатков предыдущих вариантов. Почему там тоже не указали тот же самый минус?
Ведь example.com/about будет выдавать всегда один и тот же язык из кеша CDN'а, так как адрес не изменен. А постановка серверов *перед CDN — это самый настоящий бред. Во-первых если надо скрыть адрес конечного сервера, то "переводчика" хватит, во-вторых — напоминает решение, когда человек не умеет пользоваться кешем и не понимает для чего нужен CDN.
*отредактировал
dusterio
1) Если перед – нет, не ради галочки, а чтобы пользователю меньше движений делать. Представим кейс, что есть веб-сайт http://direct.site.ru, есть обёрнутый в CF https://www.site.ru. Пользователь решил добавить https://www.site.es, у него стоит выбор – выбрать как origin direct.site.ru или www.site.ru. Оба сайта (напрямую и через CF) возвращают один и тот же контент за исключением случаев, когда оригинальный сайт лежит (тут CF может показать свою красивую ошибку). Таким образом, разница сравнительно небольшая – переводить ответ от оригинального сервера или переводить ответ от edge-точки CF.
2) Смысл статьи – предложить новый метод локализации, который сейчас практически не используется вообще никем.
3) Тут Вы неправильно немного поняли концепцию, и частично виноват я сам. У каждого переведенного сайта отличаются адреса всех страниц. На выбор пользователя – это может быть домен, может быть URI.
Неважно куда приходит запрос на, скажем, site.es/index – на CloudFlare, CloudFont, CDN77 или мой собственный edge-сервер. Адрес site.es/index уникально идентифицирует конкретную страницу на конкретном языке.
nikitasius
Общий вопрос:
Зачем проксировать траффик через CF, если можно сразу напрямую на переводчика? Нагрузки на канал и на сервер это не снизит (так как у нас или ИПшники CF, или ИПшник переводчика как allow).
Частный вопрос:
CF -> конечный сервер который отдает RU для RU домена и ES для ES домена. Классическая схема. Если нельзя никак вставить косыль переводов в конечный сервер, то переводчик нужно разместить между CF и конечным сервером (когда переводчик проксирует ответы на CF). Иначе теряется весь смысл CDN.
Но этим вы америку не открыли, а это просто обычная схема использовалия CF.
Но это все работает у вас в случае разных доменных зон или поддоменов (ru.example.com, es.example.com).
Но в случае одно единственного домена (example.com) нельзя стабильно отдавать двум пользователям кешируемый контент на 2х разных языках через GET запрос (POST'ы не кешируют) в CF для одной и той же страницы. И если такое удалось — это все дело в pop'ах (точки забора контента) и юзерам проксировался ответ, забранный через разные сервера.
Это CDN. А сервер *после CDN — это не CDN решение, а увеличение задержек.
nikitasius
перед — после, правлю опечатки.
В общем — перед СДН — ОК. После СДН — это не решение и потеря преимущества.
nikitasius
CF — translater — server = OK
translater — CF — server = NOT OK
dusterio
А что если CDN-переводчик сам неплохо кэширует? :-) кэширующий CDN – дело тривиальное, поэтому их так много сегодня.
Пользователи, которые любят CF и лояльны к нему, остаются как: CF – translator – origin, а кто-то кто нашёл сервис отдельно и не знает про CF может просто: translator – origin
Схема translator – CF – origin просто как пример была дана, как доказательство, что по большому счету – разницы то нет откуда вернулся HTML контент, его можно перевести. Это нерационально, но это гибкость дает
nikitasius
Давайте по порядку:
Теперь переводчик уже не сервер, а CDN сеть… вы определитесь!
Тогда удалите текст про CDN, так как он нужен здесь как козе баян.
Просто вдумаеться в сие безобразие: CF проксирует и кеширует ответы от origin на… translator, который раздает контент всем оставшимся.
Это использование CDN-кешируюшего сервера как просто кеширующего сервера, добавление в схему лишнего звена (раз CDN вам не нужен), когда для задачи (кеширование ответов и передачи на translator) хватит обычного nginx!
Ответьте мне, зачем вы в вашей статье используете термины CDN, когда зарубаете на корню саму идую и все преимущества CDN?
dusterio
Потому что предложенная модель – это CDN, это content delivery network.
Имеется оригинальный сервер с контентом, имеется сеть пограничных серверов (с функцией перевода), которые контент распространяют.
CloudFlare – это CDN, который умеет оптимизировать картинки, минимизировать JavaScript, вставлять Google Analytics. Тут предлагается CDN, который умеет парсить HTML/JavaScript и заменять в нём тексты.
Цепочка CF<->переводчик упомянута только для подчеркивания гибкости, поддержки legacy клиентов и так далее. Это не является рекомендацией, просто это работает.
Термин использован правильно!
nikitasius
При использовании схемы: сервер translator — CF — origin это больше не CDN. Это вредный совет, как убить преимушества CDN и раздавать данные клиентам с одного сервера.
dusterio
Прошу учесть, что если, к примеру, Вы добавляете эстонский язык на сайте, то Вам не особо критично будет иметь глобальную сеть PoP от CloudFlare.
Я согласен, что это нерационально во многих случаях. Просто это будет работать, если кому-то уж сильно так надо :)
nikitasius
Тогда убираем текст и отсылки к CDN. Текущий заголовок: "Альтернативный способ локализации веб-сайтов: мутирующий контент CDN". Можно изменить на "Альтернативный способ локализации веб-сайтов: мутирующий контент".
Потому, что в случае сервером переводчиком, который проксирует и модифицирует контент от CDN сети CF никаким CDN и не пахнет, а пахнет неумением использовать кеш.
Теперь если речь идет и вашей собственной сети CDN из 40+ серверов как минимум — где об этом инфа в статье, раз уж пиарите свой продукт? Где примеры конфигов?
Ничего этого в статье нету.
У вас даже не указан частный случай с доменными зонами, который в видео показывается. Чертовски желтый заголовок и влажная фантазия "прикручу ка я CDN в свой велосипед, и пусть все считают что тут CDN!".
Далее:
Резать преимущества CDN — это не нерационально, а это просто идиотизм. Если человеку надо перевести свой сайт на другой язык в другой доменной зоне, он это сделает до CDN. Так как у такого человека есть доступ к инфраструктуре проекта. Он же не последний придурок, чтобы проксировать ответ от сервера через CDN на сервер переводчик и снова на CDN, чтобы клиенты получили ответ.
Парень из Кореи добавил английский язык на свой сайт таким же макаром, вам из австралии не особо критично с огромным пингом?