Вступление


Большинство веб-разработчиков сталкивалось с задачей перевода веб-сайта на несколько языков. Миссия это достаточно простая, и решение, как правило, относится к рутине. Уверен, что многие согласятся с утверждением, что локализация – это скучная, некреативная часть проекта.

В этой статье я хотел бы вынести на обсуждение альтернативную модель перевода веб-сайтов. Если попытаться описать принцип в одном предложении, то это: 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:

  1. Клиент запрашивает адрес X;
  2. Если адрес X есть в кэше, то он немедленно возвращается из кэша;
  3. Если адреса X нет в кэше, то пограничный сервер делает запрос на оригинальный сайт, а затем возвращает ответ клиенту. В зависимости от заголовков в ответе оригинального сайта и правил, установленных на сайте, ресурс X теперь может быть помещен в кэш.



Поведение CDN-переводчика:

  1. Клиент запрашивает адрес X;
  2. Если адрес X есть в кэше, то он немедленно возвращается из кэша как есть;
  3. Если адреса X нет в кэше, то пограничный сервер делает запрос на оригинальный сайт, а затем применяет правила мутации – подменяет ссылки, заменяет переведенные тексты. В зависимости от заголовков в ответе оригинального сайта и правил, установленных на сайте, ресурс X может быть помещен в кэш.

Шаг 2b в деталях


Получив ответ от оригинального сайта, у пограничного сервера стоит задача, как его перевести. Предлагаемая тактика:

  1. Обратить внимание на заголовок Content-type. Если значение не входит в список поддерживаемых, то не пытаться трансформировать контент;
  2. Обратить внимание на размер ответа. Если размер выше установленной границы – не пытаться трансформировать контент;
  3. Начать парсинг и редактирование контента. Пример для HTML-страницы: пройтись по всем узлам DOM, у которых есть текстовый узел-потомок. Запросить в репозитории переведённый текст, передав как параметры исходный текст и контекст.
  4. Заменив необходимые куски контента, возвращаем результат пользователю. Если заголовки и правила позволяют, то кэшируем результат.

