Сегодняшний кейс о том, как произвести локализацию разработанного вами сайта на другую языковую версию с
автоматизацией перевода языковых фраз с помощью сервиса Яндекс.Перевод. В кейсе рассматриваются два ключевых
механизма: обработка CSV-файлов с языковыми фразами и автоматический перевод русскоязычных фраз на украинский язык.
Итак начнем. Сейчас я занимаюсь переносом (с серьезной переделкой) одного достаточно крупного сайта на Битрикс. Процесс очень интересный и о нем постараюсь в ближайшее время сделать более подробный пост с раскрытием ключевых моментов. А сейчас, о локализации.
Для начала была сделана русскоязычная версия сайта (заказчик данную версию выбрал основной). Абсоллютно все кирилические надписи во всех компонентах в обязательном порядке были вынесены в языковые фразы. Очень рекомеую не оставлять в языковых фразах дефолтные заготовки (в стиле "а вдруг пригодится") - не пригодятся! (А если будут нужны - лучше новые создадите, зато себе работу сократите в проверке корректности перевода). Также рекомендую все общие фразы,которые используются в большом количестве шаблонов компонентов, выносить в языковой файл хедера (правда, для страниц, подключаемых без хедера фразы все равно придется дублировать или принудительно подключать языковой файл хедера).
В общем виде подключение языковых фраз выглядит, примерно, так:
use \Bitrix\Main\Localization\Loc as Loc;
Loc::loadMessages(__FILE__);
Loc::getMessage('MSG_KEY');
Начинающие разработчики 1С-Битрикс (хотя и многие уже достаточно опытные разработчики тоже) все оставляют кирилицу прямо в шаблоне, считая, что потом, позже, перенесут. И так и остается вся эта каша на годы в шаблоне. Хорошо, если сайт в одной языковой версии устраивает своих посетителей годами и производить его локализацию нет необходимости, а че если нет, как говорит Семен Слепаков? Поэтому приучайте себя всегда выносить кирилицу в языковые фразы. Не вы, так кто-то другой обязательно выразят вам за это свою благодарность.
Теперь считаем, что вся кирилица хранится в языковых фразах и перевод не будет столь уж сложным. Для того, чтобы вытянуть все языковые фразы для перевода воспользуемся модулем "Перевод" системы 1С-Битрикс (обладателям редакций без данного модуля придется писать скрипт, который за вас вытянет все языковые файлы и сформирует CSV-файл).
Открываем в админке Настройки - Локализация - Просмотр файлов. В поле "Путь" вписываем относительный
путь до шаблона сайта. Ниже выбираем вкладку "Выгрузка файла" и на ней - "Выгрузить все переводы". Нажимаем
"экспортировать". Таким образом получили CSV файл, состоящий из такого набора колонок:
"file"; "key"; "ru"; "ua"; "en"
Если у вас другой набор установленных в системе языковых файлов - последние 3 колонки будут отличаться, но я рассматриваю вариант, когда в системе русский, украинский и английский языки.
Файл кладем где-то в системе (я обычно помещаю в upload), а в нашем скрипте-обработчике сразу добавляем первую фразу с указанием полного, абсоллютного пути до файла:
$fileName = $_SERVER["DOCUMENT_ROOT"] . '/upload/bitrix_templates_template.csv';
И файлик, в который запишем результат:
$fileName = $_SERVER["DOCUMENT_ROOT"] . '/upload/bitrix_templates_template_res.csv';
Дальше подключаем битриксовый класс для обработки CSV:
require_once($_SERVER["DOCUMENT_ROOT"] . "/bitrix/modules/main/classes/general/csv_data.php");
Битриксовую библиотеку я использую только для записи CSV-файлов. Для чтения уже давно (при использовании битриксового класса около года назад обнаружил проблемы для сложного файла, у которого символ-разделитель присутствовал также и в значениях) использую такую функцию:
function csv_to_array($filename='', $delimiter=',')
{
if(!file_exists($filename) || !is_readable($filename)) return FALSE;
$header = NULL;
$data = array();
if (($handle = fopen($filename, 'r')) !== FALSE)
{
while (($row = fgetcsv($handle, 1000, $delimiter)) !== FALSE)
{
if(!$header){
$header = $row;
foreach ($row as $key=>$value) {
if (strlen($value) > 0)$header[$key] = GetTranslitCode($value);
}
} else {
foreach ($row as $key=>$value) {$row[$key]=$value;}
$data[] = array_combine($header, $row);
}
}
fclose($handle);
}
return $data;
}
GetTranslitCode - это обертка вокруг обычной битриксовой функции транслитерации:
function GetTranslitCode($text)
{
$text = trim($text);
if (strlen($text) <= 0) return false;
$params = Array(
"replace_space" => "-",
"replace_other" => "-",
"delete_repeat_replace" => "true",
"use_google" => "false",
);
return CUtil::translit($text, "ru", $params);
}
Прогоняем наш csv-файл через обработку csv-файла и получаем ассоциативный массив из строк файла с заголовками в виде ключей:
$arRows = csv_to_array($fileName, ';');
Создаем с перезапись результирующий файл:
$fp = fopen($resFileName, 'w+'); fclose($fp);
Записываем сразу в результирующий файл заголовки:
$csvFile = new CCSVData();
$fields_type = 'R';
$delimiter = ";";
$csvFile->SetFieldsType($fields_type);
$csvFile->SetDelimiter($delimiter);
$arrHeaderCSV = array("file", "key", "ru", "ua", "en");
$csvFile->SaveFile($resFileName, $arrHeaderCSV);
Дальше приступаем к переводу. Для работы я использовал класс Yandex_Translate, который немного подправил, т.к. описанный в хабростатье метод уже достатно давно не работает. В классе я исправил урл, на который отправляется запрос, а также настроил отправку персонального ключа, который передается при определении класса
Создаем экземпляр класс переводчика и прогоняем весь массив языковых фраз через функцию перевода. Каждую строку сразу же сохраняем в файл:
$translator = new Yandex_Translate('ключ, полученный тут: api.yandex.ru/key/form.xml?service=trnsl');
$srPresent = array();
foreach ($arRows as $num => $row) {
if(strlen($row['ru'])<=0) continue;
if(strlen($row['ua'])<=0){
if(isset($srPresent[$row['ru']])){
$row['ua'] = $srPresent[$row['ru']];
} else {
$text = json_decode($translator->yandexTranslate('ru', 'uk', $row['ru']))->text;
$row['ua'] = $text[0];
$srPresent[$row['ru']] = $row['ua'];
}
}
$csvFile->SaveFile($resFileName, array($row['file'],$row['key'],$row['ru'],$row['ua'],$row['en']));
}
Теперь осталось открыть полученный файл в любом табличном процессоре, просмотреть корректность перевода, где нужно - подправить и залить файл обратно в модуль перевода.
Для загрузки языковых фраз снова открываем в админке Настройки - Локализация - Просмотр файлов. В поле
"Путь" вписываем относительный
путь до шаблона сайта. Ниже выбираем вкладку "Загрузка файла" и на ней выбираем наш файл, выбираем направление
перевода "полностью заменить ..." (или только новые, если вы делали частичный перевод) и нажимаем "Импортировать"
Все! Теперь языковые фразы созданы и можно спокойно заниматься реализацией второй локализации сайта.
В процессе проработки данного механизма были использованы материалы:
- Замечательный материал по работе с CSV в 1С-Битрикс
- Основной материал по работе класса Яндекс.Перевода
- Сам класс, модифицированный мной
- и, онечно же, API самого механизма Яндекс.Перевод
UPDATE 2016-02-23
В продолжение темы: Для интернет-магазина очень важный момент - отображение значений свойств, а в мультиязычности это один из самых сложных моментов. Свойства отображаются в умном фильтре, в списке товаров, в карточке товара, но у битрикса нет возможности для одного и того же свойства задать вариации в разных языковых версиях.
Для значений свойств этот недостаток можно обойти с помощью свойства типа "Справочник", заполняя UF_NAME основным языком, а другое поле (например, UF_DESCRIPTION) - забираем под второй язык. Но этого нельзя сделать для названий свойств - нет поля, в котором это хранить.
Из всей этой проблемы родилось решение: Создал отдельный языковой файл props.php, который подключил рядом с языковым файлом хедера:
if(LANGUAGE_ID=='ua'){
Loc::loadMessages($_SERVER['DOCUMENT_ROOT'].SITE_TEMPLATE_PATH.'/props.php');
}
Вот таким нехитрым способом загружаем свои отдельные значения для украинского языка
Дальше, дописываем функцию получения значений из справочника:
function GetFromVacabulary($filterEntity, $filterValues,$arSelect = array('ID', 'UF_NAME', 'UF_XML_ID','UF_DESCRIPTION'))
{
if(!is_array($filterEntity) || empty($filterEntity)) return false;
$cacheParams = array(
'filterEntity' => $filterEntity,
'filterValues' => $filterValues,
'lang' => LANGUAGE_ID
);
$cache_id = md5(serialize($cacheParams));
$cache_dir = __CLASS__ . '/' . $cacheParams['module'];
$obCache = new CPHPCache;
if ($obCache->InitCache(36000, $cache_id, $cache_dir)) {
$arResult = $obCache->GetVars();
$arResult['CACHE'] = 'Y';
} elseif (\Bitrix\Main\Loader::includeModule('highloadblock') && $obCache->StartDataCache()) {
if(LANGUAGE_ID=='ua'){
$translator = new \Yandex_Translate($this::YANDEX_TRANSLATOR_KEY);
}
global $CACHE_MANAGER;
$CACHE_MANAGER->StartTagCache($cache_dir);
$hlblock_requests = \Bitrix\Highloadblock\HighloadBlockTable::getList(array("filter" =>$filterEntity))->fetch();
if (!isset($hlblock_requests['ID'])) return false;
$entity_requests = HL\HighloadBlockTable::compileEntity($hlblock_requests);
$entity_requests_data_class = $entity_requests->getDataClass();
$main_query_requests = new Entity\Query($entity_requests_data_class);
$main_query_requests->setSelect($arSelect);
if (!empty($filterValues)){
$main_query_requests->setFilter($filterValues);
}
$result_requests = $main_query_requests->exec();
$result_requests = new CDBResult($result_requests);
$arResult['ITEMS'] = array();
while ($row_requests = $result_requests->Fetch()) {
if(LANGUAGE_ID=='ua'){
if(strlen($row_requests['UF_DESCRIPTION'])>0){
$row_requests['UF_NAME'] = $row_requests['UF_DESCRIPTION'];
} else {
$text = json_decode($translator->yandexTranslate('ru', 'uk', $row_requests['UF_NAME']))->text;
$text['ua'] = $text[0];
$row_requests['UF_NAME'] = $text['ua'];
$entity_requests_data_class::update($row_requests['ID'],array('UF_DESCRIPTION'=>$text['ua']));
}
}
$arResult['ITEMS'][$row_requests["UF_XML_ID"]] = $row_requests;
$CACHE_MANAGER->RegisterTag('hl' . $cacheParams['entity_id'] . $row_requests["UF_XML_ID"]);
}
$CACHE_MANAGER->RegisterTag('hl' . $cacheParams['entity_id']);
$CACHE_MANAGER->EndTagCache();
$obCache->EndDataCache($arResult);
} else {
$arResult['ITEMS'] = array();
}
return $arResult;
}
Функция получает на входе параметры для выбора, какой справочник взять, и параметры для фильтрации - какие значения нужны. На выходе для украинского языка будем иметь украиноязычные значения (если не заполнены - настроил перевод с помощью Яндекс.Переводчика).
Дальше - займемся названиями свойств. Вот пример для обработки умного фильтра:
$putLinesToLangFile = '';
foreach ($arResult['ITEMS'] as $key=>$arItem) {
if($arItem['PROPERTY_TYPE']=='S' && $arItem['USER_TYPE']=='directory'){
$tmp = $mainEntity->GetFromVacabulary(
array('TABLE_NAME'=>$arItem['USER_TYPE_SETTINGS']['TABLE_NAME']),
array('UF_XML_ID'=>array_keys($arItem['VALUES']))
);
if(!empty($tmp['ITEMS'])){
foreach ($tmp['ITEMS'] as $hl_code => $hl_val) {
$arItem['VALUES'][$hl_code]['VALUE'] = $hl_val['UF_NAME'];
}
}
}
if(strlen(Loc::getMessage($arItem['CODE']))>0){
$arItem['NAME'] = Loc::getMessage($arItem['CODE']);
} else {
$arItem['NAME'] = $mainEntity->TranslateText($arItem['NAME']);
$putLinesToLangFile .= '$MESS["'.$arItem['CODE'].'"] = "'.$arItem['NAME'].'";'.PHP_EOL;
}
$arResult['ITEMS'][$key] = $arItem;
}
if(strlen($putLinesToLangFile)>0){
$current = file_get_contents($_SERVER['DOCUMENT_ROOT'].SITE_TEMPLATE_PATH.'/lang/ua/props.php');
$current = substr($current,0,-2).$putLinesToLangFile.'?>';
file_put_contents($_SERVER['DOCUMENT_ROOT'].SITE_TEMPLATE_PATH.'/lang/ua/props.php', $current);
}
Сначала прогоняем значения справочников через функцию, описанную выше, а затем - занимаемся обработкой названий свойста.
На основании символьного кода свойства проверяем наличие языковой фразы - если фразы нет, то производим автоматический перевод с помощью Яндекс.Переводчика и Записываем полученный результат в заготовку для записи в языковой файл. Далее, когда все свойства перебраны в языковой файл производим запись полученных языковых фраз: считываем наш кастомный языковой файл и дописываем в него новые фразы.
Вот такое отличное решение родилось. Честно говоря, долго не мог придумать, как же обойти вариант работы со свойствами ...
Для поиска языковых фраз в файлах, можно воспользоваться регулярным выражением:
([А-Яа-я])+ // для слов ([а-я]+(\s+[а-я]+)*) // для фраз, разделенных пробелами