W ostatnim projekcie przygotowałem aplikacje bez użycia ciasteczek (ang. cookies) i sesji, ale tylko za pomocą przełączania parametru w adresie (URL).
Dlaczego takie rozwiązanie?
Ponieważ sesje i ciasteczka działają świetnie, ale nie rozwiązują naszego problemu tłumaczenia linków poprzez specjalną funkcję z GetText (
__();
) oraz przede wszystkim wygenerowany link nie określa języka w przypadku wygaśnięcia ciasteczek i/lub sesji.W wielu przykładach ustawianie wersji językowej odbywa się w Kontrollerze aplikacji (Controller) lub jego Komponencie (Component). W naszym przypadku to nie zadziała prawidłowo gdyż używamy tłumaczeń w Routerze aplikacji (routes.php).
Niektóre z artykułów poruszających tą tematykę rozwiązuje problem poprzez ustawienie języka i odświeżenie strony (
$this->redirect();
). Mnie osobiście takie rozwiązanie się nie podoba!Trasowanie linków powinno wyglądać jak te:
/ -> strona startowa dla domyślnej wersji językowej (język polski) /en -> strona startowa dla angielskiej wersji językowej /de -> strona startowa dla niemieckiej wersji językowej /rejestracja -> strona z rejestracji w języku domyślnym /en/register -> strona z rejestracji w języku angielskim /de/anmeldung -> strona z rejestracji w języku niemieckimNatomiast celowo nie powinny działać linki jak te poniżej:
/pl/register -> polska wersja ze slugiem w wersji angielskiej /en/anmeludng -> wersja angielska ze slugiem w wersji niemieckiejDlaczego tak? W mojej opinii, nie powinno mieć miejsca mieszanie się wersji językowych w linkach (głównie za sprawą SEO).
Moje rozwiązanie polega na ustawieniu języka przed uruchomieniem Kontrollera (Controller) i to jest moją największa niezadowalająca część całego kodu, później napisze dlaczego.
Przygotujmy naszą aplikację.
bootstrap.php (app/Config/):
define('DEFAULT_LANGUAGE', 'pl'); Configure::write('Config.languages', array( 'pl' => 'Język polski', 'en' => 'English version', 'de' => 'Deutsch Sprache'));W pierwszej linijce definiujemy stałą (ang. constans), zgodnie z klasą L10n jako język polski. Następnie przygotowujemy tablicę konfiguracyjną z możliwymi dostępnymi językami w naszym systemie.
Dwuliterowe kody języków przyjąłem zgodnie ze standardem ISO 632-9.
routes.php (app/Config/):
$language = substr(Router::url(''), 0, 2); $languages = array_keys(Configure::read('Config.languages')); if(!in_array($language, array_diff($languages, array(DEFAULT_LANGUAGE)))) { $language = DEFAULT_LANGUAGE; $schema = ''; } else { $schema = '/:language'; } Configure::write('Config.language', $language) /* PagesController */ Router::connect('/', array( 'controller' => 'pages', 'action' => 'display', 'home')); Router::connect('/:language', array( 'controller' => 'pages', 'action' => 'display', 'home'), array( 'language' => implode('|', $languages))); /* UsersController */ Router::connect($schema .'/'. __('rejestracja', true), array( 'controller' => 'users', 'action' => 'register'), array( 'persist' => array( 'language')));Tak, właśnie te pierwsze linijki powyżej są najbardziej nieprofesjonalne. Ich zadaniem jest sprawdzenie jaki kod języka w adresie reprezentuje w zapisanej konfiguracji
Config.languages
. Następnie tworzymy schemat do trasowania oraz zapisanie w konfiguracji jaki dany język został wybrany (Config.language
).W przykładzie powyżej pokazałem przykładowe 3 schematy trasowania linków, dla strony startowej z językiem domyślnym oraz wybranym przez użytkownika, a także link do strony z formularzem rejestracyjnym.
Zastosowałem tutaj parametr
persist
. Ma on na celu dodanie parametru language zgodnie ze schematen :/language
. Więcej w Router API.AppHelper.php (app/Views/Helpers/):
function url($url = null, $full = false) { if($this->params['language'] == DEFAULT_LANGUAGE) { unset($this->params['language']); } return parent::url($url, $full); }Musimy nadpisać metodę
url();
w celu wyeliminowania parametru language, gdy parametr ma wartość taką jak domyślny język. Powodem tej zmiany jest uniknięcie duplikowania linków (/rejestracja i /pl/rejestracja).Teraz możemy przetestować nasze linki za pomocą kodu umieszczonego w naszym szablonie:
default.ctp (app/Views/Layouts/):
foreach(Configure::read('Config.languages') as $code => $language) { echo $this->Html->link($language, array( 'controller' => 'pages', 'action' => 'display', 'home', 'language' => $code)) .' '; } echo $this->Html->link(__('register', true), array( 'controller' => 'users', 'action' => 'register'));Statyczną treść i trasowanie mamy za sobą.
Tłumaczenie statycznej treści oraz obsługa programu Poedit zostały dobrze opisane w książce "CakePHP 1.3 Programowanie aplikacji. Receptury" napisanej przez Mariano Iglesiasa (oryginalny tytuł w języku angielskim to "CakePHP 1.3. Application Development Cookbook").
Teraz przyszedł czas na treści dynamiczne przechowywane w bazie (np. MySQL). CakePHP domyślnie ustawia zmienną
$locale
w Modelu za pomocą zmiennej pobranej z konfiguracji (Config.language
). Niestety te rozwiązanie nie uwzględnia wyświetlania treści jeśli ta nie została przetłumaczona, natomiast dobrym rozwiązaniem jest wyświetlenie w tym przypadku treści oryginalnej. Jak to zrobić znajdziemy poniżej:AppController.php (app/Controller/):
if(Configure::read('Config.language') !== DEFAULT_LANGUAGE) { $this->{$this->modelClass}->locale = array(Configure::read('Config.language'), DEFAULT_LANGUAGE); } else { $this->{$this->modelClass}->locale = DEFAULT_LANGUAGE; }Powyższy kod należy umieścić w metodzie
beforeFilter();
najlepiej na samym początku.Dlaczego ustawiamy zmienną
$locale
jeśli jest ona przypisywana domyślnie? Dlatego, iż domyślnie Cake nie wyświetli nam treści oryginalnej w przypadku braku jej tłumaczenia, natomiast zastosowana tutaj tablica z pierwszym parametrem wybranego języka oraz drugim dla języka domyślnego rozwiązuje ten problem.Nie zapomnijmy tutaj o stworzeniu tabeli i18n w bazie danych oraz dodaniu Translate Behavior wraz z określonymi polami do tłumaczenia dynamicznego.
Teraz należy rozwiązać tworzenia przyjaznych linków (tzw. slugs). W przypadku jeśli nasze tabele w bazie zawierają pola name oraz slug, to stworzymy slug właśnie z pola name. Kod poniżej zilustruje tę zależność:
AppController.php (app/Controller/):
if(!empty($this->request->data) && $this->{$this->modelClass}->hasField('name') && $this->{$this->modelClass}->hasField('slug')) { $this->request->data[$this->modelClass]['slug'] = $this->{$this->modelClass}->createSlug($this->request->data[$this->modelClass]['name'], $this->id); }Modyfikujemy tablice z danymi poprzez metodę
createSlug();
, którą zaraz stworzymy w Modelu aplikacji. Dodam, że powyższy kod należy umieścić także w metodzie beforeFilter();
, zaraz po ustawieniu zmiennej $locale
.AppModel.php (app/Model/):
function createSlug($string, $id = null, $field = 'slug', $separator = '_') { $slug = substr(strtolower(Inflector::slug($string, $separator)), 0, 250); if(!is_null($id)) { $params = array( 'conditions' => array( $field => $slug, 'not' => array( $this->name .'.id' => $id)), 'recursive' => -1); } else { $params = array( 'conditions' => array( $field => $slug), 'recursive' => -1); } $i = 0; while(count($this->find('all', $params))) { if(!preg_match('/'. $separator .'{'. strlen($separator) .'}[0-9]+$/', $slug)) { $slug .= $separator . ++$i; } else { $slug = preg_replace('/[0-9]+$/', ++$i, $slug); } $params['conditions'][$field] = $slug; } return $slug; }Na początku z pola name tworzymy slug za pomocą klasy Inflector o długości maksymalnej 250 znaków. Następnie tworzymy wyrażenia wyszukujące, inne w przypadku dodania nowego rekordu i inne dla edycji już istniejącego (
$this->id
) aby przepuścić wynik przez pętle while();
. Pętla ma za zadanie stworzyć kolejny unikalny link (w przypadku istniejącej identycznej wartości pola slug dodanie kolejnego numeru). W takim przypadku obliczymy numer wykorzystując inkrementację (++$i;
) i dodamy przyrostek, w postaci: _numer.Teraz do przetestowania dynamicznego trasowania linków stworzymy przykładowy schemat: routes.php (app/Config/):
Router::connect($schema .'/'. __('artykuly', true) .'/:slug', array( 'controller' => 'articles', 'action' => 'view'), array( 'pass' => array( 'slug'), 'persist' => array( 'language')));I to wszystko! Zapraszm do testowania i komentowania.
Całość testowana była na PHP 5.4/CakePHP 2.2.4 i domyślnie ustawionym modułem Apache mod_rewrite.
Kawał dobrej roboty.
OdpowiedzUsuńDzięki!
UsuńWitam. Świetny artykuł, podziękowania dla Autora. Mam jednak pytanie odnośnie tej części routes.php: /* UsersController */
OdpowiedzUsuńRouter::connect($schema .'/'. __('Zaloguj', true), array(
'controller' => 'users',
'action' => 'login'), array(
'persist' => array(
'language')));
Chodzi o to, że mam link, który w pasku ładnie uzyskuje przyjazny wygląd i w wersji językowej domyślnej przerzuca na właściwą stronę (/Zaloguj). Problem jest wtedy kiedy, mamy wybraną wersję angielską (/en/Zaloguj- wtedy pojawia się komunikat missing controller. Co może być przyczyną, że nie chce wczytać strony logowania???? Ponadto, jest drugi błąd w części kodu: $language = substr(Router::url(''), 0, 2); Przerobiłem na $language = substr(Router::url(''), -2); i zadziałało. Pozdrawiam i proszę o podpowiedź.
Dzięki!
UsuńLinia z kodem $language = substr(Router::url(''), 0, 2); jest jak najbardziej prawidłowa.
Sprawdź co zwraca Router::url(''); Powinien zwrócić ciąg znaków zaczynający się od "en/...". Jeśli testujesz to na localhost/ to pewnie w ciągu znaków zwraca jeszcze nazwę folderu...
Dzięki za odpowiedź. Poprawiłem w kodzie na $language = substr(Router::url(''), 0, 2); i Router::url(''); zwraca mi nazwę katalogu i język, czyli u mnie (/portal/pl lub /portal/en). I faktycznie routing wczytuje strony w wersji językowej ale nie tłumaczy tekstów z plików z Locale. Nie wiem za bardzo o co chodzi? Pracuję na xampie na localhost. Rozumiem, ze powinienem wyciąć z ciągu tę nazwę folderu?
OdpowiedzUsuńTak, powinieneś wyciąć nazwę folderu lub pokombinować z Router::url() z parametrami w funkcji url (http://book.cakephp.org/2.0/en/development/routing.html#Router::url).
OdpowiedzUsuńPliki locale .po i .mo powinny znajdować się bodajże w app/Locale/en/LC_MESSAGES/. Jeśli są tam i mają prawidłową składnie to powinny zadziałać (w php.ini musi być włączone rozszerzenie gettext).
Już wiem co jest nie tak. Przez to, że Router::url(''); wczytuje u mnie nazwę katalogu czyli np. /portal/en, to na potrzeby testowe na localhost nie mogę użyć kodu: $language = substr(Router::url(''), 0, 2);, bo wycina on pierwsze dwie literki (czyli język). Wymyśliłem więc, że wytnę dwie ostatnie: $language = substr(Router::url(''), -2); i to działa dla strony głównej. Jednak jak chcę wycinając dwie ostatnie z adresu np: /portal/en/register był błąd i to prawidłowo, bo w Router::url('') miałem 'er' czyli albo to zacznę testować bezpośrednio na serwerze albo muszę wymyślić jakiś trick aby pozbyć się nazwy katalogu w adresie. W każdym razie bardzo dziękuję za odpowiedź, bo rozjaśnił mi się problem, a straciłem już na to całą noc:) Pozdrawiam
OdpowiedzUsuńSzybka Gotówka - https://redirect.qxa.pl/YxNh9 - Uzyskaj Szybką Pożyczkę
OdpowiedzUsuńSzybka Gotówka - https://redirect.qxa.pl/YxNh9 - Uzyskaj Szybką Pożyczkę
OdpowiedzUsuńPożyczka https://redirect.qxa.pl/IlApq Sprawdź!
OdpowiedzUsuń