Welcome to php club

База данных

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


ВНИМАНИЕ: перед чтением данного материала прочитайте статью «Кодировки».

Многоязыковые данные небольшого объема

Для начала предлагаю обратить свое внимание на объем текста, который нам необходимо сохранить. Для примера возьмем изображение. Как правило, описание иллюстрации – это небольшой по объему текст, ведь мы будем писать его в atl HTML тега <IMG>, или просто подписывать иллюстрацию. Обычно для этого хватает 100 – 255 символов. Если предположить, что наш сайт говорит на 5 языках, то общий объем текста максимально составит 1,5 килобайта. В связи с чем, мы принимаем решение хранить все это в базе в одном поле типа TEXT.


Разумеется, мы каким-то образом должны отделить данные для разных языков друг от друга. Для этого мы выработаем решение о разработке определенного формата хранения данных в этом поле.


Я предлагаю следующий способ:



Мы выбрали определенное сочетание знаков, которое врядли может встретиться в обычном тексте. Каждая языковая строка будет предваряться сочетанием КЛЮЧА (двухбуквенного сочетания языка) и знаков :=, а сами сообщения будут разделяться двойной тильдой (~). После того, как строка оформлена указанным способом, мы посылаем ее в базу данных.


Какие преимущества дает нам такой способ хранения данных? Во-первых, данные на любом языке у нас всегда под рукой, во-вторых, сохраняется возможность поиска, причем поиск мы можем вести средствами базы данных и сразу на всех имеющихся языках, поскольку языковые данные у нас не деформируются и находятся в доступной форме.


Теперь давайте напишем еще 3 функции, которые облегчат нам формирование «упакованной» строки указанного формата, а также обеспечат легкость получения данных из «упакованной» строки. В своей работе эти функции будут опираться на первые две, которые даны выше, поскольку финальный результат должен удовлетворять требованию, что результат должен быть полностью готов к употреблению.


Первая функция. Ее задача принять массив определенного формата и сформировать на его основе строку указанного формата.


В данном примере появится еще одна константа – это DEFAULT_LANG. Данная константа определяет, какой язык принят за язык поумолчанию, то есть, на каком языке будет «говорить» сайт, если пользователь в данный момент не указал своего языка.


<?php
function LangDataPack($data) {
    
// Задаем пустую строку – здесь будет результат работы
    
$result = '';
    
// Получаем список ключей из массива $GLOBALS['site_languages']
    
$languages = array_keys($GLOBALS['site_languages']);
    
// Организовываем цикл по количеству языков,
    // поддерживаемых сайтом (определяем по количеству ключей)
    
foreach ($languages as $lang) {
        
// Если $result не пуст, значит мы перешли к очередному языку
        // и надо отделить языковые данные сочетанием '~~'
        
if (!empty($result)) {
            
$result .= '~~';
        }
        
// Если есть необходимость, конвертируем строку в кодировку базы
        
$data[$lang] = StringToBase($lang, $data[$lang]);
        
// Проверяем, есть ли в нашем массиве строка на языке, принятом
        // поумолчанию, если нет - вернем FALSE (ошибку)
        
if (empty($data[$lang]) && DEFAULT_LANG == $lang) {
            return
FALSE;
        
// Если текста нет, и данный язык не является языком поумолчанию,
        // заносим пустую строку
        
} elseif (empty($data[$lang])) {
            
$data[$lang] = '';
        }
        
// Формируем строку результата, добавляя к ней данные на текущем языке
        
$result .= $lang . ':=' . $data[$lang];
    }
    return
$result;
}
?>


В качестве параметра эта функция принимает массив следующего вида:


<?php
array('lang1' => 'string', 'lang2' => 'string')
?>


Где lang1 и lang2 – это двухбуквенное сочетание языка, на котором написан string (например, array('en' => 'english', 'ru' => 'русский')). В результате работы функции вы получите строку в кодировке UTF-8 вида en:=english~ru:=русский.


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


И так, функция, которая формирует «упакованную», строку написана и отработала, вы получили нужные вам данные и можете спокойно поместить их в базу. При таком подходе помечать запись в базе двухбуквенным кодом языка не надо, потомучто запись будет содержать сразу все языковые данные.


Может возникнуть ситуация, когда вы внезапно добавили в проект еще один язык. Из дальнейших примеров вы увидите, что подобный способ хранения данных не вызовет никаких трудностей с добавлением нового языка.


Переходим к следующей функции. Ее задача – восстановить «упакованный» массив, который обработала первая функция.


<?php
function LangDataUnPack($data) {
    
// Задаем пустую строку – здесь будет результат работы
    
$result = '';
    
// В начале создаем массив с перечнем языковых данных
    // Для этого мы разрезаем строку по оставленным ранее меткам (~~)
    
$data = explode('~~', $data);
    
// Организовываем цикл на основе количества выделенных выше элементов
    
for ($i = 0;$i < count($data); $i++) {
        
// Теперь отделяем собственно языковые данные
        // от двухбуквенного кода языка
        
$temp = explode(':=', $data[$i]);
        
// При необходимости производим изменение кодировки текста
        
$result[$temp[0]] = StringFromBase($temp[0], $temp[1]);
    }
    return
$result;
}
?>


В качестве параметра наша функция принимает «упакованную» строку. В результате ее работы вы получите восстановленный массив, формат которого описан выше.


