1<?php 2/* vim: set expandtab tabstop=4 shiftwidth=4: */ 3// 4// Copyright (c) 2003 Laurent Bedubourg 5// 6// This library is free software; you can redistribute it and/or 7// modify it under the terms of the GNU Lesser General Public 8// License as published by the Free Software Foundation; either 9// version 2.1 of the License, or (at your option) any later version. 10// 11// This library is distributed in the hope that it will be useful, 12// but WITHOUT ANY WARRANTY; without even the implied warranty of 13// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14// Lesser General Public License for more details. 15// 16// You should have received a copy of the GNU Lesser General Public 17// License along with this library; if not, write to the Free Software 18// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 19// 20// Authors: Laurent Bedubourg <laurent.bedubourg@free.fr> 21// 22 23//require_once "PEAR.php"; 24 25define('GETTEXT_NATIVE', 1); 26define('GETTEXT_PHP', 2); 27 28function get_text_init($managerType = GETTEXT_NATIVE) { 29 global $GetText; 30 if (!isset($GetText)) { 31 32 if ($managerType == GETTEXT_NATIVE) 33 { 34 if (function_exists('gettext')) 35 { 36 $GetText = new gettext_native_support(); 37 return; 38 } 39 } 40 // fail back to php support 41 $GetText = new gettext_php_support(); 42 } 43} 44 45function raise_error($str) { 46 error_log($str); 47 return 1; 48} 49 50function is_error($err) { 51 return $err > 0; 52} 53 54/** 55* Interface to gettext native support. 56* 57* @author Laurent Bedubourg <laurent.bedubourg@free.fr> 58* @access private 59*/ 60class gettext_native_support 61{ 62 var $_interpolation_vars = array(); 63 var $domain_path; 64 65 /** 66 * Set gettext language code. 67 * @throws GetText_Error 68 */ 69 function set_language($lang_code, $encoding) 70 { 71 putenv("LANG=$lang_code"); 72 putenv("LC_ALL=$lang_code"); 73 putenv("LANGUAGE=$lang_code"); 74 75 //$set = setlocale(LC_ALL, "$lang_code"); 76 //$set = setlocale(LC_ALL, "$encoding"); 77 78 // cover a couple of country/encoding variants 79 $up = strtoupper($encoding); 80 $low = strtolower($encoding); 81 $lshort = strtr($up, '-',''); 82 $ushort = strtr($low, '-',''); 83 84 if ($lang_code == 'C') 85 $set = setlocale(LC_ALL,'C'); 86 else 87 $set = setlocale(LC_ALL, $lang_code.".".$encoding, 88 $lang_code.".".$up, $lang_code.".".$low, 89 $lang_code.".".$ushort, $lang_code.".".$lshort); 90 91 setlocale(LC_NUMERIC, 'C'); // important for numeric presentation etc. 92 if ($set === false) 93 { 94 if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') // don't do this test if server is WIN 95 return 0; 96 $str = sprintf('language code "%s", encoding "%s" not supported by your system', 97 $lang_code, $encoding); 98 //$err = new GetText_Error($str); 99 //return PEAR::raise_error($err); 100 return raise_error("1 " . $str); 101 } 102 //return 0; 103 } 104 /** 105 * Check system support for given language nedded for gettext. 106 */ 107 function check_support($lang_code, $encoding) 108 { 109 if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') // don't do this test if server is WIN 110 return true; 111 $old = setlocale(LC_CTYPE, '0'); // LC_MESSAGES does not exist on Win 112 $up = strtoupper($encoding); 113 $low = strtolower($encoding); 114 $lshort = strtr($up, '-',''); 115 $ushort = strtr($low, '-',''); 116 117 $test = setlocale(LC_ALL, 118 $lang_code.".".$encoding, 119 $lang_code.".".$up, 120 $lang_code.".".$low, 121 $lang_code.".".$ushort, 122 $lang_code.".".$lshort) !== false; 123 setlocale(LC_ALL, $old); 124 setlocale(LC_NUMERIC, 'C'); 125 return $test; 126 } 127 /** 128 * Add a translation domain. 129 */ 130 function add_domain($domain, $path=false, $version='') 131 { 132 if ($path === false) 133 $path = $this->domain_path; 134 if ($path === false) 135 $path = "./locale"; 136 if ($domain == "") 137 $domain = "?"; 138 if ($version) { 139 // To avoid need for apache server restart after change of *.mo file 140 // we have to include file version as part of filename. 141 // This is alternative naming convention: $domain = $version.'/'.$domain; 142 $domain .= '-'.$version; 143 } 144 bindtextdomain($domain, $path); 145 //bind_textdomain_codeset($domain, $encoding); 146 textdomain($domain); 147 } 148 149 /** 150 * Retrieve translation for specified key. 151 * 152 * @access private 153 */ 154 function _get_translation($key) 155 { 156 return gettext($key); 157 } 158 159 160 /** 161 * Reset interpolation variables. 162 */ 163 function reset() 164 { 165 $this->_interpolation_vars = array(); 166 } 167 168 /** 169 * Set an interpolation variable. 170 */ 171 function set_var($key, $value) 172 { 173 $this->_interpolation_vars[$key] = $value; 174 } 175 176 /** 177 * Set an associative array of interpolation variables. 178 */ 179 function set_vars($hash) 180 { 181 $this->_interpolation_vars = array_merge($this->_interpolation_vars, 182 $hash); 183 } 184 185 /** 186 * Retrieve translation for specified key. 187 * 188 * @param string $key -- gettext msgid 189 * @throws GetText_Error 190 */ 191 function gettext($key) 192 { 193 $value = $this->_get_translation($key); 194 if ($value === false) { 195 $str = sprintf('Unable to locate gettext key "%s"', $key); 196 //$err = new GetText_Error($str); 197 //return PEAR::raise_error($err); 198 return raise_error("2 " . $str); 199 } 200 201 while (preg_match('/\$\{(.*?)\}/sm', $value, $m)) { 202 list($src, $var) = $m; 203 204 // retrieve variable to interpolate in context, throw an exception 205 // if not found. 206 $var2 = $this->_get_var($var); 207 if ($var2 === false) { 208 $str = sprintf('Interpolation error, var "%s" not set', $var); 209 //$err = new GetText_Error($str); 210 //return PEAR::raise_error($err); 211 return raise_error("3 " . $str); 212 } 213 $value = str_replace($src, $var2, $value); 214 } 215 return $value; 216 } 217 218 /** 219 * Retrieve an interpolation variable value. 220 * 221 * @return mixed 222 * @access private 223 */ 224 function _get_var($name) 225 { 226 if (!array_key_exists($name, $this->_interpolation_vars)) { 227 return false; 228 } 229 return $this->_interpolation_vars[$name]; 230 } 231} 232 233 234/** 235* Implementation of get_text support for PHP. 236* 237* This implementation is abble to cache .po files into php files returning the 238* domain translation hashtable. 239* 240* @access private 241* @author Laurent Bedubourg <laurent.bedubourg@free.fr> 242*/ 243class gettext_php_support extends gettext_native_support 244{ 245 var $_path = 'locale/'; 246 var $_lang_code = false; 247 var $_domains = array(); 248 var $_end = -1; 249 var $_jobs = array(); 250 251 /** 252 * Set the translation domain. 253 * 254 * @param string $lang_code -- language code 255 * @throws GetText_Error 256 */ 257 function set_language($lang_code, $encoding) 258 { 259 // if language already set, try to reload domains 260 if ($this->_lang_code !== false and $this->_lang_code != $lang_code) 261 { 262 foreach ($this->_domains as $domain) 263 { 264 $this->_jobs[] = array($domain->name, $domain->path); 265 } 266 $this->_domains = array(); 267 $this->_end = -1; 268 } 269 270 $this->_lang_code = $lang_code; 271 272 // this allow us to set the language code after 273 // domain list. 274 while (count($this->_jobs) > 0) 275 { 276 list($domain, $path) = array_shift($this->_jobs); 277 $err = $this->add_domain($domain, $path); 278 // error raised, break jobs 279 /*if (PEAR::is_error($err)) { 280 return $err; 281 }*/ 282 if (is_error($err)) 283 { 284 return $err; 285 } 286 } 287 } 288 /** 289 * Check system support for given language (dummy). 290 */ 291 function check_support($lang_code, $encoding) 292 { 293 return true; 294 } 295 /** 296 * Add a translation domain. 297 * 298 * @param string $domain -- Domain name 299 * @param string $path optional -- Repository path 300 * @throws GetText_Error 301 */ 302 function add_domain($domain, $path = false, $version ='') 303 { 304 if ($path === false) 305 $path = $this->domain_path; 306 if ($path === false) 307 $path = "./locale"; 308 309 if ($version) { 310 $domain .= '-'.$version; 311 } 312 313 if (array_key_exists($domain, $this->_domains)) 314 { 315 return; 316 } 317 318 if (!$this->_lang_code) 319 { 320 $this->_jobs[] = array($domain, $path); 321 return; 322 } 323 // Don't fill the domains with false data, it increased the error.log 324 if (strpos($domain, $this->_lang_code) === false) 325 return; 326 327 $err = $this->_load_domain($domain, $path); 328 if ($err != 0) 329 { 330 return $err; 331 } 332 333 $this->_end++; 334 } 335 336 /** 337 * Load a translation domain file. 338 * 339 * This method cache the translation hash into a php file unless 340 * GETTEXT_NO_CACHE is defined. 341 * 342 * @param string $domain -- Domain name 343 * @param string $path optional -- Repository 344 * @throws GetText_Error 345 * @access private 346 */ 347 function _load_domain($domain, $path = "./locale") 348 { 349 $src_domain = $path . "/$this->_lang_code/LC_MESSAGES/$domain.po"; 350 $php_domain = $path . "/$this->_lang_code/LC_MESSAGES/$domain.php"; 351 352 if (!file_exists($src_domain)) 353 { 354 $str = sprintf('Domain file "%s" not found.', $src_domain); 355 //$err = new GetText_Error($str); 356 //return PEAR::raise_error($err); 357 return raise_error("4 " . $str); 358 } 359 360 $d = new gettext_domain(); 361 $d->name = $domain; 362 $d->path = $path; 363 if (!file_exists($php_domain) || (filemtime($php_domain) < filemtime($src_domain))) 364 { 365 366 // parse and compile translation table 367 $parser = new gettext_php_support_parser(); 368 $hash = $parser->parse($src_domain); 369 if (!defined('GETTEXT_NO_CACHE')) 370 { 371 $comp = new gettext_php_support_compiler(); 372 $err = $comp->compile($hash, $src_domain); 373 /*if (PEAR::is_error($err)) { 374 return $err; 375 }*/ 376 if (is_error($err)) 377 { 378 return $err; 379 } 380 } 381 $d->_keys = $hash; 382 } 383 else 384 { 385 $d->_keys = include $php_domain; 386 } 387 $this->_domains[] = &$d; 388 } 389 390 /** 391 * Implementation of gettext message retrieval. 392 */ 393 function _get_translation($key) 394 { 395 for ($i = $this->_end; $i >= 0; $i--) 396 { 397 if ($this->_domains[$i]->has_key($key)) 398 { 399 return $this->_domains[$i]->get($key); 400 } 401 } 402 return $key; 403 } 404} 405 406/** 407* Class representing a domain file for a specified language. 408* 409* @access private 410* @author Laurent Bedubourg <laurent.bedubourg@free.fr> 411*/ 412class gettext_domain 413{ 414 var $name; 415 var $path; 416 417 var $_keys = array(); 418 419 function has_key($key) 420 { 421 return array_key_exists($key, $this->_keys); 422 } 423 424 function get($key) 425 { 426 return $this->_keys[$key]; 427 } 428} 429 430/** 431* This class is used to parse gettext '.po' files into php associative arrays. 432* 433* @access private 434* @author Laurent Bedubourg <laurent.bedubourg@free.fr> 435*/ 436class gettext_php_support_parser 437{ 438 var $_hash = array(); 439 var $_current_key; 440 var $_current_value; 441 442 /** 443 * Parse specified .po file. 444 * 445 * @return hashtable 446 * @throws GetText_Error 447 */ 448 function parse($file) 449 { 450 $this->_hash = array(); 451 $this->_current_key = false; 452 $this->_current_value = ""; 453 454 if (!file_exists($file)) 455 { 456 $str = sprintf('Unable to locate file "%s"', $file); 457 //$err = new GetText_Error($str); 458 //return PEAR::raise_error($err); 459 return raise_error($str); 460 } 461 $i = 0; 462 $lines = file($file); 463 foreach ($lines as $line) 464 { 465 $this->_parse_line($line, ++$i); 466 } 467 $this->_store_key(); 468 469 return $this->_hash; 470 } 471 472 /** 473 * Parse one po line. 474 * 475 * @access private 476 */ 477 function _parse_line($line, $nbr) 478 { 479 $line = str_replace("\\\"", "'", $line); // Should be inside preg_match, but I couldn't find the solution. This works. 480 if (preg_match('/^\s*?#/', $line)) { return; } 481 if (preg_match('/^\s*?msgid \"(.*?)(?!<\\\)\"/', $line, $m)) { 482 $this->_store_key(); 483 $this->_current_key = $m[1]; 484 return; 485 } 486 if (preg_match('/^\s*?msgstr \"(.*?)(?!<\\\)\"/', $line, $m)) { 487 $this->_current_value .= $m[1]; 488 return; 489 } 490 if (preg_match('/^\s*?\"(.*?)(?!<\\\)\"/', $line, $m)) { 491 $this->_current_value .= $m[1]; 492 return; 493 } 494 } 495 496 /** 497 * Store last key/value pair into building hashtable. 498 * 499 * @access private 500 */ 501 function _store_key() 502 { 503 if ($this->_current_key === false) return; 504 $this->_current_value = str_replace('\\n', "\n", $this->_current_value); 505 $this->_hash[$this->_current_key] = $this->_current_value; 506 $this->_current_key = false; 507 $this->_current_value = ""; 508 } 509} 510 511 512/** 513* This class write a php file from a gettext hashtable. 514* 515* The produced file return the translation hashtable on include. 516* 517* @throws GetText_Error 518* @access private 519* @author Laurent Bedubourg <laurent.bedubourg@free.fr> 520*/ 521class gettext_php_support_compiler 522{ 523 /** 524 * Write hash in an includable php file. 525 */ 526 function compile(&$hash, $source_path) 527 { 528 $dest_path = preg_replace('/\.po$/', '.php', $source_path); 529 $fp = @fopen($dest_path, "w"); 530 if (!$fp) 531 { 532 $str = sprintf('Unable to open "%s" in write mode.', $dest_path); 533 //$err = new GetText_Error($str); 534 //return PEAR::raise_error($err); 535 return raise_error($str); 536 } 537 fwrite($fp, '<?php' . "\n"); 538 fwrite($fp, 'return array(' . "\n"); 539 foreach ($hash as $key => $value) 540 { 541 $key = str_replace("'", "\\'", $key); 542 $value = str_replace("'", "\\'", $value); 543 fwrite($fp, ' \'' . $key . '\' => \'' . $value . "',\n"); 544 } 545 fwrite($fp, ');' . "\n"); 546 fwrite($fp, '?>'); 547 fclose($fp); 548 } 549} 550 551/* 552 Set current gettext domain path 553*/ 554function set_ext_domain($path='') { 555 global $path_to_root, $GetText; 556 static $domain_stack = array(''); 557 558 if ($path) // save path on domain stack 559 array_unshift($domain_stack, $path); 560 else 561 { 562 array_shift($domain_stack); 563 $path = $domain_stack[0]; 564 } 565 566 $lang_path = $path_to_root . ($path ? '/' : '') .$path.'/lang'; 567 // ignore change when extension does not provide translation structure and test for valid gettext. 568 if (file_exists($lang_path) && isset($GetText)) 569 $GetText->add_domain($_SESSION['language']->code, 570 $lang_path, $path ? '' : $_SESSION['language']->version); 571} 572?> 573