diff --git a/include/class.i18n.php b/include/class.i18n.php index c023566d90f0fc7ad154c91d004f113c232c7c51..160de51a84045aebe1d22f15f1b007b868c1f52b 100644 --- a/include/class.i18n.php +++ b/include/class.i18n.php @@ -362,9 +362,9 @@ class Internationalization { return TextDomain::lookup()->getTranslation($locale) ->translate($msgid); } - function _NL($msgid, $plural, $count, $locale) { + function _NL($msgid, $plural, $n, $locale) { return TextDomain::lookup()->getTranslation($locale) - ->ngettext($msgid); + ->ngettext($msgid, $plural, $n); } } } diff --git a/include/class.plugin.php b/include/class.plugin.php index 8d09c7713f425446cd389fefd775e3c20103630d..5a29ce483be7aa0797165d8430ac70a25d33fb89 100644 --- a/include/class.plugin.php +++ b/include/class.plugin.php @@ -333,10 +333,11 @@ abstract class Plugin { } function getId() { return $this->id; } - function getName() { return $this->info['name']; } + function getName() { return $this->__($this->info['name']); } function isActive() { return $this->ht['isactive']; } function isPhar() { return $this->ht['isphar']; } function getInstallDate() { return $this->ht['installed']; } + function getInstallPath() { return $this->ht['install_path']; } function getIncludePath() { return realpath(INCLUDE_DIR . $this->info['install_path'] . '/' @@ -446,7 +447,8 @@ abstract class Plugin { * `7afc8bf80b0555bed88823306744258d6030f0d9 `, which will resolve to a * string like the following: * ``` - * "v=1; i=storage:s3; s=MEUCIFw6A489eX4Oq17BflxCZ8+MH6miNjtcpScUoKDjmblsAiEAjiBo9FzYtV3WQtW6sbhPlJXcoPpDfYyQB+BFVBMps4c=; V=0.1;" + * "v=1; i=storage:s3; s=MEUCIFw6A489eX4Oq17BflxCZ8+MH6miNjtcpScUoKDjmb + * lsAiEAjiBo9FzYtV3WQtW6sbhPlJXcoPpDfYyQB+BFVBMps4c=; V=0.1;" * ``` * Which is a simple semicolon separated key-value pair string with the * following keys @@ -554,6 +556,98 @@ abstract class Plugin { <?php break; } } + + /** + * Function: __ + * + * Translate a single string (without plural alternatives) from the + * langauge pack installed in this plugin. The domain is auto-configured + * and detected from the plugin install path. + */ + function __($msgid) { + if (!isset($this->translation)) { + // Detect the domain from the plugin install-path + $groups = array(); + preg_match('`plugins/(\w+)(?:.phar)?`', $this->getInstallPath(), $groups); + + $domain = $groups[1]; + if (!$domain) + return $msgid; + + $this->translation = self::translate($domain); + } + list($__, $_N) = $this->translation; + return $__($msgid); + } + + // Domain-specific translations (plugins) + /** + * Function: translate + * + * Convenience function to setup translation functions for other + * domains. This is of greatest benefit for plugins. This will return + * two functions to perform the translations. The first will translate a + * single string, the second will translate a plural string. + * + * Parameters: + * $domain - (string) text domain. The location of the MO.php file + * will be (path)/LC_MESSAGES/(locale)/(domain).mo.php. The (path) + * can be set via the $options parameter + * $options - (array<string:mixed>) Extra options for the setup + * "path" - (string) path to the folder containing the LC_MESSAGES + * folder. The (locale) setting is set externally respective to + * the user. If this is not set, the directory of the caller is + * assumed, plus '/i18n'. This is geared for plugins to be + * built with i18n content inside the '/i18n/' folder. + * + * Returns: + * Translation utility functions which mimic the __() and _N() + * functions. Note that two functions are returned. Capture them with a + * PHP list() construct. + * + * Caveats: + * When desiging plugins which might be installed in versions of + * osTicket which don't provide this function, use this compatibility + * interface: + * + * // Provide compatibility function for versions of osTicket prior to + * // translation support (v1.9.4) + * function translate($domain) { + * if (!method_exists('Plugin', 'translate')) { + * return array( + * function($x) { return $x; }, + * function($x, $y, $n) { return $n != 1 ? $y : $x; }, + * ); + * } + * return Plugin::translate($domain); + * } + */ + static function translate($domain, $options=array()) { + + // Configure the path for the domain. If no + $path = @$options['path']; + if (!$path) { + # Fetch the working path of the caller + $bt = debug_backtrace(false); + $path = dirname($bt[0]["file"]) . '/i18n'; + } + $path = rtrim($path, '/') . '/'; + + $D = TextDomain::lookup($domain); + $D->setPath($path); + $trans = $D->getTranslation(); + + return array( + // __() + function($msgid) use ($trans) { + return $trans->translate($msgid); + }, + // _N() + function($singular, $plural, $n) use ($trans) { + return $trans->ngettext($singular, $plural, $n); + }, + ); + } } ?> diff --git a/include/class.translation.php b/include/class.translation.php index 9a093dcd17e66d955a27dbae951f2cd18336bb53..06d25b05f839ecfa1682a29a4d894c872d84c6cc 100644 --- a/include/class.translation.php +++ b/include/class.translation.php @@ -573,7 +573,7 @@ class Translation extends gettext_reader { return Format::encode($string, 'utf-8', $this->charset); } - static function buildHashFile($mofile, $outfile=false) { + static function buildHashFile($mofile, $outfile=false, $return=false) { if (!$outfile) { $stream = fopen('php://stdout', 'w'); } @@ -638,7 +638,11 @@ class Translation extends gettext_reader { ); // Serialize the PHP array and write to output - fwrite($stream, sprintf('<?php return %s;', var_export($table, true))); + $contents = sprintf('<?php return %s;', var_export($table, true)); + if ($return) + return $contents; + else + fwrite($stream, $contents); } } @@ -694,10 +698,12 @@ class TextDomain { $locale_names = self::get_list_of_locales($locale); $input = null; foreach ($locale_names as $T) { - $phar_path = 'phar://' . $bound_path . $T . ".phar/" . $subpath; - if (file_exists($phar_path)) { - $input = $phar_path; - break; + if (substr($bound_path, 7) != 'phar://') { + $phar_path = 'phar://' . $bound_path . $T . ".phar/" . $subpath; + if (file_exists($phar_path)) { + $input = $phar_path; + break; + } } $full_path = $bound_path . $T . "/" . $subpath; if (file_exists($full_path)) { diff --git a/include/class.upgrader.php b/include/class.upgrader.php index 3dad00b6a34e5ccc1a4bb27026f105aedf31b9ed..6246e542d0b524b3dec26364297a506d9aab18ce 100644 --- a/include/class.upgrader.php +++ b/include/class.upgrader.php @@ -406,7 +406,7 @@ class StreamUpgrader extends SetupWizard { $shash = substr($phash, 9, 8); //Log the patch info - $logMsg = sprintf(_S("Patch %s applied successfully "), $phash); + $logMsg = sprintf(_S("Patch %s applied successfully"), $phash); if(($info = $this->readPatchInfo($patch)) && $info['version']) $logMsg.= ' ('.$info['version'].') '; diff --git a/include/client/register.inc.php b/include/client/register.inc.php index 44f9fa22b5c3508228b74cad971e411c8c90aa0c..78b42ac9474fc251c3e937f5989677cbc2a70a3e 100644 --- a/include/client/register.inc.php +++ b/include/client/register.inc.php @@ -79,7 +79,7 @@ $info = Format::htmlchars(($errors && $_POST)?$_POST:$info); <input type="hidden" name="username" value="<?php echo $info['username']; ?>"/> <?php foreach (UserAuthenticationBackend::allRegistered() as $bk) { if ($bk::$id == $info['backend']) { - echo $bk::$name; + echo $bk->getName(); break; } } ?> diff --git a/include/staff/staff.inc.php b/include/staff/staff.inc.php index 8b73bb557a8d401b200a4e47b78cd237afe6bbb6..1dd7d105ada2a28c76b917221460563b6604c9f7 100644 --- a/include/staff/staff.inc.php +++ b/include/staff/staff.inc.php @@ -147,7 +147,7 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); <option value="<?php echo $ab::$id; ?>" <?php if ($info['backend'] == $ab::$id) echo 'selected="selected"'; ?>><?php - echo $ab::$name; ?></option> + echo $ab->getName(); ?></option> <?php } ?> </select> </td> diff --git a/include/staff/templates/user-register.tmpl.php b/include/staff/templates/user-register.tmpl.php index 1af7e4af89c8cb686894367b4274847f66c31475..5834256d61ed7b53d94838b56918ee964609513c 100644 --- a/include/staff/templates/user-register.tmpl.php +++ b/include/staff/templates/user-register.tmpl.php @@ -65,7 +65,7 @@ echo sprintf(__( <option value="<?php echo $ab::$id; ?>" <?php if ($info['backend'] == $ab::$id) echo 'selected="selected"'; ?>><?php - echo $ab::$name; ?></option> + echo $ab->getName(); ?></option> <?php } ?> </select> </td> diff --git a/setup/cli/modules/i18n.php b/setup/cli/modules/i18n.php index a863e5349c05f3d83f88a0e9da9a35eef61a902f..1adab5370afa5db6bacc16435d069c650de91f37 100644 --- a/setup/cli/modules/i18n.php +++ b/setup/cli/modules/i18n.php @@ -30,9 +30,15 @@ class i18n_Compiler extends Module { 'help' => "Language pack to be signed"), 'pkey' => array('-P', '--pkey', 'metavar'=>'key-file', 'help' => 'Private key for signing'), + 'root' => array('-R', '--root', 'matavar'=>'path', + 'help' => 'Specify a root folder for `make-pot`'), + 'domain' => array('-D', '--domain', 'metavar'=>'name', + 'default' => '', + 'help' => 'Add a domain to the path/context of PO strings'), ); - static $crowdin_api_url = 'http://i18n.osticket.com/api/project/osticket-official/{command}'; + static $project = 'osticket-official'; + static $crowdin_api_url = 'http://i18n.osticket.com/api/project/{project}/{command}'; function _http_get($url) { #curl post @@ -51,7 +57,9 @@ class i18n_Compiler extends Module { function _request($command, $args=array()) { - $url = str_replace('{command}', $command, self::$crowdin_api_url); + $url = str_replace(array('{command}', '{project}'), + array($command, self::$project), + self::$crowdin_api_url); $args += array('key' => $this->key); foreach ($args as &$a) @@ -84,7 +92,7 @@ class i18n_Compiler extends Module { $this->_build($options['lang']); break; case 'make-pot': - $this->_make_pot(); + $this->_make_pot($options); break; case 'sign': if (!$options['file'] || !file_exists($options['file'])) @@ -196,17 +204,13 @@ class i18n_Compiler extends Module { if (is_resource($msgfmt)) { fwrite($pipes[0], $po_file); fclose($pipes[0]); - $mo_output = fopen('php://temp', 'r+b'); $mo_input = fopen('php://temp', 'r+b'); fwrite($mo_input, stream_get_contents($pipes[1])); rewind($mo_input); require_once INCLUDE_DIR . 'class.translation.php'; - Translation::buildHashFile($mo_input, $mo_output); - rewind($mo_output); - $mo = stream_get_contents($mo_output); + $mo = Translation::buildHashFile($mo_input, false, true); $phar->addFromString('LC_MESSAGES/messages.mo.php', $mo); fclose($mo_input); - fclose($mo_output); } } @@ -379,8 +383,9 @@ class i18n_Compiler extends Module { while (list(,$T) = each($tokens)) { switch ($T[0]) { case T_STRING: + case T_VARIABLE: if ($funcdef) - break;; + break; if ($T[1] == 'sprintf') { foreach ($this->__find_strings($tokens, $funcs) as $i=>$f) { // Only the first on gets the php-format flag @@ -430,11 +435,11 @@ class i18n_Compiler extends Module { // 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); + $string = preg_replace("`\n\s*`", "\\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); + $strings = array_filter(explode("\n", $string)); if (count($strings) > 1) array_unshift($strings, ""); @@ -494,29 +499,33 @@ class i18n_Compiler extends Module { } } - function _make_pot() { + function _make_pot($options) { error_reporting(E_ALL); $funcs = array( '__' => array('forms'=>1), + '$__' => array('forms'=>1), '_S' => array('forms'=>1), '_N' => array('forms'=>2), + '$_N' => array('forms'=>2), '_NS' => array('forms'=>2), '_P' => array('context'=>1, 'forms'=>1), '_NP' => array('context'=>1, 'forms'=>2), // This is an error '_' => array('forms'=>0), ); - $files = Test::getAllScripts(); + $root = realpath($options['root'] ?: ROOT_DIR); + $domain = $options['domain'] ? '('.$options['domain'].')/' : ''; + $files = Test::getAllScripts(true, $root); $strings = array(); foreach ($files as $f) { - $F = str_replace(ROOT_DIR, '', $f); + $F = str_replace($root.'/', $domain, $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 $call) { self::__addString($strings, $call, $F); } } - $strings = array_merge($strings, $this->__getAllJsPhrases()); + $strings = array_merge($strings, $this->__getAllJsPhrases($root)); $this->__write_pot($strings); } @@ -544,12 +553,12 @@ class i18n_Compiler extends Module { $E['context'] = $call['context']; } - function __getAllJsPhrases() { + function __getAllJsPhrases($root=ROOT_DIR) { $strings = array(); $funcs = array('__'=>array('forms'=>1)); - foreach (glob_recursive(ROOT_DIR . "*.js") as $s) { + foreach (glob_recursive($root . "*.js") as $s) { $script = file_get_contents($s); - $s = str_replace(ROOT_DIR, '', $s); + $s = str_replace($root, '', $s); $this->stderr->write($s."\n"); $calls = array(); preg_match_all('/__\(\s*[^\'"]*(([\'"])(?:(?<!\\\\)\2|.)+\2)\s*[^)]*\)/', diff --git a/setup/test/tests/class.test.php b/setup/test/tests/class.test.php index 317de6fee8bc14132d331256094b9ca19d971167..5dd6097dcb127a1a3391d36361872ed0e6b5dec2 100644 --- a/setup/test/tests/class.test.php +++ b/setup/test/tests/class.test.php @@ -15,6 +15,9 @@ class Test { '/include/plugins/', '/include/h2o/', '/include/mpdf/', + + # Includes in the core-plugins project + '/lib/', ); function __construct() { @@ -28,8 +31,8 @@ class Test { function teardown() { } - static function getAllScripts($excludes=true) { - $root = get_osticket_root_path(); + static function getAllScripts($excludes=true, $root=false) { + $root = $root ?: get_osticket_root_path(); $scripts = array(); foreach (glob_recursive("$root/*.php") as $s) { $found = false;