You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
731 lines
27 KiB
731 lines
27 KiB
<?php
|
|
|
|
require_once INCLUDE_DIR . 'class.format.php';
|
|
|
|
class i18n_Compiler extends Module {
|
|
|
|
var $prologue = "Manages translation files from Crowdin";
|
|
|
|
var $arguments = array(
|
|
"command" => array(
|
|
'help' => "Action to be performed.",
|
|
"options" => array(
|
|
'list' => 'Show list of available translations',
|
|
'build' => 'Compile a language pack',
|
|
'similar' => 'Find very similar strings',
|
|
'make-pot' => 'Build the PO file for gettext translations',
|
|
'sign' => 'Sign a language pack',
|
|
),
|
|
),
|
|
'file(s)' => array(
|
|
'required' => false,
|
|
'help' => 'File(s) to be signed, used with `sign`',
|
|
),
|
|
);
|
|
|
|
var $options = array(
|
|
"key" => array('-k','--key','metavar'=>'API-KEY',
|
|
'help'=>'Crowdin project API key. This can be omitted if
|
|
CROWDIN_API_KEY is defined in the ost-config.php file'),
|
|
"lang" => array('-L', '--lang', 'metavar'=>'code',
|
|
'help'=>'Language code (used for building)'),
|
|
'file' => array('-f', '--file', 'metavar'=>'FILE',
|
|
'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'),
|
|
'dns' => array('-d', '--dns', 'default' => false, 'metavar' => 'zone-id',
|
|
'help' => 'Write signature to DNS (via this AWS HostedZoneId)'),
|
|
'zlib' => array('-z', '--zlib', 'default' => false,
|
|
'action' => 'store_true', 'help' => 'Compress PHAR with zlib'),
|
|
'bzip2' => array('-j', '--bzip2', 'default' => false,
|
|
'action' => 'store_true', 'help' => 'Compress PHAR with bzip2'),
|
|
'branch' => array('-b', '--branch', 'help' => 'Use a Crowdin branch
|
|
(other than the root)'),
|
|
);
|
|
|
|
var $epilog = "Note: If updating DNS, you will need to set
|
|
AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY in the AWS credentials
|
|
profile in your home folder or in your environment. See AWS
|
|
configuration docs for more information";
|
|
|
|
static $project = 'osticket-official';
|
|
static $crowdin_api_url = 'http://i18n.osticket.com/api/project/{project}/{command}';
|
|
|
|
function _http_get($url) {
|
|
$this->stdout->write(">>> Downloading $url\n");
|
|
#curl post
|
|
$ch = curl_init();
|
|
curl_setopt($ch, CURLOPT_URL, $url);
|
|
curl_setopt($ch, CURLOPT_USERAGENT, 'osTicket/'.THIS_VERSION);
|
|
curl_setopt($ch, CURLOPT_HEADER, FALSE);
|
|
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, FALSE);
|
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
|
|
$result=curl_exec($ch);
|
|
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
curl_close($ch);
|
|
|
|
return array($code, $result);
|
|
}
|
|
|
|
function _request($command, $args=array()) {
|
|
|
|
$url = str_replace(array('{command}', '{project}'),
|
|
array($command, self::$project),
|
|
self::$crowdin_api_url);
|
|
|
|
$args += array('key' => $this->key);
|
|
if ($branch = $this->getOption('branch', false))
|
|
$args += array('branch' => $branch);
|
|
$url .= '?' . Http::build_query($args);
|
|
|
|
return $this->_http_get($url);
|
|
}
|
|
|
|
function run($args, $options) {
|
|
$this->key = $options['key'];
|
|
if (!$this->key && defined('CROWDIN_API_KEY'))
|
|
$this->key = CROWDIN_API_KEY;
|
|
|
|
function get_osticket_root_path() { return ROOT_DIR; }
|
|
require_once(ROOT_DIR.'setup/test/tests/class.test.php');
|
|
|
|
switch (strtolower($args['command'])) {
|
|
case 'list':
|
|
if (!$this->key)
|
|
$this->fail('API key is required');
|
|
$this->_list();
|
|
break;
|
|
case 'build':
|
|
if (!$this->key)
|
|
$this->fail('API key is required');
|
|
if (!$options['lang'])
|
|
$this->fail('Language code is required. See `list`');
|
|
$this->_build($options['lang'], $options);
|
|
break;
|
|
case 'similar':
|
|
$this->find_similar($options);
|
|
break;
|
|
case 'make-pot':
|
|
$this->_make_pot($options);
|
|
break;
|
|
case 'sign':
|
|
if (count($args) < 2)
|
|
$this->fail('Specify a language pack to sign');
|
|
foreach (range(1, count($args)-1, 1) as $i) {
|
|
$plugin = $args[$i];
|
|
if (!is_file($args[$i]))
|
|
$this->fail($args[$i].': No such file');
|
|
$this->_sign($args[$i], $options);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
function _list() {
|
|
error_reporting(E_ALL);
|
|
list($code, $body) = $this->_request('status');
|
|
$d = new DOMDocument();
|
|
$d->loadXML($body);
|
|
|
|
$xp = new DOMXpath($d);
|
|
foreach ($xp->query('//language') as $c) {
|
|
$name = $code = '';
|
|
foreach ($c->childNodes as $n) {
|
|
switch (strtolower($n->nodeName)) {
|
|
case 'name':
|
|
$name = $n->textContent;
|
|
break;
|
|
case 'code':
|
|
$code = $n->textContent;
|
|
break;
|
|
}
|
|
}
|
|
if (!$code)
|
|
continue;
|
|
$this->stdout->write(sprintf("%s (%s)\n", $code, $name));
|
|
}
|
|
}
|
|
|
|
function _build($lang, $options) {
|
|
list($code, $zip) = $this->_request("download/$lang.zip");
|
|
|
|
if ($code !== 200)
|
|
$this->fail('Language is not available'."\n");
|
|
|
|
$temp = tempnam('/tmp', 'osticket-cli');
|
|
$f = fopen($temp, 'w');
|
|
fwrite($f, $zip);
|
|
fclose($f);
|
|
$zip = new ZipArchive();
|
|
$zip->open($temp);
|
|
unlink($temp);
|
|
|
|
$lang = str_replace('-','_',$lang);
|
|
@unlink(I18N_DIR."$lang.phar");
|
|
$phar = new Phar(I18N_DIR."$lang.phar");
|
|
$phar->startBuffering();
|
|
if ($options['zlib'])
|
|
$phar->compress(Phar::GZ, 'phar');
|
|
if ($options['bzip2'])
|
|
$phar->compress(Phar::BZ2, 'phar');
|
|
|
|
$po_file = false;
|
|
|
|
$branch = false;
|
|
if ($options['branch'])
|
|
$branch = trim($options['branch'], '/') . '/';
|
|
for ($i=0; $i<$zip->numFiles; $i++) {
|
|
$info = $zip->statIndex($i);
|
|
if ($branch && strpos($info['name'], $branch) !== 0) {
|
|
// Skip files not part of the named branch
|
|
continue;
|
|
}
|
|
$contents = $zip->getFromIndex($i);
|
|
if (!$contents)
|
|
continue;
|
|
if (strpos($info['name'], '/messages.po') !== false) {
|
|
$po_file = $contents;
|
|
// Don't add the PO file as-is to the PHAR file
|
|
continue;
|
|
}
|
|
elseif (!$branch && !file_exists(I18N_DIR . 'en_US/' . $info['name'])) {
|
|
// Skip files in (other) branches
|
|
continue;
|
|
}
|
|
$lname = $branch ? explode('/', $info['name'], 2)[1] : $info['name'];
|
|
$phar->addFromString($lname, $contents);
|
|
}
|
|
|
|
// TODO: Add i18n extras (like fonts)
|
|
// Redactor language pack
|
|
//
|
|
$langs = array($lang);
|
|
if (strpos($lang, '_') !== false) {
|
|
@list($short) = explode('_', $lang);
|
|
$langs[] = $short;
|
|
}
|
|
foreach ($langs as $l) {
|
|
if (file_exists(I18N_DIR . "vendor/redactor/{$lang}.js")) {
|
|
$phar->addFromString('js/redactor.js', file_get_contents(
|
|
I18N_DIR . "vendor/redactor/{$lang}.js"));
|
|
break;
|
|
}
|
|
}
|
|
if ($code != 200)
|
|
$this->stderr->write($lang . ": Cannot find Redactor language file\n");
|
|
|
|
// JQuery UI Datepicker
|
|
// https://github.com/jquery/jquery-ui/tree/master/ui/i18n
|
|
foreach ($langs as $l) {
|
|
list($code, $js) = $this->_http_get(
|
|
'https://raw.githubusercontent.com/jquery/jquery-ui/master/ui/i18n/datepicker-'
|
|
.str_replace('_','-',$l).'.js');
|
|
// If locale-specific version is not available, use the base
|
|
// language version (de if de_CH is not available)
|
|
if ($code == 200)
|
|
break;
|
|
}
|
|
if ($code == 200)
|
|
$phar->addFromString('js/jquery.ui.datepicker.js', $js);
|
|
else
|
|
$this->stderr->write(str_replace('_','-',$lang)
|
|
.": Unable to fetch jQuery UI Datepicker locale file\n");
|
|
|
|
// True type fonts for PDF printing
|
|
$langs = (include I18N_DIR . 'langs.php');
|
|
$info = $langs[$lang];
|
|
if (isset($info['fonts'])) {
|
|
foreach ($info['fonts'] as $name => $types) {
|
|
foreach ($types as $code => $fullname) {
|
|
list($ttf, $url) = $fullname;
|
|
if (!$url)
|
|
continue;
|
|
list($code, $file) = $this->_http_get($url);
|
|
if ($code == 200)
|
|
$phar->addFromString('fonts/'.$ttf, $file);
|
|
else
|
|
$this->stderr->write(
|
|
"*** Unable to fetch $url\n");
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add in the messages.mo.php file
|
|
if ($po_file) {
|
|
$pipes = array();
|
|
$msgfmt = proc_open('msgfmt -o- -',
|
|
array(0=>array('pipe','r'), 1=>array('pipe','w')),
|
|
$pipes);
|
|
if (is_resource($msgfmt)) {
|
|
fwrite($pipes[0], $po_file);
|
|
fclose($pipes[0]);
|
|
$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';
|
|
$mo = Translation::buildHashFile($mo_input, false, true);
|
|
$phar->addFromString('LC_MESSAGES/messages.mo.php', $mo);
|
|
fclose($mo_input);
|
|
}
|
|
}
|
|
|
|
// Add in translation of javascript strings
|
|
$phrases = array();
|
|
if ($mo && ($js = $this->__getAllJsPhrases())) {
|
|
$mo = (eval (substr($mo, 5))); # Chop off <?php
|
|
foreach ($js as $c) {
|
|
foreach ($c['forms'] as $f) {
|
|
$phrases[$f] = @$mo[$f] ?: $f;
|
|
}
|
|
}
|
|
$phar->addFromString(
|
|
'js/osticket-strings.js',
|
|
sprintf('(function($){$.oststrings=%s;})(jQuery);',
|
|
JsonDataEncoder::encode($phrases))
|
|
);
|
|
}
|
|
|
|
// Include a manifest
|
|
include_once INCLUDE_DIR . 'class.mailfetch.php';
|
|
|
|
$po_header = Mail_Parse::splitHeaders($mo['']);
|
|
$info = array(
|
|
'Build-Date' => date(DATE_RFC822),
|
|
'Phrases-Version' => $po_header['X-Osticket-Major-Version'],
|
|
'Build-Version' => trim(`git describe`),
|
|
'Build-Major-Version' => MAJOR_VERSION,
|
|
'Language' => $po_header['Language'],
|
|
#'Phrases' =>
|
|
#'Translated' =>
|
|
#'Approved' =>
|
|
'Id' => 'lang:' . $lang,
|
|
'Last-Revision' => $po_header['PO-Revision-Date'],
|
|
'Version' => (int)(strtotime($po_header['PO-Revision-Date']) / 10000),
|
|
);
|
|
$phar->addFromString(
|
|
'MANIFEST.php',
|
|
sprintf('<?php return %s;', var_export($info, true)));
|
|
|
|
// TODO: Sign files
|
|
|
|
// Use a very small stub
|
|
$phar->setStub('<?php __HALT_COMPILER();');
|
|
$phar->setSignatureAlgorithm(Phar::SHA1);
|
|
$phar->stopBuffering();
|
|
}
|
|
|
|
function _sign($plugin, $options) {
|
|
if (!file_exists($plugin))
|
|
$this->fail($plugin.': Cannot find file');
|
|
elseif (!file_exists("phar://$plugin/MANIFEST.php"))
|
|
$this->fail($plugin.': Should be a plugin PHAR file');
|
|
$info = (include "phar://$plugin/MANIFEST.php");
|
|
$phar = new Phar($plugin);
|
|
|
|
if (!function_exists('openssl_get_privatekey'))
|
|
$this->fail('OpenSSL extension required for signing');
|
|
if (!$options['pkey'] || !file_exists($options['pkey']))
|
|
$this->fail('Signing private key (-P) required');
|
|
$private = openssl_get_privatekey(
|
|
file_get_contents($options['pkey']));
|
|
if (!$private)
|
|
$this->fail('Unable to read private key');
|
|
$signature = $phar->getSignature();
|
|
$seal = '';
|
|
openssl_sign($signature['hash'], $seal, $private,
|
|
OPENSSL_ALGO_SHA1);
|
|
if (!$seal)
|
|
$this->fail('Unable to generate verify signature');
|
|
|
|
$this->stdout->write(sprintf("Signature: %s\n",
|
|
strtolower($signature['hash'])));
|
|
$seal =
|
|
sprintf('"v=1; i=%s; s=%s; V=%s;"',
|
|
$info['Id'], base64_encode($seal), $info['Version']);
|
|
|
|
if ($options['dns']) {
|
|
if (!is_file(INCLUDE_DIR . 'aws.phar'))
|
|
$this->fail('Unable to include AWS phar file. Download to INCLUDE_DIR');
|
|
require_once INCLUDE_DIR . 'aws.phar';
|
|
|
|
$aws = Aws\Common\Aws::factory(array());
|
|
$client = $aws->get('Route53');
|
|
|
|
try {
|
|
$resp = $client->changeResourceRecordSets(array(
|
|
'HostedZoneId' => $options['dns'],
|
|
'ChangeBatch' => array(
|
|
'Changes' => array(
|
|
array(
|
|
'Action' => 'CREATE',
|
|
'ResourceRecordSet' => array(
|
|
'Name' => "{$signature['hash']}.updates.osticket.com.",
|
|
'Type' => 'TXT',
|
|
'TTL' => 172800,
|
|
'ResourceRecords' => array(
|
|
array(
|
|
'Value' => $seal,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
));
|
|
$this->stdout->write(sprintf('%s: %s', $resp['ChangeInfo']['Comment'],
|
|
$resp['ChangeInfo']['Status']));
|
|
}
|
|
catch (Exception $ex) {
|
|
$this->stdout->write("Seal: $seal\n");
|
|
$this->fail('!! AWS Update Failed: '.$ex->getMessage());
|
|
}
|
|
}
|
|
else {
|
|
$this->stdout->write("Seal: $seal\n");
|
|
}
|
|
}
|
|
|
|
function __read_next_string($tokens) {
|
|
$string = array();
|
|
|
|
while (list(,$T) = each($tokens)) {
|
|
switch ($T[0]) {
|
|
case T_CONSTANT_ENCAPSED_STRING:
|
|
// Strip 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['comments'][] = $text;
|
|
break;
|
|
case T_WHITESPACE:
|
|
// noop
|
|
continue 2;
|
|
case T_STRING_VARNAME:
|
|
case T_NUM_STRING:
|
|
case T_ENCAPSED_AND_WHITESPACE:
|
|
case '.':
|
|
$string['constant'] = false;
|
|
break;
|
|
case '[':
|
|
// Not intended to be translated — array index
|
|
return null;
|
|
default:
|
|
return array($string, $T);
|
|
}
|
|
}
|
|
}
|
|
function __read_args($tokens, $proto=false) {
|
|
$args = array('forms'=>array());
|
|
$arg = null;
|
|
$proto = $proto ?: array('forms'=>1);
|
|
|
|
while (list($string,$T) = $this->__read_next_string($tokens)) {
|
|
// Add context and forms
|
|
if (isset($proto['context']) && !isset($args['context'])) {
|
|
$args['context'] = $string['form'];
|
|
}
|
|
elseif (count($args['forms']) < $proto['forms'] && $string) {
|
|
if (isset($string['constant']) && !$string['constant']) {
|
|
throw new Exception($string['form'] . ': Untranslatable string');
|
|
}
|
|
$args['forms'][] = $string['form'];
|
|
}
|
|
elseif ($string) {
|
|
$this->stderr->write(sprintf("%s: %s: Too many arguments\n",
|
|
$string['line'] ?: '?', $string['form']));
|
|
}
|
|
|
|
// Add usage and comment info
|
|
if (!isset($args['line']) && isset($string['line']))
|
|
$args['line'] = $string['line'];
|
|
if (isset($string['comments']))
|
|
$args['comments'] = array_merge(
|
|
@$args['comments'] ?: array(), $string['comments']);
|
|
|
|
// Handle the terminating token from ::__read_next_string()
|
|
switch ($T[0]) {
|
|
case ')':
|
|
return $args;
|
|
}
|
|
}
|
|
}
|
|
|
|
function __get_func_args($tokens, $args) {
|
|
while (list(,$T) = each($tokens)) {
|
|
switch ($T[0]) {
|
|
case T_WHITESPACE:
|
|
continue 2;
|
|
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:
|
|
case T_VARIABLE:
|
|
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 2;
|
|
$constants = $funcs[$T[1]];
|
|
if ($info = $this->__get_func_args($tokens, $constants))
|
|
$T_funcs[] = $info;
|
|
break;
|
|
case T_COMMENT:
|
|
case T_DOC_COMMENT:
|
|
$translate = false;
|
|
$hints = array();
|
|
if (preg_match('`^/\*+\s*@(\w+)`m', $T[1])) {
|
|
foreach (preg_split('`,\s*`m', $T[1]) as $command) {
|
|
$command = trim($command, " \n\r\t\"*/\\");
|
|
@list($command, $args) = explode(' ', $command, 2);
|
|
switch ($command) {
|
|
case '@context':
|
|
$hints['context'] = trim($args, " \"*\n\t\r");
|
|
case '@trans':
|
|
$translate = true;
|
|
default:
|
|
continue 2;
|
|
}
|
|
}
|
|
}
|
|
if ($translate) {
|
|
// Find the next textual token
|
|
list($S, $T) = $this->__read_next_string($tokens);
|
|
$string = array('forms'=>array($S['form']), 'line'=>$S['line'])
|
|
+ $hints;
|
|
if (isset($S['comments']))
|
|
$string['comments'] = $S['comments'];
|
|
$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 -- preserve up to on
|
|
$string = preg_replace("`\n`u", "\\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 = array_filter(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"',
|
|
'"X-Osticket-Major-Version: '.MAJOR_VERSION.'\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) {
|
|
if ($line = trim($line))
|
|
print "#. {$line}\n";
|
|
}
|
|
}
|
|
}
|
|
foreach ($S['usage'] as $ref) {
|
|
print "#: ".$ref."\n";
|
|
}
|
|
if ($f = @$S['flags']) {
|
|
print "#, ".implode(', ', $f)."\n";
|
|
}
|
|
if (isset($S['context'])) {
|
|
print "msgctxt ";
|
|
$this->__write_string($S['context']);
|
|
}
|
|
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 find_strings($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),
|
|
);
|
|
$root = realpath($options['root'] ?: ROOT_DIR);
|
|
$domain = $options['domain'] ? '('.$options['domain'].')/' : '';
|
|
$files = Test::getAllScripts("*.php", $root);
|
|
$strings = array();
|
|
foreach ($files as $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);
|
|
}
|
|
}
|
|
return array_merge($strings, $this->__getAllJsPhrases($root));
|
|
}
|
|
|
|
function _make_pot($options) {
|
|
$strings = $this->find_strings($options);
|
|
$this->__write_pot($strings);
|
|
}
|
|
|
|
function find_similar($options) {
|
|
$strings = $this->find_strings($options);
|
|
$roots = array();
|
|
foreach ($strings as $root=>$S) {
|
|
$roots[] = array(substr(mb_strtoupper($root), 0, 255), $S['usage'][0], $root);
|
|
}
|
|
sort($roots);
|
|
$length = count($roots);
|
|
foreach ($roots as $idx=>$root) {
|
|
list($phrase, $usage, $orig) = $root;
|
|
$i = $idx;
|
|
$similar = ((int) strlen($phrase) * 0.1) ?: 1;
|
|
while (++$i < $length) {
|
|
list($other, $other_usage, $other_orig) = $roots[$i];
|
|
if (levenshtein($phrase, $other) < $similar) {
|
|
$this->stdout->write(sprintf(
|
|
"'%s' (%s) and '%s' (%s)\n",
|
|
$orig, $usage, $other_orig, $other_usage
|
|
));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static function __addString(&$strings, $call, $file=false) {
|
|
if (!($forms = @$call['forms']))
|
|
// Transation of non-constant
|
|
return;
|
|
$primary = $forms[0];
|
|
// Normalize the $primary string
|
|
$primary = preg_replace(array("`\\\(['$])`", '`(?<!\\\)"`'), array("$1", '\"'), $primary);
|
|
if (isset($call['context']))
|
|
$primary = $call['context'] . "\x04" . $primary;
|
|
if (!isset($strings[$primary])) {
|
|
$strings[$primary] = array('forms' => $forms);
|
|
}
|
|
$E = &$strings[$primary];
|
|
|
|
if (isset($call['line']) && $file)
|
|
$E['usage'][] = "{$file}:{$call['line']}";
|
|
if (isset($call['flags']))
|
|
$E['flags'] = array_unique(array_merge(@$E['flags'] ?: array(), $call['flags']));
|
|
if (isset($call['comments']))
|
|
$E['comments'] = array_merge(@$E['comments'] ?: array(), $call['comments']);
|
|
if (isset($call['context']))
|
|
$E['context'] = $call['context'];
|
|
}
|
|
|
|
function __getAllJsPhrases($root=ROOT_DIR) {
|
|
$strings = array();
|
|
$root = rtrim($root, '/') . '/';
|
|
$funcs = array('__'=>array('forms'=>1));
|
|
foreach (glob_recursive($root . "*.js") as $s) {
|
|
$script = file_get_contents($s);
|
|
$s = str_replace($root, '', $s);
|
|
$this->stderr->write($s."\n");
|
|
$calls = array();
|
|
preg_match_all('/(?:function\s+)?__\(\s*[^\'"]*(([\'"])(?:(?<!\\\\)\2|.)+\2)\s*[^)]*\)/',
|
|
$script, $calls, PREG_OFFSET_CAPTURE | PREG_SET_ORDER);
|
|
foreach ($calls as $c) {
|
|
if (!($call = $this->__find_strings(token_get_all('<?php '.$c[0][0]), $funcs, 0)))
|
|
continue;
|
|
$call = $call[0];
|
|
|
|
list($lhs) = str_split($script, $c[1][1]);
|
|
$call['line'] = strlen($lhs) - strlen(str_replace("\n", "", $lhs)) + 1;
|
|
|
|
self::__addString($strings, $call, $s);
|
|
}
|
|
}
|
|
return $strings;
|
|
}
|
|
}
|
|
|
|
Module::register('i18n', 'i18n_Compiler');
|
|
?>
|