Pomoc - Szukaj - Użytkownicy - Kalendarz
Pełna wersja: [klasa PHP5] Klasa wspierająca wielojęzykowość
Forum PHP.pl > Forum > Gotowe rozwiązania > Algorytmy, klasy, funkcje
everth
Poniższa klasa powinna pomóc w tworzeniu wielojęzykowych stron.

Przy projektowaniu wzorowałem się na podobnej funkcji w bibliotece Qt4 - gdzie metoda tr() ułatwia znacznie tworzenie templatek z tłumaczeniami, właściwie bez ingerencji w kod. Moja klasa próbuje to (trochę nieporadnie, ale cóż) zaimplementować w PHP. Główne założenia:
  • niezależność od źródła danych (wydzielony interfejs dla modelu danych z tłumaczeniami)
  • łatwość użycia w kodzie (szybka skrótowa funkcja $U())
  • automatyczne tworzenie bazy na podstawie wpisów w kodzie (w trybie appendMode dodajemy unikatowe ID i (opcjonalnie) tłumaczenie)
  • dobra dokumentacja - na obecnym etapie podstawowa dokumentacja w phpDoc
  • elastyczność - klasa w zasadzie projektowana była dla statycznych elementów tekstowych (napisy na przyciskach, napisy w stopce, itd.), ale ponieważ są to ciągi tekstowe to z łatwością można sobie dostosować np. do przechowywania ścieżek do plików graficznych z różnymi napisami, przy odrobinie wysiłku można w ten sposób dodawać nawet artykuły wielojęzykowe (po prostu generujemy ID dla artykułu i zawijamy w funkcję $U() - np. $U('12334') - później tworzymy wersje językowe dla tego ID)

Przykładowa baza w sql na początku ma dwie kolumny - id z identyfikatorami oraz kolumnę aktualnego języka (np. "pl_PL"), dodając nową kolumnę - dodajemy nowy język, według mnie ułatwia to tworzenie i edycję arkusza z tłumaczeniami np. w Excelu lub Calcu (po przerzuceniu tabeli w CSV). Przykładowa struktura:
  1. CREATE TABLE `Translations` (
  2. `id` varchar(40) CHARACTER SET latin1 NOT NULL,
  3. `pl_PL` mediumtext, -- nie jestem w stanie określić jak długie mogą być ciągi tekstowe tutaj
  4. `en_GB` mediumtext,
  5. PRIMARY KEY (`id`)
  6. ) ENGINE=MyISAM DEFAULT CHARSET=utf8

Przykładowe użycie:
  1. //Z użyciem trybu appendMode (dodajemy UIDy oraz przykładowe tłumaczenia w polskim)
  2. $locale = new Translation($pdo);
  3. $locale->setLang('pl_PL');
  4. $locale->appendMode(1); //włączamy appendMode
  5. $locale->setup();
  6.  
  7. echo <<<EB
  8. <ul>
  9. <li>{$U('M_HOME','Główna')}</li>
  10. <li>{$U('M_ABOUT','O nim')}</li>
  11. <li>{$U('M_PORTFOLIO','Portfolio')}</li>
  12. </ul>
  13. EB;
  14.  
  15. //Bez appendMode - sam odczyt UIDów (w przypadku braku pusty string)
  16. $locale->__destruct(); //niestety obiekty Translation nie tolerują żadnego ponownego wywołania bez ręcznego zniszczenia poprzedniej instancji - a ponieważ obiekt tworzy co najmniej 2 referencje do siebie to metoda unset() zawodzi, niestety to obecnie najsłabsze ogniwo - nie powinno się tworzyć więcej niż jedną instancję w przebiegu skryptu!
  17. $locale = new Translation($pdo);
  18. $locale->setLang('pl_PL');
  19. $locale->setup();
  20. echo <<<EB
  21. <ul>
  22. <li>{$U('M_HOME')}</li>
  23. <li>{$U('M_ABOUT')}</li>
  24. <li>{$U('M_PORTFOLIO')}</li>
  25. </ul>
  26. EB;

