1<?php 2/********************************************************************* 3 class.gettext.php 4 5 This implements a `Translation` class that is loosely based on the PHP 6 gettext pure-php module. It includes some code from the project and some 7 code which is based in part at least on the PHP gettext project. 8 9 This extension to the PHP gettext extension using a specially crafted MO 10 file which is a PHP hash array. The file can be built using a utility 11 method in this class. 12 13 Jared Hancock <jared@osticket.com> 14 Copyright (c) 2006-2014 osTicket 15 http://www.osticket.com 16 17 PHP gettext extension is copyrighted separately: 18 --------------- 19 Copyright (c) 2003, 2009 Danilo Segan <danilo@kvota.net>. 20 Copyright (c) 2005 Nico Kaiser <nico@siriux.net> 21 22 This file is part of PHP-gettext. 23 24 PHP-gettext is free software; you can redistribute it and/or modify 25 it under the terms of the GNU General Public License as published by 26 the Free Software Foundation; either version 2 of the License, or 27 (at your option) any later version. 28 29 PHP-gettext is distributed in the hope that it will be useful, 30 but WITHOUT ANY WARRANTY; without even the implied warranty of 31 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 32 GNU General Public License for more details. 33 34 You should have received a copy of the GNU General Public License 35 along with PHP-gettext; if not, write to the Free Software 36 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 37 --------------- 38 39 Released under the GNU General Public License WITHOUT ANY WARRANTY. 40 See LICENSE.TXT for details. 41 42 vim: expandtab sw=4 ts=4 sts=4: 43**********************************************************************/ 44 45/** 46 * Provides a simple gettext replacement that works independently from 47 * the system's gettext abilities. 48 * It can read MO files and use them for translating strings. 49 * The files are passed to gettext_reader as a Stream (see streams.php) 50 * 51 * This version has the ability to cache all strings and translations to 52 * speed up the string lookup. 53 * While the cache is enabled by default, it can be switched off with the 54 * second parameter in the constructor (e.g. whenusing very large MO files 55 * that you don't want to keep in memory) 56 */ 57class gettext_reader { 58 //public: 59 var $error = 0; // public variable that holds error code (0 if no error) 60 61 //private: 62 var $BYTEORDER = 0; // 0: low endian, 1: big endian 63 var $STREAM = NULL; 64 var $short_circuit = false; 65 var $enable_cache = false; 66 var $originals = NULL; // offset of original table 67 var $translations = NULL; // offset of translation table 68 var $pluralheader = NULL; // cache header field for plural forms 69 var $total = 0; // total string count 70 var $table_originals = NULL; // table for original strings (offsets) 71 var $table_translations = NULL; // table for translated strings (offsets) 72 var $cache_translations = NULL; // original -> translation mapping 73 74 75 /* Methods */ 76 77 78 /** 79 * Reads a 32bit Integer from the Stream 80 * 81 * @access private 82 * @return Integer from the Stream 83 */ 84 function readint() { 85 if ($this->BYTEORDER == 0) { 86 // low endian 87 $input=unpack('V', $this->STREAM->read(4)); 88 return array_shift($input); 89 } else { 90 // big endian 91 $input=unpack('N', $this->STREAM->read(4)); 92 return array_shift($input); 93 } 94 } 95 96 function read($bytes) { 97 return $this->STREAM->read($bytes); 98 } 99 100 /** 101 * Reads an array of Integers from the Stream 102 * 103 * @param int count How many elements should be read 104 * @return Array of Integers 105 */ 106 function readintarray($count) { 107 if ($this->BYTEORDER == 0) { 108 // low endian 109 return unpack('V'.$count, $this->STREAM->read(4 * $count)); 110 } else { 111 // big endian 112 return unpack('N'.$count, $this->STREAM->read(4 * $count)); 113 } 114 } 115 116 /** 117 * Constructor 118 * 119 * @param object Reader the StreamReader object 120 * @param boolean enable_cache Enable or disable caching of strings (default on) 121 */ 122 function __construct($Reader, $enable_cache = true) { 123 // If there isn't a StreamReader, turn on short circuit mode. 124 if (! $Reader || isset($Reader->error) ) { 125 $this->short_circuit = true; 126 return; 127 } 128 129 // Caching can be turned off 130 $this->enable_cache = $enable_cache; 131 132 $MAGIC1 = "\x95\x04\x12\xde"; 133 $MAGIC2 = "\xde\x12\x04\x95"; 134 135 $this->STREAM = $Reader; 136 $magic = $this->read(4); 137 if ($magic == $MAGIC1) { 138 $this->BYTEORDER = 1; 139 } elseif ($magic == $MAGIC2) { 140 $this->BYTEORDER = 0; 141 } else { 142 $this->error = 1; // not MO file 143 return false; 144 } 145 146 // FIXME: Do we care about revision? We should. 147 $this->revision = $this->readint(); 148 149 $this->total = $this->readint(); 150 $this->originals = $this->readint(); 151 $this->translations = $this->readint(); 152 } 153 154 /** 155 * Loads the translation tables from the MO file into the cache 156 * If caching is enabled, also loads all strings into a cache 157 * to speed up translation lookups 158 * 159 * @access private 160 */ 161 function load_tables() { 162 if (is_array($this->cache_translations) && 163 is_array($this->table_originals) && 164 is_array($this->table_translations)) 165 return; 166 167 /* get original and translations tables */ 168 if (!is_array($this->table_originals)) { 169 $this->STREAM->seekto($this->originals); 170 $this->table_originals = $this->readintarray($this->total * 2); 171 } 172 if (!is_array($this->table_translations)) { 173 $this->STREAM->seekto($this->translations); 174 $this->table_translations = $this->readintarray($this->total * 2); 175 } 176 177 if ($this->enable_cache) { 178 $this->cache_translations = array (); 179 /* read all strings in the cache */ 180 for ($i = 0; $i < $this->total; $i++) { 181 $this->STREAM->seekto($this->table_originals[$i * 2 + 2]); 182 $original = $this->STREAM->read($this->table_originals[$i * 2 + 1]); 183 $this->STREAM->seekto($this->table_translations[$i * 2 + 2]); 184 $translation = $this->STREAM->read($this->table_translations[$i * 2 + 1]); 185 $this->cache_translations[$original] = $translation; 186 } 187 } 188 } 189 190 /** 191 * Returns a string from the "originals" table 192 * 193 * @access private 194 * @param int num Offset number of original string 195 * @return string Requested string if found, otherwise '' 196 */ 197 function get_original_string($num) { 198 $length = $this->table_originals[$num * 2 + 1]; 199 $offset = $this->table_originals[$num * 2 + 2]; 200 if (! $length) 201 return ''; 202 $this->STREAM->seekto($offset); 203 $data = $this->STREAM->read($length); 204 return (string)$data; 205 } 206 207 /** 208 * Returns a string from the "translations" table 209 * 210 * @access private 211 * @param int num Offset number of original string 212 * @return string Requested string if found, otherwise '' 213 */ 214 function get_translation_string($num) { 215 $length = $this->table_translations[$num * 2 + 1]; 216 $offset = $this->table_translations[$num * 2 + 2]; 217 if (! $length) 218 return ''; 219 $this->STREAM->seekto($offset); 220 $data = $this->STREAM->read($length); 221 return (string)$data; 222 } 223 224 /** 225 * Binary search for string 226 * 227 * @access private 228 * @param string string 229 * @param int start (internally used in recursive function) 230 * @param int end (internally used in recursive function) 231 * @return int string number (offset in originals table) 232 */ 233 function find_string($string, $start = -1, $end = -1) { 234 if (($start == -1) or ($end == -1)) { 235 // find_string is called with only one parameter, set start end end 236 $start = 0; 237 $end = $this->total; 238 } 239 if (abs($start - $end) <= 1) { 240 // We're done, now we either found the string, or it doesn't exist 241 $txt = $this->get_original_string($start); 242 if ($string == $txt) 243 return $start; 244 else 245 return -1; 246 } else if ($start > $end) { 247 // start > end -> turn around and start over 248 return $this->find_string($string, $end, $start); 249 } else { 250 // Divide table in two parts 251 $half = (int)(($start + $end) / 2); 252 $cmp = strcmp($string, $this->get_original_string($half)); 253 if ($cmp == 0) 254 // string is exactly in the middle => return it 255 return $half; 256 else if ($cmp < 0) 257 // The string is in the upper half 258 return $this->find_string($string, $start, $half); 259 else 260 // The string is in the lower half 261 return $this->find_string($string, $half, $end); 262 } 263 } 264 265 /** 266 * Translates a string 267 * 268 * @access public 269 * @param string string to be translated 270 * @return string translated string (or original, if not found) 271 */ 272 function translate($string) { 273 if ($this->short_circuit) 274 return $string; 275 $this->load_tables(); 276 277 if ($this->enable_cache) { 278 // Caching enabled, get translated string from cache 279 if (array_key_exists($string, $this->cache_translations)) 280 return $this->cache_translations[$string]; 281 else 282 return $string; 283 } else { 284 // Caching not enabled, try to find string 285 $num = $this->find_string($string); 286 if ($num == -1) 287 return $string; 288 else 289 return $this->get_translation_string($num); 290 } 291 } 292 293 /** 294 * Sanitize plural form expression for use in PHP eval call. 295 * 296 * @access private 297 * @return string sanitized plural form expression 298 */ 299 function sanitize_plural_expression($expr) { 300 // Get rid of disallowed characters. 301 $expr = preg_replace('@[^a-zA-Z0-9_:;\(\)\?\|\&=!<>+*/\%-]@', '', $expr); 302 303 // Add parenthesis for tertiary '?' operator. 304 $expr .= ';'; 305 $res = ''; 306 $p = 0; 307 for ($i = 0, $k = strlen($expr); $i < $k; $i++) { 308 $ch = $expr[$i]; 309 switch ($ch) { 310 case '?': 311 $res .= ' ? ('; 312 $p++; 313 break; 314 case ':': 315 $res .= ') : ('; 316 break; 317 case ';': 318 $res .= str_repeat( ')', $p) . ';'; 319 $p = 0; 320 break; 321 default: 322 $res .= $ch; 323 } 324 } 325 return $res; 326 } 327 328 /** 329 * Parse full PO header and extract only plural forms line. 330 * 331 * @access private 332 * @return string verbatim plural form header field 333 */ 334 function extract_plural_forms_header_from_po_header($header) { 335 $regs = array(); 336 if (preg_match("/(^|\n)plural-forms: ([^\n]*)\n/i", $header, $regs)) 337 $expr = $regs[2]; 338 else 339 $expr = "nplurals=2; plural=n == 1 ? 0 : 1;"; 340 return $expr; 341 } 342 343 /** 344 * Get possible plural forms from MO header 345 * 346 * @access private 347 * @return string plural form header 348 */ 349 function get_plural_forms() { 350 // lets assume message number 0 is header 351 // this is true, right? 352 $this->load_tables(); 353 354 // cache header field for plural forms 355 if (! is_string($this->pluralheader)) { 356 if ($this->enable_cache) { 357 $header = $this->cache_translations[""]; 358 } else { 359 $header = $this->get_translation_string(0); 360 } 361 $expr = $this->extract_plural_forms_header_from_po_header($header); 362 $this->pluralheader = $this->sanitize_plural_expression($expr); 363 } 364 return $this->pluralheader; 365 } 366 367 /** 368 * Detects which plural form to take 369 * 370 * @access private 371 * @param n count 372 * @return int array index of the right plural form 373 */ 374 function select_string($n) { 375 // Expression reads 376 // nplurals=X; plural= n != 1 377 if (!isset($this->plural_expression)) { 378 $matches = array(); 379 if (!preg_match('`nplurals\s*=\s*(\d+)\s*;\s*plural\s*=\s*(.+$)`', 380 $this->get_plural_forms(), $matches)) 381 return 1; 382 383 $this->plural_expression = create_function('$n', 384 sprintf('return %s;', str_replace('n', '($n)', $matches[2]))); 385 $this->plural_total = (int) $matches[1]; 386 } 387 $func = $this->plural_expression; 388 $plural = $func($n); 389 return ($plural > $this->plural_total) 390 ? $this->plural_total - 1 391 : $plural; 392 } 393 394 /** 395 * Plural version of gettext 396 * 397 * @access public 398 * @param string single 399 * @param string plural 400 * @param string number 401 * @return translated plural form 402 */ 403 function ngettext($single, $plural, $number) { 404 if ($this->short_circuit) { 405 if ($number != 1) 406 return $plural; 407 else 408 return $single; 409 } 410 411 // find out the appropriate form 412 $select = $this->select_string($number); 413 414 // this should contains all strings separated by NULLs 415 $key = $single . chr(0) . $plural; 416 417 418 if ($this->enable_cache) { 419 if (! array_key_exists($key, $this->cache_translations)) { 420 return ($number != 1) ? $plural : $single; 421 } else { 422 $result = $this->cache_translations[$key]; 423 $list = explode(chr(0), $result); 424 return $list[$select]; 425 } 426 } else { 427 $num = $this->find_string($key); 428 if ($num == -1) { 429 return ($number != 1) ? $plural : $single; 430 } else { 431 $result = $this->get_translation_string($num); 432 $list = explode(chr(0), $result); 433 return $list[$select]; 434 } 435 } 436 } 437 438 function pgettext($context, $msgid) { 439 $key = $context . chr(4) . $msgid; 440 $ret = $this->translate($key); 441 if (strpos($ret, "\004") !== FALSE) { 442 return $msgid; 443 } else { 444 return $ret; 445 } 446 } 447 448 function npgettext($context, $singular, $plural, $number) { 449 $key = $context . chr(4) . $singular; 450 $ret = $this->ngettext($key, $plural, $number); 451 if (strpos($ret, "\004") !== FALSE) { 452 return $singular; 453 } else { 454 return $ret; 455 } 456 457 } 458} 459 460class FileReader { 461 var $_pos; 462 var $_fd; 463 var $_length; 464 465 function __construct($filename) { 466 if (is_resource($filename)) { 467 $this->_length = strlen(stream_get_contents($filename)); 468 rewind($filename); 469 $this->_fd = $filename; 470 } 471 elseif (file_exists($filename)) { 472 473 $this->_length=filesize($filename); 474 $this->_fd = fopen($filename,'rb'); 475 if (!$this->_fd) { 476 $this->error = 3; // Cannot read file, probably permissions 477 return false; 478 } 479 } else { 480 $this->error = 2; // File doesn't exist 481 return false; 482 } 483 $this->_pos = 0; 484 } 485 486 function read($bytes) { 487 if ($bytes) { 488 fseek($this->_fd, $this->_pos); 489 490 // PHP 5.1.1 does not read more than 8192 bytes in one fread() 491 // the discussions at PHP Bugs suggest it's the intended behaviour 492 $data = ''; 493 while ($bytes > 0) { 494 $chunk = fread($this->_fd, $bytes); 495 $data .= $chunk; 496 $bytes -= strlen($chunk); 497 } 498 $this->_pos = ftell($this->_fd); 499 500 return $data; 501 } else return ''; 502 } 503 504 function seekto($pos) { 505 fseek($this->_fd, $pos); 506 $this->_pos = ftell($this->_fd); 507 return $this->_pos; 508 } 509 510 function currentpos() { 511 return $this->_pos; 512 } 513 514 function length() { 515 return $this->_length; 516 } 517 518 function close() { 519 fclose($this->_fd); 520 } 521 522} 523 524/** 525 * Class: Translation 526 * 527 * This class is strongly based on the gettext_reader class. It makes use of 528 * a few simple optimizations for the context of osTicket 529 * 530 * * The language packs are pre-compiled and distributed (which means 531 * they can be customized). 532 * * The MO file will always be processed by PHP code 533 * * osTicket uses utf-8 output exclusively (for web traffic anyway) 534 * 535 * These allow us to optimize the MO file for the osTicket project 536 * specifically and make enough of an optimization to allow using a pure-PHP 537 * source gettext library implementation which should be roughly the same 538 * performance as the libc gettext library. 539 */ 540class Translation extends gettext_reader implements Serializable { 541 542 var $charset; 543 544 const META_HEADER = 0; 545 546 function __construct($reader, $charset=false) { 547 if (!$reader) 548 return $this->short_circuit = true; 549 550 // Just load the cache 551 if (!is_string($reader)) 552 throw new RuntimeException('Programming Error: Expected filename for translation source'); 553 $this->STREAM = $reader; 554 555 $this->enable_cache = true; 556 $this->charset = $charset; 557 $this->encode = $charset && strcasecmp($charset, 'utf-8') !== 0; 558 $this->load_tables(); 559 } 560 561 function load_tables() { 562 if (isset($this->cache_translations)) 563 return; 564 565 $this->cache_translations = (include $this->STREAM); 566 } 567 568 function translate($string) { 569 if ($this->short_circuit) 570 return $string; 571 572 // Caching enabled, get translated string from cache 573 if (isset($this->cache_translations[$string])) 574 $string = $this->cache_translations[$string]; 575 576 if (!$this->encode) 577 return $string; 578 579 return Charset::transcode($string, 'utf-8', $this->charset); 580 } 581 582 static function buildHashFile($mofile, $outfile=false, $return=false) { 583 if (!$outfile) { 584 $stream = fopen('php://stdout', 'w'); 585 } 586 elseif (is_string($outfile)) { 587 $stream = fopen($outfile, 'w'); 588 } 589 elseif (is_resource($outfile)) { 590 $stream = $outfile; 591 } 592 593 if (!$stream) 594 throw new InvalidArgumentException( 595 'Expected a filename or valid resource'); 596 597 if (!$mofile instanceof FileReader) 598 $mofile = new FileReader($mofile); 599 600 $reader = new parent($mofile, true); 601 602 if ($reader->short_circuit || $reader->error) 603 throw new Exception('Unable to initialize MO input file'); 604 605 $reader->load_tables(); 606 607 // Get basic table 608 if (!($table = $reader->cache_translations)) 609 throw new Exception('Unable to read translations from file'); 610 611 // Transcode the table to UTF-8 612 $header = $table[""]; 613 $info = array(); 614 preg_match('/^content-type: (.*)$/im', $header, $info); 615 $charset = false; 616 if ($content_type = $info[1]) { 617 // Find the charset property 618 $settings = explode(';', $content_type); 619 foreach ($settings as $v) { 620 @list($prop, $value) = explode('=', trim($v), 2); 621 if (strtolower($prop) == 'charset') { 622 $charset = trim($value); 623 break; 624 } 625 } 626 } 627 if ($charset && strcasecmp($charset, 'utf-8') !== 0) { 628 foreach ($table as $orig=>$trans) { 629 $table[Charset::utf8($orig, $charset)] = 630 Charset::utf8($trans, $charset); 631 unset($table[$orig]); 632 } 633 } 634 635 // Add in some meta-data 636 $table[self::META_HEADER] = array( 637 'Revision' => $reader->revision, // From the MO 638 'Total-Strings' => $reader->total, // From the MO 639 'Table-Size' => count($table), // Sanity check for later 640 'Build-Timestamp' => gmdate(DATE_RFC822), 641 'Format-Version' => 'A', // Support future formats 642 'Encoding' => 'UTF-8', 643 ); 644 645 // Serialize the PHP array and write to output 646 $contents = sprintf('<?php return %s;', var_export($table, true)); 647 if ($return) 648 return $contents; 649 else 650 fwrite($stream, $contents); 651 } 652 653 static function resurrect($key) { 654 if (!function_exists('apcu_fetch')) 655 return false; 656 657 $success = true; 658 if (($translation = apcu_fetch($key, $success)) && $success) 659 return $translation; 660 } 661 function cache($key) { 662 if (function_exists('apcu_add')) 663 apcu_add($key, $this); 664 } 665 666 667 function serialize() { 668 return serialize(array($this->charset, $this->encode, $this->cache_translations)); 669 } 670 function unserialize($what) { 671 list($this->charset, $this->encode, $this->cache_translations) 672 = unserialize($what); 673 $this->short_circuit = ! $this->enable_cache 674 = 0 < $this->cache_translations ? count($this->cache_translations) : 1; 675 } 676} 677 678if (!defined('LC_MESSAGES')) { 679 define('LC_ALL', 0); 680 define('LC_CTYPE', 1); 681 define('LC_NUMERIC', 2); 682 define('LC_TIME', 3); 683 define('LC_COLLATE', 4); 684 define('LC_MONETARY', 5); 685 define('LC_MESSAGES', 6); 686} 687 688class TextDomain { 689 var $l10n = array(); 690 var $path; 691 var $codeset; 692 var $domain; 693 694 static $registry; 695 static $default_domain = 'messages'; 696 static $current_locale = ''; 697 static $LC_CATEGORIES = array( 698 LC_ALL => 'LC_ALL', 699 LC_CTYPE => 'LC_CTYPE', 700 LC_NUMERIC => 'LC_NUMERIC', 701 LC_TIME => 'LC_TIME', 702 LC_COLLATE => 'LC_COLLATE', 703 LC_MONETARY => 'LC_MONETARY', 704 LC_MESSAGES => 'LC_MESSAGES' 705 ); 706 707 function __construct($domain) { 708 $this->domain = $domain; 709 } 710 711 function getTranslation($category=LC_MESSAGES, $locale=false) { 712 $locale = $locale ?: self::$current_locale 713 ?: self::setLocale(LC_MESSAGES, 0); 714 715 if (isset($this->l10n[$locale])) 716 return $this->l10n[$locale]; 717 718 if ($locale == 'en_US') { 719 $this->l10n[$locale] = new Translation(null); 720 } 721 else { 722 // get the current locale 723 $bound_path = @$this->path ?: './'; 724 $subpath = self::$LC_CATEGORIES[$category] . 725 '/'.$this->domain.'.mo.php'; 726 727 // APC short-circuit (if supported) 728 $key = sha1($locale .':lang:'. $subpath); 729 if ($T = Translation::resurrect($key)) { 730 return $this->l10n[$locale] = $T; 731 } 732 733 $locale_names = self::get_list_of_locales($locale); 734 $input = null; 735 foreach ($locale_names as $T) { 736 if (substr($bound_path, 7) != 'phar://') { 737 $phar_path = 'phar://' . $bound_path . $T . ".phar/" . $subpath; 738 if (file_exists($phar_path)) { 739 $input = $phar_path; 740 break; 741 } 742 } 743 $full_path = $bound_path . $T . "/" . $subpath; 744 if (file_exists($full_path)) { 745 $input = $full_path; 746 break; 747 } 748 } 749 // TODO: Handle charset hint from the environment 750 $this->l10n[$locale] = $T = new Translation($input); 751 $T->cache($key); 752 } 753 return $this->l10n[$locale]; 754 } 755 756 function setPath($path) { 757 $this->path = $path; 758 } 759 760 static function configureForUser($user=false) { 761 $lang = Internationalization::getCurrentLanguage($user); 762 $info = Internationalization::getLanguageInfo($lang); 763 if (!$info) 764 // Not a supported language 765 return; 766 767 // Define locale for C-libraries 768 putenv('LC_ALL=' . $info['code']); 769 self::setLocale(LC_ALL, $info['code']); 770 } 771 772 static function setDefaultDomain($domain) { 773 static::$default_domain = $domain; 774 } 775 776 /** 777 * Returns passed in $locale, or environment variable $LANG if $locale == ''. 778 */ 779 static function get_default_locale($locale='') { 780 if ($locale == '') // emulate variable support 781 return getenv('LANG'); 782 else 783 return $locale; 784 } 785 786 static function get_list_of_locales($locale) { 787 /* Figure out all possible locale names and start with the most 788 * specific ones. I.e. for sr_CS.UTF-8@latin, look through all of 789 * sr_CS.UTF-8@latin, sr_CS@latin, sr@latin, sr_CS.UTF-8, sr_CS, sr. 790 */ 791 $locale_names = $m = array(); 792 $lang = null; 793 if ($locale) { 794 if (preg_match("/^(?P<lang>[a-z]{2,3})" // language code 795 ."(?:_(?P<country>[A-Z]{2}))?" // country code 796 ."(?:\.(?P<charset>[-A-Za-z0-9_]+))?" // charset 797 ."(?:@(?P<modifier>[-A-Za-z0-9_]+))?$/", // @ modifier 798 $locale, $m) 799 ) { 800 801 if ($m['modifier']) { 802 // TODO: Confirm if Crowdin uses the modifer flags 803 if ($m['country']) { 804 $locale_names[] = "{$m['lang']}_{$m['country']}@{$m['modifier']}"; 805 } 806 $locale_names[] = "{$m['lang']}@{$m['modifier']}"; 807 } 808 if ($m['country']) { 809 $locale_names[] = "{$m['lang']}_{$m['country']}"; 810 } 811 $locale_names[] = $m['lang']; 812 } 813 814 // If the locale name doesn't match POSIX style, just include it as-is. 815 if (!in_array($locale, $locale_names)) 816 $locale_names[] = $locale; 817 } 818 return array_filter($locale_names); 819 } 820 821 static function setLocale($category, $locale) { 822 if ($locale === 0) { // use === to differentiate between string "0" 823 if (self::$current_locale != '') 824 return self::$current_locale; 825 else 826 // obey LANG variable, maybe extend to support all of LC_* vars 827 // even if we tried to read locale without setting it first 828 return self::setLocale($category, self::$current_locale); 829 } else { 830 if (function_exists('setlocale')) { 831 $ret = setlocale($category, $locale); 832 if (($locale == '' and !$ret) or // failed setting it by env 833 ($locale != '' and $ret != $locale)) { // failed setting it 834 // Failed setting it according to environment. 835 self::$current_locale = self::get_default_locale($locale); 836 } else { 837 self::$current_locale = $ret; 838 } 839 } else { 840 // No function setlocale(), emulate it all. 841 self::$current_locale = self::get_default_locale($locale); 842 } 843 return self::$current_locale; 844 } 845 } 846 847 static function lookup($domain=null) { 848 if (!isset($domain)) 849 $domain = self::$default_domain; 850 if (!isset(static::$registry[$domain])) { 851 static::$registry[$domain] = new TextDomain($domain); 852 } 853 return static::$registry[$domain]; 854 } 855} 856 857require_once INCLUDE_DIR . 'class.orm.php'; 858class CustomDataTranslation extends VerySimpleModel { 859 860 static $meta = array( 861 'table' => TRANSLATION_TABLE, 862 'pk' => array('id') 863 ); 864 865 const FLAG_FUZZY = 0x01; // Source string has been changed 866 const FLAG_UNAPPROVED = 0x02; // String has been reviewed by an authority 867 const FLAG_CURRENT = 0x04; // If more than one version exist, this is current 868 const FLAG_COMPLEX = 0x08; // Multiple strings in one translation. For instance article title and body 869 870 var $_complex; 871 872 static function lookup($msgid, $flags=0) { 873 if (!is_string($msgid)) 874 return parent::lookup($msgid); 875 876 // Hash is 16 char of md5 877 $hash = substr(md5($msgid), -16); 878 879 $criteria = array('object_hash'=>$hash); 880 881 if ($flags) 882 $criteria += array('flags__hasbit'=>$flags); 883 884 return parent::lookup($criteria); 885 } 886 887 static function getTranslation($locale, $cache=true) { 888 static $_cache = array(); 889 890 if ($cache && isset($_cache[$locale])) 891 return $_cache[$locale]; 892 893 $criteria = array( 894 'lang' => $locale, 895 'type' => 'phrase', 896 ); 897 898 $mo = array(); 899 foreach (static::objects()->filter($criteria) as $t) { 900 $mo[$t->object_hash] = $t; 901 } 902 903 return $_cache[$locale] = $mo; 904 } 905 906 static function translate($msgid, $locale=false, $cache=true, $type='phrase') { 907 global $thisstaff, $thisclient; 908 909 // Support sending a User as the locale 910 if (is_object($locale) && method_exists($locale, 'getLanguage')) 911 $locale = $locale->getLanguage(); 912 elseif (!$locale) 913 $locale = Internationalization::getCurrentLanguage(); 914 915 // Perhaps a slight optimization would be to check if the selected 916 // locale is also the system primary. If so, short-circuit 917 918 if ($locale) { 919 if ($cache) { 920 $mo = static::getTranslation($locale); 921 if (isset($mo[$msgid])) 922 $msgid = $mo[$msgid]->text; 923 } 924 elseif ($p = static::lookup(array( 925 'type' => $type, 926 'lang' => $locale, 927 'object_hash' => $msgid 928 ))) { 929 $msgid = $p->text; 930 } 931 } 932 return $msgid; 933 } 934 935 /** 936 * Decode complex translation message. Format is given in the $text 937 * parameter description. Complex data should be stored with the 938 * FLAG_COMPLEX flag set, and allows for complex key:value paired data 939 * to be translated. This is useful for strings which are translated 940 * together, such as the title and the body of an article. Storing the 941 * data in a single, complex record allows for a single database query 942 * to fetch or update all data for a particular object, such as a 943 * knowledgebase article. It also simplifies search indexing as only one 944 * translation record could be added for all the translatable elements 945 * for a single translatable object. 946 * 947 * Caveats: 948 * ::$text will return the stored, complex text. Use ::getComplex() to 949 * decode the complex storage format and retrieve the array. 950 * 951 * Parameters: 952 * $text - (string) - encoded text with the following format 953 * version \x03 key \x03 item1 \x03 key \x03 item2 ... 954 * 955 * Returns: 956 * (array) key:value pairs of translated content 957 */ 958 function decodeComplex($text) { 959 $blocks = explode("\x03", $text); 960 $version = array_shift($blocks); 961 962 $data = array(); 963 switch ($version) { 964 case 'A': 965 while (count($blocks) > 1) { 966 $key = array_shift($blocks); 967 $data[$key] = array_shift($blocks); 968 } 969 break; 970 default: 971 throw new Exception($version . ': Unknown complex format'); 972 } 973 974 return $data; 975 } 976 977 /** 978 * Encode complex content using the format outlined in ::decodeComplex. 979 * 980 * Caveats: 981 * This method does not set the FLAG_COMPLEX flag for this record, which 982 * should be set when storing complex data. 983 */ 984 static function encodeComplex(array $data) { 985 $encoded = 'A'; 986 foreach ($data as $key=>$text) { 987 $encoded .= "\x03{$key}\x03{$text}"; 988 } 989 return $encoded; 990 } 991 992 function getComplex() { 993 if (!$this->flags && self::FLAG_COMPLEX) 994 throw new Exception('Data consistency error. Translation is not complex'); 995 if (!isset($this->_complex)) 996 $this->_complex = $this->decodeComplex($this->text); 997 return $this->_complex; 998 } 999 1000 static function translateArticle($msgid, $locale=false) { 1001 return static::translate($msgid, $locale, false, 'article'); 1002 } 1003 1004 function save($refetch=false) { 1005 if (isset($this->text) && is_array($this->text)) { 1006 $this->text = static::encodeComplex($this->text); 1007 $this->flags |= self::FLAG_COMPLEX; 1008 } 1009 return parent::save($refetch); 1010 } 1011 1012 static function create($ht=false) { 1013 if (!is_array($ht)) 1014 return null; 1015 1016 if (is_array($ht['text'])) { 1017 // The parent constructor does not honor arrays 1018 $ht['text'] = static::encodeComplex($ht['text']); 1019 $ht['flags'] = ($ht['flags'] ?: 0) | self::FLAG_COMPLEX; 1020 } 1021 return new static($ht); 1022 } 1023 1024 static function allTranslations($msgid, $type='phrase', $lang=false) { 1025 $criteria = array('type' => $type); 1026 1027 if (is_array($msgid)) 1028 $criteria['object_hash__in'] = $msgid; 1029 else 1030 $criteria['object_hash'] = $msgid; 1031 1032 if ($lang) 1033 $criteria['lang'] = $lang; 1034 1035 try { 1036 return static::objects()->filter($criteria)->all(); 1037 } 1038 catch (OrmException $e) { 1039 // Translation table might not exist yet — happens on the upgrader 1040 return array(); 1041 } 1042 } 1043} 1044 1045class CustomTextDomain { 1046 1047} 1048 1049// Functions for gettext library. Since the gettext extension for PHP is not 1050// used as a fallback, there is no detection and compat funciton 1051// installation for the gettext library function calls. 1052 1053function _gettext($msgid) { 1054 return TextDomain::lookup()->getTranslation()->translate($msgid); 1055} 1056function __($msgid) { 1057 return _gettext($msgid); 1058} 1059function _ngettext($singular, $plural, $number) { 1060 return TextDomain::lookup()->getTranslation() 1061 ->ngettext($singular, $plural, $number); 1062} 1063function _dgettext($domain, $msgid) { 1064 return TextDomain::lookup($domain)->getTranslation() 1065 ->translate($msgid); 1066} 1067function _dngettext($domain, $singular, $plural, $number) { 1068 return TextDomain::lookup($domain)->getTranslation() 1069 ->ngettext($singular, $plural, $number); 1070} 1071function _dcgettext($domain, $msgid, $category) { 1072 return TextDomain::lookup($domain)->getTranslation($category) 1073 ->translate($msgid); 1074} 1075function _dcngettext($domain, $singular, $plural, $number, $category) { 1076 return TextDomain::lookup($domain)->getTranslation($category) 1077 ->ngettext($singular, $plural, $number); 1078} 1079function _pgettext($context, $msgid) { 1080 return TextDomain::lookup()->getTranslation() 1081 ->pgettext($context, $msgid); 1082} 1083function _dpgettext($domain, $context, $msgid) { 1084 return TextDomain::lookup($domain)->getTranslation() 1085 ->pgettext($context, $msgid); 1086} 1087function _dcpgettext($domain, $context, $msgid, $category) { 1088 return TextDomain::lookup($domain)->getTranslation($category) 1089 ->pgettext($context, $msgid); 1090} 1091function _npgettext($context, $singular, $plural, $n) { 1092 return TextDomain::lookup()->getTranslation() 1093 ->npgettext($context, $singular, $plural, $n); 1094} 1095function _dnpgettext($domain, $context, $singular, $plural, $n) { 1096 return TextDomain::lookup($domain)->getTranslation() 1097 ->npgettext($context, $singular, $plural, $n); 1098} 1099function _dcnpgettext($domain, $context, $singular, $plural, $category, $n) { 1100 return TextDomain::lookup($domain)->getTranslation($category) 1101 ->npgettext($context, $singular, $plural, $n); 1102} 1103 1104// Custom data translations 1105function _H($tag) { 1106 return substr(md5($tag), -16); 1107} 1108 1109interface Translatable { 1110 function getTranslationTag(); 1111 function getLocalName($user=false); 1112} 1113 1114do { 1115 if (PHP_SAPI != 'cli') break; 1116 if (empty ($_SERVER['argc']) || $_SERVER['argc'] < 2) break; 1117 if (empty ($_SERVER['PHP_SELF']) || FALSE === strpos ($_SERVER['PHP_SELF'], basename(__FILE__)) ) break; 1118 $file = $argv[1]; 1119 Translation::buildHashFile($file); 1120} while (0); 1121