Izvedba višejezičnog sajta: gettext i php
Ovaj tutorial je ujedno i follow-up na topic: Izvedba višejezičnog sajta
Za i protiv
Gettext je odličan u situacijama gdje postoji puno različitih riječi odnosno fraza koje treba prevesti. Držanje prevodljivih fraza u ovakvim ili onakvim arrayima postaje komplicirano kad njihova ukupna količina naraste.
Za:
- jednostavno prikupljanje prevodljivih fraza s cijelog sitea, s malom mogućnošću pogreške
- jednostavno prevođenje - korištenjem softvera koji podržavaju .po kataloge
- jednostavno ubacivanje prijevoda u web site
- jednostavno dodavanje novih prevodljivih fraza odnosno promjena postojećih - gettext prepoznaje ako se radi o djelomičnoj promjeni teksta prevodljive fraze, i kao mogući prijevod automatski nudi prijašnju verziju prijevoda, tako da prevoditelji imaju manje posla
Protiv:
- potrebno je ponovo kreirati cijeli katalog za svaku promjenu - zna biti nezgodno ako se radi o sitnim promjenama
- na malim projektima ponekad overhead, druga rješenja mogu biti prikladnija ako se radi o samo nekoliko desetaka fraza koje su specifične za pojedini jezik
Preduvjeti
- web server
- php
- gettext modul
- unix: gettext modul treba biti ukompajliran u php: --with-gettext
- win: gettext modul treba biti uključen u php.ini datoteci: extension=php_gettext.dll
- gettext utilities
Malo o gettextu..
Preneseno iz gettext manuala:
Specifically, the GNU gettext utilities are a set of tools that provides a framework within which other free packages may produce multi-lingual messages.
These tools include :
- A set of conventions about how programs should be written to support message catalogs.
- A directory and file naming organization for the message catalogs themselves.
- A runtime library supporting the retrieval of translated messages.
- A few stand-alone programs to massage in various ways the sets of translatable strings, or already translated strings.
GNU gettext is designed to minimize the impact of internationalization on program sources, keeping this impact as small and hardly noticeable as possible. Internationalization has better chances of succeeding if it is very light weighted, or at least, appear to be so, when looking at program sources.
The Translation Project also uses the GNU gettext distribution as a vehicle for documenting its structure and methods. This goes beyond the strict technicalities of documenting the GNU gettext proper. By so doing, translators will find in a single place, as far as possible, all they need to know for properly doing their translating work. Also, this supplemental documentation might also help programmers, and even curious users, in understanding how GNU gettext is related to the remainder of the Translation Project, and consequently, have a glimpse at the big picture.
Korak po korak..
- prevodljive fraze se na odgovarajući način označe u source datotekama
- sve source datoteke se proparsaju s gettext utilitijem 'xgettext' koji kreira .po datoteku (katalog prevodljivih fraza)
- ako već postoji katalog fraza, njega se spoji s novim katalogom koristeći gettext utility 'msgmerge'
- .po datoteka se editira tj. u nju se upišu prijevodi za svaku od fraza. To se može napraviti u običnom text editoru ili kroz neku od aplikacija koje podržavaju rad s .po katalozima (npr: poEdit)
- od prevedene .po datoteke se kreira binarna .mo datoteka korištenjem utilitija 'msgfmt'. Tu datoteku će php funkcija _() koristiti za traženje prijevoda za pojedinu frazu
- u php-u se setiraju parametri ovisno o trenutno odabranom jeziku
- svaki poziv na _("fraza") će vratiti frazu prevedenu na trenutno odabrani jezik (ako prijevod za tu frazu postoji). ako prijevod ne postoji, _() funkcija će vratiti originalnu frazu
Označavanje prevodljivih fraza u source datotekama
Sve prevodljive fraze se pišu u defaultnom jeziku, i enkapsuliraju u _(""). npr. Danas je dobar dan postaje _("Danas je dobar dan."). Sintaksa _() je zapravo alias na php funkciju gettext() koja će u katalogu s prijevodima pronaći odgovarajući prijevod za traženu frazu.
Default jezik: ako se radi o siteu primarno na hrvatskom jeziku, čiji sadržaj se treba prezentirati i na drugim jezicima, onda predlažem hrvatski kao defaultni jezik. U ostalim slučajevima, engleski bi trebao biti default.
Primjer A:
<?php
echo _("Danas je dobar dan");
?>
Primjer B:
<div>_("Danas je dobar dan")</div>
Primjer B pokazuje označavanje prevodljive fraze u html sourceu. U tom slučaju, html source treba obraditi prije nego što ga se pošalje klijentu (browseru), odnosno svako pojavljivanje _("fraza") zamijeniti s outputom koji će dati _() php funkcija. To je najlakše napraviti korištenjem output buffera.
<?php
ob_start();
?>
<div>_("Danas je dobar dan")</div>
<?php
$buffer = ob_get_contents();
ob_end_clean();
$buffer = preg_replace('/_\("(.+?)"\)/se', "_('\\1')", $buffer);
echo $buffer;
?>
Kad već koristimo output buffering, možemo otići korak dalje i kompresirati output koji šaljemo prema browseru. Tekstualni podaci su iznimno pogodni za kompresiju (za razliku od slika), zbog čega se postižu odlični rezultati sažimanja. U najvećem broju slučajeva output ima 10-15% veličine originala.
Primjer B2:
<?php
ob_start();
?>
<div>_("Danas je dobar dan")</div>
<?php
$buffer = ob_get_contents();
ob_end_clean();
$buffer = preg_replace('/_\("(.+?)"\)/se', "_('\\1')", $buffer);
ob_start('ob_gzhandler');
echo $buffer;
?>
Kreiranje kataloga prevodljivih fraza
Ako postoji veći broj source datoteka koje sadržavaju prevodljive fraze, nezgodno je pokretati xgettext za svaku od source datoteka.
Ovo je skripta koju smo napisali i koristimo ju za automatsko parsanje svih source datoteka unutar direktorijskog stabla pojedinog web sitea.
Napomena: ako se radi o windows platformi, path do gettext utilitija mora biti unesen u PATH environment varijablu.
Workflow:
- rekurzivno prolazi kroz sve datoteke unutar stabla direktorija od kuda je pokrenuta
- svaku datoteku koja odgovara definiranim kriterijima (ima odgovarajuću extenziju) parsa korištenjem xgettext utilitija i dodaje u privremeni katalog fraza
- ako u direktoriju locale/language/LC_MESSAGES postoji .po katalog, spaja novi katalog s postojećim, u suprotnom samo kreira novi katalog
- ako u direktoriju locale/language/LC_MESSAGES postoji .po katalog, kreira njegovu binarnu .mo verziju
<?php
/*
* Gettext utility
* - creation of .po catalogs - recursive crawl through directory tree
* - update of .mo binary catalogs for all available languages
*
* (C) Copyright Burza d.o.o. (http://web.burza.hr)
* Vanja Bertalan
*
* Usage
* - should be run as a shell script, from the web sites' root directory
* - locale directory should exist, along with coresponding
* language subdirectories
* - each language subdirectory should have LC_MESSAGES subdirectory
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, US*
*/
/* CONFIGURATION START */
$exclude_dirs = 'images|javascript|sql|tools|css'; // directory names that should be skipped
$extensions = 'php|html|tpl'; // filename extensions that should be parsed
$translations_dir = '/tools/translations'; // directory for storage of created catalogs
$domain = 'catalog'; // domain name, basename of .po and .mo files
/* CONFIGURATION END */
@set_time_limit (0);
@ignore_user_abort(1);
$no_location = 0;
$file_list = array();
$file_no = 0;
$start_dir = preg_replace('/\\\/', '/', getcwd());
$start_dir = preg_replace('/\/tools$/', '', $start_dir);
$locale_dir = $start_dir . '/locale/';
$translations_dir = $start_dir . $translations_dir;
// get translatable files
crawl($start_dir);
chdir($translations_dir);
// create fresh catalog file from translatable files
foreach ($file_list as $filename) {
echo "$filename\n";
$input = file($filename);
$out = '';
foreach ($input as $line) {
$out .= $line;
}
$out = preg_replace('/"(_\("[^"]*"\)[^"]*)"/s', "\\1", $out);
$out = preg_replace('/\'(_\("[^"]*"\)[^"]*)\'/s', "\\1", $out);
$filename .= '.tmp';
$handle = fopen($filename, 'wb');
fwrite($handle, $out);
fclose($handle);
$command = 'xgettext --keyword=_ --sort-by-file --default-domain=' . $domain . ".allstrings -C --force ";
if ($file_no) {
$command .= "--join-existing ";
}
if ($no_location) {
$command .= "--no-location ";
}
$command .= $filename;
system($command, $return_code);
if ( $return_code != 0 ) {
echo "error: $filename\n";
}
unlink ($filename);
$file_no++;
}
// get language directories
if ($handle = opendir($locale_dir)) {
while (false !== ($file = readdir($handle))) {
if (is_dir($locale_dir.$file) && !preg_match('/^\.{1,2}$/', $file)) {
$dirs[] = $file;
}
}
closedir($handle);
}
// create po file for every language
// if po file already exists, merge it with new catalog
foreach ($dirs as $d) {
$def_file_po = $locale_dir . $d . "/LC_MESSAGES/" . $domain . ".po";
$def_file_mo = $locale_dir . $d . "/LC_MESSAGES/" . $domain . ".mo";
if (is_file($def_file_po)) {
echo "\ncatalogue $d already exists\nmerging changes to translations directory: \n";
$command = "msgmerge -o $domain.$d.po $def_file_po $domain.allstrings.po";
system($command, $return_code);
$command = "msgfmt -o $def_file_mo $def_file_po";
system($command, $return_code);
echo "created binary catalogue for domain $d\n";
} else {
echo "\ncatalogue $d does not exists\ncreating new catalog in translations directory\n";
copy("$domain.allstrings.po", "$domain.$d.po");
}
}
unlink ("$domain.allstrings.po");
function crawl($dir) {
global $domain, $file_list, $exclude_dirs, $extensions;
echo "$dir\n";
if ($handle = opendir($dir)) {
while (false !== ($file = readdir($handle))) {
if (is_dir($dir.'/'.$file) && !preg_match('/^\.{1,2}$/', $file) && !preg_match("/($exclude_dirs)/", $file)) {
$dirs[] = $file;
}
if (is_file($dir.'/'.$file)) {
$files[] = $file;
}
}
closedir($handle);
}
if (is_array($files)) {
foreach ($files as $f) {
if (!preg_match("/\.($extensions)$/", $f)) { continue; }
$file = $dir.'/'.$f;
echo "\t$f\n";
array_push($file_list, $file);
}
}
if (is_array($dirs)) {
foreach ($dirs as $d) {
$file = $dir.'/'.$d;
crawl($file);
}
}
}
?>
Download skripte: create_translations.php.txt (preimenovati u .php)
Napomena: apache treba restartati nakon što se updatea binarni .mo katalog.
PHP implementacija
Da bi _() funkcija znala za koji jezik treba prikazivati prijevode, treba joj javiti o kojem se jeziku radi i gdje se nalaze katalozi s prijevodima.
U primjeru je definiran i $language_code za hrvatski jezik, no on nam zapravo nije potreban, jer će _() po defaultu prikazati originalnu frazu ako prijevod nije pronađen.
<?php
switch ($language) {
case 'en':
$language_code = 'en_US';
break;
case 'hr':
default:
$language = 'hr';
$language_code = 'hr_HR';
break;
}
if ((ini_get('safe_mode') == FALSE) && (getenv('LC_ALL') != $language_code)) {
putenv('LC_ALL=' . $language_code);
}
setlocale(LC_ALL, $language_code);
bindtextdomain('catalog', 'locale/'); // 'catalog' je naziv (basename) .po odnosno .mo datoteka, 'locale/' je naziv direktorija unutar kojih se nalaze prijevodi
textdomain('catalog'); // 'catalog' je naziv (basename) .po odnosno .mo datoteka
?>
Cijeli kod bi izgledao ovako:
<?php
ob_start();
$language = $_GET['language'] ? $_GET['language'] : ($_SESSION['language'] ? $_SESSION['language'] : 'hr');
switch ($language) {
case 'en':
$language_code = 'en_US';
break;
case 'hr':
default:
$language = 'hr';
$language_code = 'hr_HR';
break;
}
$_SESSION['language'] = $language;
if ((ini_get('safe_mode') == FALSE) && (getenv('LC_ALL') != $language_code)) {
putenv('LC_ALL=' . $language_code);
}
setlocale(LC_ALL, $language_code);
bindtextdomain('catalog', 'locale/');
textdomain('catalog');
echo Danas je dobar dan;
?>
<div>Danas je dobar dan</div>
<?php
$buffer = ob_get_contents();
ob_end_clean();
$buffer = preg_replace('/_\("(.+?)"\)/se', "_('\\1')", $buffer);
ob_start('ob_gzhandler');
echo $buffer;
?>
Linkovi
Nastavak rasprave ili pitanja -> na
forumu.
Autor: njava