Kod źródłowy Translation wraz z klasami pomocniczymi jest pod tym linkiem
wookieb
  1. $GLOBALS[$this->global_alias] = $this;

WTF?! Poczytaj o właściwościach statycznych.

Czy mi się dobrze wydaje, czy do pobrania każdej wartości wywoływane jest zapytanie do bazy danych?questionmark.gif (w translacji DB)

@access nie stosujesz jak piszesz dla php5

Jeżeli zwracasz egzemplarz tej samej klasy możesz dać @return self

W preperestatement masz "podwojony" kod dla update.
Po co robisz addslashes skoro możesz skorzystać z bindowania wartości, które takie "zabezpieczenie" Ci załatwia?

A teraz puenta. Jaka jest różnica w stosowania twojej klasy od gettext?
Czyż nie lepiej było np stworzyć klasę do zarządzania wpisami właśnie dla tegoż wyżej wynalazku?


Dobra a teraz słowa uznania:
- Ładny kod (formatowanie, phpdoc)
- Dobre oddzielenie źródeł translacji
everth
Aleś po mnie pojechał smile.gif Więc teraz postaram się choć trochę usprawiedliwić (to jest wersja pre-pre-alfa - a nie wyglancowany produkt ostateczny)
  1. Cytat
    WTF?! Poczytaj o właściwościach statycznych.
    To jest najbardziej niepodobająca się mi część kodu. Co do właściwości statycznych - one raczej nie znajdują tu zastosowania, ja wyprowadzam tą referencję na zewnątrz obiektu (choć nie powinienem) tylko po to żeby móc się do niej odwołać w funkcji wrapperze $U(), tylko po to. Szukałem sposobu który pozwoliłby określić aktualną zewnętrzną referencję obiektu z wewnątrz obiektu (żeby użyć w funkcji wrapperze) - nic nie udało mi się znaleźć. Stąd ten hack z wyprowadzeniem dodatkowej referencji w GLOBALS (i to przez niego mam problemy z usuwaniem obiektu Translation). Istniała też możliwość przekształcenia obiektu w singletona (teraz uważam że tak byłoby nawet lepiej), tylko po drodze uznałem że niektórzy mogę chcieć mieć kilka instancji obiektów obok siebie, z różnymi językami -> tak wynikły z tego problemy, których już nie zdążyłem/nie chciało mi się dziś rozwiązywać
  2. Cytat
    Czy mi się dobrze wydaje, czy do pobrania każdej wartości wywoływane jest zapytanie do bazy danych? (w translacji DB)

    tu miałem zagwozdkę, bo rzeczywiście zastanawiałem czy nie pobrać całości jednym zapytaniem. Ostatecznie zdecydowałem o użyciu spreparowanych zapytań i bindingu. Dlaczego? Bo nie mam pojęcia jak wielkie mogą być te tabele, a ostrożność każe mi zakładać najgorsze. Jak ktoś chce to z łatwością napisze sobie własne LocaliseDbOne (lub rozszerzy obecną klasę) posługujące się jednokrotnym zapytaniem
  3. Cytat
    @access nie stosujesz jak piszesz dla php5
    Jeżeli zwracasz egzemplarz tej samej klasy możesz dać @return self
    phpDoc - to jest moje pierwsze użycie po zapoznaniu się z dokumentacją smile.gif
  4. Cytat
    W preperestatement masz "podwojony" kod dla update.
    Po co robisz addslashes skoro możesz skorzystać z bindowania wartości, które takie "zabezpieczenie" Ci załatwia?
    - to wynik nieprecyzyjnego czyszczenia kodu - pierwsze to przeoczenie, drugie to zaszłość po wcześniejszym rozwiązaniu (na bindowanie przestawiłem się już pod koniec)
  5. Cytat
    A teraz puenta. Jaka jest różnica w stosowania twojej klasy od gettext?
    - no to mnie zastrzeliłeś, bo rzeczywiście nie wiedziałem o tym rozwiązaniu (ogólnie o całej tej kategorii w manualu), jedyne co mam na swoją obronę to to, że ja mogę składować dane w bazie (a gettext składuje je w plikach *.mo - a one nie są czasem binarne?), lub w czymkolwiek do czego napiszę klasę. Jak znów będę miał trochę czasu to pomyślę nad gettext - może nawet da się go wepchnąć w interfejs TranslationModel
