Как да си направим многоезичен сайт?
04-07-2013
Здравейте, наскоро гледах в един форум една тема, в която потребителя питаше да си оправи код за мулти-езичен сайт, който отново е предоставен в същия сайт.
Реших да проверя какви скриптове са предоставени в сайта и честно да ви кажа, не останах много впечатлен, затова реших да направя свой урок,

Та, нека започваме!

Файловете, които ще са ви нужни са, както следват:

- Config/
-- config.php
- Languages/
-- XX.php
- function.php
- index.php
- main.php


Като XX.php са двубуквените инициали на езиците(Пример: bg, en, fr, de, tr и т.н.)

Ето как изглежда конфигурационния файл



<?php

/**
* Config
* * Конфигурационен файл *
*
*
* @author relax4o
* @copyright 2011
*/

return array(

'language' => array(
'default' => 'bg'
),

'cookie' => array(
'path' => '/',
'expire' => 7200,
'domain' => '',
'secure' => FALSE,
'httponly' => TRUE
)
);



/** CONFIGURATION **/

function loadConfigFile($configFile = false) {

$configOptions = array();

if ($configFile == false)
$configFile = 'config';


if(is_readable("Config/{$configFile}.php")){
$configOptions = require ("Config/{$configFile}.php");
}

return $configOptions;
}

function getConfig($index, $configFile = false) {

$configOptions = array();
$configOptions = loadConfigFile($configFile);

if(strpos($index, '.')){
$i = $configOptions;
$parts = explode('.', $index);

foreach($parts as $val){
if(isset($i[$val]))
$i = $i[$val];
else
break;
}

return $i;
}

if(isset($configOptions[$index]))
return $configOptions[$index];
}

/** END CONFIGURATION **/



Това са управляващите функции за конфигурационните файлове. Относно тях няма да задълбавам в този урок, понеже ще създам нов такъв. Ще представя моя начин за управление на конфигурационните файлове.

Та нека продължим със същественото:

Първо нека създадем функцията, която зарежда езиковия файл.

Всеки запознат с основите на PHP би трябвало да знае как се създава функция:



function loadLanguage() {
//...
}



Това ще е основната функция за зареждане на езиковите файлове.

Ето как изглежда вътрешността на функцията

loadLanguage()

$languageData = array();

$currentLanguage = (!isset($_COOKIE['language']))
? getConfig('language.default')
: $_COOKIE['language'];


if(empty($languageData))
$languageData = require ("Languages/{$currentLanguage}.php");

return $languageData;



Сега нека обясня подробно.
Първото нещо, което правим е да създадем празен масив, в който ще вкарваме данните, които взимаме от езиковия файл. Понеже самия езиков файл е от тип масив, ни е нужен празен масив, който да попълваме.
Следващото нещо, което трябва да се направи е да вземем текущия език. За тази цел, първо трябва да проверим дали има вече зададена бисквитка, в която са записани инициалите на езика. Ако не е зададена бисквитка с име language, то взимаме езика, който е зададен по подразбиране в конфигурационния файл, който взимам с функцията, която съм показал по-горе и която ще обясня в друг урок.


getConfig('language.default')



След тези действия идва ред да проверим дали $languageData случайно няма някакви данни вътре. За тази цел се подсигуряваме с empty(), който очакваме да върне TRUE, т.е. да покаже, че масива е празен. Ако е празен можем свободно да му вкараме данните на масива на съответния език вътре, за да ги използваме в друга функция, която ще я нарека getLanguageText(), която ще обясня по-подробно след това.

Както виждате, викам езиковия файл чрез require и го присвоявам на празния масив. Как става това ? Ами много просто става, когато самия файл е връщащ. Какво имам предвид ? Сега ще Ви покажа как изглежда един езиков файл.

Languages/bg.php

<?php

return array(
'title' => 'Някакво заглавие',
'menu' => array(
'services' => 'Услуги',
'prices' => 'Цени',
'clients' => 'Клиенти',
'aboutus' => 'За нас',
'contacts' => 'Контакти'
)

);

?>



Както виждате в самия файл направо връщам масив чрез return. Не го обработвам, не му правя нищо, просто го връщам. Това ми позволява, когато извикам файла чрез require/include да го присвоя на променлива без никакъв проблем.
След това ще обясня подробно как да използваме точно тези файлове.

Да продължим нататък!
След като присвоим самия масив го връщаме чрез return и приключваме с тази функция.

Сега нека пристъпим към следващата функция, която има за цел да сменя езика.


changeLanguage($language, $uri)


function changeLanguage($language, $uri) {

if(!isset($_COOKIE['language'])){
setcookie('language', $language,
time() + getConfig('cookie.expire'),
getConfig('cookie.path'),
getConfig('cookie.domain'),
getConfig('cookie.secure'),
getConfig('cookie.httponly')
);
}
else{
if(isset($_COOKIE['language']) && $_COOKIE['language'] !== $language){
setcookie('language', $language,
time() + getConfig('cookie.expire'),
getConfig('cookie.path'),
getConfig('cookie.domain'),
getConfig('cookie.secure'),
getConfig('cookie.httponly')
);
}
}

header('Location: ' . $uri);
}


Тази функция приема 2 параметъра(аргумента). Единия е инициалите на държавите, втория приема страницата, която сме сменили езика, за да можем да върнем потребителя на същата страница, на която е бил.

Та, какво прави тази функция. Проверява дали има зададена бисквитка с име language и ако няма създаваме такава, като взимаме от конфигурационния файл, настройките за бисквитките и ако всичко мине добре пращаме header() с локация страницата, на която е бил потребителя на сайта.
В else блока, отново правим проверка дали е сетната бисквитка и ако е сетната такава, то правим проверка дали избрания език е различен от този записан на бисквитката. Ако е различен, то се налага да променим бисквитката, а именно да запишем новата стойност, която е подадена на $language. Ако е сетната бисквитка и съвпадат езиците, то се прескачат двата блока и директно пускаме header() със страницата, на която е бил посетителя.


И сега, дойде ред на най-интересната функция - извеждане на текста.

getLanguageText($index, $delimeter = ' ')


function getLanguageText($index, $delimiter = ' ') {

$languageData = array();
$allIndexes = array();
$languageData = loadLanguage();

if(is_array($index)) {
foreach($index as $values) {
array_push($allIndexes, getLanguageText($values));
}
return join($delimiter, $allIndexes);
}

if (strpos($index, '.')) {
$data = $languageData;
$parts = explode('.', $index);

foreach($parts as $value) {
if(isset($data[$value]))
$data = $data[$value];
else
break;
}

return $data;
}

if(isset($languageData[$index]))
return $languageData[$index];
}


Ето какво прави и тя. Тази функция приема 2 параметъра - $index и $delimiter. Първия аргумент може да получава, както масив, така и нормално да се подава низ на функцията. След като обясня функцията ще покажа как се използва най-пълноценно. Втория параметър, както изглежда е някакъв разделител, който по подразбиране съм го задал да е празно място(space).
Първото нещо, което трябва да направим е да си дефинираме две променливи, които ще бъдат от тип масив(array). Това са $languageData и $allIndexes. Присвоявам им първоначално празен масив, за да избегнем досадни NOTICE грешки от рода, че не е дефинирана еди-си-каква променлива.
На първата променлива от тип масив и присвояваме вече заредения език, чрез loadLanguage(), чиято функция също връща масив.
След това имаме 3 отделни проверки, които служат за 3 отделни начина на ползване на функцията.
Да започнем с първия начин:

Първи начин


function getLanguageText($index, $delimiter = ' ') {

$languageData = array();
$allIndexes = array();
$languageData = loadLanguage();

#------------------------------------------------------
if(is_array($index)) {
foreach($index as $values) {
array_push($allIndexes, getLanguageText($values));
}
return join($delimiter, $allIndexes);
}
#------------------------------------------------------

if (strpos($index, '.')) {
$data = $languageData;
$parts = explode('.', $index);

foreach($parts as $value) {
if(isset($data[$value]))
$data = $data[$value];
else
break;
}

return $data;
}

if(isset($languageData[$index]))
return $languageData[$index];
}


Идеята на този начин е да можем да извикваме от няколко индекса текстове, които да ги свържем чрез разделителя($delimiter). След всички обяснения ще давам примери за отделните начини.
Какво се случва с този условен оператор ? Ами първо на първо проверяваме дали аджеба $index е масив. Е да речем, че за случая е масив. Така! Пропускаме в блока... понеже това е масив, е нужно да го завъртим чрез някакъв цикъл. Аз лично съм избрал най-простия за този случай, а именно foreach.
Докато въртим цикъла, всяка стойност($value) я добавяме чрез array_push в все още празния масив $allIndexes, като вкарваме стойност, която отново минава през същата функция, след което чрез join(implode) свързваме всички вкарани стойности в масива, чрез $delimiter, който както казах по подразбиране е празно място(space).
Но какво се случва, когато минаваме стойностите на масива през същата функция ? Ами, когато се опрости самия масив, то той получава данни, които отговарят на следващите 2 начина, според които определяме какво да върнем. Нека обясним сега и другите 2 начина, за да ви се опресни какво по-точно става, когато минаваме повторно с функцията.

Втори начин


