Viacjazyčný web v CakePHP – dynamický obsah
publikované: od: Tibor PiňoV dnešnom článku si ukážeme ako jednoducho pomocou CakePHP vytvoriť viacjazyčný web, ktorý okrem prekladu statického obsahu, ako sú rôzne popisky a doplňujúce informácie, dokáže efektívne pracovať aj s prekladom obsahu uloženého v databáze.
Ako prekladať statický obsah v CakePHP sme si ukázali v článku Viacjazyčný web v CakePHP. I keď je už článok pomerne starý a mnohé veci sa zjednodušili, podstata stále ostáva rovnaká a určite vám odporúčam pred ďalším čítaním najprv prečítať tento článok.
Trieda TranslateBehavior
Mozgom celej viacjazyčnej podpory v CakePHP je trieda TranslateBehavior, ktorá sa stará u ukladanie, aktualizáciu a získavanie viacjazyčného obsahu z databázy. Poďme si v krátkosti predstaviť celý mechanizmus práce s viacjazyčným obsahom. Skutočná sila tohto riešenie spočíva v tom, že trieda TranslateBehaviour funguje ako behavior, čiže magicky pridáva funkcionalitu do vašich existujúcich modelov. To okrem iného znamená, že vôbec nemusíte meniť vaše už existujúce databázové tabuľky.
Princíp fungovania je nasledovný. Predstavme si, že chceme prekladať články v našom systéme. Pridáme teda k nášmu modelu Article behavior TranslateBehavior a určíme ktoré atribúty z tabuľky článkov sa majú prekladať. TranslateBehaviour nám bude pri každom uložení článku do databázy ukladať jazykové mutácie atribútov, ktoré sme určili na preklad, do špeciálnej tabuľky. Čiže od tohto momentu budeme mať k našemu článku dostupné aj preložené jednotlivé časti.
Dosť bolo teórie, poďme sa pozrieť na implementáciu celého riešenia pomocou TranslateBehavior.
Implementácia
Predstavme si situáciu, že máme fungujúci systém článkov a chceme do tohto systému pridať podporu pre viacjazyčné články. Články máme uložené v tabuľke articles a o dáta z tejto tabuľky sa stará model Articles.
1. krok – vytvorenie tabuľky i18n
Ako sme si už spomenuli vyššie, TranslateBehavior funguje ako nadstavba k už fungujúcim modelom a preto prvým a nevyhnutným krokom je vytvorenie tabuľky i18n v našej databáze, do ktorej sa bude ukladať viacjazyčný obsah. Učiniť tak môžeme spustením skriptu v našej databáze, ktorý nájdeme v adresári app/config/sql/i18n.sql. Po spustení tohto skriptu sa v databáze vytvorí tabuľka, ktorá má nasledovnú štruktúru
id int(10) NOT NULL auto_increment,
locale varchar(6) NOT NULL,
model varchar(255) NOT NULL,
foreign_key int(10) NOT NULL,
field varchar(255) NOT NULL,
content mediumtext
Akú úlohu plnia jednotlivé atribúty v tejto tabuľke si vysvetlíme neskôr na konkrétnom príklade.
2. krok – pridanie TranslateBehavior k modelu
<?php
class Article extends AppModel {
public $actsAs = array(
'Translate'
);
}
?>
TranslateBehaviour sa pridáva k modelu ako každá iná behavior funkcionalita a to direktívou $actAs. Po týchto dvoch krokoch máme TranslateBehavior nainštalovaný.
3. krok – prekladáme dynamický obsah
Zatiaľ sme síce určili, že by sme chceli prekladať naše články, ale nikde sme neurčili, ktoré časti nášho článku chceme prekladať. V našom prípade tak urobíme pre časti title a text a to následovne
<?php
class Article extends AppModel {
public $actsAs = array(
'Translate' => array(
'title' => 'title', 'text' => 'text'
)
);
}
?>
Nastavenie modelu máme týmto hotové a môžeme sa pustiť do implementácie view časti. Naše súbory pre vkladanie článkov by vyzerali pred použitím TranslateBehavior následovne
<? echo $form->input('Article.title'); ?>
<? echo $form->input('Article.text',array('cols' =>'10','rows'=>'10','type'=>'textarea')); ?>
Vyššie uvedený kód je štandardný pre vytvorenie input prvkov a preto ho nie je treba bližšie špecifikovať. Poďme sa pozrieť, ako bude vypadať rovnaký kód, ktorý nám umožní vkladať niekoľko jazykových verzií pre input title ako i pre input text
<? echo $form->input('Article.title.slo'); ?>
<? echo $form->input('Article.title.eng'); ?>
<? echo $form->input('Article.text.slo',array('cols' => '10','rows'=>'10','type'=>'textarea')); ?>
<? echo $form->input('Article.text.eng', array('cols' => '10','rows'=>'10','type'=>'textarea')); ?>
Po napísaní článku a jeho viacjazyčných mutácií môžeme článok uložiť obvyklým spôsobom volaním metódy $this->Article->sanve() z controlleru ArticlesController. To znamená, že v prípade controlleru nie je potrebné pri ukladaní článku vykonávať žiadne zmeny.
Poďme sa pozrieť, ako bude vypadať naša databáza po uložení takéhoto článku a čo sa v nej zmenilo. Predstavme si, že sme odoslali formulár s hodnotami titulok sk, text sk pre slovenskú mutáciu a title en a text en pre anglickú mutáciu. Naše tabuľku budú vypadať následovne
tabuľka articles
+----+-------------+--------------------+--------+---------------------+
| id | title | text | user | created |
+----+-------------+--------------------+--------+---------------------+
| 1 | titulok sk | text sk | Linus | 2010-02-06 |
+----+-------------+--------------------+--------+---------------------+
tabuľka i18n
+----+--------+-------+---------+-------------+------------+
| id | locale | field | model | foreign_key | content |
+----+--------+-------+---------+-------------+------------+
| 1 | slo | title | Article | 1 | titulok sk |
| 2 | eng | title | Article | 1 | title en |
| 3 | slo | text | Article | 1 | text sk |
| 4 | eng | text | Article | 1 | text en |
+----+--------+-------+---------+-------------+------------+
Ako môžeme vidieť TranslateBehavior funguje podľa očakávania a ako bolo povedané, k nášmu článku udržuje v špeciálnej tabuľke i18n jazykové mutácie. Čo znamenajú jednotlivé stĺpce tabuľky? Stĺpec locale ukladá označenie jazykovej mutácie daného riadku, hodnoty pochádzajú z triedy l10n z premennej $__l10nMap a rovnako sa používajú aj vo view pre jednotlivé input prvky. Stĺpec field označuje názov atribútu z pôvodnej tabuľky články. Čiže v našom prípade sa jedná o title a text, ktoré pôvodne nájdeme v tabuľke articles. Stĺpec foreign_key predstavuje cudzí kľúč z tabuľky articles::id. Stĺpec content obsahuje samotnú jazykovú mutáciu. Myslím si, že viac nie je potrebné vysvetľovať celý mechanizmus a môžeme rovno prejsť na zobrazovanie jednotlivých jazykových mutácií.
4. krok – zobrazujeme viacjazyčný obsah
Prvým krokom pre zobrazenie viacjazyčného obsahu je nastavenie routovacích pravidiel tak, aby sme mohli v našej aplikácii prepínať medzi jednotlivými jazykovými verziami. Pri tomto sa budeme riadiť pravidlom, že zobrazenie jazykovej mutácie obsahu je úplne nezávislé na užívateľových nastaveniach – čiže aktuálny jazyk aplikácie budeme určovať podľa URL a nie podľa hodnôt uložených buď v cookies alebo v sessions.
Routovanie
Rozhodli sme, že jazyk aplikácie bude určovať URL. Formát URL bude nasledovný http://www.tvoja-adresa.sk/jazyk/clanok/id
Routavacie pravidlo bude vyzerať následovne
Router::connect('/:lang',array('controller'=>'pages','action'=>'display','home'),array('lang' => 'sk|en'));
Router::connect('/:lang/clanok/:id',array('controller'=>'articles','action'=>'view'),array('pass'=>array('id'),'lang' => 'sk|en'));
Link helper
Po nastavení routovacieho pravidla budeme potrebovať link helper, ktorý nám ku každému odkazu pridá aktuálny jazyk. Možno sa pýtate, prečo potrebujeme helper a nebude priamo používať metódu $html->link() následovne
<? $html->link(__('o nás',true),array('controller'=>'articles','action'=>'view','id'=>'o-nas','lang'=>$language)); ?>
Je to z dôvodu, aby sme nemuseli zakaždým posielať tejto metóde hodnotu aktuálneho jazyka a navyše sa v budúcnosti môže náš kód zmeniť a tým by sme museli ručne upravovať všetky odkazy. Náš helper si teda nazveme LangHelper a uložíme ho do súboru app/view/helpers/lang_helper.php
<?
class LangHelper extends AppHelper {
public $helpers = array('Html');
public function link( $title, $url = NULL, $htmlAttributes = array ( ), $confirmMessage = false, $escapeTitle = true ) {
if(@$this->params['lang']) {
$url['lang'] = $this->params['lang'];
} else {
$url['lang'] = 'sk';
}
return $this->Html->link($title,$url,$htmlAttributes,$confirmMessage,$escapeTitle);
}
}
?>
Následne môžeme vytovoriť odkaz na náš článok
<? echo $lang->link(__('o nás',true),array('controller'=>'articles','action'=>'view','id'=>'1')); ?>
Vyššie uvedený odkaz nám vygeneruje URL v tvare http://www.tvoja-adresa.sk/sk/clanok/1 za predpokladu, že aktuálny jazyk aplikácie je sk. Ako je možné, že sa vygenerovala adresa práve v tomto tvare? Je to z dôvodu, že CakePHP automaticky pozná podľa nastavených routovacích pravidiel, aký odkaz na akú URL chceme vytvoriť.
Controller a prepínanie jazyka
Po nastavení routovacích pravidiel sa môžeme pustiť do samotného nastavenia jazyka v našej aplikácii. Jazyk sa nastavuje direktívou Configure::write(‘Config.language’,$locale) kde premenná $locale predstavuje hodnotu z triedy l10n a jej premennej $__l10nCatalog. Nastavenie budeme vykonávať v AppController v metóde beforeFilter()
<?
class AppController extends Controller {
public function beforeFilter() {
$this->set_language();
}
private function set_language() {
$code = @$this->params['lang'];
if(@$code) {
App::import('l10n');
$l10n = new L10n();
$language = $l10n->catalog($code);
$locale = $language['locale'];
} else {
$locale = 'slo';
}
Configure::write('Config.language',$locale);
}
}
?>
Následne môžeme v našej aplikácii prepínať jazyk pomocou URL http://www.vasa-adresa.sk/sk pre slovenský jazyk alebo http://www.vasa-adresa.sk/en pre anglický jazyk
Zobrazujeme článok
Článok môžeme načítať a následne zobraziť štandardným spôsobom, ako ho poznáme z bežného používania. V ArticlesController si načítame článok
public function view($seo_link = null) {
$article = $this->Article->findBySeo_link($seo_link);
$this->set(compact("article"));
}
A následne ho môžeme zobraziť vo view
<article class="article">
<h1><? echo $article['Article']['title']; ?></h1>
<div><? echo $article['Article']['text']; ?></div>
</article>
Ako ste si určite všimli, nikde sme pri zobrazení neurčovali aká jazyková mutácia sa má zobrazovať. CakePHP to robí v tomto prípade automaticky a z databázy vyťahuje jazykovú mutáciu podľa direktívy Configure::write(‘Config.language’,$locale) a premenná $article má tvar ako v prípade, keď sme TranslateBehavior nepoužívali, avšak s tým rozdielom, že vždy obsahuje jazykovú mutáciu podľa aktuálne nastaveného jazyka.
Načo si dať pozor
Narazil som na menšiu komplikáciu pri editácii článku. Keď sa pokúšate vybrať všetky jazykové mutácie prostredníctvom jedného SQL dotazu, CakePHP nám vráti dáta v nasledovnom formáte
Array
(
[Article] => Array
(
[id] => 1
[title] => titulok sk
[text] => text sk
[locale] => slo
)
[title] => Array
(
[0] => Array
(
[id] => 44
[locale] => eng
[model] => Article
[foreign_key] => 12
[field] => title
[content] => title en
)
[1] => Array
(
[id] => 43
[locale] => slo
[model] => Article
[foreign_key] => 12
[field] => title
[content] => titulok sk
)
)
[text] => Array
(
[0] => Array
(
[id] => 47
[locale] => eng
[model] => Article
[foreign_key] => 12
[field] => text
[content] => text en
)
[1] => Array
(
[id] => 48
[locale] => slo
[model] => Article
[foreign_key] => 12
[field] => text
[content] => text sk
)
)
)
To znamená, že jazykové mutácie sa nachádzajú v premenných $article['title'] a $article['text'], preto ich treba ešte preformátovať do formátu $article['Article']['title']['jazyk']. Môžeme to urobiť následovne
$this->data = $this->Article->findById($id);
$title = array();
$text = array();
foreach ($this->data['title'] as $item) {
$title[$item['locale']] = $item['content'];
}
foreach ($this->data['text'] as $item) {
$text[$item['locale']] = $item['content'];
}
$this->data['Article']['title'] = $title;
$this->data['Article']['text'] = $text;
Našiel som nejaké návody, žeby sa pri editácii mala nastaviť premenná locale ako pole, ktoré bude obsahovať zoznam všetkých jazykov a to následovne Configure::write(‘Config.language’,array(’slo’,'eng’)) avšak mne tento spôsob nefungoval. Neviem, možno je to len moja neznalosť, možno bol problém niekde inde.
Týmto je náš článok u konca. Myslím si, že CakePHP ponúka elegantný spôsob ako sa vyrovnať s viacjazyčným obsahom a ako v mnohých prípadoch, nám dokáže ušetriť množstvo času a práce. Rád uvítam vaše nápady a pripomienky v diskusii k článku.