Skip to content
Snippets Groups Projects
Commit b6162fd3 authored by Jared Hancock's avatar Jared Hancock
Browse files

i18n: Make plugins translatable

parent c00d56ac
No related branches found
No related tags found
No related merge requests found
......@@ -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);
}
}
}
......
......@@ -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);
},
);
}
}
?>
......@@ -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)) {
......
......@@ -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'].') ';
......
......@@ -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;
}
} ?>
......
......@@ -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>
......
......@@ -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>
......
......@@ -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*[^)]*\)/',
......
......@@ -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;
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment