Аккордеон про архитектуру и локализацию
За последние полгода было много статей о том, как написать крутой фреймворк объемом ~100 строк. Подкатом история о том, как написать ~2000 строк и ни одного фреймворка.
История вопроса
Перед тем, как перейти к технической части, хотелось бы ознакомиться с историей вопроса. На вопрос «зачем?», хороший ответ даёт вот эта статья. О том, что такое локализация и интернализация, рассказывал Антон Немцев на DUMP`е в Екатеринбурге. Александр Герасимов делал доклад на тему типографики в Минске на Frontend Dev Conf. Александр Тевосян из Badoo говорил об особенностях верстки многоязычных сайтов на MoscowJS, а его коллега, Глеб Дейкало, писал о многоступенчатом переводе. О том, как писать сами тексты так, чтобы их было легко переводить уже шла речь в докладе Кристины Ярошевич на Гипербатон`е в рамках конференции Translation Forum Russia, кроме того, на схожую тему писал Павел Доронин.
На Хабре можно найти очень много статей на различные темы, связанные с локализацией. Крупные компании типа Alconost или ABBYY пишут их пачками.
Вот несколько особо полезных
Статьи и доклады, описанные выше, отвечают на многие вопросы. Но вопрос по организации кода описан лишь частично. И таки да, в своей книге «Сюрреализм на JavaScript» я частично уже описывал логику локализации и её работу с DOM. Но настало время написать ещё несколько слов о коде и его общей структуре.
Введение
Почему на клиенте?
Тут все зависит от задачи, которую мы решаем. Если речь идет о сайте — то тут лучше сервер. Если это приложение, то лучше развернуть инфраструктуру на клиенте. С одной стороны, клиент может работать в режиме оффлайн. С другой стороны, мы сократим запросы к серверу, если клиенты будут использовать локальные файлы. Кроме того, иногда у нас нет сервера, или время работы серверного разработчика стоит так дорого, что тратить его в рамках этой задачи не имеет смысла.
Почему на JavaScript?
Как фронтенд разработчики, мы ещё можем выбрать HTML или CSS. Как использовать HTML, лично я не представляю. Как заставить работать CSS в рамках задачи локализации писал Антон Лунев (http://habrahabr.ru/post/121075/). Но и в первом, и во втором случае, у нас теряется гибкость и возможности, которые нам предоставляет JavaScript.
Что есть из готового?
Не считая различных функций в 30 строк со stackoverflow, и внутренних инструментов ребят из Badoo, Яндекса, Alconost и т.п., на данный момент есть несколько вполне сносных решений:
Аккордеон про хранение переводов
У нас есть переменные, в которые записан перевод фраз для конкретного языка. Далее во всем приложение вместо использования самих фраз, подставляются соответствующие переменные. Таким образом, изменив значение переменных, мы можем изменить язык всего приложения.
Например, в игре «Жмурики» ребята взяли и записали переводы всех фраз в отдельные переменные (ПЕРЕМЕННЫЕ КАРЛ! Хорошо, что хоть не в глобальные!). Работа с переменными может хорошо оправдать себя в двух случая:
- у нас есть жесткие ограничения по железу (например, мы программируем какие-нибудь дешевые видео приставки);
- у нас жеская система сертификации (например, мы пишем софт для игровых автоматов);
В-первом случае у нас урезаны ресурсы, и приходится жестко экономить на всем, чтобы уложиться в ограничения по памяти. Развернуть хорошую логическую архитектуру в таких условиях не всегда возможно.
Во-втором случае мы пытаемся исключить даже саму возможность случайно переключить параметры. Если посмотреть на те же игровые автоматы, то все вероятности проигрыша жестко регламентированны законодательством конкретной страны. Поэтому случай, когда в России стоят автоматы для США, а в Болгарии — автоматы для Венгрии, исключен.
Игра «Жмурики» работает в вконтакте и не попадает ни под одно ограничение, оговоренное выше. Своим примером она наглядно показывает, что качество кода и архитектуры системы локализации не играет вообще никакой роли. Да, возможно, кому-то будет неудобно рефакторить код или добавлять новые фичи. Да, возможно, такой подход приведет к большему количеству багов. Но факт есть факт, продукт: в продакшене, приносит деньги, всех устраивает.
Хранить каждую фразу в новой переменной не очень удобно, да и сами переводы обычно выносят в отдельный файл конфигурации. Поэтому разработчики большинства систем локализации предпочитают использовать популярные форматы хранения информации, такие как JSON и XML. Большинство Java и Python разработчиков обычно предпочитает работать с XML, а фронтендеры с JSON. Тут уж кто к чему привык. Стандартные форматы позволяют легко использовать одни и те же переводы совершенно разным системам локализации, как в контексте одного языка программирования, так и в контексте абсолютно разных языков.
Есть довольно большое количество людей, которые пошли другим путем. Так Ruby`исты любят использовать YML, Флешеры предпочитаю XLIFF, C`ишники — формат PO, а например ребята из l20n.org придумали свой формат, с тегами и атрибутами. Давайте кратко рассмотрим их.
YML
YML — это как XML, только «человеко-читаемый». Так же в нем можно хранить RegExp`ы в явном виде.
Было:
<translation>
<hello>Привет, Мир!</hello>
</translation>
Стало:
translation:
hello: "Привет, Мир!"
PO
Его используют в основном для локализации свободного ПО, т.к. это просто. Разработчик оборачивает все строки в какую-нибудь функцию, например gettext(), а далее эти строки являются ключом для перевода. Если перевода нет, подставляется исходная строка.
msgid "Hello, World!"
msgstr "Привет, Мир!"
Т.к. ни один из вышеперечисленных форматов не является родным и удобным для фронтендеров, мы идем дальше. Пример файла с переводами в игре «Долина сладостей»:
При использовании формата JSON в JavaScript`е некоторые системы локализации не поддерживают вложенных свойств. Пример такого файла вы можете видеть выше. Это влечет неочевидную структуру перевода. В большинстве систем локализации таких проблем нет, и вышеизложенный код с легкостью можно преобразовать.
Было:
{
...
"inviteFriend": "Пригласите друзей",
"inviteFriend2Lines": "пригласите<br> друзей",
"InviteFriend2Lines": "Пригласить<br> друга",
...
}
Стало:
{
...
"invite": {
"friends": {
"line1": "Пригласите друзей",
"line2": "пригласите<br> друзей"
},
"friend": "Пригласить<br> друга",
...
}
При таком подходе структура перевода становится более очевидной. Идентификаторы фраз принимают вид «invite.friends» и вытаскиваются из JSON-объекта функцией вида:
function getProperty (json, key) {
key = key.split(/[\.]+/gim);
for (var i = 0, l = key.length; i < l; i++) {
var propertyName = key[i];
if (!json[propertyName]) {
return null;
}
json = json[propertyName];
}
return json;
}
Следующий шаг — попытаться отразить в этой структуре падежи, т.к. иногда есть необходимость вывода числительных. Классический пример: «2 яблока», «1 яблоко», «нет яблок». Уже упомянутые выше ребята из l20n решили проблему так:
<brandShortName {
*nominative: "Aurora",
genitive: "Aurore",
dative: "Aurori",
accusative: "Auroro",
locative: "Aurori",
instrumental: "Auroro"
}>
А те, кто использует Angular, пишут так:
{{numMessages, plural,
one {У вас одно яблоко}
two {У вас два яблока}
other {У вас множество яблок}
}}
Я, к примеру, отказался вовсе от затеи с числительными и падежами, т.к. это:
- Никогда не попадалось лично мне на практике;
- Сильно усложнит систему и плохо повлияет на автоматизацию;
- Резко увеличит вероятность ошибки;
- Увеличит требования к качеству и количество необходимых переводов;
- Противоречит правилам «легкой локализации" (см. доклады Ярошевич и Ильяхова);
- Как правило, не играет решающей роли в интерфейсе и легко обходится с помощью изменения UI;
Классическую задачу с яблоками, можно решить пиктограммами. Вот несколько примеров:
Создание юнитов в Red Alert
Инвентарь в MineCraft
Возможно, это специфика игростроя, но и в обычных SPA приложениях (админки, утилиты и т.п.), так же всегда удавалось отобразить подобные счетчики корректно и без падежей.
Быстрое отображение статистики в баннерной системе
В игре «КосмоСим» параметры показаны числами и пиктограммами
Конечно, есть множество случаев, где, хотим мы того или нет, проблемму надо решить. Если у вас как раз такой случай, то возможно вам поможет статья Павла Доронина или костыль из "...".replace(/.../gim, "...");
В чем проблемма с шаблонами?
Проблемма в том, что любой вариант, в котором мы придумываем некий шаблон сразу становится не универсальным, т.к. универсальных шаблонов нет. Если мы будем этот вариант автоматизировать, то у нас все строки, в независимости от содержания, будут обрабатываться регулярками для поиска переменных. Это много лишней работы. Если мы обойдем это и изначально пометим строки с шаблоном в данных, то у нас возрастет количество данных, которые нам надо хранить. Таким образом работа с шаблонами, это всегда загадка с двумя стульями: «корректность перевода» vs. «простота и универсальность системы».
Аккордеон про HTML
Хранение переводов мы уже обсудили, самое время перейти к HTML. Практически все, что написано ниже уже было в книге «Сюрреализм на JavaScript», а то, чего не было, было у Немноцва и Тевосяна.
Если говорить о верстке HTML-страницы, то нам необходимо каким-то образом пометить DOM-элементы, тексты внутри которых нуждается в переводе. Как правило, разработчики присваивают элементам различные атрибуты, и пишут id перевода в этот аттрибут. Например:
<!-- angular-translate -->
<p translate="hello">Привет, Мир!</p>
<!-- i18next (http://i18next.com/) -->
<p data-i18n="hello">Привет, Мир!</p>
<!-- l20n (http://l20n.org/) -->
<p data-l10n-id="hello">Привет, Мир!</p>
<!-- jquery.localize.js (https://github.com/coderifous/jquery-localize) -->
<p data-localize="hello">Привет, Мир!</p>
Но! Мало кто анализирует объект перевода, полученный по идентификатору, и тег, к которому он будет применен. Если этого не делать, то задачи типа:
- поменять value и placeholder у поля ввода;
- поменять картинку;
- поменять подключаемый шрифт;
и т.п. будут решаться костылями, либо не будут решаться вообще. Примером ситуации, в который вы можете почувствовать дискомфорт, выбрав непродуманную систему, может стать следующий код:
<img src="scheme_ru.png"
alt="Схема электрооборудования"
title="Схема электрооборудования автомобиля ВАЗ 2114"
translate="id_перевода_картинки"/>
<input type="text"
placeholder="Введите поисковый запрос"
value="яндекс переводчик"
translate="id_перевода_поля"/>
<link rel="stylesheet" type="text/css"
href="css/ru.css"
translate="id_перевода_поля"/>
В своей системе локализации мы различаем тип полученного перевода и тип HTML-элемента. Таким образом, получив перевод типа строка «scheme_en.png» и имея HTML-элемент IMG, мы можем предположить, что, скорее всего, нужно изменить атрибут src. Если же мы работаем с элементом INPUT, то, скорее всего, нуждаемся в переводе атрибута placeholder, а типу LINK, соответственно, делаем замену аттрибута href, если не заданы никакие другие параметры. Кроме того, в ситуациях, где правила работы с элементом не заданны или неоднозначны, мы используем переводы типа «объект» и, с помощью перебора его свойств, присваиваем значение одноименным атрибутам тега, с которым работаем. Рассмотрим перевод для примеров, которые были даны выше:
{
"id_перевода_картинки": {
"src": "scheme_uk.png",
"alt": "Схема електрообладнання",
"title": "Схема електрообладнання автомобіля ВАЗ 2114",
},
"id_перевода_поля": {
"placeholder": "Введіть пошуковий запит",
"value": "яндекс перекладач"
},
"id_перевода_поля": "css/ua.css"
}
Получив возможность изменять атрибуты тегов, мы плавно переходим к решению задачи изменения интерфейса для разных национальных групп. Как уже было не раз сказано: «- Локализация, это не только перевод текстов, но ещё и адаптация продукта, под культурные особенности конкретной страны». Поэтому нам необходима возможность автоматически переключать стили и изменять картинки.
Немного боли
Неоднократно в комментариях, и статьях я слышал мнение о том, что не следует отображать флаги стран, и пиктограммы. Если человек чего-то не знает, он всегда включит анлийский и разберется. Категорически не согласен с этой точкой зрения. Дело в том, что анлийский я знаю плохо. Плохой перевод для меня гораздо лучше, чем его отсутствие. И таких как я очень много.
Предположим, есть точно такой же парень в Грузии или Израиле. Это обычный среднестатитический парень этой страны. На анлийский он будет смотреть примерно так же, как вы на грузинский или идиш. Ниже предствален скриншот небольшой игры на HTML. Если бы не пиктограммы на кнопках, с какой попытки вы бы попали в меню переключения языков? Если бы языки не сопровождались флагами, насколько долго и болезненно вы бы блуждали взгядом по скиску из 50 языков?
Зеркалить не забываем
Об этом уже было сказано очень много, поэтому добавить нечего. Сверстали элемент? Не забудьте приложить к нему rtl.css (это поможет rtl-css). Список всех rtl пропишите в системе локализации. Она должна знать, в каком случае нужно их подключить.
Например, так:
LanguageApplication({
rightRules: [
"module/button/button__right.css",
"module/popup_text/popup_text__right.css",
"module/progress_bar/progress_bar_right.css",
"module/popup_window/popup_window__right.css",
"module/popup_window/popup_window__animation_elements_right.css",
"js/applications/language/language__right.css",
"css/right.css"
]
});
Аккордеон про Архитектуру
Теперь, когда мы кратко описали возможные форматы хранения переводов и немного потыкали в HTML, можно добавить немного боли :) Зачем писать свою систему локализации, когда на рынке есть, как минимум пять готовых и широко используемых решений? Хороший вопрос, давайте посмотрим, как работает локализация в общем виде.
У нас есть два файла: русский и английский. По User-Agent`у определяем язык пользователя и подключаем либо первый, либо второй.
Простая задача, все красиво и понятно. Любая система локализации показывает вам демку, которая работает по этому принципу. Но у нас было по другому. Несколько мобильных порталов собирались из одних и тех же модулей. Порталы были похожи, но некоторые разделы у них отличались. На одном мы продавали игры, на другом — книги, и на обоих можно было купить мелодию. Были различные онлайн турниры и чемпионаты. Некоторые проводились только в России, другие во всем СНГ. Чтобы не плодить ресурсы, перевод был разбит на несколько файлов. Для каждого конкретного мобильного портала комбинация этих переводов была своя. Т.к. количество разделов в русской и английской версии могло не совпадать (и в половине случаев не совпадало), количество переводов в одной комбинации так же менялось от языка к языку в рамках даже одного портала.
Что мы видим на схеме? Пачку файлов. А для каждой пачки фалов должен быть свой файл конфигурации, который описывает количество, порядок и версию необходимых переводов в сборке.
«Отлично! Вот теперь все как надо!» — думал я на тот момент. Но, как вы уже догадались, это было только начало пути. Через пол года я начал работу над приложением психологических тестов. Все довольно банально: выбираете тест, отвечаете на вопросы, публикуете себе на стену ваш психологический размер груди. Все бы хорошо, но в России таких тестов уже полно. А вот в какой-нибудь Чехии или Хорватии нет. У них меньше население и программистов у них тоже меньше. Мелкий рынок сбыта не привлекателен для крупных игроков. Поэтому можно писать «самый лучший тест» для России и пытаться завоевать ~146 млн. человек, либо тоже самое для Хорватии, где обитает ~4 млн. человек. Забегая вперед скажу, что проект провалился, но на этапе разработки, это ещё не было очевидно.
Что у нас есть? Примерно, 30 тестов на момент публикации приложения. Каждый тест — это введение, далее около 10 вопросов и минимум три вывода. Все это умножаем на три языка. Итого: 30 * (1 + 10 + 3) * 3 = 90 переводов со сложной структурой, объемом не менее 2000 символов. Если приложение «выстрелит», тогда количество тестов увеличиться в два, три раза, а количество переводов умножится минимум на пять. Даже если пользователь не будет проходить все тесты на всех языках, надо помнить о том, что у нас одностраничное приложение. И это приложение медленно, но стабильно кушает память и с каждым новым тестом она у нас исчезает.
Что прилетает вместе с тестом? Вместе с тестом прилетает много разных файлов. Один из них — конфигурация перевода. В нём описаны все языки, на которых этот тест доступен. Там же прописаны адреса, откуда следует грузить соответствующие переводы.
Т.к. у нас приложение состоит из модулей, вполне очевидно, что перевод интерфейса лежит отдельно, а перевод каждого теста — отдельно. Как уже было сказано выше, это дает нам возможность переиспользовать ресурсы.
Смотрите, на картинке видно, как к пакету «перевод интерфейса» прилетел пакет «перевод теста». Что делает система локализации? Правильно, она объединяет две конфигурации, и получает третью. А после, взяв в оборот итоговый конфиг, начинает думать над тем, какие переводы ей сейчас нужны. Когда всё додумает и загрузит, она снова объединит результат, получив единый JSON на выходе. Именно поэтому JSON`у и будет формироваться конечный перевод фраз.
Самое время перекурить и «передать привет» Глебу Дейкало, который, как уже было сказано выше, писал о «многоступенчатом переводе». Путем объединения конфигураций и переводов мы можем получить ту же самую многоступенчатость, описанную в его статье. Только для этого нам нужно сливать переводы в рамках разных языков по заранее оговоренному порядку.
Вернемся к нашей памяти. Теперь, когда мы вдоль и поперек обмазались файлами конфигурации и знаем порядок их объединения, можно попробовать сделать очистку памяти после завершения теста. Для этого нам необходимо удалить загруженный пакет и переводы, которые он добавил. Далее объединить остаток в итоговый перевод.
Хорошая получилась штука. Теперь мы можем подгружать и выгружать из памяти целые блоки переводов. Но мой коллега, из соседнего отдела предложил решение лучше и проще — писать переводы тестов в одну переменную. Таким образом, перевод каждого следующего теста, затирал бы предыдущие переводы. Жалко, что к тому моменту, все, что описано выше уже было реализовано при том два раза.
Почему два? Потому что я ошибся, когда описывал структуру файла конфигурации. Выше я уже писал, что система должна быть универсальной для онлайн и офлайн работы. Если мы работаем оффлайн, то подгружать переводы можно только прямым подключением JS файла. Этот файл при инициализации передаст нам некий объект с данными. Т.к. в будущем мы собираемся его удалить, объект нам необходимо подписать неким id, притом этот же id нам необходимо хранить в файле конфигурации. Когда мы будем удалять конфигурацию, получив список id переводов, мы заодно удалим все переводы, которые были загружены по её просьбе. Структуру файла конфигурации я описал так:
{
id: "id конфигурации",
translations: {
ru: {
b: "/lang/b.js",
a: "/lang/a.js"
}
}
}
Кто на кого накатит обновления: «перевод А на Б» или «Б на А»? Если повезет, то «А на Б», но он не обязан этого делать, т.к. структура не массив, следовательно порядок не должен иметь значения. Кроме того, еще одна ошибка была в том, что очередность загрузки файлов влияла на порядок сборки. Следовательно, пришлось переделать:
{
id: "id конфигурации",
translations: {
ru: [
{ id: "b", url: "/lang/b.js" },
{ id: "a", url: "/lang/a.js" }
]
}
}
Это система локализации? Нет, это система загрузки конфигурации. Ещё одна ошибка, которую я допустил, это то, что в первой версии я не выделил её в отельный класс. Вообще отдельных классов и модулей получается довольно много:
- Какой-нибудь загрузчик файлов. JS файлы сами себя не загрузят;
- Что-нибудь, что нарисует нам иконку текущего языка и выведет список других, с возможностью выбора;
- Штука для объединения нескольких объектов и конфигов в один (была описана выше);
- Как минимум, самая простая библиотека для памяти (нам надо помнить выбранный пользователем язык);
В большинстве «готовых» модулей локализации, этого нет. Почему? Возможно, потому, что те кто их пишут редко их используют.
Интересный факт:
Ни один из сайтов систем локализации на JS не локализирован.
Карл! Когда мы писали локализацию, мы хотя бы сами пользовались ей! Пользовались сами Карл!
Обновляем DOM только по требованию
Не в каждой системе локализации методы «переключить язык» и «обновить DOM-дерево» разделены. Это может создать трудности, если вам нужно совершить операцию непосредственно после того, как все переводы будут готовы. Так же, во многих реализациях DOM-дерево обновляется после окончания загрузки каждого файла перевода. Например, переключили мы язык на «Армянский». Система начинает грузить три файла перевода: основной интерфейс, настройки, перевод какого-нибудь плагина. В такой ситуации вполне логично обновить DOM всего один раз — после окончания загрузки последнего файла в списке.
English, motherf**ker! Do you speak it?
Теперь-то мы готовы к локализации? Нет! Теперь мы готовы определиться с инфраструктурой, которая будет вокруг этой системы. Формат и структуру файла с переводом определяет программист, а его содержание переводчик или менеджер проекта. Эти люди могут быть не только не знакомы с выбранной структурой хранения информации, но и отказаться от попыток её принять и понять. Они с легкостью могут удалить кавычки в JSON-объекте или случайно стереть какой-либо тег в XML`е, кроме того, многие из них вообще предпочитают работать с файлами в формате Microsoft Word (DOCX).
Задача инфраструктуры не только предоставить менеджерам инструмент для локализации, но и обеспечить стабильность и простоту сборки и обновления переводов. Системы локализации часто страдают от инфраструктурных ошибок и недаработок. Большая трудоемкость обновления файлов с переводами и поддержка их в актуальном состоянии нередко становятся причиной отключения одного или нескольких языков.
Менеджеры игры «Война грибов» используют для работы с переводами таблицу Excel, которая потом обрабатывается парсером и превращается в XML документ, который используется системой локализации при сборке проекта.
Ребята из «Evilibrium» решили не работать с файлами напрямую и сделали точно такую же табличку в Google Docs. Логика проста — отсылать ссылку переводчикам проще, чем отсылать им файл. Поэтому все работают с переводом онлайн, а при сборке табличка точно так же скачивается и парсится в нужный XML.
На данный момент существует множество онлайн сервисов для работы с переводами. Кто-то лучше, кто-то хуже, но практически все справляются с задачей взаимодействия с переводчиком или группой переводчиков. Так же, большинство сервисов предоставляют возможность авто перевода вашего приложения. В интернете есть множество статей на эту тему, но т.к. эта статья про баяны, то грех не написать свою программу для переводов.
Перед продолжением хочу сказать пару слов о том, почему, на мой взгляд, авто перевод это нормально. Во-первых, если писать текст соблюдая ряд правил (просто, коротко, с прямым порядком слов и т.п.) то авто перевод может достигать точности в 90% (см. доклад Кристины Ярошевич). Во-вторых, в большинстве приложений используется довольно стандартный и скудный набор фраз: открыть, закрыть, сохранить, загрузить, новая игра, выход, далее, отмена и т.п. Их очень трудно перевести неправильно. В-третьих, мы с вами, как носители русского, здатні більш-менш розуміти багато мови слов'янської мовної групи. Следовательно, мы можем проверить их перевод на наличие грубых ошибок и без переводчика. Но это лично мое мнение, так например Павел Доронин в своей статье придерживался прямо противоположного мнения: «- Плохо локализованный продукт гораздо хуже вообще нелокализованного».
Для локализации очередного приложения мне нужно было нарезать файлы с переводом. Фрилансеры, которые присылали перевод, постоянно ломали JSON. Вскоре, вместо JSON`а я стал отсылать им обычный текст. В первом приближении стояла задача из текста фрилансеров получать валидные объекты. Для этой задачи на коленке было сделано приложение в два окошка (текст на входе, JSON на выходе). Как разбить текст на фразы? Давайте посмотрим на примере:
Текст в TEXTAREA:
фраза 1
фраза 2
Как видит его обычный человек:
фраза 1
фраза 2
Как видит его программист:
фраза 1\n
фраза 2
Отлично! У нас есть символ, по которому мы можем разбить текст. Кроме того, т.к. это скрытый символ, переводчик не сможет его случайно сломать или затереть (по крайней мере, в большинстве случаев). Преобразуем текст в массив через split(), а далее подставляем его в объект, который загоняем в JSON.stringify() и получаем необходимый валидный файл.
Т.к. шаблон JSON`а может быть разный, то лучше вынести его в отдельный файл. А раз уж мы его вынесли в отдельный файл, может научить его генерировать не только JSON`ы? С этими мыслями я добавил в программу ещё одно окошко, в котором можно редактировать функцию «генерации шаблона». На входе она получает массив переведенных фраз, а на выходе отдает JSON, XML или что-то другое.
Когда нужно было переводить следующее приложение, я подумал, что можно сильно сэкономить и увеличить объем переводов, если прикрутить какой-нибудь авто переводчик. Сказано-сделано. Теперь программа могла взять текст, перевести его на кучу языков, и выдать окончательные валидные JSON`ы.
После этого я стал писать скрипт, который бы нарезал портянку JSON`нов в файлы JavaScript`а. Потом подумал, и вместо этого написал небольшой локальный сервер на NodeJS. Теперь после получения очередного перевода от Яндекс.Переводчика программа стала отсылать данные в ноду, которая резала нужные файлы налету.
Результат очень порадовал. Теперь небольшие игры стали идти со стандартным переводом примерно на 45-50 языков. Так было выпущено несколько мелких продуктов, для которых локализация не была критически важным местом, но являлась приятным дополнением.
Тестирование показало, что авто перевод ошибался в одних и тех же местах. Например, слово «Назад» переводил как «Ago» или «Тому» (в смысле «20 лет тому назад»). Т.к. фразы были одни и те же начали появляться костыли. В какой-то момент я решил их убрать и добавить словарь. Так у программы появилось третье окно.
Теперь словарь содержал поля типа:
{
ru: {
en: {
"Назад": "Back"
...
Т.к. в ходе добавления словаря, я решил немного отрефакторить приложение, то внес много дополнительных правок. Заодно решил почитать больше о локализации. В этот момент я и наткнулся на статью Дениса Лукъянова «Обзор 7 онлайн-сервисов для локализации ПО» и осознал, что все это время я писал велосипед…
Ну что поделать, велосипед так велосипед. Из принципа решил доделать и внёс последний штрих. Теперь нода стала резать перевод сразу в кучу форматов: JS, JSON, XML, PO, YML, CSV и сохранять эти файлы в две папки: «чистое» и «грязное». Если мы изменим файлы в папке «грязное», то в словаре можем нажать «синхронизация» и нода автоматом сгенерирует словарь по найденным различиям. В данный момент синхронизация работает только по половине форматов, т.к. не ясно стоит ли продолжать возиться с программой или нет.
Можете перейти на сайт и посмотреть, как все это выглядит или даже скачать (ссылка на файл в настройках). Если хотя бы 10 человек отпишется в необходимости продолжить, буду доводить её до ума, т.к. хочется еще несколько функций:
- сохранение настроек, т.к. на работе приходится переключаться между множеством проектов;
- выбор пути сохранения переводов, чтобы файлы сразу попадали в билд нужного проекта без ручной переноски;
- выбор форматов сохранения, т.к. PO, YML, XML, JSON на практике мной не используются, то и смысла каждый раз генерировать их нет;
Эту статью я написал еще неделю назад, но не могу остановиться с рефакторингом вышеописанного велосипеда. Переписал одно, переписал другое, и вот уже вроде нельзя никому показывать, пока третье не исправлю. В общем будем считать это рабочим прототипом.
Локализация без локализации
Система локализации — это, прежде всего, система управления значением некого набора констант. Выбранный язык в ней, это просто ключ, которым можно переключать эти наборы. Изменив ключ в одном месте, вы меняете константы во всем приложении. И если вы осознаете эту мысль, то, возможно, посмотрите на возможности систем локализации под другим углом. Давайте вместе рассмотрим несколько кейсов нестандартного использования системы.
Смена внешнего вида приложения
В своей работе мы используем один и тот же набор модулей, для создания типовых мобильных сайтов продажи контента. Для каждого партнёра мы меняем часть стилей, в которые так же входит набор иконок. Кроме того, в дни праздников или рекламных акций, мы меняем внешний вид определенных сайтов для создания подходящего настроения у целевой аудитории (например, новогодний стиль или тема в честь «дня Святого Валентина»). Чтобы использовать одни и те же шаблоны верстки, нам пришлось убрать из них адреса картинок. Каждую такую картинку мы пометили неким идентификатором, а все адреса записали в JSON-объект с соответствующими ключами. Для того, что бы система работала, нам нужен был некий механизм, который автоматически прописывал бы всем картинкам актуальные адреса, исходя из текущей конфигурации и темы. Следовательно, этот механизм должен с одной стороны получать конфиги, а с другой — возвращать значение по заданному идентификатору. Так же хотелось бы, чтобы он мог пройтись по DOM-дереву и обновить все помеченные картинки самостоятельно, если DOM уже сформирован. Вам функционал описанной системы ничего не напоминает?
Пример JSON-объекта с адресами картинок:
{
header: {
menu: {
normal: "/images/201409/menu__normal.png",
hover: "/images/201409/menu__hover.png"
},
search: "/images/201407/search__blue.png"
},
...
footer: {
icon: "/images/201408/question.png",
up: "/images/201407/up.png"
}
}
Такой механизм один в один повторяет логику работы системы локализации. А если так, то зачем его создавать? Мы можем заставить модуль локализации выполнять эту работу, только в качестве значения языка использовать название нужной темы. Кроме того, как уже было сказано выше, наша система локализации также может менять адреса подключаемых стилей, а значит, мы получим контроль в том числе и над подключаемым CSS-кодом. Если вы не хотите смешивать переводы и языки в рамках одного файла конфигурации, то вам не обойтись без ООП. ООП отвечает на вопрос, как создать два экземпляра локализации для разных целей (один накатывает переводы, другой — стили).
Смена коэффициентов в математических формулах
Программисты из компании «Uniсum», которая разрабатывает софт для игровых автоматов, закладывали в файлы локализации ещё и математические коэффициенты для расчета выигрыша. Это было связано с тем, что игровое законодательство может иметь существенные различия в зависимости от страны, в которой предполагается использовать их ПО. Так например средний процент денежного выигрыша игровых автоматов мог колебаться в диапазоне от 60% до 95%.
Кроме того, при разработке игры с различными уровнями сложности, вы так же можете использовать систему локализации для хранения и быстрой смены коэффициентов, влияющих на игровой баланс.
Смена сюжетной линии
Если сюжет вашей игры описан в виде графа и сохранен как JSON-объект, то передав его системе локализации, умеющей выполнять операции слияния объектов, вы можете создавать патчи и дополнения к основной сюжетной линии. Впрочем, самих сюжетных линий вы тоже можете создать несколько и выбирать их случайным образом при запуске игры (тут ключом станет не «выбранный язык», а «выбранный сюжет»).
Изменение правил игры
Чем отличается блекджек от игры в очко? Мало чем. Главное отличие — количество карт в колоде 54, а не 36. Если вы когда-нибудь будете писать онлайн блекджек (как это делал я пол года назад), то у вас будет вариант запихать колоду в JSON, а его сунуть в экземпляр системы локализации и добавить в настройки опцию «переключение правил». Теперь наша система локализации легким движением руки превращается в систему выбора правил игры.
Итоги и выводы
JavaScript:
- храним переводы формате JSON или XML;
- разбиваем перевод на модули;
- обновляем DOM только по требованию;
- чистим память;
- даём возможность выбрать язык;
- помним сделанный выбор;
Продумываем систему сборки:
- исходных данных от переводчика в формат для программистов;
- группы переводов отдельных модулей в один окончательный;
Выбираем готовую из статьи Дениса Лукъянова.
При верстке:
- думать об RTL;
- ограничивать длинну текста;
- указатель на перевод должен быть в аттрибутах;
- не пытаемся писать в CSS переводы через псевдоэлементы;
Используем rtl-css, смотрим Александра Тевосяна.
Текст:
- храним в UTF-8;
- шрифты должны поддерживать все языки;
- тестируем псевдолокализацией;
- избегаем избыточной оптимизации;
Читаем Павла Доронина.
В тексте убрать:
- синонимы;
- сокращения;
- причастные обороты;
- отглагольные существительные;
- страдательный залог;
- бессмысленные глаголы;
- обезличенные конструкции;
- предлог по, при;
- много падежей без предлогов;
- пропуск слов;
Посмотреть Кристину Ярошевич.
Добавить:
- простые предложения;
- прямой порядок слов;
- информационный стиль;
Для информационного стиля:
- удалить стоп слова;
- выделить структуру;
- дублируем информацию картинками;
Используем глав. ред., смотрим Максима Ильяхова, готовим скриншоты.
Немного плюшек:
{
ar: "عربي", // Арабский
az: "Azərbaycan", // Азербайджанский
be: "Беларускі", // Беларусский
bg: "Български", // Болгарский
bs: "Bosanski", // Боснийский
ca: "Català", // Каталанский
cs: "Čeština", // Чешский
da: "Dansk", // Датский
de: "Deutsch", // Немецкий
el: "Ελληνικά", //