Репозиторий было бы логично реализовать как отдельностоящий 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)


  1. nikitasius
    14.03.2016 12:34
    +1

    Получается, у вас CloudFlare -> сервер переводчик -> главный сервер, и по сути переводчик проксирует и модифицирует контент для CloudFlare?
    Как решается проблема 2 часового кеша? ведь запросив hello.php через CF из штатов я получу английский текст, а запросив из франции, если кто-то запросил (с местных pop 'ов) с английской локалью, я получу английский вариант, вместо французского. А чистить кеш по АПИ — у них лимит на колво запросов.

    Для модификации контента на стороне nginx средствами nginx есть ngx_http_substitutions_filter_module, правда придется поиграть с location и переменными, чтобы логику реализовать. Я просто испольщую lang=ru или через cookies.


    1. dusterio
      14.03.2016 13:19

      Сервер-переводчик может стоять по обе стороны от CloudFlare, в общем-то.

      Разные языковые версии привязаны либо к разным доменам (domain.es, domain.ru и так далее), либо URI (/es/news, /ru/news и так далее) — поэтому описанной проблемы с кэшем не будет. Кэш всегда использует полный адрес как ID объекта. Другой адрес — другой объект.

      Указанный модуль для nginx работает несколько примитивно — он построчно считывает буфер и применяет regular expressions. Таким способом гибкий выбор места (селекторы) реализовать нельзя — текущая строка не знает о следующей строке. Ну и самое главное — сомневаюсь, что кто-то захочет 10 тысяч правил в конфигурацию NGINX добавлять ;-) Это решение слишком низкоуровневое


      1. nikitasius
        14.03.2016 14:12

        Сервер-переводчик может стоять по обе стороны от CloudFlare, в общем-то.

        Если перед — тогда какой смысл от CF? Галочки ради? Если после — смотрите мой коммент. Так что стоять-то может, только безтолку.

        либо URI (/es/news, /ru/news и так далее) — поэтому описанной проблемы с кэшем не будет

        Тогда какой вообще смысл от этой статьи?
        Вы рассматриваете пачку способов, рассуждаете в JS

        Поделиться ссылкой в социальных сетях также будет невозможно;

        Затем решение с CDN, читатель настроен на разрешение недостатков предыдущих вариантов. Почему там тоже не указали тот же самый минус?
        Ведь example.com/about будет выдавать всегда один и тот же язык из кеша CDN'а, так как адрес не изменен. А постановка серверов *перед CDN — это самый настоящий бред. Во-первых если надо скрыть адрес конечного сервера, то "переводчика" хватит, во-вторых — напоминает решение, когда человек не умеет пользоваться кешем и не понимает для чего нужен CDN.

        *отредактировал


        1. dusterio
          14.03.2016 15:44

          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 уникально идентифицирует конкретную страницу на конкретном языке.


          1. nikitasius
            14.03.2016 17:55

            Общий вопрос:

            Если перед – нет, не ради галочки, а чтобы пользователю меньше движений делать.

            Зачем проксировать траффик через CF, если можно сразу напрямую на переводчика? Нагрузки на канал и на сервер это не снизит (так как у нас или ИПшники CF, или ИПшник переводчика как allow).

            Частный вопрос:

            решил добавить www.site.es

            domain.es, domain.ru и так далее

            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 решение, а увеличение задержек.


            1. nikitasius
              14.03.2016 17:59

              перед — после, правлю опечатки.
              В общем — перед СДН — ОК. После СДН — это не решение и потеря преимущества.


              1. nikitasius
                14.03.2016 18:03

                CF — translater — server = OK
                translater — CF — server = NOT OK


                1. dusterio
                  15.03.2016 02:55

                  А что если CDN-переводчик сам неплохо кэширует? :-) кэширующий CDN – дело тривиальное, поэтому их так много сегодня.

                  Пользователи, которые любят CF и лояльны к нему, остаются как: CF – translator – origin, а кто-то кто нашёл сервис отдельно и не знает про CF может просто: translator – origin

                  Схема translator – CF – origin просто как пример была дана, как доказательство, что по большому счету – разницы то нет откуда вернулся HTML контент, его можно перевести. Это нерационально, но это гибкость дает


                  1. nikitasius
                    15.03.2016 13:57

                    Давайте по порядку:

                    А что если CDN-переводчик сам неплохо кэширует?

                    Теперь переводчик уже не сервер, а CDN сеть… вы определитесь!

                    Схема translator – CF – origin просто как пример была дана, как доказательство, что по большому счету – разницы то нет откуда вернулся HTML контент

                    Тогда удалите текст про CDN, так как он нужен здесь как козе баян.

                    Просто вдумаеться в сие безобразие: CF проксирует и кеширует ответы от origin на… translator, который раздает контент всем оставшимся.

                    Это использование CDN-кешируюшего сервера как просто кеширующего сервера, добавление в схему лишнего звена (раз CDN вам не нужен), когда для задачи (кеширование ответов и передачи на translator) хватит обычного nginx!

                    Ответьте мне, зачем вы в вашей статье используете термины CDN, когда зарубаете на корню саму идую и все преимущества CDN?


                    1. dusterio
                      15.03.2016 14:01

                      Потому что предложенная модель – это CDN, это content delivery network.

                      Имеется оригинальный сервер с контентом, имеется сеть пограничных серверов (с функцией перевода), которые контент распространяют.

                      CloudFlare – это CDN, который умеет оптимизировать картинки, минимизировать JavaScript, вставлять Google Analytics. Тут предлагается CDN, который умеет парсить HTML/JavaScript и заменять в нём тексты.

                      Цепочка CF<->переводчик упомянута только для подчеркивания гибкости, поддержки legacy клиентов и так далее. Это не является рекомендацией, просто это работает.

                      Термин использован правильно!


                      1. nikitasius
                        15.03.2016 14:09

                        Потому что предложенная модель – это CDN, это content delivery network.

                        При использовании схемы: сервер translator — CF — origin это больше не CDN. Это вредный совет, как убить преимушества CDN и раздавать данные клиентам с одного сервера.


                        1. dusterio
                          15.03.2016 15:52

                          Прошу учесть, что если, к примеру, Вы добавляете эстонский язык на сайте, то Вам не особо критично будет иметь глобальную сеть PoP от CloudFlare.

                          Я согласен, что это нерационально во многих случаях. Просто это будет работать, если кому-то уж сильно так надо :)


                          1. nikitasius
                            15.03.2016 16:15

                            Тогда убираем текст и отсылки к CDN. Текущий заголовок: "Альтернативный способ локализации веб-сайтов: мутирующий контент CDN". Можно изменить на "Альтернативный способ локализации веб-сайтов: мутирующий контент".

                            Потому, что в случае сервером переводчиком, который проксирует и модифицирует контент от CDN сети CF никаким CDN и не пахнет, а пахнет неумением использовать кеш.

                            Теперь если речь идет и вашей собственной сети CDN из 40+ серверов как минимум — где об этом инфа в статье, раз уж пиарите свой продукт? Где примеры конфигов?

                            Ничего этого в статье нету.

                            У вас даже не указан частный случай с доменными зонами, который в видео показывается. Чертовски желтый заголовок и влажная фантазия "прикручу ка я CDN в свой велосипед, и пусть все считают что тут CDN!".

                            Далее:

                            Я согласен, что это нерационально во многих случаях. Просто это будет работать, если кому-то уж сильно так надо :)

                            Резать преимущества CDN — это не нерационально, а это просто идиотизм. Если человеку надо перевести свой сайт на другой язык в другой доменной зоне, он это сделает до CDN. Так как у такого человека есть доступ к инфраструктуре проекта. Он же не последний придурок, чтобы проксировать ответ от сервера через CDN на сервер переводчик и снова на CDN, чтобы клиенты получили ответ.

                            добавляете эстонский язык на сайте, то Вам не особо критично будет иметь глобальную сеть PoP от CloudFlare.

                            Парень из Кореи добавил английский язык на свой сайт таким же макаром, вам из австралии не особо критично с огромным пингом?


  1. zirf
    14.03.2016 13:16

    К сожалению, все способы локализации, как их не автоматизируй, будут страдать одним недостатком — писать их придется на очень упрощенном языке, без идиом и специфических оборотов. Проблема в том, что значение отдельных слов зависит от контекста. То есть пойти дальше:
    welcome
    signup
    и чего то подобного пока все равно трудно, поэтому задача сложности перевода и не выходит за немудреные файлы словарей или базы. Речь даже не о переводе, а подстановке понятий.
    Отсюда возникает вторая мудреная грабля. Перевод, который даст сам API, кто сказал, что он будет заведомо качественным? А вот повлиять Вы на него уже просто так не сможете, без дополнительных затрат.


    1. dusterio
      14.03.2016 13:22

      Вы совершенно правы, но тут проблема частично решается предлагаемым параметром 'контекст'. Если 'welcome' попалось внутри меню — пусть это будет "Приветствие", а если внутри параграфа — хочу "Добро пожаловать". Это, вероятно, не универсальное решение, но уже шаг вперед.

      Перевод от API — за ним можно ставить онлайн-рынок переводчиков вроде Gengo, там качество контролируется, можно закреплять конкретных переводчиков, оспаривать тексты и так далее. Скорее всего, спустя месяц работы у большинства клиентов проблема отпадет — переводчики привыкнут к требованиям


  1. michael_vostrikov
    14.03.2016 14:55

    Запросить в репозитории переведённый текст, передав как параметры исходный текст и контекст.
    контекст было бы удобно задавать вроде URL:selector

    Навскидку такие минусы:
    — Верстка поменялась — половина переводов не работает.
    — Перевод фраз с переменными числами, разные словоформы для разных чисел — 2 files, 10 files / 2 файла, 10 файлов
    — Локализация дат — месяц-день-год или день-месяц-год
    — Перевод многозначных слов типа "on", "from" или "to". Можно каждое такое слово оборачивать в span с определенным классом, но это кажется не очень удобным решением.


    1. dusterio
      14.03.2016 15:36

      Это ожидаемые вопросы и они решаемые ;-)
      1) Если правильная конвенция по именам в CSS — верстка ничего ломать не будет. Если конвенции нет, то при смене дизайна или верстки достаточно в правилах адреса переписать.
      2) Во-первых, это вполне себе решается в данной модели. Во-вторых, таких сценариев достаточно немного и они повторяются от сайта к сайту — вероятно, можно сделать какое-то общее решение.
      3) Тоже самое, что в 2.
      4) Если подобные слова используются где-то по отдельности, не как часть фразы – это может быть проблемой. Но думаю, что можно найти рациональное решение.


      1. michael_vostrikov
        14.03.2016 16:47

        Таких сценариев больше, чем кажется. "Поле {fieldname} должно быть заполнено". Вместо {fieldname} может быть какое угодно название, при этом в верстке это просто одно строка текста. "Привет, {username}". "Привет" переводить надо, {username} нет. По пробелу разбивать?
        Это может подойти для простых сайтов с парой страниц, но не более. Для сложных проще управлять переводами на своем сервере, чем на удаленном CDN.


        1. dusterio
          14.03.2016 17:11

          Одно из рациональных решений, что приходит в голову – API получает N запросов на перевод строк, которые находятся в одном и том же месте и совпадают на процент X. Приложение понимает, что это, наверное, строка с переменной и предлагает пользователю быстро это подтвердить.

          С другой стороны, этот пост-рендерный взгляд на ресурс позволяет нормально переводить тексты из БД, а не из словарей. Скажем, у нас онлайн-магазин, и мы хотим показывать "Матрёшка" как "Babushka" на английском сайте. Большинство разработчиков сейчас добавят дополнительное значение где-то в хранилище данных, таким образом далее усложняя локализацию – теперь у нас часть переводов в файлах, часть в базах.

          А с точки зрения CDN – это обыкновенный текст, переводить и редактировать его можно вместе со всем остальным. Мне кажется, это достаточно удобно.


  1. kresh
    14.03.2016 16:50

    По моему, это очень крутая идея.


  1. t1gor
    15.03.2016 10:29

    минусы есть — вы отдаете переводы (а соответственно и ответственность за них) вендорам (читай — Api) и полностью теряете над ними контроль. А еще у проекта появляется еще одна зависимость. имхо


    1. dusterio
      15.03.2016 11:51

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

      Можно добавить на стороне CDN возможность выгрузки всех переводов в удобных форматах, чтобы уверить пользователя, что его не привязали. Захотел – сделал себе экспорт в текстовые файлы и ушёл.

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