Введение
В рамках своего проекта я столкнулся с задачей сделать текущий сайт компании мультиязычным. Более точно: создать возможность быстро и просто перевести сайт на английский, польский, итальянский и т.д.
Поиск в интернете показал, что существующие варианты создания мультиязычного сайта крайне громоздки и неэффективны. Подключать сторонние библиотеки зачастую проблемно, а советы по написанию своего решения связаны с большим объёмом однотипной работы.
Написание альтернативного метода смены локали заняло у меня всего несколько часов, а поддержание семантического единства и вовсе сводит к минимуму изменения при последующем добавлении новых страниц.
Исходные файлы примера сайта с автоматическим переводом можно скачать на github
Существующие альтернативы
Когда задача только появилась в разработке, первым шагом, разумеется, стало исследование готовых решений и советов на форумах о том, как наиболее просто и правильно реализовать возможность смены локали. Самые популярные интернет ресурсы для создания мультиязычных сайтов предлагаю следующие решения:
- Создание дублирующих html блоков с текстом на разных языках, из которых только один оставляется активным для пользователя, а остальные прячутся (display: none).
Очевидным минусом такого способа является невероятно быстрое увеличение кода и мгновенная потеря читабельности и поддерживаемости кода. Кроме того, такое решение является уязвимым для ошибок в тексте и масштабируемости с точки зрения увеличения количества языков (далее локалей).
- Подключение стороннего сервиса машинного перевода (такого как google translate) с большим количеством встроенных языков и минимальными изменениями в исходном коде страницы.
Когда задача только появилась в task list мы использовали этот способ как самый очевидный и удобный, однако опыт работы с клиентами – носителями языка из соединённых штатов и Израиля показал, что машинный перевод часто допускает ошибки при смене локали, а пользователи сайтов крайне резко реагируют на подобные ошибки перевода. В конце концом стратегические партнеры настойчиво посоветовали сменить способ изменения локали, и от этого способа пришлось отказаться.
- Смена языка при помощи возможностей js или сторонних библиотек/фреймворков, таких как jQuery, основанных на поиске и прямом изменении DOM элементов.
Особенностью такого подхода является поиск огромного количества js селекторов, текст внутри которых необходимо заменить. Такой подход может хорошо работать для небольших проектов, однако при увеличении количества страниц пропорционально увеличивается количества функций замены текста, что ведет к потере эффективности в больших проектах.
Альтернативное решение
Основой подхода, который я предлагаю, как альтернативу существующим способам является, как ни странно не база, написанного мной js кода, которая является в целом тривиальной, а правило оформление селекторов, поддерживание которого позволяет гибко и просто настроить перевод любого количество страниц на любой язык без изменения кодовой базы и излишнего дублирования данных.
В изменении локали при альтернативном подходе выделяются три основных игрока:
- html страница с установленным правилом оформления селекторов блоков с текстом
- общий js сервис, основная задача которого состоит в замене textContet DOM элементов согласно правилу оформления селекторов
- JSON файл локали, содержащий в себе структуру с содержанием html блоков на всех языках, используемых при смене локали
Соблюдение правила оформления селекторов изменяемых элементов позволяет избавиться от необходимости менять js код сервиса смены локали, что является большим плюсом с точки зрения масштабируемости проекта.
Правило построения селекторов
Большинство методов смены локали страницы (среди приведенных альтернатив 1,3 и частично 2) предполагают необходимость каким-либо способом «пометить» изменяемый html блок, как правильно при помощи изменении поля class. Этот же механизм использует и альтернативный вариант.
Первым шагом оформления селекторов является разделение исходной страницы на функциональные блоки верхнего уровня. На странице нашей компании — это блоки:
Каждому блоку даем условное название, например,
-
Меню (menu)
-
Визитная карточка (home)
-
Пример работы сервиса (example)
-
Партнеры (clients)
-
Область применения сервиса (userfulBlock)
-
Примеры работы сервиса (examples)
-
Контакты и обратная связь (contacts)
После этого мы далее разбиваем каждый блок на более мелкие функциональные блоки, как это делается при применении библиотеки React.
Выделенным областям присваиваем свои имена и получаем структуру вида:
-
menu
-
home main, description, buttons
-
example statistics, headline, description, buttons
-
clients buttons
-
userfulBlock headline, userfulCards, elseBlock
-
examples headline, cards
-
contacts headline, description, contacts, form
Далее продолжаем эту процедуру пока не достигнем блоков, содержащих исходный текст.
В итоге получаем готовую структуру JSON файла локали, содержащий все необходимые тексты для изменения языка. Также, исходя из этого алгоритма определяется правило построения селекторов:
Каждый селектор начинается с ключевого слова locale и далее, согласно стилю dash case добавляются имена всех родительских блоков включая блок, содержащий исходный текст, например, описание примера в первой карточке будем иметь селектор locale-example-cards-description
Пример полученного файла json локали можно увидеть на github
Сервис смены локали
Сервис смены локали представляет собой модуль, содержащий функцию загрузки файла локали
loadLocale(defLang)
с необязательным параметром defLang – язык установленные после загрузки локали (язык по умолчанию), а также основную функцию изменения текущей локали
changeLocale(lang)
с указанием требуемого языка.
Функция загрузки локали
Функция загрузки локали использует стандартный XMLHttpRequest запрос за данными. Использование именного этого стандарта обусловлено желанием минимизировать количество зависимостей и простотой использования запроса. После получения файла локали в консоль выводится оповещение о получении данных, а также вызывается функция изменения локали на язык по умолчанию в случае, если этот язык был передан в функцию как необязательный параметр. Ознакомиться с кодом функции можно тут:
function loadLocale(defLang) {
var xhr = new XMLHttpRequest();
xhr.open("GET", 'http://localhost:3000/locale.json', true);
xhr.onreadystatechange = saveLocale.bind(this);
xhr.onerror = function () { console.log("no found page"); };
xhr.send();
function saveLocale() { if (xhr.readyState == XMLHttpRequest.DONE && xhr.status == 200) {
locale = JSON.parse(xhr.responseText);
console.log("locale loaded");
if(defLang) changeLocale(defLang);
} }
}
Функция изменения локали
Типы данных
Представляет собой рекурсивную функцию, основной задачей которой являет обход объекта, содержащий локаль страницы (используя DFS алгоритм). Использование рекурсии при построении функции позволяет закодировать алгоритм максимально просто и лаконично, однако слишком большая глубина рекурсии может привести к переполнению стека. Особенности обхода данной проблемы можно найти на одноименном форуме, либо же ознакомившись с соответствующими статьями на habr.com.
В основе работы рекурсивной функции заложена обработка 4 типов данных:
- поле содержащее строку исходного текста, используемого для добавления на страницу.
Например:"main": "Продающий квест из вашего видео"
- поле содержащее массив строк исходного текста, используемого для добавления на
страницу. Такое поле необходимо для создания списков, элементы которых могут менять
порядок. Например:"menu":["Home","Example","Clients","Info","Contacts"]
- Вложенная структура данных, содержащая свой набор полей, необходимая для построения
архитектуры страницы. Например:"home": { "main": "selling quest from your video", "description": "for social networks & sites", "buttons": ["try","order"] }
- Массив вложенных структур данных, с одинаковым набором используемых полей. Такие
массивы используются, когда появляются списки одинаковых блоков кода, например,
карточек участников команды, или портфолио или тарифов оказываемых услуг.
Например:"usefulCards": [ { "headline": "Marketers and agencies", "statistics": ["convers 26%", "retent 25%"], "button": "ORDER" }, { "headline": "Production studios and TV platforms", "statistics": ["convers 24%", "retent 33%"], "button": "ORDER" }, { "headline": "Conference creators", "statistics": ["convers 65%", "retent 15%"], "button": "ORDER" }, { "headline": "Bloggers and streamers", "statistics": ["convers 24%", "retent 33%"], "button": "ORDER" } ],
На сайте это может выглядеть так:
Пример на сайте
Функции обработки
Обработка типа данных с исходным текстом осуществляется отдельной функцией
function getText(key, object, name,startIndex)
Принимающей на вход название поля структуры с исходным текстом, текущий объект локали, содержащий текст, который нужно добавить и текущее имя селектора, необходимое для поиска DOM элемента.
function getText(key, object, name, startIndex) {
var elementKey=0;
if(startIndex) elementKey = startIndex;
for ( ; elementKey < document.getElementsByClassName(name + "-" + key).length; elementKey++)
if (!isNaN(elementKey)) document.getElementsByClassName(name + "-" + key)[elementKey].textContent = object[key];
}
Обработка массива строк с исходным текстом также осуществляется отдельной функцией
function getArrayText(key, object, name,startIndex)
Сигнатура и тело этой функции ничем не отличается от прошлой за исключением того, что элементам DOM присваиваются элементы из массива.
function getArrayText(key, object, name, startIndex) {
var elementKey=0;
if(startIndex) elementKey = startIndex;
for ( ; elementKey < document.getElementsByClassName(name + "-" + key).length; elementKey++)
if (!isNaN(elementKey)) document.getElementsByClassName(name + "-" + key)[elementKey].textContent = object[key][elementKey % object[key].length];
}
Основная рекурсивная функция замены текста занимается классификацией текущего поля локали в один 4 приведенных выше типов и соответствующей реакцией на полученный тип:
function changeText(name, object, startIndex) {
for (key in object)
if (Array.isArray(object[key]) && typeof object[key] != 'string' && typeof object[key][0] == 'string') getArrayText(key, object, name);
else if (typeof object[key] == "object" ){
if(isNaN(key)) changeText(name + "-" + key, object[key]);
else changeText(name, object[key],key);
}
else getText(key, object, name, startIndex);
}
На вход эта функция принимает текущую языковую локаль и корневой селектор (в данном случае “locale”). Далее при обнаружении вложенной структуры или массива структур функция будет рекурсивно вызывать саму себя, соответствующе изменяя входные параметры.
Основным плюсом альтернативного подхода является то, что описанный выше сервис не требует никаких функциональных изменений, и добавляется как js файл, использующий созданный вами файл локали.
Заключение
Суть описанного выше подхода заключается в зафиксированном правиле описания селекторов и построения файла локали. Благодаря этому появляется уникальная возможность перевода любых страниц из коробки и переиспользования уже переведенного материала.
Описанный выше алгоритм построения селекторов не является обязательным и критичным для работы сервиса. Сервис является гибким для расширения и добавления новых методов и алгоритмов, а также для построения имен селекторов и структуры json локали. Возможным плюсом будет являться сохранение локали в cookie браузера и изменение локали, в зависимости от местоположения пользователя сервиса.
Исходные файлы примера сайта с автоматическим переводом можно скачать на github.
Автор: Nick_Zabolotskiy