diff --git a/setup/cli/modules/i18n.php b/setup/cli/modules/i18n.php index a9a4117c4b15f59d6914dbd11edf741b1c5544b7..3c044660a670800a7e49f4f23e7804463d00059b 100644 --- a/setup/cli/modules/i18n.php +++ b/setup/cli/modules/i18n.php @@ -10,7 +10,8 @@ class i18n_Compiler extends Module { var $arguments = array( "command" => "Action to be performed. - list - Show list of available translations" + list - Show list of available translations + make-pot - Build the PO file for gettext translations" ); var $options = array( @@ -69,6 +70,9 @@ class i18n_Compiler extends Module { $this->fail('Language code is required. See `list`'); $this->_build($options['lang']); break; + case 'make-pot': + $this->_make_pot(); + break; } } @@ -124,6 +128,227 @@ class i18n_Compiler extends Module { // TODO: Sign files } + + function __read_next_string($tokens) { + $string = array(); + + while (list(,$T) = each($tokens)) { + switch ($T[0]) { + case T_CONSTANT_ENCAPSED_STRING: + // String leading and trailing ' and " chars + $string['form'] = preg_replace(array("`^{$T[1][0]}`","`{$T[1][0]}$`"),array("",""), $T[1]); + $string['line'] = $T[2]; + break; + case T_DOC_COMMENT: + case T_COMMENT: + switch ($T[1][0]) { + case '/': + if ($T[1][1] == '*') + $text = trim($T[1], '/* '); + else + $text = ltrim($T[1], '/ '); + break; + case '#': + $text = ltrim($T[1], '# '); + } + $string['comment'] = $text; + break; + case T_WHITESPACE: + // noop + continue; + case T_STRING_VARNAME: + case T_NUM_STRING: + case T_ENCAPSED_AND_WHITESPACE: + case '.': + $string['constant'] = false; + break; + default: + return array($string, $T); + } + } + } + function __read_args($tokens, $constants=1) { + $args = array('forms'=>array()); + $arg = null; + + while (list($string,$T) = $this->__read_next_string($tokens)) { + if (count($args['forms']) < $constants && $string) { + if (isset($string['constant']) && !$string['constant']) { + throw new Exception($string['form'] . ': Untranslatable string'); + } + $args['forms'][] = $string['form']; + $args['line'] = $string['line']; + if (isset($string['comment'])) + $args['comments'][] = $string['comment']; + } + + switch ($T[0]) { + case ')': + return $args; + } + } + } + + function __get_func_args($tokens, $args) { + while (list(,$T) = each($tokens)) { + switch ($T[0]) { + case T_WHITESPACE: + continue; + case '(': + return $this->__read_args($tokens, $args); + default: + // Not a function call + return false; + } + } + } + function __find_strings($tokens, $funcs, $parens=0) { + $T_funcs = array(); + $funcdef = false; + while (list(,$T) = each($tokens)) { + switch ($T[0]) { + case T_STRING: + if ($funcdef) + break;; + if ($T[1] == 'sprintf') { + foreach ($this->__find_strings($tokens, $funcs) as $i=>$f) { + // Only the first on gets the php-format flag + if ($i == 0) + $f['flags'] = array('php-format'); + $T_funcs[] = $f; + } + break; + } + if (!isset($funcs[$T[1]])) + continue; + $constants = $funcs[$T[1]]; + if ($info = $this->__get_func_args($tokens, $constants)) + $T_funcs[] = $info; + break; + case T_COMMENT: + case T_DOC_COMMENT: + if (strpos($T[1], '* trans *') !== false) { + // Find the next textual token + list($S, $T) = $this->__read_next_string($tokens); + $string = array('forms'=>array($S['form']), 'line'=>$S['line']); + if (isset($S['comment'])) + $string['comments'][] = $S['comment']; + $T_funcs[] = $string; + } + break; + // Track function definitions of the gettext functions + case T_FUNCTION: + $funcdef = true; + break; + case '{'; + $funcdef = false; + case '(': + $parens++; + break; + case ')': + // End of scope? + if (--$parens == 0) + return $T_funcs; + } + } + return $T_funcs; + } + + function __write_string($string) { + // Unescape single quote (') and escape unescaped double quotes (") + $string = preg_replace(array("`\\\(['$])`", '`(?<!\\\)"`'), array("$1", '\"'), $string); + // Preserve embedded newlines + $string = str_replace("\n", "\\n\n", $string); + // Word-wrap long lines + $string = rtrim(preg_replace('/(?=[\s\p{Ps}])(.{1,76})(\s|$|(\p{Ps}))/uS', + "$1$2\n", $string), "\n"); + $strings = explode("\n", $string); + + if (count($strings) > 1) + array_unshift($strings, ""); + foreach ($strings as $line) { + print "\"{$line}\"\n"; + } + } + function __write_pot_header() { + $lines = array( + 'msgid ""', + 'msgstr ""', + '"Project-Id-Version: osTicket '.trim(`git describe`).'\n"', + '"POT-Create-Date: '.date('Y-m-d H:i O').'\n"', + '"Report-Msgid-Bugs-To: support@osticket.com\n"', + '"Language: en_US\n"', + '"MIME-Version: 1.0\n"', + '"Content-Type: text/plain; charset=UTF-8\n"', + '"Content-Transfer-Encoding: 8bit\n"', + '"X-Generator: osTicket i18n CLI\n"', + ); + print implode("\n", $lines); + print "\n"; + } + function __write_pot($strings) { + $this->__write_pot_header(); + foreach ($strings as $S) { + print "\n"; + if ($c = @$S['comments']) { + foreach ($c as $comment) { + foreach (explode("\n", $comment) as $line) { + $line = trim($line); + print "#. {$line}\n"; + } + } + } + foreach ($S['usage'] as $ref) { + print "#: ".$ref."\n"; + } + if ($f = @$S['flags']) { + print "#, ".implode(', ', $f)."\n"; + } + print "msgid "; + $this->__write_string($S['forms'][0]); + if (count($S['forms']) == 2) { + print "msgid_plural "; + $this->__write_string($S['forms'][1]); + print 'msgstr[0] ""'."\n"; + print 'msgstr[1] ""'."\n"; + } + else { + print 'msgstr ""'."\n"; + } + } + } + + function _make_pot() { + error_reporting(E_ALL); + $funcs = array('__'=>1, '_N'=>2); + function get_osticket_root_path() { return ROOT_DIR; } + require_once(ROOT_DIR.'setup/test/tests/class.test.php'); + $files = Test::getAllScripts(); + $strings = array(); + foreach ($files as $f) { + $F = str_replace(ROOT_DIR, '', $f); + $this->stderr->write("$F\n"); + $tokens = new ArrayObject(token_get_all(fread(fopen($f, 'r'), filesize($f)))); + foreach ($this->__find_strings($tokens, $funcs, 1) as $calls) { + if (!($forms = $calls['forms'])) + // Transation of non-constant + continue; + $primary = $forms[0]; + if (!isset($strings[$primary])) { + $strings[$primary] = array('forms' => $forms); + } + $E = &$strings[$primary]; + + if (isset($calls['line'])) + $E['usage'][] = "{$F}:{$calls['line']}"; + if (isset($calls['flags'])) + $E['flags'] = array_unique(array_merge(@$E['flags'] ?: array(), $calls['flags'])); + if (isset($calls['comments'])) + $E['comments'] = array_merge(@$E['comments'] ?: array(), $calls['comments']); + } + } + $this->__write_pot($strings); + } } Module::register('i18n', 'i18n_Compiler'); diff --git a/setup/test/run-tests.php b/setup/test/run-tests.php index 326f8be18f9ba349b2d908eab241be1f60d817d4..1b641a8926686bf30b33ffb1b1f3edbc384271db 100644 --- a/setup/test/run-tests.php +++ b/setup/test/run-tests.php @@ -7,34 +7,11 @@ $selected_test = (isset($argv[1])) ? $argv[1] : false; require_once "tests/class.test.php"; -if (!function_exists('get_osticket_root_path')) { - function get_osticket_root_path() { - # Hop up to the root folder - $start = dirname(__file__); - for (;;) { - if (file_exists($start . '/main.inc.php')) break; - $start .= '/..'; - } - return realpath($start); - } -} $root = get_osticket_root_path(); define('INCLUDE_DIR', "$root/include/"); define('PEAR_DIR', INCLUDE_DIR."pear/"); ini_set('include_path', './'.PATH_SEPARATOR.INCLUDE_DIR.PATH_SEPARATOR.PEAR_DIR); -if (!function_exists('glob_recursive')) { - # Check PHP syntax across all php files - function glob_recursive($pattern, $flags = 0) { - $files = glob($pattern, $flags); - foreach (glob(dirname($pattern).'/*', GLOB_ONLYDIR|GLOB_NOSORT) as $dir) { - $files = array_merge($files, - glob_recursive($dir.'/'.basename($pattern), $flags)); - } - return $files; - } -} - $fails = array(); function show_fails() { diff --git a/setup/test/tests/class.test.php b/setup/test/tests/class.test.php index a7f6c2fa044da245b2912c15e395755059d351c9..317de6fee8bc14132d331256094b9ca19d971167 100644 --- a/setup/test/tests/class.test.php +++ b/setup/test/tests/class.test.php @@ -5,7 +5,7 @@ class Test { var $warnings = array(); var $name = ""; - var $third_party_paths = array( + static $third_party_paths = array( '/include/JSON.php', '/include/htmLawed.php', '/include/PasswordHash.php', @@ -17,9 +17,6 @@ class Test { '/include/mpdf/', ); - function Test() { - call_user_func_array(array($this, '__construct'), func_get_args()); - } function __construct() { assert_options(ASSERT_CALLBACK, array($this, 'fail')); error_reporting(E_ALL & ~E_WARNING); @@ -31,14 +28,13 @@ class Test { function teardown() { } - /*static*/ - function getAllScripts($excludes=true) { + static function getAllScripts($excludes=true) { $root = get_osticket_root_path(); $scripts = array(); foreach (glob_recursive("$root/*.php") as $s) { $found = false; if ($excludes) { - foreach ($this->third_party_paths as $p) { + foreach (self::$third_party_paths as $p) { if (strpos($s, $p) !== false) { $found = true; break; @@ -105,4 +101,28 @@ class Test { return $line; } } + +if (!function_exists('glob_recursive')) { + # Check PHP syntax across all php files + function glob_recursive($pattern, $flags = 0) { + $files = glob($pattern, $flags); + foreach (glob(dirname($pattern).'/*', GLOB_ONLYDIR|GLOB_NOSORT) as $dir) { + $files = array_merge($files, + glob_recursive($dir.'/'.basename($pattern), $flags)); + } + return $files; + } +} + +if (!function_exists('get_osticket_root_path')) { + function get_osticket_root_path() { + # Hop up to the root folder + $start = dirname(__file__); + for (;;) { + if (file_exists($start . '/main.inc.php')) break; + $start .= '/..'; + } + return realpath($start); + } +} ?>