Многоязычность сайта c автопереводом

Данный пост описывает кейс, позволяющий проработать мультиязычность сайта с автоматическим переводом всех динамических сущностей (точнее многих динамических сущностей).

Существует несколько вариантов создания мультиязычности на сайтах. Основной вариант, предлагаемый командой 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'];
}

Вот и все! свойства также переведены. По аналогии проходим по всем другим разделам сайта, проверяем корректность переводов и радуемся. Удачного вам перевода!

Количество показов: 7330
01.06.2017

Возврат к списку

Если вам была полезна статья можете отблагодарить автора:
Ethereum:

0x16Df809287333C49D3A237296C6248A6c08702Bc

Разработка сайта

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

Сопровождение сайта

Вы можете подать заявку на сопровождение вашего сайта на базе 1С-Битрикс. Сопровождение включает в себя: проверка актуальности обновлений сайта, проверка актуальности резервной копии, консультации по сайту. Опишите в заявке, какие еще объемы планируются на сопровождении и на какой срок вы планируете заключить договор на сопровождение - мы подберем подходящий вам бюджет на сопровождение

Работы по сайту

Вы можете подать заявку на выполнение определенного объема работ по сайту. Опишите в заявке объем работ. Это может быть разработка какого-то нового функционала, доработки по имеющемуся функционалу, доработки под требования сео-специалистов. На основании заявки вам будет сформирован бюджет работ, а также названы сроки на выполнение тех или иных работ.