PHP/Internacjonalizacja
Internacjonalizacja
[edytuj]W tym rozdziale przyjrzymy się zagadnieniom związanym z dostosowywaniem naszych witryn do konkretnych języków i ustawień regionalnych. PHP posiada odpowiedni zestaw narzędzi, który czyni to zadanie nietrudnym w wykonaniu. Lecz uważaj! Nieuważne skonfigurowanie mechanizmu może być przyczyną błędów!
Dlaczego "Ż" jest literą?
[edytuj]Uruchom pewien prosty skrypt:
<?php
echo strtolower('ŻÓŁTA WODA');
?>
W zależności od komputera, pokaże on napis żółta woda albo ŻóŁta woda. Zastanówmy się, skąd jeden interpreter wiedział, że Ż jest literą polskiego alfabetu, która również podlega zamianie, a drugi nie? Odpowiedź kryje się w ustawieniach regionalnych tych maszyn, znanych z terminologii linuksowej pod nazwą locale. PHP domyślnie wykonuje wszystkie operacje na tekstach, ustawia formatowanie walut oraz liczb na podstawie ustawień systemowych. Aby zmienić opcje regionalne dla aktualnie wykonywanego skryptu, należy skorzystać z funkcji setlocale(). Za pierwszy parametr podajemy flagi określające, które aspekty regionalizacji chcemy zmodyfikować, a wszystkie kolejne to identyfikatory możliwych ustawień. PHP będzie próbował każdego z nich po kolei, aż trafi na taki, który jest zainstalowany w systemie. Dzięki temu jedną funkcją można przystosować skrypt do pracy zarówno z serwerami opartymi o Linux, jak i Windows. Poniżej przedstawiamy jeszcze raz skrypt strtolower() skonfigurowany do pracy z polskimi ustawieniami:
<?php
setlocale(LC_ALL, 'pl_PL', 'pl', 'Polish_Poland.28592');
echo strtolower('ŻÓŁTA WODA');
?>
Pierwsze dwa identyfikatory (pl_PL oraz pl) dotyczą systemu Linux. Trzeci przeznaczony jest dla rodziny Windows. Listę aktualnie zainstalowanych ustawień można znaleźć w Panel sterowania → Opcje regionalne → Zaawansowane.
Uwaga!
|
Polska data
[edytuj]Pisząc aplikację przeznaczoną na rynek międzynarodowy, powinniśmy pomyśleć także o formatowaniu dat. Poznana już przez nas funkcja date() nawet przy pracy z polskimi ustawieniami wyświetli nam angielskie nazwy miesięcy. Sprawę rozwiązuje strftime(), przy której musimy pamiętać o innej składni formatowania daty. Przyjrzyjmy się przykładowi:
<?php
setlocale(LC_ALL, 'pl_PL', 'pl', 'Polish_Poland.28592');
// Dzień - nazwa miesiąca - rok
echo strftime('%d %B %Y');
?>
Po uruchomieniu okazuje się, że nawet ten skrypt nie jest doskonały, ponieważ wprawdzie generuje spolszczoną datę, ale niepoprawną gramatycznie! Otrzymujemy komunikat np. 17 kwiecień 2006, a tymczasem poprawną formą jest 17 kwietnia 2006. W niuanse gramatyczne wgłębiać się nie będziemy - po prostu jest tak, a nie inaczej i musimy samodzielnie napisać sobie funkcję, która nam to przeformatuje. Umożliwiając dodawanie obsługi nowych języków do aplikacji, powinniśmy pomyśleć o możliwości jej podmiany tak, aby twórca nakładki językowej mógł zaprogramować datę zgodnie z wymogami swego języka. Oto przykład takiej funkcji dla języka polskiego. Jest to modyfikacja strftime() dodająca nowy znacznik - %F będący właśnie odmienioną nazwą miesiąca:
<?php
function localStrftime($format, $timestamp = 0)
{
if($timestamp == 0)
{
// Sytuacja, gdy czas nie jest podany - używamy aktualnego.
$timestamp = time();
}
// Nowy kod - %F dla odmienionej nazwy miesiąca
if(strpos($format, '%F') !== false)
{
$mies = date('m', $timestamp);
// odmienianie
switch($mies)
{
case 1:
$mies = 'stycznia';
break;
case 2:
$mies = 'lutego';
break;
case 3:
$mies = 'marca';
break;
case 4:
$mies = 'kwietnia';
break;
case 5:
$mies = 'maja';
break;
case 6:
$mies = 'czerwca';
break;
case 7:
$mies = 'lipca';
break;
case 8:
$mies = 'sierpnia';
break;
case 9:
$mies = 'września';
break;
case 10:
$mies = 'października';
break;
case 11:
$mies = 'listopada';
break;
case 12:
$mies = 'grudnia';
break;
}
// dodawanie formatowania
return strftime(str_replace('%F', $mies, $format), $timestamp);
}
return strftime($format, $timestamp);
} // end localStrftime();
echo localStrftime('%d %F %Y');
?>
Teraz otrzymujemy prawidłowy tekst: 17 kwietnia 2006.
Oto kilka przydatnych kodów dla funkcji strftime():
- %A - pełna nazwa dnia tygodnia.
- %B - pełna nazwa dnia miesiąca.
- %d - numer dnia miesiąca (od 1 do 31)
- %H - godzina w formacie 24-godzinnym
- %m - numer miesiąca (od 1 do 12)
- %M - minuta
- %S - sekunda
- %T - aktualny czas (równoważnik %H:%M:%S)
- %Y - rok jako liczba czterocyfrowa
- %% - znak %
Więcej kodów można znaleźć na stronie dokumentacji PHP.
Wielojęzyczny interfejs
[edytuj]Kolejnym krokiem na drodze umożliwienia obcokrajowcom przeglądania naszych witryn jest stworzenie kilku wersji językowych. Realizuje się to, tworząc nakładki językowe, w których poszczególnym komunikatom interfejsu przypisane są odpowiednie identyfikatory. Następnie w kodzie aplikacji, zamiast pisać odpowiednie teksty, wywołujemy specjalną funkcję i podajemy ID tekstu do wstawienia. Ta wprowadza w tym miejscu odpowiedni tekst w zależności od wybranego języka. Ponieważ nie znamy jeszcze programowania obiektowego, będziemy musieli napisać odpowiednią bibliotekę strukturalnie:
<?php
$preferencje = array();
$tekstyI18n = array();
function zainstalowanyJezyk($jezyk)
{
// Tutaj mozemy umiescic kod sprawdzajacy, czy nasz serwis posiada
// wersje w podanym jezyku
switch($jezyk)
{
case 'pl':
case 'de':
case 'fr':
case 'en':
return true;
}
return false;
} // end zainstalowanyJezyk();
function uzyjJezyk($jezyk)
{
global $preferencje;
if(zainstalowanyJezyk($jezyk))
{
$preferencje['jezyk'] = $jezyk;
}
} // end uzyjJezyk();
function wybierzGrupe($grupa)
{
global $preferencje, $tekstyI18n;
$tekstyI18n[$grupa] = @parse_ini_file('jezyki/'.$preferencje['jezyk'].'/'.$grupa.'.php');
} // end wybierzGrupe();
function wstaw($grupa, $id)
{
global $tekstyI18n;
if(isset($tekstyI18n[$grupa][$id]))
{
return $tekstyI18n[$grupa][$id];
}
return strtoupper($grupa.':'.$id);
} // end wstaw();
?>
W powyższym kodzie tekstom przypisane są nie tylko identyfikatory - podzielono je także na ładowane oddzielnie grupy. W ten sposób strona z newsami może załadować sobie tylko treści komunikatów dla newsów, z pominięciem np. tekstów przeznaczonych dla forum dyskusyjnego. Oto opis funkcji:
- zainstalowanyJezyk() - zwraca true, jeżeli podany język jest obsługiwany przez nasz skrypt.
- uzyjJezyk() - ustawia podany język.
- wybierzGrupe() - ładuje podaną grupę na podstawie aktualnych ustawień języka. Do wczytania grupy używamy funkcji parse_ini_file() przetwarzającej podany plik identycznym parserem, jak ten, który przetwarza php.ini. Dzięki temu zyskujemy duże możliwości małym kosztem.
- wstaw() - wstawia tekst o podanym ID należący do odpowiedniej grupy na podstawie aktualnych ustawień językowych.
Pliki językowe należy umieszczać w katalogu jezyki/kodjezyka/nazwagrupy.php. Oto przykładowy plik jezyki/pl/global.php:
komunikat = "Komunikat"
hello_world = "Witaj swiecie!"
Korzysta z niego poniższy przykład:
<?php
require('./i18n.php');
uzyjJezyk('pl');
wybierzGrupe('global');
echo '<h2>'.wstaw('global', 'komunikat').'</h2>';
echo '<p>'.wstaw('global', 'hello_world').'</p>';
?>
Na początku ładujemy naszą bibliotekę i wybieramy język. Później wczytujemy grupę i wyświetlamy komunikaty. To wszystko - jesteśmy posiadaczami wielojęzycznego interfejsu. Pamiętaj, że jest to tylko przykładowy kod. Aby był on w pełni sprawny, powinieneś umożliwić użytkownikowi zmianę języka, a także zadbać o prawidłową obsługę błędów. Zwróć też uwagę, że dane obecnego systemu przechowywane są w tablicach globalnych. Nie jest to rozwiązanie najlepsze, ale na tym etapie wiedzy powinno nam wystarczyć. Kiedy poznamy programowanie obiektowe, będziesz mógł przepisać ten kod z jego wykorzystaniem, dzięki czemu zyska on na elegancji i bezpieczeństwie.
Do PHP można doinstalować także uniksowy moduł do obsługi wielojęzycznych interfejsów o nazwie gettext. Korzystanie z niego jest jednak dość ryzykowne, ponieważ wiele serwerów nie obsługuje go i aplikacje oparte o ten moduł nie będą działać.
Automatyczna detekcja języka
[edytuj]Przeglądarka internetowa to cenne źródło informacji o internaucie. Przesyła ona m.in. informację o języku, w jakim ona pracuje. Możemy przyjąć, że jest to rodzimy język użytkownika i na podstawie tego automatycznie ustawić mu odpowiednią wersję językową strony. Całość zawarta jest w $_SERVER['HTTP_ACCEPT_LANGUAGE'].
<?php
$zainstalowane = array( // 1
'pl' => 'polski',
'en' => 'angielski',
'fr' => 'francuski'
);
$jezyki = explode(';', $_SERVER['HTTP_ACCEPT_LANGUAGE']); // 2
$jezyki = explode(',', $jezyki[0]);
$uzyty = null;
foreach($jezyki as $jezyk)
{
if(isset($zainstalowane[$jezyk])) // 3
{
$uzyty = $jezyk;
break;
}
}
if(is_null($uzyty)) // 4
{
$uzyty = 'en';
}
echo 'Wybrany język to '.$zainstalowane[$uzyty];
?>
Analiza powyższego skryptu:
- Tablica z nazwami zainstalowanych języków - tylko do celów kontrolnych.
- Najpierw musimy usunąć niepotrzebne nam informacje. HTTP_ACCEPT_LANGUAGE posiada odpowiedni format informacji: języki;ważności, gdzie kody języków oddzielone są przecinkami. Druga część nie jest nam w ogóle potrzebna i możemy ją usunąć. Stąd w drugim explode pojawia się odwołanie $jezyki[0] wskazujące po prostu na pierwszą, istotną część przekazu.
- Pętlą przelatujemy całą tablicę. Jeśli zauważymy, że dany język jest zainstalowany, ustawiamy go i przerywamy pętlę.
- Jeżeli zmienna $uzyty ma w tym miejscu dalej domyślną wartość null, oznacza to, że żaden z języków użytkownika nie jest obsługiwany. W tym wypadku zmuszamy go do czytania po angielsku.
Naturalnie wypadałoby zadbać, aby powyższy skrypt był bardziej przyjazna użytkownikowi i na pierwszym miejscu sprawdzał informacje zapisane w ciastkach. Dopiero kiedy takowych nie będzie, można pobawić się w detekcję. Inaczej użytkownik nie będzie w stanie zmienić "narzuconego" mu języka nawet, jeśli jego własny będzie obsługiwany przez naszą witrynę.
Internacjonalizacja plikami .mo
[edytuj]Takie wykonanie skryptu jest niewygodne. Najbardziej popularnym sposobem jest budowanie tego na plikach .mo. Utwórz plik lang.php:
<?php
if ($_SERVER['HTTP_ACCEPT_LANGUAGE'] == "pl_PL") // 1
{
setlocale(LC_ALL, 'pl_PL');
}
else
{
setlocale(LC_ALL, 'en_US');
}
bindtextdomain("domena", "./locale"); //2
bind_textdomain_codeset("domena", 'UTF-8'); //3
textdomain("domena"); //4
?>
- Przeglądarki wysyłają informacje o języku. Jeśli użytkownik ma ustawiony w przeglądarce język polski, zobaczy polską wersję strony, jeśli nie - angielską.
- Przygotowuje domenę tekstową. Zmień "domena" na coś innego.
- Ustawia domenę na kodowanie UTF-8. Zmień "domena" na coś innego.
- Wybiera domenę. Zmień "domena" na coś innego.
Ten plik dołączamy do każdych plików, które wymagają internacjonalizacji funkcją <pre>include("./lang.php");</pre> Teraz należy zamienić wszystkie teksty tak jak tu:
<?php
echo "Polska wiadomość";
// =
echo _("Polska wiadomość");
Pobieramy edytor plików .po, np. Poedit i tworzymy pliki .mo (obsługa poniżej). Ważna jest także budowa struktury katalogów.
| |-lang.php |-en_US -|LC_MESSAGES -|domena.mo |-locale -|-pl_PL -|LC_MESSAGES -|domena.mo | (katalog zawiera plik lang.php i folder locale. W nim znajdują się podfoldery z kodami języków (en_US i pl_PL). W każdym podfolderze języków znajduje się folder LC_MESSAGES, którego zawartością w obu podfolderach języków jest plik domena.mo z przetłumaczonymi tekstami.)
Obsługa PoEdit
[edytuj]Obsługa jest bardzo prosta - wybieramy Plik > Nowy katalog, uzupełniamy dane. Zakładka ścieżki jest bardzo ważna. Pierwsze pole NIE może być ścieżką do katalogu z naszymi plikami, lecz do wcześniejszego katalogu (np. dla D:\Serwer\www\gettext\gettext podajemy D:\Serwer\www\gettext) a w polu niżej za pomocą przycisku nr. 2 dodajemy ścieżkę D:\Serwer\www\gettext\gettext oraz jeśli potrzebne jej podkatalogi.
Testowanie
[edytuj]Aby przetestować skrypty, należy zainstalować GNU gettext i:
- W przypadku systemu Windows - dodać php_gettext.dll do podkatalogu extensions katalogu PHP i aktywować ją w php.ini,
- W przypadku linuxów należy ją dokompilować do php, XAMPP dla Linuksa zawiera ją wkompilowaną.
Zakończenie
[edytuj]Internacjonalizacja jest bardzo ważnym zagadnieniem. Jeśli naprawdę myślimy o ekspansji na rynki zagraniczne, nie lekceważmy jej. Czas zainwestowany w uczynienie serwisu bardziej przyjaznym obcokrajowcom w przyszłości zaprocentuje ich dobrymi wrażeniami z pobytu u nas.