Данный пост описывает кейс, позволяющий проработать мультиязычность сайта с автоматическим переводом всех динамических сущностей (точнее многих динамических сущностей).
Существует несколько вариантов создания мультиязычности на сайтах. Основной вариант, предлагаемый командой 1С-Битрикс - это дополнительная лицензия на каждый языковой сайт. В этом способе есть большое количество как положительных таки отрицательных качеств. Из отрицательных качеств использования доп.лицензии для каждой из языковых версий кроме необходимости докупать каждой новой лицензии возникает также и большое количество чисто технических вопросов. Самым трудоемким является настройка функционала оформления заказа, т.к. для каждой языковой версии придется настраивать все свойства заказа.
Рассмотрим вариант работы сайта с тремя языками: русский, как основной язык, используемый для индексации всеми поисковыми системами и т.п.; украинский и английский - как дополнительные языки для отображения пользователям.
Сначала было принято решение о том, что все должно работать без изменения url-адреса (т.е. без поддоменов или вынесения языковых версий в подпапки). Был реализовал функицонал русского и английского языков и все было замечательно, пока на проект не посмотрели сеошники :( В чем минус данного решения? В том, что если поисковики проиндексируют не верную языковую версию - индексация будет кашей - часть страниц будет проиндексирована на одном языке, часть - на другом. Поэтому было принято решение о вынесении дополнительных языковых версий в подпапки.
Для изменения языка необходимо просто определить константу текущего ящыка, LANGUAGE_ID.
Для того, чтобы определить системную константу с текущим языком, необходимо прописать в файле dbconn.php определение константы:
$requestedDir = $_SERVER['REQUEST_URI']; if ( stripos($requestedDir, '/bitrix/admin') === false && stripos($requestedDir, '/bitrix/updates/') === false && (!defined('ADMIN_SECTION') || ADMIN_SECTION == false) && (!defined('BX_PUBLIC_TOOLS') || BX_PUBLIC_TOOLS == false) ) { if (isset($_REQUEST['lang'])) { $lang = 'ru'; if(strpos($_SERVER['REQUEST_URI'],'/en/')!==false){ $lang = 'en'; } elseif (strpos($_SERVER['REQUEST_URI'],'/ua/')!==false){ $lang = 'ua'; } else { $lang = 'ru'; } define('LANGUAGE_ID', $lang); setcookie('user_lang',$lang,time() + 60 * 60 * 24 * 30 * 12 * 2, "/"); } elseif (isset($_COOKIE['user_lang'])){ $lang = $_COOKIE['user_lang']; if (in_array($lang, array('en', 'ru', 'ua')) && $lang!=='ru') { $RedirectTo = '/'.$lang.$_SERVER['REQUEST_URI']; header('Location: ' . $RedirectTo);die; } } }
С помощью данной конструкции мы подключаем типовой системный механизм языковых файлов, т.е. нужно следить, чтобы во всех шаблонах компонентов все статические фразы были прописаны в языковых файлах. Для включаемых областей можно в пути к файлу добавлять константу с текущим языком (или в нашем случае, просто размещать в соответствующей языковой папке):
$APPLICATION->IncludeComponent("bitrix:main.include",".default", array( "AREA_FILE_SHOW" => "page", "AREA_FILE_SUFFIX" => "inc_" . LANGUAGE_ID, "EDIT_TEMPLATE" => "clear.php", ) );
Таким образом, со статическими файлами можно решить вопрос - тут не большая проблема. А вот с динамическими данными - посложнее. Для названий сущностей инфоблоков (разделов и элементов) можно создать доп.свойства для каждого нового языка.
Создаем обработчики для событий создания и обновления разделов инфоблока (события OnBeforeIBlockSectionAdd и OnBeforeIBlockSectionUpdate):
function OnBeforeIBlockSectionAddUpdateHandler(&$arFields) { if (isset($arFields['UF_NAME_EN']) || isset($arFields['UF_NAME_UA'])) { $translator = new \Pai\YandexTranslate(); // класс, реализующий перевод фраз if(isset($arFields['UF_NAME_EN']) && strlen($arFields['UF_NAME_EN']) <= 0){ $text = $translator->TranslateText($arFields['NAME'], 'en', 'ru'); if (strlen($text) > 0) { $text = htmlspecialchars($text); $arFields['UF_NAME_EN'] = $text; } } if (isset($arFields['UF_NAME_UA']) && strlen($arFields['UF_NAME_UA']) <= 0) { $text = $translator->TranslateText($arFields['NAME'], 'uk', 'ru'); if (strlen($text) > 0) { $text = htmlspecialchars($text); $arFields['UF_NAME_UA'] = $text; } } } }
Таким образом, прогоняем названия разделов через переводчик и вновь полученные значения сохраняем в служебные поля. Пример класса-переводчика описан был ранее. Функция перевода имеет вид:
function TranslateText($text,$to,$from='ru'){ if($to!==$from){ $resText = json_decode($this->yandexTranslate($from, $to, $text))->text; if(strlen($resText[0])>0) return $resText[0]; else return $text; } else { return $text; } }
Для описаний раздела считаю опасным проводить переводы вручную, но если нужно - можно реализовать аналогично. Я под описание раздела создаю также доп. поля типа html (на marketplace под это есть спеицальное решение), но заполнять эти свойства доверяю менеджерам.
С разделами разобрались. С элементами сложнее. Для перевода названия воспользуемся тем же функционалом, что и для разделов: создаем свойства для названий под каждый язык: NAME_EN, NAME_UA
Пишем обработчик на создание и обработку элементов инфоблока (события OnBeforeIBlockElementAdd и OnBeforeIBlockUpdate):
function OnBeforeIBlockElementAddUpdateHandler(&$arFields) { $NameLangProps = array('en'=>array(38),'uk'=>array(121)); foreach ($NameLangProps as $lang=>$arProps) { foreach ($arProps as $propKey) { if(is_array($arFields['PROPERTY_VALUES'][$propKey])){ $keys = array_keys($arFields[$propKey]); if(strlen($arFields['PROPERTY_VALUES'][$propKey][$keys[0]]['VALUE'])<=0){ $translator = new \Pai\Lareto\YandexTranslate(); $text = $translator->TranslateText($arFields['NAME'], $lang, 'ru'); if (strlen($text) > 0) { $text = htmlspecialchars($text); $arFields['PROPERTY_VALUES'][$propKey][$keys[0]]['VALUE'] = $text; } } } } } }
В самом начале определяем массив с идентификаторами свойств, которые нужно получить (из всех инфоблоков - проверку на инфоблок не ставил). Дальше проверяем есть ли в текущей обработке эти свойства. Если они есть и пустые - производим перевод и сохраняем полученные значения. Если есть еще какие-то свойства, которые тоже нужно переводить - их нужно дописать тут же, в этом обработчике.
Для вывода меню из разедлов, например, каталога, обычно используется компонент "bitrix:menu.sections". Для того чтобы
выводить в меню языковые фразы, сохраненные в соответсвующих файлах доп. полях, необходимо кастомизировать данный компонент.
Для этого выносим компонент в отдельное пространство имен и в нем вносим правки: в функции
$rsSections = CIBlockSection::GetList
нужно расширить список запрашиваемых из базы полей - добавить
получение всех пользовательских полей (или только поля для ваших языков, если не планируется раширение списка доступных языков);
т.е. функция запроса данных будет иметь вид:
$rsSections = CIBlockSection::GetList($arOrder, $arFilter, false, array( "ID", "DEPTH_LEVEL", "NAME", "SECTION_PAGE_URL", "UF_*" ));
Дальше дорабатываем функцию перебора разделов. В итоге перебор методов примет вид:
if(LANGUAGE_ID!=='ru'){ $translator = new \Pai\YandexTranslate(); } while($arSection = $rsSections->GetNext()) { if(LANGUAGE_ID!=='ru'){ $needUpdate = false; if(isset($arSection['UF_NAME_'.strtoupper(LANGUAGE_ID)])){ if(strlen($arSection['UF_NAME_'.strtoupper(LANGUAGE_ID)])>0){ // заменяем название раздела: $arSection['~NAME'] = $arSection['~UF_NAME_'.strtoupper(LANGUAGE_ID)]; } } else { // esli perevod otsutstvuet :( $text = $translator->TranslateText($arSection['NAME'], LANGUAGE_ID, 'ru'); if (strlen($text) > 0) { $text = htmlspecialchars($text); $arSection['UF_NAME_'.strtoupper(LANGUAGE_ID)] = $text; // заменяем название раздела: $arSection['~NAME'] = $text; // определяем необходимость записи изменения в базу: $needUpdate = true; } } if($needUpdate){ $bs = new CIBlockSection; $res = $bs->Update($arSection['ID'], array('UF_NAME_'.strtoupper(LANGUAGE_ID)=>$arSection['UF_NAME_'.strtoupper(LANGUAGE_ID)])); } // если вы делаете с выносом каждого языка в подпапки для языка, то тут вносим правку урла: $arSection["~SECTION_PAGE_URL"] = '/'.LANGUAGE_ID.$arSection["~SECTION_PAGE_URL"]; } $arResult["SECTIONS"][] = array( "ID" => $arSection["ID"], "DEPTH_LEVEL" => $arSection["DEPTH_LEVEL"], "~NAME" => $arSection["~NAME"], "~SECTION_PAGE_URL" => $arSection["~SECTION_PAGE_URL"], ); $arResult["ELEMENT_LINKS"][$arSection["ID"]] = array(); }
В результате, меню будет переведено.
Дальше нужно обработать списки товаров. Для этого в файле result_modifier.php шаблона компонента catalog.section делаем обработку:
if (!empty($arResult['ITEMS'])) { if(LANGUAGE_ID!=='ru'){ $translator = new \Pai\YandexTranslate(); foreach ($arResult['ITEMS'] as $key=>$arItem) { $needUpdate = false; if(strlen($arItem['PROPERTIES']['NAME_'.strtoupper(LANGUAGE_ID)]['VALUE'])>0){ $arResult['ITEMS'][$key]['NAME'] = $arItem['PROPERTIES']['NAME_'.strtoupper(LANGUAGE_ID)]['VALUE']; } else { $text = $translator->TranslateText($arItem['NAME'], LANGUAGE_ID, 'ru'); if (strlen($text) > 0) { $text = htmlspecialchars($text); $arItem['PROPERTIES']['NAME_'.strtoupper(LANGUAGE_ID)]['VALUE'] = $text; // заменяем название: $arResult['ITEMS'][$key]['NAME'] = $text; // определяем необходимость записи изменения в базу: $needUpdate = true; } } if($needUpdate){ $PROPERTY_CODE = 'NAME_'.strtoupper(LANGUAGE_ID); $PROPERTY_VALUE = $arItem['PROPERTIES']['NAME_'.strtoupper(LANGUAGE_ID)]['VALUE']; CIBlockElement::SetPropertyValuesEx(intval($arItem['ID']), intval($arItem['IBLOCK_ID']), array($PROPERTY_CODE => $PROPERTY_VALUE) ); } // если языковая версия лежит в подпапке - правим путь к детальной карточке товара: $arResult['ITEMS'][$key]['DETAIL_PAGE_URL'] = '/'.LANGUAGE_ID.$arItem['DETAIL_PAGE_URL']; } } }
Дальше - самая сложная часть - карточка товара. Перевести основные поля - не проблема - всегда можно создать соотвествующие дополнительные свойства, но вот что делать со свойствами, описывающими карточку товара? Нельзя же наплодить так много свойств. В данном случае необходимо создать отдельное место для хранения переведенных фраз - это или файл, или highload-инфоблок. До того, как мне понадобилось создать еще один язык (изначально задача стояла создать только русскую и английскую языковые версии), я сделал реализацию через файл, но при добавлении еще одного языка увидел, на сколько это, во первых, не рационально, а, во-вторых, неуправляемо - фразы переводятся в автоматическом режиме и у менеджера нет возможности редактировать полученный от сервиса переводов вариант. Поэтому было принято решение о вынесении фраз с переводом в отдельный Highload инфоблок.
Для начала получим название товара из свойства (обработка в файле result_modifier.php шаблона компонента карточки товара):
if (LANGUAGE_ID !== 'ru') { $translator = new \Pai\YandexTranslate(); if(strlen($arResult['PROPERTIES']['NAME_'.strtoupper(LANGUAGE_ID)]['VALUE'])>0){ $arResult['NAME'] = $arResult['PROPERTIES']['NAME_'.strtoupper(LANGUAGE_ID)]['VALUE']; } else { $text = $translator->TranslateText($arResult['NAME'], LANGUAGE_ID, 'ru'); if (strlen($text) > 0) { $text = htmlspecialchars($text); $arResult['PROPERTIES']['NAME_'.strtoupper(LANGUAGE_ID)]['VALUE'] = $text; // заменяем название: $arResult['NAME'] = $text; // записываем изменения в базу: $PROPERTY_CODE = 'NAME_'.strtoupper(LANGUAGE_ID); $PROPERTY_VALUE = $text; CIBlockElement::SetPropertyValuesEx(intval($arResult['ID']), intval($arResult['IBLOCK_ID']), array($PROPERTY_CODE => $PROPERTY_VALUE) ); } } }
Дальше обрабатываем свойства. Для этого создаем highload-инфоблок cо строчными полями:
- UF_TEXT - текст для перевода
- UF_LANGUAGE_ID - идентификатор языка
- UF_RESULT - результат перевода
Переводить нужно названия свойств и значения свойств. Для подключения к сущности highload-блока можно воспользоваться функцией:
function GetEntityDataClass($HlBlockId) { if (empty($HlBlockId) || $HlBlockId < 1) { return false; } $hlblock = \Bitrix\Highloadblock\HighloadBlockTable::getById($HlBlockId)->fetch(); $entity = \Bitrix\Highloadblock\HighloadBlockTable::compileEntity($hlblock); $entity_data_class = $entity->getDataClass(); return $entity_data_class; }
Получаем фразы для перевода:
foreach ($arResult['DISPLAY_PROPERTIES'] as $key=>$arProperty){ $array2translate[$key] = array( 'name'=>$arProperty['NAME'], 'value'=>$arProperty['DISPLAY_VALUE'] ); $keysForTranslate[] = $array2translate[$key]['name']; if(strlen($array2translate[$key]['value'])>0 && !in_array($array2translate[$key]['value'],$keysForTranslate)){ $keysForTranslate[] = $array2translate[$key]['value']; } }
В итоге, в массиве $keysForTranslate
будет массив фраз для перевода. Проверяем в базе имеющиеся переводы:
$entity_data_class = GetEntityDataClass(XX); // XX - идентификатор хайлоад-инфоблока $rsData = $entity_data_class::getList(array( 'select' => array('*'), 'order' => array('UF_TEXT' => 'ASC'), 'filter' => array('UF_TEXT' => $keysForTranslate,'UF_LANGUAGE_ID'=>LANGUAGE_ID) )); $keysForTranslated = array(); while($el = $rsData->fetch()){ $keysForTranslated[$el['UF_TEXT']] = $el['UF_RESULT']; } foreach ($keysForTranslate as $item) { if(!isset($keysForTranslated[$item])){ $text = $translator->TranslateText($item, LANGUAGE_ID, 'ru'); if(strlen($text)>0){ // запишем результат перевода в справочник $result = $entity_data_class::add(array( 'UF_TEXT' => $item, 'UF_LANGUAGE_ID' => LANGUAGE_ID, 'UF_RESULT' => $text )); $keysForTranslated[$item] = $text; } } }
Дальше - заполняем свойства переводами:
foreach ($array2translate as $key=>$item) { $array2translate[$key] = array( 'name'=>isset($keysForTranslated[$item['name']]) ? $keysForTranslated[$item['name']] : $item['name'], 'value'=>isset($keysForTranslated[$item['value']]) ? $keysForTranslated[$item['value']] : $item['value'], ); $arResult['DISPLAY_PROPERTIES'][$key]['NAME'] = $array2translate[$key]['name']; if(strlen($array2translate[$key]['value'])>0) $arResult['DISPLAY_PROPERTIES'][$key]['DISPLAY_VALUE'] = $array2translate[$key]['value']; }
Вот и все! свойства также переведены. По аналогии проходим по всем другим разделам сайта, проверяем корректность переводов и радуемся. Удачного вам перевода!
Разработка сайта
Подайте заявку на разработку сайта на базе готового решения от компании 1С-Битрикс или одного из партнеров компании. Максимально подробно опишите, чему будет посвящен сайт, если это интернет-магазин - что он будет продавать, нужна ли мультиязычность, будут ли разные типы цен (розница, опт, крупный опт), будет ли интеграция с 1С, будет ли выгрузка товаров на различные торговые площадки...
Сопровождение сайта
Вы можете подать заявку на сопровождение вашего сайта на базе 1С-Битрикс. Сопровождение включает в себя: проверка актуальности обновлений сайта, проверка актуальности резервной копии, консультации по сайту. Опишите в заявке, какие еще объемы планируются на сопровождении и на какой срок вы планируете заключить договор на сопровождение - мы подберем подходящий вам бюджет на сопровождение
Работы по сайту
Вы можете подать заявку на выполнение определенного объема работ по сайту. Опишите в заявке объем работ. Это может быть разработка какого-то нового функционала, доработки по имеющемуся функционалу, доработки под требования сео-специалистов. На основании заявки вам будет сформирован бюджет работ, а также названы сроки на выполнение тех или иных работ.