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.
 
 
 
 
 
 

598 lines
23 KiB

#!/usr/bin/php
<?php
// (c) Copyright by authors of the Tiki Wiki CMS Groupware Project
//
// All Rights Reserved. See copyright.txt for details and a complete list of authors.
// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details.
// $Id$
namespace Tiki\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\ProgressBar;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Command\HelpCommand;
use Language;
use Language_FileType_Php;
if (isset($_SERVER['REQUEST_METHOD'])) {
die('Only available through command-line.');
}
$tikiBase = realpath(__DIR__ . '/../..');
require_once $tikiBase . '/vendor_bundled/vendor/autoload.php';
/**
* Add a singleton command "englishupdate" using the Symfony console component for this script
*
* Class EnglishUpdateCommand
* @package Tiki\Command
*/
class EnglishDevUpdateCommand extends Command
{
/**
* Run svn diff command
* @param array $revisions revisions to use in diff
* @param int $lag number of days to search previously
* @return mixed diff result
*/
private function getSvnDiff($revisions, $lag = 0)
{
if ($lag > 0) {
// current time minus number of days specified through lag
$rev = date('{"Y-m-d H:i"}', time() - $lag * 60 * 60 * 24);
$rev = '-r ' . $rev;
} else {
$rev = '-r ' . implode(":", $revisions);
}
$raw = shell_exec("svn diff $rev 2>&1");
// strip any empty translation strings now to avoid complexities later
$raw = preg_replace('/tra?\(["\'](\s*?)[\'"]\)/m', '', $raw);
// $output->writeln($raw, OutputInterface::VERBOSITY_DEBUG);
return $this->separatePhpTpl($raw, 'svn');
}
/**
* Run git diff command
* @param array $revisions revisions to use in diff
* @param int $lag number of days to search previously
* @return mixed diff result
*/
private function getGitDiff($revisions, $lag = 0)
{
if ($lag > 0) {
// current time minus number of days specified through lag
$rev = 'HEAD \'HEAD@{' . $lag . ' weeks ago}\'';
} else {
$rev = implode(" ", $revisions);
}
$raw = shell_exec("git diff $rev 2>&1");
// strip any empty translation strings now to avoid complexities later
$raw = preg_replace('/tra?\(["\'](\s*?)[\'"]\)/m', '', $raw);
// $output->writeln($raw, OutputInterface::VERBOSITY_DEBUG);
return $this->separatePhpTpl($raw, 'git');
}
protected function configure()
{
$this
->setName('englishupdate')
->setDescription("Update translation files with updates made to English strings. Will compare working copy by default.")
->addOption(
'scm',
null,
InputOption::VALUE_REQUIRED,
'Source code management type: svn or git'
)
->addOption(
'revision',
'r',
InputOption::VALUE_REQUIRED,
'Revision numbers may be selected eg. 63000:63010, or simply 63000 to update strings from 63000 onward.'
)
->addOption(
'lag',
'l',
InputOption::VALUE_REQUIRED,
'Search through previous commits by X number of days, for updated translation strings. Working copy will be ignored.'
)
->addOption(
'audit',
'a',
InputOption::VALUE_NONE,
'Reports any translation strings that have been broken. Will not change repository. '
)
->addOption(
'email',
'e',
InputOption::VALUE_REQUIRED,
'Email address to send a message to if untranslated strings are found. Must be used in conjunction with "audit".'
)
->addOption(
'diff-command',
null,
InputOption::VALUE_REQUIRED,
'Set a shell command to return the diff (ex. in case of a based git repository) override svn default diff. Options revision and lag will be ignored.'
)
->addOption(
'git',
null,
InputOption::VALUE_NONE,
'Set thi if diff-command is based on git'
);
}
/**
* The total number of changed strings
* @var int
*/
private $stringCount = 0;
/**
* The number of identical original & changed pairs found.
* @var int
*/
private $duplicates = 0;
/**
* An array of all the language directories in Tiki
* @var array
*/
private $languages;
/**
*
* Seperates svn diff output into changes made in PHP and TPL files
*
* @param $content string raw svn diff output
* @param string $diff git or svn depending on the version control used to generate the diff.
*
* @return array with [0] containing PHP and [1] containing TPL strings
*/
private function separatePhpTpl($content, $diff = 'svn')
{
if ($diff == 'git') {
preg_match_all('/^diff --git .+(php|tpl)$\nindex .+\n([\w\W]+?)(?=\n^diff --git.+\n|\n$)/m', $content, $phpTpl);
} else {
$content .= "\nIndex: \n="; // used as a dummy to match the last entry
// Separate php and tpl content
preg_match_all('/^Index:\s.+(php|tpl)$\n={10}([\w\W]+?)(?=^Index:.+\n=)/m', $content, $phpTpl);
}
$changes['php'] = '';
$changes['tpl'] = '';
$count = 0;
while ($count < count($phpTpl[1])) {
if ($phpTpl[1][$count] === 'php') {
$changes['php'] .= $phpTpl[2][$count];
} elseif ($phpTpl[1][$count] === 'tpl') {
$changes['tpl'] .= $phpTpl[2][$count];
}
$count++;
}
return $changes;
}
/**
* @param $content string diff content to split into pairs of removed and added content
*
* @return array equal pairs of added and removed diff content
*/
private function pairMatches($content)
{
/**
* @var $pairedMatches array any changes that took away and added lines.
*/
// strip some diff verbiage to prevent conflict in next match
$content = preg_replace('/(?>---|\+\+\+)\s.*\)$/m', '', $content);
// place in an array changes that have multiple lines changes
preg_match_all('/(\n[-+].*){2,}/m', $content, $diffs);
$content = $diffs[0];
unset($diffs);
$pairs = [];
foreach ($content as $diff) {
//now trim it down so its a - then + pair
if (preg_match('/^-[\s\S]*^\+.*/m', $diff, $pair)) {
// now extract a equally paired sets
$count = min(preg_match_all('/^-/m', $pair[0]), preg_match_all('/^\+/m', $pair[0]));
if ($count) {
preg_match('/(?>\n-.*){' . $count . '}(?>\n\+.*){' . $count . '}/', "\n" . $pair[0], $equilPair);
$pairs[] = $equilPair[0];
}
}
}
unset($content);
$count = 0;
$pairedMatches = [];
foreach ($pairs as $pair) {
if (preg_match_all('/^-(.*)/m', $pair, $negativeMatch)) {
if (preg_match_all('/^\+(.*)/m', $pair, $positiveMatch)) {
$pairedMatches[$count]['-'] = implode(' ', $negativeMatch[1]);
$pairedMatches[$count]['+'] = implode(' ', $positiveMatch[1]);
$count++;
}
}
}
return $pairedMatches;
}
/**
* Takes a semi-prepared list of commit changes (from a diff) and extracts pairs of original and changed translatoion strings
*
* @param $content array of equally paired diff content pairs of removed and added, previously precessed by pairMatches()
* @param $file string can be 'php' or 'tpl'. Will determine how strings are extracted.
*
* @return array extracted strings
*/
private function pairStrings($content, $file)
{
$count = 0;
$pairedStrings = [];
// set what regex to use depending on file type.
if ($file === 'php') {
$regex = '/\Wtra?\s*\(\s*([\'"])(.+?)\1\s*[\),]/';
$php = new Language_FileType_Php();
} else {
$regex = '/\{(t)r(?:\s+[^\}]*)?\}(.+?)\{\/tr\}/';
}
foreach ($content as $pair) {
if (preg_match_all($regex, $pair['-'], $negativeMatch)) {
if (preg_match_all($regex, $pair['+'], $positiveMatch)) {
// strip out any changes that have a dissimilar number of translation strings. No way to match them properly :(
if (count($negativeMatch[1]) === count($positiveMatch[1])) {
// content needs post processing based on single or double quote matches
if (isset($negativeMatch[1][0])) {
if ($negativeMatch[1][0] == "'") {
$negativeMatch[2] = $php->singleQuoted($negativeMatch[2]);
} elseif ($negativeMatch[1][0] == '"') {
$negativeMatch[2] = $php->doubleQuoted($negativeMatch[2]);
}
if ($positiveMatch[1][0] == "'") {
$positiveMatch[2] = $php->singleQuoted($positiveMatch[2]);
} elseif ($positiveMatch[1][0] == '"') {
$positiveMatch[2] = $php->doubleQuoted($positiveMatch[2]);
}
}
$pairedStrings[$count]['-'] = $negativeMatch[2];
$pairedStrings[$count]['+'] = $positiveMatch[2];
$count++;
}
}
}
}
return $pairedStrings;
}
/**
* Filters, formats & escapes paired translation strings to produce a final list of translation changes.
*
* @param $content array paired strings previously processed by pairStrings()
*
* @return array A final list of before and after translation strings to update.
*/
private function filterStrings($content)
{
$updateStrings = [];
foreach ($content as $strings) {
$count = 0;
while (isset($strings['-'][$count])) {
// strip any end punctuation from both strings to support tikis punctuations translation functionality.
if (in_array(substr($strings['-'][$count], -1), Language::punctuations)) {
$strings['-'][$count] = substr($strings['-'][$count], 0, -1);
}
if (in_array(substr($strings['+'][$count], -1), Language::punctuations)) {
$strings['+'][$count] = substr($strings['+'][$count], 0, -1);
}
if ($strings['-'][$count] !== $strings['+'][$count]) {
$updateStrings[$this->stringCount]['-'] = Language::addPhpSlashes($strings['-'][$count]);
$updateStrings[$this->stringCount]['+'] = Language::addPhpSlashes($strings['+'][$count]);
$this->stringCount++;
}
$count++;
}
}
return $updateStrings;
}
/**
* Takes a paired list of original and replacement strings and checks if they are identical
*
* @param $content array paired string, that has previously been processed by filterStrings()
*
* @return array return an array of paired strings with duplicate entries omitted
*/
private function removeIdentical($content)
{
$filtered = [];
foreach ($content as $array) {
if (! in_array($array, $filtered)) {
$filtered[] = $array;
}
}
$this->duplicates = $this->stringCount - count($filtered);
$this->stringCount -= $this->duplicates;
return $filtered;
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$tikiBase = realpath(__DIR__ . '/../..');
$output->writeln('Use of this file is deprecated, use php console.php translation:englishupdate instead');
$output->writeln('*******************************************************');
$output->writeln('* <info>Limitations</info> *');
$output->writeln('* Will not find strings if they span multiple lines. *');
$output->writeln('* *');
$output->writeln('* Will not match strings if a translation string has *');
$output->writeln('* been added or removed on the line above or below. *');
$output->writeln('*******************************************************');
$output->writeln('');
$rev = '';
// check that email is being used in audit mode
if ($input->getOption('email') && ! $input->getOption('audit')) {
$help = new HelpCommand();
$help->setCommand($this);
$help->run($input, $output);
return $output->writeln(' --email, only available when running in --audit mode.');
}
// check that scm is being used and validate
$scm = $input->getOption('scm');
if (! empty($scm) && ! in_array($scm, ['svn', 'git'])) {
$help = new HelpCommand();
$help->setCommand($this);
$help->run($input, $output);
return $output->writeln('<error> --scm, invalid value. ex: svn or git. </error>');
}
if (empty($scm)) {//detect if is svn or git repo
if (file_exists($tikiBase . DIRECTORY_SEPARATOR . '.git')) {
$scm = 'git';
} elseif (file_exists($tikiBase . DIRECTORY_SEPARATOR . '.svn')) {
$scm = 'svn';
} else {
return $output->writeln('<error>SCM not found in this tiki installation</error>');
}
}
$lag = $input->getOption('lag');
$revision = $input->getOption('revision');
$revisions = [];
// check that the --lag option is valid, and complain if its not.
if ($lag) {
if ($input->getOption('lag') < 0 || ! is_numeric($lag)) {
$help = new HelpCommand();
$help->setCommand($this);
$help->run($input, $output);
return $output->writeln('<error>Invalid option for --lag, must be a positive integer.</error>');
}
} elseif ($revision) {
$revisions = explode(':', $revision);
if (count($revisions) > 2) {
return $output->writeln('<error>Invalid amount of revisions</error>');
}
} else {
// return $output->writeln('<error>Options lag or revision are required</error>');
}
$this->languages = glob($tikiBase . '/lang/*', GLOB_ONLYDIR);
$progress = new ProgressBar($output, count($this->languages) + 7);
if ($output->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE) {
$progress->setOverwrite(false);
}
$progress->setFormatDefinition('custom', ' %current%/%max% [%bar%] -- %message%');
$progress->setFormat('custom');
$progress->setMessage('Checking System');
$progress->start();
// die gracefully if shell_exec is not enabled;
if (! is_callable('shell_exec')) {
$progress->setMessage('<error>Translation string update Failed. Could not execute shell_exec()</error>');
$progress->finish();
return false;
}
$progress->setMessage('Getting String Changes');
$progress->advance();
if ($scm == 'git') {
$diffs = $this->getGitDiff($revisions, $lag);
} else {
$diffs = $this->getSvnDiff($revisions, $lag);
}
$progress->setMessage('Finding Updated Strings');
$progress->advance();
// $output->writeln(var_export($diffs, true), OutputInterface::VERBOSITY_DEBUG);
$diffs['php'] = $this->pairMatches($diffs['php']);
$diffs['tpl'] = $this->pairMatches($diffs['tpl']);
$progress->setMessage('Found ' . count($diffs['php']) . ' PHP and ' . count($diffs['tpl']) . ' TPL changes');
$progress->advance();
// $output->writeln(var_export($diffs, true), OutputInterface::VERBOSITY_DEBUG);
$diffs['php'] = $this->pairStrings($diffs['php'], 'php');
$diffs['tpl'] = $this->pairStrings($diffs['tpl'], 'tpl');
$diffs = array_merge($diffs['php'], $diffs['tpl']);
$progress->setMessage('Found ' . count($diffs) . ' String pairs');
$progress->advance();
// $output->writeln(var_export($diffs, true), OutputInterface::VERBOSITY_DEBUG);
$diffs = $this->filterStrings($diffs);
$progress->setMessage("Found $this->stringCount translation strings");
$progress->advance();
$diffs = $this->removeIdentical($diffs);
$progress->setMessage('Found ' . $this->duplicates . ' duplicate translation strings');
$progress->advance();
if ($output->getVerbosity() >= OutputInterface::VERBOSITY_VERY_VERBOSE) {
$output->writeln("\n\n<info>Strings Being Updated</info>\n");
foreach ($diffs as $diff) {
$output->writeln('* ' . $diff['-']);
$output->writeln('* ' . $diff['+'] . "\n");
}
}
/**
* Tokens indicating that the replacement sting was found and replaced in the language file
* @ver array
*/
$string = [];
/**
* Tokens indicating that the replacement string was already present in the language file, so was skipped
* @var array
*/
$skipped = [];
/**
* Tokens indicating what language files have had changes made to them
* @var array
*/
$lang = [];
// update the language files with the new strings
if ($this->stringCount) {
foreach ($this->languages as $directory) {
$langNow = substr($directory, strrpos($directory, "/") + 1);
if (is_writable($directory . '/language.php')) {
$file = file_get_contents($directory . '/language.php');
foreach ($diffs as $key => $entry) {
// if the original string is in the language file
if (preg_match('/^"' . preg_quote($entry['-'], '/') . '[' . implode('', Language::punctuations) . ']?".*/m', $file, $match)) {
// if the replacement string does not already exist
if (! strpos($file, "\n\"" . $entry['+'] . '"')) {
// then replace the original string with an exact copy and a 'updated' copy on the next line
$replace = preg_replace('/"' . preg_quote($entry['-'], '/') . '[' . implode('', Language::punctuations) . ']?"/', '"' . $entry['+'] . '"', $match[0], 1);
$file = str_replace($match[0], $match[0] . "\n" . $replace, $file);
// keep track of overall numbers
$string[$key] = true;
$lang[$langNow] = true;
} else {
$skipped[$key] = true;
}
}
}
if (isset($lang[$langNow])) {
$progress->setMessage($langNow . "\tStrings to update");
$progress->advance();
if (! $input->getOption('audit')) {
file_put_contents($directory . '/language.php', $file);
}
} else {
$progress->setMessage($langNow . "\tNo changes to make");
$progress->advance();
}
} else {
$progress->setMessage($langNow . "\tSkipping <info>language.php not writable</info>");
$progress->advance();
}
}
}
$skippedMessage = '';
if ($this->duplicates) {
$skippedMessage = ' Skipped ' . $this->duplicates . ' duplicate strings.';
}
if ($input->getOption('audit')) {
$updateMessage = 'Out of Sync';
} else {
$updateMessage = 'Updated';
}
$progress->setMessage(count($string) . " of $this->stringCount strings $updateMessage in " . count($lang) . ' of ' . count($this->languages) . ' language files.' . $skippedMessage);
$progress->finish();
if ($input->getOption('audit')) {
if (count($string)) {
$syncMessage = "\n";
$output->writeln("\n\n<info>Updated Strings not found in Language Files</info>");
foreach ($diffs as $key => $entry) {
if (isset($string[$key])) {
$syncMessage .= '* ' . $entry['-'] . "\n";
}
}
$output->writeln($syncMessage);
if ($input->getOption('email')) {
mail($input->getOption('email'), 'Updated Strings not found in Language Files', wordwrap($tikiBase . "\n" . $syncMessage, 70, "\r\n"));
}
exit(1);
} else {
$output->writeln("\n\n<info>English and Translations are in Sync</info>\n");
}
// if were not in audit mode
} else {
if (count($string) < $this->stringCount) {
$output->writeln("\n\n<info>Strings Not Translated</info>");
foreach ($diffs as $key => $entry) {
if (! isset($string[$key]) && ! isset($skipped[$key])) {
$output->writeln('* ' . $entry['-']);
}
}
}
$output->writeln("\n\nOptionally run php get_strings.php to remove any unused translation strings.");
$output->writeln("Verify before committing.\n");
}
exit(0);
}
}
// create the application and new console
$console = new Application();
$console->add(new EnglishUpdateCommand());
$console->setDefaultCommand('translation:englishupdate');
$console->run();