Zyx
Używanie gettext() w aplikacjach PHP to lekkie nieporozumienie. Bazuje ono na locale ustawianym przez funkcję setlocale(), która owszem, ustawia locale, ale dla całego procesu serwera, a nie dla pojedynczego wątku. Oznacza to, że na hostingach współdzielonych można komuś przełączyć locale w połowie wykonywania jego skryptu i ten ktoś nawet nie będzie o tym wiedział.

Jeśli już, to klasy do tłumaczeń powinno się opierać na nowych fajnych klasach unikodowych dostępnych od PHP 5.3, np.

http://docs.php.net/manual/en/class.messageformatter.php
http://docs.php.net/manual/en/class.numberformatter.php
http://docs.php.net/manual/en/class.intldateformatter.php

Nie są to kompletne systemy, ale zwalniają programistę z wielu upierdliwości; np. klasa MessageFormatter pozwala obsługiwać argumenty i umieszczać w tekście do wyświetlenia komendy warunkowe np. do obsługi liczby mnogiej.
wookieb
1) No właśnie dlatego z tego względu nie "powinno" się korzystać z funkcji typu _ i twojego wrappera ponieważ powinno wystarczy wykorzystanie referencji do obiektu translacji.
  1. $this->translate->_('nazwa');

2) Dlatego dzieli się tłumaczenia na tzw domeny. (podobnie masz w gettext)
domena - id_ciagu

Komunikaty błędów - Za krótki tekst
Komunikaty błędów - Za długi tekst
...
Logowanie - Błędny login i hasło
itd

Poza tym wykorzystanie tutaj cache byłoby rozsądne smile.gif

5) To było akurat podchwytliwe aczkolwiek polecam zapoznanie się z całym działem internacjonalizacji w manualu PHP aby chociaż poznać pomysły rozwiązań.
Tak gettext pobiera tłumaczenia z pliku, aczkolwiek nie widzę problemu z wygenerowaniem takowego.

@Zyx o tym problemie nie słyszałem, ciekawa informacja.

Rozumiem twój problem i osobiście odradzam gettext smile.gif Tworzenie źródł translacji (TranslationModel) jest zdecydowanie dobrym kierunkiem.
Dodatkowo pytanie ciekawostka. Jakiego autoloadera stosujesz?
everth
  1. Akurat skrótowiec jest główną osią tego obiektu (patrz tr() i Qt4), zamiast wpisywać wszędzie ciąg $localise->getValue($index) (akurat też możesz to robić - po prostu wywołujesz w inicjalizacji obiektu metodę noWrapperFunction() jesli nie chcesz wrappera, zresztą nawet jak masz wrappera to możesz metodę wywołać ręcznie).
    Teraz wymyśliłem żeby zarządzanie tymi zewnętrznymi referencjami wydzielić w klasie singletonie TranslationInstanceDispatcher - zobaczę co z tego wyjdzie.


  2. Żadnego - po pierwsze z magiczną metodą __autoloader spotkałem się dopiero tutaj biggrin.gif (jakieś 2 tygodnie temu) - wcześniej po prostu używałem require_once (cóż, na naukę nigdy nie jest za późno). Na razie jest to mi niepotrzebne - najpierw chcę doprowadzić tę klasę do w miarę stabilnego działania. Autoloader można dodać w każdej chwili.

----------------------------------------------------------------------------------------------------------------------------
Poprawiłem trochę kod. Wprowadziłem pewne zmiany zarówno w klasie Translation, jak i w klasie TranslationDb oraz TranslationModel. Poprawione źródła są tutaj