И, наконец, третья функция, которая позволит быстро и эффективно получать нужные языковые данные, при этом она будет обеспечивать некоторый сервис. Сервисные функции будут заключаться в том, что в случае, когда вы запрашиваете строку на языке, на котором ее не вводили, она вернет вам строку на языке поумолчанию (вот он нам и пригодился). То есть, если вам нужен текст на английском, языком поумолчанию у вас является русский, и строки на английском языке нет – вы получите строку на русском языке. Таким образом, добавление нового языка в проект не вызовет сбоев сайта. Просто временно, пока вы не введете строки на новом языке, они будут выдаваться на языке поумолчанию.


<?php
function LangDataString($lang, $data) {
    
// Задаем пустую строку – здесь будет результат работы
    
$result = '';
    
// В начале создаем массив с перечнем языковых данных
    // Для этого мы разрезаем строку по оставленным ранее меткам (~~)
    
$data = explode('~~', $data);
    
// Организовываем цикл на основе количества выделенных выше элементов
    
for ($i = 0;$i < count($data); $i++) {
        
// Теперь отделяем собственно языковые данные
        // от двухбуквенного кода языка
        
$temp = explode(':=', $data[$i]);
        
// Если текущий язык - язык поумолчанию,
        // сохраняем языковые данные
        
if (DEFAULT_LANG == $temp[0]) {
            
// В случае необходимости, меняем кодировку
            
$default = StringFromBase($temp[0], $temp[1]);
        }
        
// Если текущая строка на языке, который вам нужен,
        // сохраняем языковые данные
        
if ($lang == $temp[0]) {
            
// В случае необходимости, меняем кодировку
            
$result = StringFromBase($temp[0], $temp[1]);
        }
    }
    
// Проверяем содержимое нужного вам языка
    // Если пусто (языковых данных нет),
    // Присваиваем языковые данные языка поумолчанию
    
if (empty($result)) {
        
$result = $default;
    }
    return
$result;
}
?>


В качестве параметра функция принимает все ту же «упакованную» строку из базы. В результате вы получите сообщение на нужном вам языке в нужной кодировке.


ЗАМЕЧАНИЕ:
Очень важно помнить, что если вы выводите информацию в браузер в кодировке отличной от UTF-8, последняя функция даст сбой в том случае, если язык по умолчанию у вас не английский. Это произойдет потому, что кодировка, скажем, русского языка может не совпадать с китайской или испанской кодировкой – в такой кодировке просто нет русских символов. В таком случае вы должны каким-то образом предусмотреть подобный вариант и решить самим, что для вас в данном случае будет лучше. Вы всегда можете определить языком поумолчанию английсикй, при этом проблемы с кодировками не возникнет (см. пункт «Несколько слов о различных кодировках»).


Как отмечено выше, возможности поиска также сохраняются. Искать в таких данных вы можете командой LIKE. Разумеется, придется ограничить поисковый текст символами %, например, '%поиск%'.

Многоязыковые данные большого объема

В предыдущей части мы рассмотрели с вами возможности по хранению небольших объемов многоязычных данных. Давайте посмотрим, какие варианты у нас есть для хранения относительно больших данных.
Прежде всего, давайте внимательно посмотрим на сами данные. Скажем, для примера, мы возьмем с вами новости, предположим, что они у нас появляются синхронно на всех сайтах. Из чего она может состоять?


  • id – уникальный идентификатор;
  • date_start – дата публикации;
  • date_stop – дата, по достижении которой, новость убирается с главной страницы и помещается в архив;
  • visibe – признак видимости новости на сайте;
  • subject – тема новости (заголовок);
  • short_body – короткий вариант новости;
  • full_body – полный вариант новости;

Для наших исследований такой структуры будет достаточно. Совершенно очевидно, что у каждой языковой версии поля id, date_start, date_stop и visible будут одинаковыми и нам нет никакой необходимости их дублировать. У нас есть несколько вариантов решения проблемы.


Первый вариант – создать в таблице, наряду с общими полями, отдельные поля под каждую языковую версию. У этого подхода есть один недостаток – при добавлении в проект нового языка мы столкнемся с тем, что нам придется расширять нашу таблицу, добавляя к ней новые поля. Не всегда это можно сделать безболезненно и могут возникнуть отдельные проблемы, такие же проблемы возникнут при удалении какого-либо языка из проекта. К тому же, это просто неудобно. А что делать, с новостями, которые были добавлены значительно раньше добавления нового языка и, следовательно, тексты к которым для данного языка отсутствуют? Также могут возникнуть трудности с поиском (в том случае, если вы попытаетесь искать сразу во всех языковых версиях сайта) – поиск даст значительную нагрузку на сервер и, думается, будет достаточно медленным, потомучто вместо 1 поля придется обрабатывать Х*колличество языков полей. Разумеется, любую проблему можно решить, но я предлагаю пойти другим путем.


Во втором варианте я предлагаю разделить нашу новость на две таблицы. Первая таблица будет содержать общие данные, а вторая – языковые.


Давайте посмотрим, как будут выглядеть наши таблицы:



Таким образом, на каждую запись из первой таблицы у нас может быть сколько угодно записей во второй таблице. У всех этих записей будет одинаковый news_id, но разный идентификатор языка news_lang.


В результате, нам совершенно нестрашно добавление любого языка в наш проект, поскольку таблицы никак не зависят от количества языков.


Давайте посмотрим, каким образом можно работать с этой связкой таблиц.


Попробуем выбрать новости для главной страницы русской версии сайта.



Жирным шрифтом я выделил основные связывающие элементы.


NOW() в данном случае означает сегодняшнюю дату, например, вы можете ее получить следующей PHP командой: date('Y-m-d') http://php.net/date.


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


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


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



А теперь вызовем на редактирование все языковые варианты новости с номером 10.



Думаю, что примеры довольно наглядны. В дальнейшем, предлагаю вам поэкспериментировать самим.


 
Комментариев нет. [Показать комментарии/форму]