function getLanguageText($index, $delimiter = ' ') {

$languageData = array();
$allIndexes = array();
$languageData = loadLanguage();

if(is_array($index)) {
foreach($index as $values) {
array_push($allIndexes, getLanguageText($values));
}
return join($delimiter, $allIndexes);
}

#------------------------------------------------------
if (strpos($index, '.')) {
$data = $languageData;
$parts = explode('.', $index);

foreach($parts as $value) {
if(isset($data[$value]))
$data = $data[$value];
else
break;
}

return $data;
}
#------------------------------------------------------

if(isset($languageData[$index]))
return $languageData[$index];
}


За втория начин са ни нужни данни от рода: "menu.price, category.web, category.desktop" и т.н. Когато има точка това означава, че търсим в масив, чиято стойност на ключ също е масив със ключове и стойности. Можем да си позволим дори и нещо от рода: "category.web.client.language", което трябва да изглежда нещо такова:



array("category" =>
array("web" =>
array("client" =>
array("language" => "Език");


И да изведе в случая "Език". Но това естествено е просто глупав пример. Не е казано, че в един под масив е позволено само една стойност. Примерно в подмасив web, както има client, което в случая е за client-side, така може да има и server, за server-side.
Какво се случва в самия блок ?
Първо си създаваме нова променлива, която съм я кръстил $data и която приема вече взетия от loadLanguage() масив.
Следва да си разделим на части $index, чрез explode(), за да можем да вземем всички части поотделно, като ги запазваме в автоматично генерирания масив $parts.
След като разделим индексите започваме да ги минаваме през цикъл... отново. Всяка една част бива проверена като им се прави проверка дали съществува такъв индекс. Ако съществува го присвояваме на $data(все пак $data няма конкретен тип, който да приема), ако ли не прекъсваме цикъла и връщаме едно НИЩО. И накрая остава да върнем резултата, който е запазен последно на $data.
Толкова по втори начин.

Трети начин


function getLanguageText($index, $delimiter = ' ') {

$languageData = array();
$allIndexes = array();
$languageData = loadLanguage();

if(is_array($index)) {
foreach($index as $values) {
array_push($allIndexes, getLanguageText($values));
}
return join($delimiter, $allIndexes);
}

if (strpos($index, '.')) {
$data = $languageData;
$parts = explode('.', $index);

foreach($parts as $value) {
if(isset($data[$value]))
$data = $data[$value];
else
break;
}

return $data;
}

#------------------------------------------------------
if(isset($languageData[$index]))
return $languageData[$index];
#------------------------------------------------------
}


Този начин приема само единични стойности, като например: "title, author, aboutus" и т.н.
Тука няма нищо особено, просто проверяваме дали подадения индекс съществува в генерирания масив в началото, и ако съществува го връщаме, за да го изведем.

Приключихме с функциите! Сега идва ред да покажем как можем да използваме тази "джаджа".
За целта най-просто е като се използва диспечер. Какво представлява диспечера? Ами нищо особено - файл за управление на страниците. Правят се нужните проверки и ако минат отваряме желаната страница.
Защо използвам диспечер? Ами за да избегна навсякъде да слагам един и същи код, навсякъде да запазвам URI-то в сесия, за да връщам потребителя на същата страница при смяна на език и т.н.

Ето какво представлява моя набързо начаткан диспечер

index.php

<?php

/**
* Index
* * Диспечер *
*
* @author relax4o
* @copyright 2011
*/
session_start();
require_once ('functions.php');

$url = isset($_GET['url']) ? $_GET['url'] : NULL;
$lang = (isset($_GET['lang']) && $_GET['lang'] != '') ? $_GET['lang'] : NULL;


if (file_exists("{$url}.php") && !is_dir($url)) {
$_SESSION['uri'] = $_SERVER['REQUEST_URI'];
require_once ("{$url}.php");
}
elseif (isset($lang) && !is_dir($lang)) {
if (file_exists("Languages/{$lang}.php")) {
changeLanguage($lang, $_SESSION['uri']);
}
else {
# ако не съществува вадим или записваме грешка 404 (Не е намерен)
}
}
else {
$_SESSION['uri'] = $_SERVER['REQUEST_URI'];
require_once ("main.php");
}


Нищо особено, имам 2 променливи и 3 блока за изпълняване, но нека вървим стъпка по стъпка.
Ето какво се случва: като за начало си стартираме сесия, понеже по-надолу в кода ще ни е нужно използването на сесии, за запазване на текущата страница.
Веднага след стартирането на сесията извикваме functions.php, където са функциите, които до сега описвах и обяснявах.
След викането идва ред да си дефинираме 2 променливи. Едната е $url за страниците, а другата е $lang, която приема езика при смяна. И двете взимат информация от метода GET. Проверките, които извършвам са нищо особено, просто проверявам дали съществува метод GET подаден в url бара и ако са зададени ги присвоявам на променливите, иначе присвоявам NULL. Разбира се, вие можете да си направите и други проверки, можете и чрез rewrite mod да си промените начина на въвеждане и т.н. Аз лично представям най-опростен вариант, който е само показен.

Следват проверките. С първата проверка проверяваме дали съществува файл с име $url, т.е. с име, което е подадено в адрес бара и ако съществува проверяваме дали не е директория(папка). Ако върне TRUE условието запазваме на сесия $_SESSION['uri'] текущата страница и след това извикваме файла, за да отворим страницата.
Следващата проверка е за езика, която нас ни интересува в случая повече. Там проверяваме дали е сетната променливата, ако е сетната и не е директория, то правим проверка дали съществува файла(езика), който е подаден на ?lang=. Ако съществува, то викаме функцията changeLanguage() като за първи параметър и подаваме посочения език, а като втори параметър и подаваме вече създадената сесия $_SESSION['uri'], за да сме сигурни, че при смяна на езика ще се върнем на същата страница.
Ако пък не съществува такъв език, то или извеждаме грешка, или си я записваме за нас, или не правим нищо.

И третия блок е следния: запазваме отново в сесия текущата страница, която в случая е main.php(понеже index.php ми играе роля на диспечер) и си извикваме main.php.

Е това е за диспечера.

Сега нека представим как се използва взимането на текст от избрания език.

Нека използваме езиков файл, за да можем да се ориентираме кое какво ще извежда.




<?php

/**
* Български език
* * Файла, който съдържа българските фрази *
*
* @author relax4o
* @copyright 2011
*/

return array(

'title' => 'Заглавие',
'subtitle' => array(
'toString' => 'под заглавие',
'subtitle2' => 'под заглавие 2',
'more' => array(
'more1' => 'още 1',
'more2' => 'още 2')
),

'menu' => array(

'services' => array(
'toString' => 'Услуги',
'test' => array(
'test' => 'тест1')
),

'prices' => 'Цени',
'clients' => 'Клиенти',
'aboutus' => 'За нас',
'contacts' => 'Контакти'
)

);

?>



Първи начин - множествено викане чрез масив и разделител



// пример 1 - с разделител
echo getLanguageText(array('title', 'subtitle.toString'), ' - ');
//резултат: Заглавие - под заглавие

// пример 2 - без разделител(празно място по подразбиране)
echo getLanguageText(array('title', 'subtitle.toString', 'subtitle.subtitle2'));
//резултат: Заглавие под заглавие под заглавие 2

// пример 3 - влизане в дълбочина с разделител
echo getLanguageText(array('title', 'subtitle.more.more1'), ',');
//резултат: Заглавие,още 1




Втори начин - взимане на индекси в дълбочина


// пример 1 - първостепенно
echo getLanguageText('menu.prices');
// резултат: Цени

// пример 2 - второстепенно
echo getLanguageText('menu.services.toString');
// резултат: Услуги

// пример 3 - многостепенно
echo getLanguageText('menu.services.test.test');
// резултат: тест1




Трети начин - обикновен начин :D


echo getLanguageText('title');
// резултат: Заглавие

Това са 3 начина за ползване. Сега нека да обясня една подробност. Може би забелязахте, че на няколко места използвам toString като краен индекс. Това е така, защото например Услуги ми е с под менюто, но аз искам да взема всъщност връхната точка, която е Услуги(services) и за да не правим нов индекс в масива от рода servicesText => 'Услуги' просто правете в самия подмасив ключ toString и името на самия индекс.
Не съм сигурен дали го обясних като хората, но се надявам да сте ме разбрали какво имам предвид.

Е, това е. Май урока стана учудващо дълъг за такова нещо, но се надявам да е полезен за всички.

Моля, ако представяте моя урок във форум или някъде другаде да споменавате автора и от къде сте го взели.

А ето и ДЕМО

Линк към целия скрипт

Оригинален източник: www.relax4o.com

Урока е мой и е авторски директно копиран от блога ми. Моля копирането му в други сайтове да става след удобрението ми или поне да бъде посочен оригиналния източник на статията.


Извинявам се, че кода е така наблъскан, но липсва преформатиране на скриптовете.

/ Трябва да сте регистриран за да напишете коментар /
От: stoqnski
14:54 26-07-2013
Супер урок браво !
От: ivko
22:30 30-03-2015
Не работи
От: Revelation
19:22 31-03-2015
Да, аз пускам не работещи кодове. Умно. Кажи кое не ти работи, за да ти се помогне.
1