Poniżej trochę taki skrótowy changelog:
  1. za twoją radą @wookieweb poczytałem trochę o klasach i metodach statycznych. Rzeczywiście pomogło smile.gif. Zaimplementowałem statyczną klasę TranslationInstanceDispatcher której zadaniem jest zarządzanie zewnętrznymi referencjami klasy Translation. Pomogło na tyle, że wreszcie można postawić w tym samym czasie dwie lub więcej instancji obiektu Translation bez narażenia się na konflikt zmiennych.
  2. Poprawiłem dokumentację zgodnie z sugestiami. Klasa TranslationYAML została udokumentowana (ale nie była jeszcze testowana!)
  3. Zmodyfikowałem interfejs TranslationModel - dodałem dwie metody: setup() której zadaniem jest przygotować obiekt do pracy (podobnie jak w klasie Translation) oraz cache() - metoda specyficzna na razie tylko dla klasy TranslationDb (na obecnym etapie poza klasą TranslationDb obydwie są zaślepione)
  4. Jak wyżej w klasie TranslationDb wprowadziłem tryb cache w którym cała tabela jest pobierana na samym początku (dokładnie w momencie wywołania metody setup()), późniejszy odczyt odbywa się z tablicy w pamięci PHP. Ten tryb obecnie jest domyślny, wyłączając go przechodzimy do starego odpytywania.
  5. Mniejsze bugi i niedopatrzenia na które natknąłem się poprawiając kod

Jutro (właściwie to dziś) jak będę w stanie to zobaczę na ten model domenowy.

Poniżej krótki przykład kilku instancji obiektu Translation:
Tabela i dane
  1. CREATE TABLE `Translations` (
  2. `id` varchar(40) CHARACTER SET latin1 NOT NULL,
  3. `pl_PL` mediumtext,
  4. `en_GB` tinytext,
  5. PRIMARY KEY (`id`)
  6. ) ENGINE=MyISAM DEFAULT CHARSET=utf8;
  7. INSERT INTO `Translations` VALUES ('M_HOME','Główna','Home');
  8. INSERT INTO `Translations` VALUES ('M_ABOUT','O nim','About me');
  9. INSERT INTO `Translations` VALUES ('M_PORTFOLIO','Portfolio','My galleries');

Przykład użycia:
  1. //Dla języka polskiego
  2. $polish = new Translation($pdo);
  3. $polish->setLang('pl_PL');
  4. $polish->setup();
  5. echo <<<EB
  6. <ul>
  7. <li>{$U('M_HOME')}</li>
  8. <li>{$U('M_ABOUT')}</li>
  9. <li>{$U('M_PORTFOLIO')}</li>
  10. </ul>
  11. EB;
  12. //Dla Inglish by Polish (czyli mix)
  13. $english = new Translation($pdo);
  14. $english->setWrapperFunctionName('E'); //warto ustawić nazwę prefiksu żeby wiedzieć gdzie się odwołać
  15. $english->setLang('en_GB');
  16. $english->setup();
  17. echo <<<EB
  18. <ul>
  19. <li>{$E('M_HOME')}</li>
  20. <li>{$E('M_ABOUT')}</li>
  21. <li>{$U('M_ABOUT')}</li>
  22. <li>{$E('M_PORTFOLIO')}</li>
  23. </ul>
  24. EB;
  25. $polish->__destruct(); // niszczymy obiekt Translate ustawiony na polski, poniżej powinien wystąpić fatalError (Odwołujemy się do funkcji która już nie istnieje)
  26. echo <<<EB
  27. <ul>
  28. <li>{$U('M_HOME')}</li>
  29. <li>{$U('M_ABOUT')}</li>
  30. <li>{$U('M_PORTFOLIO')}</li>
  31. </ul>
  32. EB;
cojack
Cytat
The locale information is maintained per process, not per thread. If you are running PHP on a multithreaded server API like IIS or Apache on Windows, you may experience sudden changes in locale settings while a script is running, though the script itself never called setlocale(). This happens due to other scripts running in different threads of the same process at the same time, changing the process-wide locale using setlocale().


Zyx, czytaj dokładniej.
To jest wersja lo-fi głównej zawartości. Aby zobaczyć pełną wersję z większą zawartością, obrazkami i formatowaniem proszę kliknij tutaj.
Invision Power Board © 2001-2025 Invision Power Services, Inc.