summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlex Legler <alex@a3li.li>2014-12-23 17:49:26 +0100
committerAlex Legler <alex@a3li.li>2014-12-23 17:49:26 +0100
commite352fff59842ca14fbfd81ee1c4a64297bb598c5 (patch)
tree153f268484aa5cc41cacf912bdce8c4847df222d /MLEB/Translate/utils
downloadextensions-e352fff59842ca14fbfd81ee1c4a64297bb598c5.tar.gz
extensions-e352fff59842ca14fbfd81ee1c4a64297bb598c5.tar.bz2
extensions-e352fff59842ca14fbfd81ee1c4a64297bb598c5.zip
Add initial set of additional extensions
Diffstat (limited to 'MLEB/Translate/utils')
-rw-r--r--MLEB/Translate/utils/ExternalMessageSourceStateComparator.php264
-rw-r--r--MLEB/Translate/utils/Font.php139
-rw-r--r--MLEB/Translate/utils/FuzzyBot.php30
-rw-r--r--MLEB/Translate/utils/HTMLJsSelectToInputField.php85
-rw-r--r--MLEB/Translate/utils/JsSelectToInput.php126
-rw-r--r--MLEB/Translate/utils/MemProfile.php63
-rw-r--r--MLEB/Translate/utils/MessageGroupCache.php282
-rw-r--r--MLEB/Translate/utils/MessageGroupStates.php40
-rw-r--r--MLEB/Translate/utils/MessageGroupStatesUpdaterJob.php139
-rw-r--r--MLEB/Translate/utils/MessageGroupStats.php471
-rw-r--r--MLEB/Translate/utils/MessageHandle.php244
-rw-r--r--MLEB/Translate/utils/MessageIndex.php526
-rw-r--r--MLEB/Translate/utils/MessageIndexRebuildJob.php53
-rw-r--r--MLEB/Translate/utils/MessageTable.php418
-rw-r--r--MLEB/Translate/utils/MessageUpdateJob.php91
-rw-r--r--MLEB/Translate/utils/MessageWebImporter.php577
-rw-r--r--MLEB/Translate/utils/RcFilter.php91
-rw-r--r--MLEB/Translate/utils/ResourceLoader.php29
-rw-r--r--MLEB/Translate/utils/RevTag.php102
-rw-r--r--MLEB/Translate/utils/StatsBar.php88
-rw-r--r--MLEB/Translate/utils/StatsTable.php344
-rw-r--r--MLEB/Translate/utils/ToolBox.php41
-rw-r--r--MLEB/Translate/utils/TranslateLogFormatter.php79
-rw-r--r--MLEB/Translate/utils/TranslateMetadata.php110
-rw-r--r--MLEB/Translate/utils/TranslateSandbox.php255
-rw-r--r--MLEB/Translate/utils/TranslateSandboxEmailJob.php38
-rw-r--r--MLEB/Translate/utils/TranslateYaml.php224
-rw-r--r--MLEB/Translate/utils/TranslationEditPage.php307
-rw-r--r--MLEB/Translate/utils/TranslationHelpers.php1473
-rw-r--r--MLEB/Translate/utils/TranslationStats.php61
-rw-r--r--MLEB/Translate/utils/TuxMessageTable.php72
-rw-r--r--MLEB/Translate/utils/UserToggles.php109
32 files changed, 6971 insertions, 0 deletions
diff --git a/MLEB/Translate/utils/ExternalMessageSourceStateComparator.php b/MLEB/Translate/utils/ExternalMessageSourceStateComparator.php
new file mode 100644
index 00000000..505266ce
--- /dev/null
+++ b/MLEB/Translate/utils/ExternalMessageSourceStateComparator.php
@@ -0,0 +1,264 @@
+<?php
+
+/**
+ * Finds external changes for file based message groups.
+ *
+ * @author Niklas Laxström
+ * @license GPL-2.0+
+ * @since 2013.12
+ */
+class ExternalMessageSourceStateComparator {
+ /** Process all languages supported by the message group */
+ const ALL_LANGUAGES = 'all languages';
+
+ protected $changes = array();
+
+ /**
+ * Finds changes in external sources compared to wiki state.
+ *
+ * The returned array is as following:
+ * - First level is indexed by language code
+ * - Second level is indexed by change type:
+ * - - addition (new message in the file)
+ * - - deletion (message in wiki not present in the file)
+ * - - change (difference in content)
+ * - Third level is a list of changes
+ * - Fourth level is change properties
+ * - - key (the message key)
+ * - - content (the message content in external source, null for deletions)
+ *
+ * @param FileBasedMessageGroup $group
+ * @param array|string $languages
+ * @throws MWException
+ * @return array array[language code][change type] = change.
+ */
+ public function processGroup( FileBasedMessageGroup $group, $languages ) {
+ $this->changes = array();
+
+ if ( $languages === self::ALL_LANGUAGES ) {
+ $languages = $group->getTranslatableLanguages();
+
+ // This means all languages
+ if ( $languages === null ) {
+ $languages = TranslateUtils::getLanguageNames( 'en' );
+ }
+
+ $languages = array_keys( $languages );
+ } elseif ( !is_array( $languages ) ) {
+ throw new MWException( 'Invalid input given for $languages' );
+ }
+
+ // Process the source language before others
+ $sourceLanguage = $group->getSourceLanguage();
+ $index = array_search( $sourceLanguage, $languages );
+ if ( $index !== false ) {
+ unset( $languages[$index] );
+ $this->processLanguage( $group, $sourceLanguage );
+ }
+
+ foreach ( $languages as $code ) {
+ $this->processLanguage( $group, $code );
+ }
+
+ return $this->changes;
+ }
+
+ protected function processLanguage( FileBasedMessageGroup $group, $code ) {
+ wfProfileIn( __METHOD__ );
+ $cache = new MessageGroupCache( $group, $code );
+ $reason = 0;
+ if ( !$cache->isValid( $reason ) ) {
+ $this->addMessageUpdateChanges( $group, $code, $reason, $cache );
+
+ if ( !isset( $this->changes[$code] ) ) {
+ /* Update the cache immediately if file and wiki state match.
+ * Otherwise the cache will get outdated compared to file state
+ * and will give false positive conflicts later. */
+ $cache->create();
+ }
+ }
+ wfProfileOut( __METHOD__ );
+ }
+
+ /**
+ * This is the detective novel. We have three sources of information:
+ * - current message state in the file
+ * - current message state in the wiki
+ * - cached message state since cache was last build
+ * (usually after export from wiki)
+ *
+ * Now we must try to guess what in earth has driven the file state and
+ * wiki state out of sync. Then we must compile list of events that would
+ * bring those to sync. Types of events are addition, deletion, (content)
+ * change and possible rename in the future. After that the list of events
+ * are stored for later processing of a translation administrator, who can
+ * decide what actions to take on those events to bring the state more or
+ * less in sync.
+ *
+ * @param FileBasedMessageGroup $group
+ * @param string $code Language code.
+ * @param int $reason
+ * @param MessageGroupCache $cache
+ * @throws MWException
+ */
+ protected function addMessageUpdateChanges( FileBasedMessageGroup $group, $code,
+ $reason, $cache
+ ) {
+ wfProfileIn( __METHOD__ );
+ /* This throws a warning if message definitions are not yet
+ * cached and will read the file for definitions. */
+ wfSuppressWarnings();
+ $wiki = $group->initCollection( $code );
+ wfRestoreWarnings();
+ $wiki->filter( 'hastranslation', false );
+ $wiki->loadTranslations();
+ $wikiKeys = $wiki->getMessageKeys();
+
+ // By-pass cached message definitions
+ /** @var FFS $ffs */
+ $ffs = $group->getFFS();
+ if ( $code === $group->getSourceLanguage() && !$ffs->exists( $code ) ) {
+ $path = $group->getSourceFilePath( $code );
+ wfProfileOut( __METHOD__ );
+ throw new MWException( "Source message file for {$group->getId()} does not exist: $path" );
+ }
+
+ $file = $ffs->read( $code );
+
+ // Does not exist
+ if ( $file === false ) {
+ wfProfileOut( __METHOD__ );
+
+ return;
+ }
+
+ // Something went wrong
+ if ( !isset( $file['MESSAGES'] ) ) {
+ $id = $group->getId();
+ $ffsClass = get_class( $ffs );
+
+ error_log( "$id has an FFS ($ffsClass) - it didn't return cake for $code" );
+ wfProfileOut( __METHOD__ );
+
+ return;
+ }
+
+ $fileKeys = array_keys( $file['MESSAGES'] );
+
+ $common = array_intersect( $fileKeys, $wikiKeys );
+
+ $supportsFuzzy = $ffs->supportsFuzzy();
+
+ foreach ( $common as $key ) {
+ $sourceContent = $file['MESSAGES'][$key];
+ /** @var TMessage $wikiMessage */
+ $wikiMessage = $wiki[$key];
+ $wikiContent = $wikiMessage->translation();
+
+ // If FFS doesn't support it, ignore fuzziness as difference
+ $wikiContent = str_replace( TRANSLATE_FUZZY, '', $wikiContent );
+
+ // But if it does, ensure we have exactly one fuzzy marker prefixed
+ if ( $supportsFuzzy === 'yes' && $wikiMessage->hasTag( 'fuzzy' ) ) {
+ $wikiContent = TRANSLATE_FUZZY . $wikiContent;
+ }
+
+ if ( self::compareContent( $sourceContent, $wikiContent ) ) {
+ // File and wiki stage agree, nothing to do
+ continue;
+ }
+
+ // Check against interim cache to see whether we have changes
+ // in the wiki, in the file or both.
+
+ if ( $reason !== MessageGroupCache::NO_CACHE ) {
+ $cacheContent = $cache->get( $key );
+
+ /* We want to ignore the common situation that the string
+ * in the wiki has been changed since the last export.
+ * Hence we check that source === cache && cache !== wiki
+ * and if so we skip this string. */
+ if (
+ !self::compareContent( $wikiContent, $cacheContent ) &&
+ self::compareContent( $sourceContent, $cacheContent )
+ ) {
+ continue;
+ }
+ }
+
+ $this->addChange( 'change', $code, $key, $sourceContent );
+ }
+
+ $added = array_diff( $fileKeys, $wikiKeys );
+ foreach ( $added as $key ) {
+ $sourceContent = $file['MESSAGES'][$key];
+ if ( trim( $sourceContent ) === '' ) {
+ continue;
+ }
+ $this->addChange( 'addition', $code, $key, $sourceContent );
+ }
+
+ /* Should the cache not exist, don't consider the messages
+ * missing from the file as deleted - they probably aren't
+ * yet exported. For example new language translations are
+ * exported the first time. */
+ if ( $reason !== MessageGroupCache::NO_CACHE ) {
+ $deleted = array_diff( $wikiKeys, $fileKeys );
+ foreach ( $deleted as $key ) {
+ if ( $cache->get( $key ) === false ) {
+ /* This message has never existed in the cache, so it
+ * must be a newly made in the wiki. */
+ continue;
+ }
+ $this->addChange( 'deletion', $code, $key, null );
+ }
+ }
+
+ wfProfileOut( __METHOD__ );
+ }
+
+ protected function addChange( $type, $language, $key, $content ) {
+ $this->changes[$language][$type][] = array(
+ 'key' => $key,
+ 'content' => $content,
+ );
+ }
+
+ /**
+ * Compares two strings.
+ * @todo Ignore changes in different way inlined plurals.
+ * @todo Handle fuzzy state changes if FFS supports it.
+ *
+ * @param string $a
+ * @param string $b
+ * @return bool Whether two strings are equal
+ */
+ protected static function compareContent( $a, $b ) {
+ return $a === $b;
+ }
+
+ /**
+ * Writes change array as a serialized file into a known place.
+ * @param array $array Array of changes as returned by processGroup
+ * indexed by message group id.
+ * @todo does not belong to this class.
+ */
+ public static function writeChanges( $array ) {
+ // This method is almost identical with MessageIndex::store
+ wfProfileIn( __METHOD__ );
+ /* This will overwrite the previous cache file if any. Once the cache
+ * file is processed with Special:ManageMessageGroups, it is
+ * renamed so that it wont be processed again. */
+ $file = TranslateUtils::cacheFile( SpecialManageGroups::CHANGEFILE );
+ $cache = CdbWriter::open( $file );
+ $keys = array_keys( $array );
+ $cache->set( '#keys', serialize( $keys ) );
+
+ foreach ( $array as $key => $value ) {
+ $value = serialize( $value );
+ $cache->set( $key, $value );
+ }
+ $cache->close();
+ wfProfileOut( __METHOD__ );
+ }
+}
diff --git a/MLEB/Translate/utils/Font.php b/MLEB/Translate/utils/Font.php
new file mode 100644
index 00000000..1b35b614
--- /dev/null
+++ b/MLEB/Translate/utils/Font.php
@@ -0,0 +1,139 @@
+<?php
+/**
+ * Contains class with wrapper around font-config.
+ *
+ * @author Niklas Laxström
+ * @author Harry Burt
+ * @copyright Copyright © 2008-2013, Niklas Laxström
+ * @license Public Domain
+ * @file
+ */
+
+/**
+ * Wrapper around font-config to get useful ttf font given a language code.
+ * Uses wfShellExec, wfEscapeShellArg and wfDebugLog, wfGetCache and
+ * wfMemckey from %MediaWiki.
+ *
+ * @ingroup Stats
+ */
+class FCFontFinder {
+ /**
+ * Searches for suitable font in the system.
+ * @param $code \string Language code.
+ * @return bool|string Full path to the font file, false on failure
+ */
+ public static function findFile( $code ) {
+ $data = self::callFontConfig( $code );
+ if ( is_array( $data ) ) {
+ return $data['file'];
+ }
+
+ return false;
+ }
+
+ /**
+ * Searches for suitable font family in the system.
+ * @param $code \string Language code.
+ * @return bool|string Name of font family, false on failure
+ */
+ public static function findFamily( $code ) {
+ $data = self::callFontConfig( $code );
+ if ( is_array( $data ) ) {
+ return $data['family'];
+ }
+
+ return false;
+ }
+
+ protected static function callFontConfig( $code ) {
+ if ( ini_get( 'open_basedir' ) ) {
+ wfDebugLog( 'fcfont', 'Disabled because of open_basedir is active' );
+
+ // Most likely we can't access any fonts we might find
+ return false;
+ }
+
+ $cache = self::getCache();
+ $cachekey = wfMemckey( 'fcfont', $code );
+ $timeout = 60 * 60 * 12;
+
+ $cached = $cache->get( $cachekey );
+ if ( is_array( $cached ) ) {
+ return $cached;
+ } elseif ( $cached === 'NEGATIVE' ) {
+ return false;
+ }
+
+ $code = wfEscapeShellArg( ":lang=$code" );
+ $ok = 0;
+ $cmd = "fc-match $code";
+ $suggestion = wfShellExec( $cmd, $ok );
+
+ wfDebugLog( 'fcfont', "$cmd returned $ok" );
+
+ if ( $ok !== 0 ) {
+ wfDebugLog( 'fcfont', "fc-match error output: $suggestion" );
+ $cache->set( $cachekey, 'NEGATIVE', $timeout );
+
+ return false;
+ }
+
+ $pattern = '/^(.*?): "(.*)" "(.*)"$/';
+ $matches = array();
+
+ if ( !preg_match( $pattern, $suggestion, $matches ) ) {
+ wfDebugLog( 'fcfont', "fc-match: return format not understood: $suggestion" );
+ $cache->set( $cachekey, 'NEGATIVE', $timeout );
+
+ return false;
+ }
+
+ list( , $file, $family, $type ) = $matches;
+ wfDebugLog( 'fcfont', "fc-match: got $file: $family $type" );
+
+ $file = wfEscapeShellArg( $file );
+ $family = wfEscapeShellArg( $family );
+ $type = wfEscapeShellArg( $type );
+ $cmd = "fc-list $family $type $code file | grep $file";
+
+ $candidates = trim( wfShellExec( $cmd, $ok ) );
+
+ wfDebugLog( 'fcfont', "$cmd returned $ok" );
+
+ if ( $ok !== 0 ) {
+ wfDebugLog( 'fcfont', "fc-list error output: $candidates" );
+ $cache->set( $cachekey, 'NEGATIVE', $timeout );
+
+ return false;
+ }
+
+ # trim spaces
+ $files = array_map( 'trim', explode( "\n", $candidates ) );
+ $count = count( $files );
+ if ( !$count ) {
+ wfDebugLog( 'fcfont', "fc-list got zero canditates: $candidates" );
+ }
+
+ # remove the trailing ":"
+ $chosen = substr( $files[0], 0, -1 );
+
+ wfDebugLog( 'fcfont', "fc-list got $count candidates; using $chosen" );
+
+ $data = array(
+ 'family' => $family,
+ 'type' => $type,
+ 'file' => $chosen,
+ );
+
+ $cache->set( $cachekey, $data, $timeout );
+
+ return $data;
+ }
+
+ /**
+ * @return BagOStuff
+ */
+ protected static function getCache() {
+ return wfGetCache( CACHE_ANYTHING );
+ }
+}
diff --git a/MLEB/Translate/utils/FuzzyBot.php b/MLEB/Translate/utils/FuzzyBot.php
new file mode 100644
index 00000000..e9992117
--- /dev/null
+++ b/MLEB/Translate/utils/FuzzyBot.php
@@ -0,0 +1,30 @@
+<?php
+/**
+ * Do it all maintenance account
+ *
+ * @file
+ * @author Niklas Laxström
+ * @copyright Copyright © 2012-2013, Niklas Laxström
+ * @license GPL-2.0+
+ */
+
+/**
+ * FuzzyBot - the misunderstood workhorse.
+ * @since 2012-01-02
+ */
+class FuzzyBot {
+ public static function getUser() {
+ $bot = User::newFromName( self::getName() );
+ if ( $bot->isAnon() ) {
+ $bot->addToDatabase();
+ }
+
+ return $bot;
+ }
+
+ public static function getName() {
+ global $wgTranslateFuzzyBotName;
+
+ return $wgTranslateFuzzyBotName;
+ }
+}
diff --git a/MLEB/Translate/utils/HTMLJsSelectToInputField.php b/MLEB/Translate/utils/HTMLJsSelectToInputField.php
new file mode 100644
index 00000000..8035f2f4
--- /dev/null
+++ b/MLEB/Translate/utils/HTMLJsSelectToInputField.php
@@ -0,0 +1,85 @@
+<?php
+/**
+ * Implementation of JsSelectToInput class which is compatible with MediaWiki's preferences system.
+ * @file
+ * @author Niklas Laxström
+ * @copyright Copyright © 2010 Niklas Laxström
+ * @license GPL-2.0+
+ */
+
+/**
+ * Implementation of JsSelectToInput class which is extends HTMLTextField.
+ */
+class HTMLJsSelectToInputField extends HTMLTextField {
+ /**
+ * @param $value
+ * @return string
+ */
+ function getInputHTML( $value ) {
+ $input = parent::getInputHTML( $value );
+
+ if ( isset( $this->mParams['select'] ) ) {
+ /**
+ * @var JsSelectToInput $select
+ */
+ $select = $this->mParams['select'];
+ $input = $select->getHtmlAndPrepareJs() . '<br />' . $input;
+ }
+
+ return $input;
+ }
+
+ /**
+ * @param $value
+ * @return array
+ */
+ function tidy( $value ) {
+ $value = array_map( 'trim', explode( ',', $value ) );
+ $value = array_unique( array_filter( $value ) );
+
+ return $value;
+ }
+
+ /**
+ * @param $value
+ * @param $alldata
+ * @return bool|String
+ */
+ function validate( $value, $alldata ) {
+ $p = parent::validate( $value, $alldata );
+
+ if ( $p !== true ) {
+ return $p;
+ }
+
+ if ( !isset( $this->mParams['valid-values'] ) ) {
+ return true;
+ }
+
+ if ( $value === 'default' ) {
+ return true;
+ }
+
+ $codes = $this->tidy( $value );
+ $valid = array_flip( $this->mParams['valid-values'] );
+
+ foreach ( $codes as $code ) {
+ if ( !isset( $valid[$code] ) ) {
+ return wfMessage( 'translate-pref-editassistlang-bad', $code )->parseAsBlock();
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * @param $value
+ * @param $alldata
+ * @return string
+ */
+ function filter( $value, $alldata ) {
+ $value = parent::filter( $value, $alldata );
+
+ return implode( ', ', $this->tidy( $value ) );
+ }
+}
diff --git a/MLEB/Translate/utils/JsSelectToInput.php b/MLEB/Translate/utils/JsSelectToInput.php
new file mode 100644
index 00000000..12bea5a7
--- /dev/null
+++ b/MLEB/Translate/utils/JsSelectToInput.php
@@ -0,0 +1,126 @@
+<?php
+/**
+ * Code for JavaScript enhanced \<option> selectors.
+ * @file
+ * @author Niklas Laxström
+ * @copyright Copyright © 2010 Niklas Laxström
+ * @license GPL-2.0+
+ */
+
+/**
+ * Code for JavaScript enhanced \<option> selectors.
+ */
+class JsSelectToInput {
+ /// Id of the text field where stuff is appended
+ protected $targetId;
+ /// Id of the \<option> field
+ protected $sourceId;
+
+ /**
+ * @var XmlSelect
+ */
+ protected $select;
+
+ /// Id on the button
+ protected $buttonId;
+
+ /**
+ * @var string Text for the append button
+ */
+ protected $msg = 'translate-jssti-add';
+
+ public function __construct( XmlSelect $select = null ) {
+ $this->select = $select;
+ }
+
+ /**
+ * Set the source id of the selector
+ * @param string $id
+ */
+ public function setSourceId( $id ) {
+ $this->sourceId = $id;
+ }
+
+ /// @return string
+ public function getSourceId() {
+ return $this->sourceId;
+ }
+
+ /**
+ * Set the id of the target text field
+ * @param string $id
+ */
+ public function setTargetId( $id ) {
+ $this->targetId = $id;
+ }
+
+ /**
+ * @return string
+ */
+ public function getTargetId() {
+ return $this->targetId;
+ }
+
+ /**
+ * Set the message key.
+ * @param string $message
+ */
+ public function setMessage( $message ) {
+ $this->msg = $message;
+ }
+
+ /// @return string Message key.
+ public function getMessage() {
+ return $this->msg;
+ }
+
+ /**
+ * Returns the whole input element and injects needed JavaScript
+ * @throws MWException
+ * @return string Html code.
+ */
+ public function getHtmlAndPrepareJS() {
+ if ( $this->sourceId === false ) {
+ if ( is_callable( array( $this->select, 'getAttribute' ) ) ) {
+ $this->sourceId = $this->select->getAttribute['id'];
+ }
+
+ if ( !$this->sourceId ) {
+ throw new MWException( "ID needs to be specified for the selector" );
+ }
+ }
+
+ self::injectJs();
+ $html = $this->select->getHtml();
+ $html .= $this->getButton( $this->msg, $this->sourceId, $this->targetId );
+
+ return $html;
+ }
+
+ /**
+ * Constructs the append button.
+ * @param string $msg Message key.
+ * @param string $source Html id.
+ * @param string $target Html id.
+ * @return string
+ */
+ protected function getButton( $msg, $source, $target ) {
+ $html = Xml::element( 'input', array(
+ 'type' => 'button',
+ 'value' => wfMessage( $msg )->text(),
+ 'onclick' => Xml::encodeJsCall( 'appendFromSelect', array( $source, $target ) )
+ ) );
+
+ return $html;
+ }
+
+ /// Inject needed JavaScript in the page.
+ public static function injectJs() {
+ static $done = false;
+ if ( $done ) {
+ return;
+ }
+
+ RequestContext::getMain()->getOutput()->addModules( 'ext.translate.selecttoinput' );
+ }
+}
diff --git a/MLEB/Translate/utils/MemProfile.php b/MLEB/Translate/utils/MemProfile.php
new file mode 100644
index 00000000..4d626d8f
--- /dev/null
+++ b/MLEB/Translate/utils/MemProfile.php
@@ -0,0 +1,63 @@
+<?php
+if ( !defined( 'MEDIAWIKI' ) ) {
+ die();
+}
+/**
+ * Very crude tools to track memory usage
+ *
+ * @file
+ * @author Niklas Laxström
+ * @copyright Copyright © 2008, Niklas Laxström
+ * @license GPL-2.0+
+ */
+
+/// Memory usage at checkpoints
+$wgMemUse = array();
+/// Tracks the deepness of the stack
+$wgMemStack = 0;
+
+/**
+ * Call to start memory counting for a block.
+ * @param $a \string Block name.
+ */
+function wfMemIn( $a ) {
+ global $wgLang, $wgMemUse, $wgMemStack;
+
+ $mem = memory_get_usage();
+ $memR = memory_get_usage();
+
+ $wgMemUse[$a][] = array( $mem, $memR );
+
+ $memF = $wgLang->formatNum( $mem );
+ $memRF = $wgLang->formatNum( $memR );
+
+ $pad = str_repeat( ".", $wgMemStack );
+ wfDebug( "$pad$a-IN: \t$memF\t\t$memRF\n" );
+ $wgMemStack++;
+}
+
+/**
+ * Call to start stop counting for a block. Difference from start is shown.
+ * @param $a \string Block name.
+ */
+function wfMemOut( $a ) {
+ global $wgLang, $wgMemUse, $wgMemStack;
+
+ $mem = memory_get_usage();
+ $memR = memory_get_usage();
+
+ list( $memO, $memOR ) = array_pop( $wgMemUse[$a] );
+
+ $memF = $wgLang->formatNum( $mem );
+ $memRF = $wgLang->formatNum( $memR );
+
+ $memD = $mem - $memO;
+ $memRD = $memR - $memOR;
+
+ $memDF = $wgLang->formatNum( $memD );
+ $memRDF = $wgLang->formatNum( $memRD );
+
+ $pad = str_repeat( ".", $wgMemStack - 1 );
+ wfDebug( "$pad$a-OUT:\t$memF ($memDF)\t$memRF ($memRDF)\n" );
+ $wgMemStack--;
+}
diff --git a/MLEB/Translate/utils/MessageGroupCache.php b/MLEB/Translate/utils/MessageGroupCache.php
new file mode 100644
index 00000000..74298471
--- /dev/null
+++ b/MLEB/Translate/utils/MessageGroupCache.php
@@ -0,0 +1,282 @@
+<?php
+/**
+ * Code for caching the messages of file based message groups.
+ * @file
+ * @author Niklas Laxström
+ * @copyright Copyright © 2009-2013 Niklas Laxström
+ * @license GPL-2.0+
+ */
+
+/**
+ * Caches messages of file based message group source file. Can also track
+ * that the cache is up to date. Parsing the source files can be slow, so
+ * constructing CDB cache makes accessing that data constant speed regardless
+ * of the actual format.
+ *
+ * @ingroup MessageGroups
+ */
+class MessageGroupCache {
+ const NO_SOURCE = 1;
+ const NO_CACHE = 2;
+ const CHANGED = 3;
+
+ /**
+ * @var MessageGroup
+ */
+ protected $group;
+
+ /**
+ * @var CdbReader
+ */
+ protected $cache;
+
+ /**
+ * @var string
+ */
+ protected $code;
+
+ /**
+ * Contructs a new cache object for given group and language code.
+ * @param string|FileBasedMessageGroup $group Group object or id.
+ * @param string $code Language code. Default value 'en'.
+ */
+ public function __construct( $group, $code = 'en' ) {
+ if ( is_object( $group ) ) {
+ $this->group = $group;
+ } else {
+ $this->group = MessageGroups::getGroup( $group );
+ }
+ $this->code = $code;
+ }
+
+ /**
+ * Returns whether cache exists for this language and group.
+ * @return bool
+ */
+ public function exists() {
+ $old = $this->getOldCacheFileName();
+ $new = $this->getCacheFileName();
+ $exists = file_exists( $new );
+
+ if ( $exists ) {
+ return true;
+ }
+
+ // Perform migration if possible
+ if ( file_exists( $old ) ) {
+ wfMkdirParents( dirname( $new ) );
+ rename( $old, $new );
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns list of message keys that are stored.
+ * @return string[] Message keys that can be passed one-by-one to get() method.
+ */
+ public function getKeys() {
+ $value = $this->open()->get( '#keys' );
+ $array = unserialize( $value );
+
+ // Debugging for bug 69830
+ if ( !is_array( $array ) ) {
+ $filename = $this->getCacheFileName();
+ throw new MWException( "Unable to get keys from '$filename'" );
+ }
+
+ return $array;
+ }
+
+ /**
+ * Returns timestamp in unix-format about when this cache was first created.
+ * @return string Unix timestamp.
+ */
+ public function getTimestamp() {
+ return $this->open()->get( '#created' );
+ }
+
+ /**
+ * ...
+ * @return string Unix timestamp.
+ */
+ public function getUpdateTimestamp() {
+ return $this->open()->get( '#updated' );
+ }
+
+ /**
+ * Get an item from the cache.
+ * @param string $key
+ * @return string
+ */
+ public function get( $key ) {
+ return $this->open()->get( $key );
+ }
+
+ /**
+ * Populates the cache from current state of the source file.
+ * @param bool|string $created Unix timestamp when the cache is created (for automatic updates).
+ */
+ public function create( $created = false ) {
+ $this->close(); // Close the reader instance just to be sure
+
+ $messages = $this->group->load( $this->code );
+ if ( $messages === array() ) {
+ if ( $this->exists() ) {
+ // Delete stale cache files
+ unlink( $this->getCacheFileName() );
+ }
+
+ return; // Don't create empty caches
+ }
+ $hash = md5( file_get_contents( $this->group->getSourceFilePath( $this->code ) ) );
+
+ wfMkdirParents( dirname( $this->getCacheFileName() ) );
+ $cache = CdbWriter::open( $this->getCacheFileName() );
+ $keys = array_keys( $messages );
+ $cache->set( '#keys', serialize( $keys ) );
+
+ foreach ( $messages as $key => $value ) {
+ $cache->set( $key, $value );
+ }
+
+ $cache->set( '#created', $created ? $created : wfTimestamp() );
+ $cache->set( '#updated', wfTimestamp() );
+ $cache->set( '#filehash', $hash );
+ $cache->set( '#msgcount', count( $messages ) );
+ ksort( $messages );
+ $cache->set( '#msghash', md5( serialize( $messages ) ) );
+ $cache->set( '#version', '3' );
+ $cache->close();
+ }
+
+ /**
+ * Checks whether the cache still reflects the source file.
+ * It uses multiple conditions to speed up the checking from file
+ * modification timestamps to hashing.
+ * @param int $reason
+ * @return bool Whether the cache is up to date.
+ */
+ public function isValid( &$reason = 0 ) {
+ $group = $this->group;
+ $groupId = $group->getId();
+
+ $pattern = $group->getSourceFilePath( '*' );
+ $filename = $group->getSourceFilePath( $this->code );
+
+ // If the file pattern is not dependent on the language, we will assume
+ // that all translations are stored in one file. This means we need to
+ // actually parse the file to know if a language is present.
+ if ( strpos( $pattern, '*' ) === false ) {
+ $source = $group->getFFS()->read( $this->code ) !== false;
+ } else {
+ static $globCache = null;
+ if ( !isset( $globCache[$groupId] ) ) {
+ $globCache[$groupId] = array_flip( glob( $pattern, GLOB_NOESCAPE ) );
+ // Definition file might not match the above pattern
+ $globCache[$groupId][$group->getSourceFilePath( 'en' )] = true;
+ }
+ $source = isset( $globCache[$groupId][$filename] );
+ }
+
+ $cache = $this->exists();
+
+ // Timestamp and existence checks
+ if ( !$cache && !$source ) {
+ return true;
+ } elseif ( !$cache && $source ) {
+ $reason = self::NO_CACHE;
+
+ return false;
+ } elseif ( $cache && !$source ) {
+ $reason = self::NO_SOURCE;
+
+ return false;
+ } elseif ( filemtime( $filename ) <= $this->get( '#updated' ) ) {
+ return true;
+ }
+
+ // From now on cache and source file exists, but source file mtime is newer
+ $created = $this->get( '#created' );
+
+ // File hash check
+ $newhash = md5( file_get_contents( $filename ) );
+ if ( $this->get( '#filehash' ) === $newhash ) {
+ // Update cache so that we don't need to compare hashes next time
+ $this->create( $created );
+
+ return true;
+ }
+
+ // Message count check
+ $messages = $group->load( $this->code );
+ // CDB converts numbers to strings
+ $count = intval( $this->get( '#msgcount' ) );
+ if ( $count !== count( $messages ) ) {
+ // Number of messsages has changed
+ $reason = self::CHANGED;
+
+ return false;
+ }
+
+ // Content hash check
+ ksort( $messages );
+ if ( $this->get( '#msghash' ) === md5( serialize( $messages ) ) ) {
+ // Update cache so that we don't need to do slow checks next time
+ $this->create( $created );
+
+ return true;
+ }
+
+ $reason = self::CHANGED;
+
+ return false;
+ }
+
+ /**
+ * Open the cache for reading.
+ * @return MessageGroupCache
+ */
+ protected function open() {
+ if ( $this->cache === null ) {
+ $this->cache = CdbReader::open( $this->getCacheFileName() );
+ if ( $this->cache->get( '#version' ) !== '3' ) {
+ $this->close();
+ unlink( $this->getCacheFileName() );
+ }
+ }
+
+ return $this->cache;
+ }
+
+ /**
+ * Close the cache from reading.
+ */
+ protected function close() {
+ if ( $this->cache !== null ) {
+ $this->cache->close();
+ $this->cache = null;
+ }
+ }
+
+ /**
+ * Returns full path to the cache file.
+ * @return string
+ */
+ protected function getCacheFileName() {
+ $cacheFileName = "translate_groupcache-{$this->group->getId()}/{$this->code}.cdb";
+
+ return TranslateUtils::cacheFile( $cacheFileName );
+ }
+
+ /**
+ * Returns full path to the old cache file location.
+ * @return string
+ */
+ protected function getOldCacheFileName() {
+ $cacheFileName = "translate_groupcache-{$this->group->getId()}-{$this->code}.cdb";
+
+ return TranslateUtils::cacheFile( $cacheFileName );
+ }
+}
diff --git a/MLEB/Translate/utils/MessageGroupStates.php b/MLEB/Translate/utils/MessageGroupStates.php
new file mode 100644
index 00000000..1838381c
--- /dev/null
+++ b/MLEB/Translate/utils/MessageGroupStates.php
@@ -0,0 +1,40 @@
+<?php
+/**
+ * Wrapper class for using message group states.
+ *
+ * @file
+ * @author Niklas Laxström
+ * @author Siebrand Mazeland
+ * @copyright Copyright © 2012-2013 Niklas Laxström
+ * @license GPL-2.0+
+ */
+
+/**
+ * Class for making the use of message group state easier.
+ * @since 2012-10-05
+ */
+class MessageGroupStates {
+ const CONDKEY = 'state conditions';
+
+ protected $config;
+
+ public function __construct( array $config = null ) {
+ $this->config = $config;
+ }
+
+ public function getStates() {
+ $conf = $this->config;
+ unset( $conf[self::CONDKEY] );
+
+ return $conf;
+ }
+
+ public function getConditions() {
+ $conf = $this->config;
+ if ( isset( $conf[self::CONDKEY] ) ) {
+ return $conf[self::CONDKEY];
+ } else {
+ return array();
+ }
+ }
+}
diff --git a/MLEB/Translate/utils/MessageGroupStatesUpdaterJob.php b/MLEB/Translate/utils/MessageGroupStatesUpdaterJob.php
new file mode 100644
index 00000000..18b34343
--- /dev/null
+++ b/MLEB/Translate/utils/MessageGroupStatesUpdaterJob.php
@@ -0,0 +1,139 @@
+<?php
+/**
+ * Logic for handling automatic message group state changes
+ *
+ * @file
+ * @author Niklas Laxström
+ * @copyright Copyright © 2012-2013, Niklas Laxström
+ * @license GPL-2.0+
+ */
+
+/**
+ * Logic for handling automatic message group state changes
+ *
+ * @ingroup JobQueue
+ */
+class MessageGroupStatesUpdaterJob extends Job {
+
+ /**
+ * Hook: TranslateEventTranslationEdit
+ * Hook: TranslateEventTranslationReview
+ */
+ public static function onChange( MessageHandle $handle ) {
+ $job = self::newJob( $handle->getTitle() );
+ JobQueueGroup::singleton()->push( $job );
+
+ return true;
+ }
+
+ /**
+ * @param $title
+ * @return MessageGroupStatesUpdaterJob
+ */
+ public static function newJob( $title ) {
+ $job = new self( $title );
+
+ return $job;
+ }
+
+ public function __construct( $title, $params = array(), $id = 0 ) {
+ parent::__construct( __CLASS__, $title, $params, $id );
+ }
+
+ public function run() {
+ $title = $this->title;
+ $handle = new MessageHandle( $title );
+ $code = $handle->getCode();
+
+ if ( !$handle->isValid() && !$code ) {
+ return true;
+ }
+
+ $groups = self::getGroupsWithTransitions( $handle );
+ foreach ( $groups as $id => $transitions ) {
+ $group = MessageGroups::getGroup( $id );
+ $stats = MessageGroupStats::forItem( $id, $code );
+ $state = self::getNewState( $stats, $transitions );
+ if ( $state ) {
+ ApiGroupReview::changeState( $group, $code, $state, FuzzyBot::getUser() );
+ }
+ }
+
+ return true;
+ }
+
+ public static function getGroupsWithTransitions( MessageHandle $handle ) {
+ $listeners = array();
+ foreach ( $handle->getGroupIds() as $id ) {
+ $group = MessageGroups::getGroup( $id );
+
+ // No longer exists?
+ if ( !$group ) {
+ continue;
+ }
+
+ $conds = $group->getMessageGroupStates()->getConditions();
+ if ( $conds ) {
+ $listeners[$id] = $conds;
+ }
+ }
+
+ return $listeners;
+ }
+
+ public static function getStatValue( $stats, $type ) {
+ $total = $stats[MessageGroupStats::TOTAL];
+ $translated = $stats[MessageGroupStats::TRANSLATED];
+ $outdated = $stats[MessageGroupStats::FUZZY];
+ $proofread = $stats[MessageGroupStats::PROOFREAD];
+
+ switch ( $type ) {
+ case 'UNTRANSLATED':
+ return $total - $translated - $outdated;
+ case 'OUTDATED':
+ return $outdated;
+ case 'TRANSLATED':
+ return $translated;
+ case 'PROOFREAD':
+ return $proofread;
+ default:
+ throw new MWException( "Unknown condition $type" );
+ }
+ }
+
+ public static function matchCondition( $value, $condition, $max ) {
+ switch ( $condition ) {
+ case 'ZERO':
+ return $value === 0;
+ case 'NONZERO':
+ return $value > 0;
+ case 'MAX':
+ return $value === $max;
+ default:
+ throw new MWException( "Unknown condition value $condition" );
+ }
+ }
+
+ public static function getNewState( $stats, $transitions ) {
+ foreach ( $transitions as $transition ) {
+ list( $newState, $conds ) = $transition;
+ $match = true;
+
+ foreach ( $conds as $type => $cond ) {
+ $statValue = self::getStatValue( $stats, $type );
+ $max = $stats[MessageGroupStats::TOTAL];
+ $match = $match && self::matchCondition( $statValue, $cond, $max );
+ // Conditions are AND, so no point trying more if no match
+ if ( !$match ) {
+ break;
+ }
+ }
+
+ if ( $match ) {
+ return $newState;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/MLEB/Translate/utils/MessageGroupStats.php b/MLEB/Translate/utils/MessageGroupStats.php
new file mode 100644
index 00000000..05ca3f42
--- /dev/null
+++ b/MLEB/Translate/utils/MessageGroupStats.php
@@ -0,0 +1,471 @@
+<?php
+/**
+ * This file aims to provide efficient mechanism for fetching translation completion stats.
+ *
+ * @file
+ * @author Wikia (trac.wikia-code.com/browser/wikia/trunk/extensions/wikia/TranslationStatistics)
+ * @author Niklas Laxström
+ * @copyright Copyright © 2012-2013 Niklas Laxström
+ * @license GPL-2.0+
+ */
+
+/**
+ * This class abstract MessageGroup statistics calculation and storing.
+ * You can access stats easily per language or per group.
+ * Stat array for each item is of format array( total, translate, fuzzy ).
+ *
+ * @ingroup Stats MessageGroups
+ */
+class MessageGroupStats {
+ /// Name of the database table
+ const TABLE = 'translate_groupstats';
+
+ const TOTAL = 0; ///< Array index
+ const TRANSLATED = 1; ///< Array index
+ const FUZZY = 2; ///< Array index
+ const PROOFREAD = 3; ///< Array index
+
+ /// @var float
+ protected static $timeStart = null;
+ /// @var float
+ protected static $limit = null;
+
+ /**
+ * Set the maximum time statistics are calculated.
+ * If the time limit is exceeded, the missing
+ * entries will be null.
+ * @param $limit float time in seconds
+ */
+ public static function setTimeLimit( $limit ) {
+ self::$timeStart = microtime( true );
+ self::$limit = $limit;
+ }
+
+ /**
+ * Returns empty stats array. Useful because the number of elements
+ * may change.
+ * @return array
+ * @since 2012-09-21
+ */
+ public static function getEmptyStats() {
+ return array( 0, 0, 0, 0 );
+ }
+
+ /**
+ * Returns empty stats array that indicates stats are incomplete or
+ * unknown.
+ * @return array
+ * @since 2013-01-02
+ */
+ protected static function getUnknownStats() {
+ return array( null, null, null, null );
+ }
+
+ /**
+ * Returns stats for given group in given language.
+ * @param $id string Group id
+ * @param $code string Language code
+ * @return Array
+ */
+ public static function forItem( $id, $code ) {
+ $res = self::selectRowsIdLang( $id, $code );
+ $stats = self::extractResults( $res );
+
+ /* In case some code calls this for dynamic groups, return the default
+ * values for unknown/incomplete stats. Calculating these numbers don't
+ * make sense for dynamic groups, and would just throw an exception. */
+ $group = MessageGroups::getGroup( $id );
+ if ( MessageGroups::isDynamic( $group ) ) {
+ $stats[$id][$code] = self::getUnknownStats();
+ }
+
+ if ( !isset( $stats[$id][$code] ) ) {
+ $stats[$id][$code] = self::forItemInternal( $stats, $group, $code );
+ }
+
+ return $stats[$id][$code];
+ }
+
+ /**
+ * Returns stats for all groups in given language.
+ * @param $code string Language code
+ * @return Array
+ */
+ public static function forLanguage( $code ) {
+ $stats = self::forLanguageInternal( $code );
+ $flattened = array();
+ foreach ( $stats as $group => $languages ) {
+ $flattened[$group] = $languages[$code];
+ }
+
+ return $flattened;
+ }
+
+ /**
+ * Returns stats for all languages in given group.
+ * @param $id string Group id
+ * @return Array
+ */
+ public static function forGroup( $id ) {
+ $group = MessageGroups::getGroup( $id );
+ if ( $group === null ) {
+ return array();
+ }
+ $stats = self::forGroupInternal( $group );
+
+ return $stats[$id];
+ }
+
+ /**
+ * Returns stats for all group in all languages.
+ * Might be slow, might use lots of memory.
+ * Returns two dimensional array indexed by group and language.
+ * @return Array
+ */
+ public static function forEverything() {
+ $groups = MessageGroups::singleton()->getGroups();
+ $stats = array();
+ foreach ( $groups as $g ) {
+ $stats = self::forGroupInternal( $g, $stats );
+ }
+
+ return $stats;
+ }
+
+ /**
+ * Clears the cache for all groups associated with the message.
+ *
+ * Hook: TranslateEventTranslationEdit
+ * Hook: TranslateEventTranslationReview
+ */
+ public static function clear( MessageHandle $handle ) {
+ $code = $handle->getCode();
+ $ids = $handle->getGroupIds();
+ $dbw = wfGetDB( DB_MASTER );
+
+ $locked = false;
+ // Try to avoid deadlocks with duplicated deletes where there is no row
+ // @note: this only helps in auto-commit mode (which job runners use)
+ if ( !$dbw->getFlag( DBO_TRX ) && count( $ids ) == 1 ) {
+ $key = __CLASS__ . ":modify:{$ids[0]}";
+ $locked = $dbw->lock( $key, __METHOD__, 1 );
+ if ( !$locked ) {
+ return true; // raced out
+ }
+ }
+
+ $conds = array( 'tgs_group' => $ids, 'tgs_lang' => $code );
+ $dbw->delete( self::TABLE, $conds, __METHOD__ );
+ wfDebugLog( 'messagegroupstats', "Cleared " . serialize( $conds ) );
+
+ if ( $locked ) {
+ $dbw->unlock( $key, __METHOD__ );
+ }
+
+ // Hooks must return value
+ return true;
+ }
+
+ public static function clearGroup( $id ) {
+ if ( !count( $id ) ) {
+ return;
+ }
+ $dbw = wfGetDB( DB_MASTER );
+ $conds = array( 'tgs_group' => $id );
+ $dbw->delete( self::TABLE, $conds, __METHOD__ );
+ wfDebugLog( 'messagegroupstats', "Cleared " . serialize( $conds ) );
+ }
+
+ public static function clearLanguage( $code ) {
+ if ( !count( $code ) ) {
+ return;
+ }
+ $dbw = wfGetDB( DB_MASTER );
+ $conds = array( 'tgs_lang' => $code );
+ $dbw->delete( self::TABLE, $conds, __METHOD__ );
+ wfDebugLog( 'messagegroupstats', "Cleared " . serialize( $conds ) );
+ }
+
+ /**
+ * Purges all cached stats.
+ */
+ public static function clearAll() {
+ $dbw = wfGetDB( DB_MASTER );
+ $dbw->delete( self::TABLE, '*' );
+ wfDebugLog( 'messagegroupstats', "Cleared everything :(" );
+ }
+
+ protected static function extractResults( $res, $stats = array() ) {
+ foreach ( $res as $row ) {
+ $stats[$row->tgs_group][$row->tgs_lang] = self::extractNumbers( $row );
+ }
+
+ return $stats;
+ }
+
+ public static function update( MessageHandle $handle, $changes = array() ) {
+ $dbw = wfGetDB( DB_MASTER );
+ $conds = array(
+ 'tgs_group' => $handle->getGroupIds(),
+ 'tgs_lang' => $handle->getCode(),
+ );
+
+ $values = array();
+ foreach ( array( 'total', 'translated', 'fuzzy', 'proofread' ) as $type ) {
+ if ( isset( $changes[$type] ) ) {
+ $values[] = "tgs_$type=tgs_$type" .
+ self::stringifyNumber( $changes[$type] );
+ }
+ }
+
+ $dbw->update( self::TABLE, $values, $conds, __METHOD__ );
+ }
+
+ /**
+ * Returns an array of needed database fields.
+ * @param $row
+ * @return array
+ */
+ protected static function extractNumbers( $row ) {
+ return array(
+ self::TOTAL => (int)$row->tgs_total,
+ self::TRANSLATED => (int)$row->tgs_translated,
+ self::FUZZY => (int)$row->tgs_fuzzy,
+ self::PROOFREAD => (int)$row->tgs_proofread,
+ );
+ }
+
+ /**
+ * @param $code
+ * @param array $stats
+ * @return array
+ */
+ protected static function forLanguageInternal( $code, $stats = array() ) {
+ $res = self::selectRowsIdLang( null, $code );
+ $stats = self::extractResults( $res, $stats );
+
+ $groups = MessageGroups::singleton()->getGroups();
+ foreach ( $groups as $id => $group ) {
+ if ( isset( $stats[$id][$code] ) ) {
+ continue;
+ }
+ $stats[$id][$code] = self::forItemInternal( $stats, $group, $code );
+ }
+
+ return $stats;
+ }
+
+ /**
+ * @param AggregateMessageGroup $agg
+ * @return mixed
+ */
+ protected static function expandAggregates( AggregateMessageGroup $agg ) {
+ $flattened = array();
+
+ /** @var MessageGroup|AggregateMessageGroup $group */
+ foreach ( $agg->getGroups() as $group ) {
+ if ( $group instanceof AggregateMessageGroup ) {
+ $flattened += self::expandAggregates( $group );
+ } else {
+ $flattened[$group->getId()] = $group;
+ }
+ }
+
+ return $flattened;
+ }
+
+ /**
+ * @param MessageGroup $group
+ * @param array $stats
+ * @return array
+ */
+ protected static function forGroupInternal( $group, $stats = array() ) {
+ $id = $group->getId();
+ $res = self::selectRowsIdLang( $id, null );
+ $stats = self::extractResults( $res, $stats );
+
+ # Go over each language filling missing entries
+ $languages = array_keys( Language::fetchLanguageNames() );
+ // This is for calculating things in correct order
+ sort( $languages );
+ foreach ( $languages as $code ) {
+ if ( isset( $stats[$id][$code] ) ) {
+ continue;
+ }
+ $stats[$id][$code] = self::forItemInternal( $stats, $group, $code );
+ }
+
+ // This is for sorting the values added later in correct order
+ foreach ( array_keys( $stats ) as $key ) {
+ ksort( $stats[$key] );
+ }
+
+ return $stats;
+ }
+
+ protected static function selectRowsIdLang( $ids = null, $codes = null ) {
+ $conds = array();
+ if ( $ids !== null ) {
+ $conds['tgs_group'] = $ids;
+ }
+
+ if ( $codes !== null ) {
+ $conds['tgs_lang'] = $codes;
+ }
+
+ $dbr = wfGetDB( DB_MASTER );
+ $res = $dbr->select( self::TABLE, '*', $conds, __METHOD__ );
+
+ return $res;
+ }
+
+ protected static function forItemInternal( &$stats, $group, $code ) {
+ $id = $group->getId();
+
+ if ( self::$timeStart !== null && ( microtime( true ) - self::$timeStart ) > self::$limit ) {
+ return $stats[$id][$code] = self::getUnknownStats();
+ }
+
+ if ( $group instanceof AggregateMessageGroup ) {
+ $aggregates = self::getEmptyStats();
+
+ $expanded = self::expandAggregates( $group );
+ if ( $expanded === array() ) {
+ return $aggregates;
+ }
+ $res = self::selectRowsIdLang( array_keys( $expanded ), $code );
+ $stats = self::extractResults( $res, $stats );
+
+ foreach ( $expanded as $sid => $subgroup ) {
+ # Discouraged groups may belong to another group, usually if there
+ # is an aggregate group for all translatable pages. In that case
+ # calculate and store the statistics, but don't count them as part of
+ # the aggregate group, so that the numbers in Special:LanguageStats
+ # add up. The statistics for discouraged groups can still be viewed
+ # through Special:MessageGroupStats.
+ if ( !isset( $stats[$sid][$code] ) ) {
+ $stats[$sid][$code] = self::forItemInternal( $stats, $subgroup, $code );
+ }
+
+ $include = wfRunHooks( 'Translate:MessageGroupStats:isIncluded', array( $sid, $code ) );
+ if ( $include ) {
+ $aggregates = self::multiAdd( $aggregates, $stats[$sid][$code] );
+ }
+ }
+ $stats[$id][$code] = $aggregates;
+ } else {
+ $aggregates = self::calculateGroup( $group, $code );
+ }
+
+ // Don't add nulls to the database, causes annoying warnings
+ if ( $aggregates[self::TOTAL] === null ) {
+ return $aggregates;
+ }
+
+ $data = array(
+ 'tgs_group' => $id,
+ 'tgs_lang' => $code,
+ 'tgs_total' => $aggregates[self::TOTAL],
+ 'tgs_translated' => $aggregates[self::TRANSLATED],
+ 'tgs_fuzzy' => $aggregates[self::FUZZY],
+ 'tgs_proofread' => $aggregates[self::PROOFREAD],
+ );
+
+ $dbw = wfGetDB( DB_MASTER );
+ // Try to avoid deadlocks with S->X lock upgrades in MySQL
+ // @note: this only helps in auto-commit mode (which job runners use)
+ $key = __CLASS__ . ":modify:$id";
+ $locked = false;
+ if ( !$dbw->getFlag( DBO_TRX ) ) {
+ $locked = $dbw->lock( $key, __METHOD__, 1 );
+ if ( !$locked ) {
+ return $aggregates; // raced out
+ }
+ }
+
+ $dbw->insert(
+ self::TABLE,
+ $data,
+ __METHOD__,
+ array( 'IGNORE' )
+ );
+
+ if ( $locked ) {
+ $dbw->unlock( $key, __METHOD__ );
+ }
+
+ return $aggregates;
+ }
+
+ public static function multiAdd( &$a, $b ) {
+ if ( $a[0] === null || $b[0] === null ) {
+ return array_fill( 0, count( $a ), null );
+ }
+ foreach ( $a as $i => &$v ) {
+ $v += $b[$i];
+ }
+
+ return $a;
+ }
+
+ /**
+ * @param MessageGroup $group
+ * @param string $code Language code
+ * @return array ( total, translated, fuzzy, proofread )
+ */
+ protected static function calculateGroup( $group, $code ) {
+ global $wgTranslateDocumentationLanguageCode;
+ # Calculate if missing and store in the db
+ $collection = $group->initCollection( $code );
+
+ if ( $code === $wgTranslateDocumentationLanguageCode ) {
+ $ffs = $group->getFFS();
+ if ( $ffs instanceof GettextFFS ) {
+ $template = $ffs->read( 'en' );
+ $infile = array();
+ foreach ( $template['TEMPLATE'] as $key => $data ) {
+ if ( isset( $data['comments']['.'] ) ) {
+ $infile[$key] = '1';
+ }
+ }
+ $collection->setInFile( $infile );
+ }
+ }
+
+ $collection->filter( 'ignored' );
+ $collection->filter( 'optional' );
+ // Store the count of real messages for later calculation.
+ $total = count( $collection );
+
+ // Count fuzzy first.
+ $collection->filter( 'fuzzy' );
+ $fuzzy = $total - count( $collection );
+
+ // Count the completed translations.
+ $collection->filter( 'hastranslation', false );
+ $translated = count( $collection );
+
+ // Count how many of the completed translations
+ // have been proofread
+ $collection->filter( 'reviewer', false );
+ $proofread = count( $collection );
+
+ return array(
+ self::TOTAL => $total,
+ self::TRANSLATED => $translated,
+ self::FUZZY => $fuzzy,
+ self::PROOFREAD => $proofread,
+ );
+ }
+
+ /**
+ * Converts input to "+2" "-4" type of string.
+ * @param $number int
+ * @return string
+ */
+ protected static function stringifyNumber( $number ) {
+ $number = intval( $number );
+
+ return $number < 0 ? "$number" : "+$number";
+ }
+}
diff --git a/MLEB/Translate/utils/MessageHandle.php b/MLEB/Translate/utils/MessageHandle.php
new file mode 100644
index 00000000..dcb217eb
--- /dev/null
+++ b/MLEB/Translate/utils/MessageHandle.php
@@ -0,0 +1,244 @@
+<?php
+/**
+ * Class that enhances Title with stuff related to message groups
+ * @file
+ * @author Niklas Laxström
+ * @copyright Copyright © 2011-2013 Niklas Laxström
+ * @license GPL-2.0+
+ */
+
+/**
+ * Class for pointing to messages, like Title class is for titles.
+ * @since 2011-03-13
+ */
+class MessageHandle {
+ /// @var Title
+ protected $title = null;
+ /// @var String
+ protected $key = null;
+ /// @var String
+ protected $code = null;
+ /// @var String
+ protected $groupIds = null;
+ /// @var MessageGroup
+ protected $group = false;
+
+ public function __construct( Title $title ) {
+ $this->title = $title;
+ }
+
+ /**
+ * Check if this handle is in a message namespace.
+ * @return bool
+ */
+ public function isMessageNamespace() {
+ global $wgTranslateMessageNamespaces;
+ $namespace = $this->getTitle()->getNamespace();
+
+ return in_array( $namespace, $wgTranslateMessageNamespaces );
+ }
+
+ /**
+ * Recommended to use getCode and getKey instead.
+ * @return Array of the message key and the language code
+ */
+ public function figureMessage() {
+ if ( $this->key === null ) {
+ $title = $this->getTitle();
+ // Check if this is a valid message first
+ $this->key = $title->getDBKey();
+ $known = MessageIndex::singleton()->getGroupIds( $this ) !== array();
+
+ $pos = strrpos( $this->key, '/' );
+ if ( $known || $pos === false ) {
+ $this->code = '';
+ } else {
+ // For keys like Foo/, substr returns false instead of ''
+ $this->code = strval( substr( $this->key, $pos + 1 ) );
+ $this->key = substr( $this->key, 0, $pos );
+ }
+ }
+
+ return array( $this->key, $this->code );
+ }
+
+ /**
+ * Returns the identified or guessed message key.
+ * @return String
+ */
+ public function getKey() {
+ $this->figureMessage();
+
+ return $this->key;
+ }
+
+ /**
+ * Returns the language code.
+ * For language codeless source messages will return empty string.
+ * @return String
+ */
+ public function getCode() {
+ $this->figureMessage();
+
+ return $this->code;
+ }
+
+ /**
+ * Return the code for the assumed language of the content, which might
+ * be different from the subpage code (qqq, no subpage).
+ * @return String
+ * @since 2012-08-05
+ */
+ public function getEffectiveLanguageCode() {
+ global $wgContLang;
+ $code = $this->getCode();
+ if ( $code === '' || $this->isDoc() ) {
+ return $wgContLang->getCode();
+ }
+
+ return $code;
+ }
+
+ /**
+ * Determine whether the current handle is for message documentation.
+ * @return bool
+ */
+ public function isDoc() {
+ global $wgTranslateDocumentationLanguageCode;
+
+ return $this->getCode() === $wgTranslateDocumentationLanguageCode;
+ }
+
+ /**
+ * Determine whether the current handle is for page translation feature.
+ * This does not consider whether the handle corresponds to any message.
+ * @return bool
+ */
+ public function isPageTranslation() {
+ return $this->getTitle()->getNamespace() == NS_TRANSLATIONS;
+ }
+
+ /**
+ * Returns all message group ids this message belongs to.
+ * The primary message group id is always the first one.
+ * If the handle does not correspond to any message, the returned array
+ * is empty.
+ * @return array
+ */
+ public function getGroupIds() {
+ if ( $this->groupIds === null ) {
+ $this->groupIds = MessageIndex::singleton()->getGroupIds( $this );
+ }
+
+ return $this->groupIds;
+ }
+
+ /**
+ * Get the primary MessageGroup this message belongs to.
+ * You should check first that the handle is valid.
+ * @throws MWException
+ * @return MessageGroup
+ */
+ public function getGroup() {
+ $ids = $this->getGroupIds();
+ if ( !isset( $ids[0] ) ) {
+ throw new MWException( 'called before isValid' );
+ }
+
+ return MessageGroups::getGroup( $ids[0] );
+ }
+
+ /**
+ * Checks if the handle corresponds to a known message.
+ * @since 2011-03-16
+ * @return bool
+ */
+ public function isValid() {
+ if ( !$this->isMessageNamespace() ) {
+ return false;
+ }
+
+ $groups = $this->getGroupIds();
+ if ( !$groups ) {
+ return false;
+ }
+
+ // Do another check that the group actually exists
+ $group = $this->getGroup();
+ if ( !$group ) {
+ $warning = "MessageIndex is out of date – refers to unknown group {$groups[0]}. ";
+ $warning .= "Doing a rebuild.";
+ wfWarn( $warning );
+ MessageIndexRebuildJob::newJob()->run();
+
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Get the original title.
+ * @return Title
+ */
+ public function getTitle() {
+ return $this->title;
+ }
+
+ /**
+ * Get the original title.
+ * @param string $code Language code.
+ * @return Title
+ * @since 2014.04
+ */
+ public function getTitleForLanguage( $code ) {
+ return Title::makeTitle(
+ $this->title->getNamespace(),
+ $this->getKey() . "/$code"
+ );
+ }
+
+ /**
+ * Get the title for the page base.
+ * @return Title
+ * @since 2014.04
+ */
+ public function getTitleForBase() {
+ return Title::makeTitle(
+ $this->title->getNamespace(),
+ $this->getKey()
+ );
+ }
+
+ /**
+ * Check if a string contains the fuzzy string.
+ *
+ * @param $text string Arbitrary text
+ * @return bool If string contains fuzzy string.
+ */
+ public static function hasFuzzyString( $text ) {
+ return strpos( $text, TRANSLATE_FUZZY ) !== false;
+ }
+
+ /**
+ * Check if a title is marked as fuzzy.
+ * @return bool If title is marked fuzzy.
+ */
+ public function isFuzzy() {
+ $dbr = wfGetDB( DB_SLAVE );
+
+ $tables = array( 'page', 'revtag' );
+ $field = 'rt_type';
+ $conds = array(
+ 'page_namespace' => $this->title->getNamespace(),
+ 'page_title' => $this->title->getDBkey(),
+ 'rt_type' => RevTag::getType( 'fuzzy' ),
+ 'page_id=rt_page',
+ 'page_latest=rt_revision'
+ );
+
+ $res = $dbr->selectField( $tables, $field, $conds, __METHOD__ );
+
+ return $res !== false;
+ }
+}
diff --git a/MLEB/Translate/utils/MessageIndex.php b/MLEB/Translate/utils/MessageIndex.php
new file mode 100644
index 00000000..53ab2c7e
--- /dev/null
+++ b/MLEB/Translate/utils/MessageIndex.php
@@ -0,0 +1,526 @@
+<?php
+/**
+ * Contains classes for handling the message index.
+ *
+ * @file
+ * @author Niklas Laxstrom
+ * @copyright Copyright © 2008-2013, Niklas Laxström
+ * @license GPL-2.0+
+ */
+
+/**
+ * Creates a database of keys in all groups, so that namespace and key can be
+ * used to get the groups they belong to. This is used as a fallback when
+ * loadgroup parameter is not provided in the request, which happens if someone
+ * reaches a messages from somewhere else than Special:Translate. Also used
+ * by Special:TranslationStats and alike which need to map lots of titles
+ * to message groups.
+ */
+abstract class MessageIndex {
+ /// @var MessageIndex
+ protected static $instance;
+
+ /**
+ * @return MessageIndex
+ */
+ public static function singleton() {
+ if ( self::$instance === null ) {
+ global $wgTranslateMessageIndex;
+ $params = $wgTranslateMessageIndex;
+ $class = array_shift( $params );
+ self::$instance = new $class( $params );
+ }
+
+ return self::$instance;
+ }
+
+ /**
+ * Retrieves a list of groups given MessageHandle belongs to.
+ * @since 2012-01-04
+ * @param MessageHandle $handle
+ * @return array
+ */
+ public static function getGroupIds( MessageHandle $handle ) {
+ $namespace = $handle->getTitle()->getNamespace();
+ $key = $handle->getKey();
+ $normkey = TranslateUtils::normaliseKey( $namespace, $key );
+
+ $value = self::singleton()->get( $normkey );
+ if ( $value !== null ) {
+ return (array)$value;
+ } else {
+ return array();
+ }
+ }
+
+ /**
+ * @since 2012-01-04
+ * @param MessageHandle $handle
+ * @return MessageGroup|null
+ */
+ public static function getPrimaryGroupId( MessageHandle $handle ) {
+ $groups = self::getGroupIds( $handle );
+
+ return count( $groups ) ? array_shift( $groups ) : null;
+ }
+
+ /**
+ * Looks up the stored value for single key. Only for testing.
+ * @since 2012-04-10
+ * @param string $key
+ * @return string|array|null
+ */
+ protected function get( $key ) {
+ // Default implementation
+ $mi = $this->retrieve();
+ if ( isset( $mi[$key] ) ) {
+ return $mi[$key];
+ } else {
+ return null;
+ }
+ }
+
+ /// @return array
+ abstract public function retrieve();
+
+ abstract protected function store( array $array );
+
+ public function rebuild() {
+ static $recursion = 0;
+
+ if ( $recursion > 0 ) {
+ $msg = __METHOD__ . ': trying to recurse - building the index first time?';
+ wfWarn( $msg );
+
+ $recursion--;
+ return array();
+ }
+ $recursion++;
+
+ $groups = MessageGroups::singleton()->getGroups();
+
+ $new = $old = array();
+ $old = $this->retrieve();
+ $postponed = array();
+
+ /**
+ * @var MessageGroup $g
+ */
+ foreach ( $groups as $g ) {
+ if ( !$g->exists() ) {
+ continue;
+ }
+
+ # Skip meta thingies
+ if ( $g->isMeta() ) {
+ $postponed[] = $g;
+ continue;
+ }
+
+ $this->checkAndAdd( $new, $g );
+ }
+
+ foreach ( $postponed as $g ) {
+ $this->checkAndAdd( $new, $g, true );
+ }
+
+ $this->store( $new );
+ $this->clearMessageGroupStats( $old, $new );
+ $recursion--;
+
+ return $new;
+ }
+
+ /**
+ * Purge message group stats when set of keys have changed.
+ * @param array $old
+ * @param array $new
+ */
+ protected function clearMessageGroupStats( array $old, array $new ) {
+ $changes = array();
+
+ foreach ( $new as $key => $groups ) {
+ // Using != here on purpose to ignore order of items
+ if ( !isset( $old[$key] ) ) {
+ $changes[$key] = array( array(), (array)$groups );
+ } elseif ( $groups != $old[$key] ) {
+ $changes[$key] = array( (array)$old[$key], (array)$groups );
+ }
+ }
+
+ foreach ( $old as $key => $groups ) {
+ if ( !isset( $new[$key] ) ) {
+ $changes[$key] = array( (array)$groups, array() );
+ }
+ // We already checked for diffs above
+ }
+
+ $changedGroups = array();
+ foreach ( $changes as $data ) {
+ foreach ( $data[0] as $group ) {
+ $changedGroups[$group] = true;
+ }
+ foreach ( $data[1] as $group ) {
+ $changedGroups[$group] = true;
+ }
+ }
+
+ MessageGroupStats::clearGroup( array_keys( $changedGroups ) );
+
+ foreach ( $changes as $key => $data ) {
+ list( $ns, $pagename ) = explode( ':', $key, 2 );
+ $title = Title::makeTitle( $ns, $pagename );
+ $handle = new MessageHandle( $title );
+ list ( $oldGroups, $newGroups ) = $data;
+ wfRunHooks( 'TranslateEventMessageMembershipChange',
+ array( $handle, $oldGroups, $newGroups ) );
+ }
+ }
+
+ /**
+ * @param array $hugearray
+ * @param MessageGroup $g
+ * @param bool $ignore
+ */
+ protected function checkAndAdd( &$hugearray, MessageGroup $g, $ignore = false ) {
+ if ( method_exists( $g, 'getKeys' ) ) {
+ $keys = $g->getKeys();
+ } else {
+ $messages = $g->getDefinitions();
+
+ if ( !is_array( $messages ) ) {
+ return;
+ }
+
+ $keys = array_keys( $messages );
+ }
+
+ $id = $g->getId();
+
+ $namespace = $g->getNamespace();
+
+ foreach ( $keys as $key ) {
+ # Force all keys to lower case, because the case doesn't matter and it is
+ # easier to do comparing when the case of first letter is unknown, because
+ # mediawiki forces it to upper case
+ $key = TranslateUtils::normaliseKey( $namespace, $key );
+ if ( isset( $hugearray[$key] ) ) {
+ if ( !$ignore ) {
+ $to = implode( ', ', (array)$hugearray[$key] );
+ wfWarn( "Key $key already belongs to $to, conflict with $id" );
+ }
+
+ if ( is_array( $hugearray[$key] ) ) {
+ // Hard work is already done, just add a new reference
+ $hugearray[$key][] = & $id;
+ } else {
+ // Store the actual reference, then remove it from array, to not
+ // replace the references value, but to store an array of new
+ // references instead. References are hard!
+ $value = & $hugearray[$key];
+ unset( $hugearray[$key] );
+ $hugearray[$key] = array( &$value, &$id );
+ }
+ } else {
+ $hugearray[$key] = & $id;
+ }
+ }
+ unset( $id ); // Disconnect the previous references to this $id
+ }
+
+ /* These are probably slower than serialize and unserialize,
+ * but they are more space efficient because we only need
+ * strings and arrays. */
+ protected function serialize( $data ) {
+ if ( is_array( $data ) ) {
+ return implode( '|', $data );
+ } else {
+ return $data;
+ }
+ }
+
+ protected function unserialize( $data ) {
+ if ( strpos( $data, '|' ) !== false ) {
+ return explode( '|', $data );
+ }
+
+ return $data;
+ }
+}
+
+/**
+ * Storage on serialized file.
+ *
+ * This serializes the whole array. Because this format can preserve
+ * the values which are stored as references inside the array, this is
+ * the most space efficient storage method and fastest when you want
+ * the full index.
+ *
+ * Unfortunately when the size of index grows to about 50000 items, even
+ * though it is only 3,5M on disk, it takes 35M when loaded into memory
+ * and the loading can take more than 0,5 seconds. Because usually we
+ * need to look up only few keys, it is better to use another backend
+ * which provides random access - this backend doesn't support that.
+ */
+class SerializedMessageIndex extends MessageIndex {
+ /// @var array
+ protected $index;
+
+ protected $filename = 'translate_messageindex.ser';
+
+ /** @return array */
+ public function retrieve() {
+ if ( $this->index !== null ) {
+ return $this->index;
+ }
+
+ wfProfileIn( __METHOD__ );
+ $file = TranslateUtils::cacheFile( $this->filename );
+ if ( file_exists( $file ) ) {
+ $this->index = unserialize( file_get_contents( $file ) );
+ } else {
+ $this->index = $this->rebuild();
+ }
+ wfProfileOut( __METHOD__ );
+
+ return $this->index;
+ }
+
+ protected function store( array $array ) {
+ wfProfileIn( __METHOD__ );
+ $file = TranslateUtils::cacheFile( $this->filename );
+ file_put_contents( $file, serialize( $array ) );
+ $this->index = $array;
+ wfProfileOut( __METHOD__ );
+ }
+}
+
+/// BC
+class FileCachedMessageIndex extends SerializedMessageIndex {
+}
+
+/**
+ * Storage on the database itself.
+ *
+ * This is likely to be the slowest backend. However it scales okay
+ * and provides random access. It also doesn't need any special setup,
+ * the database table is added with update.php together with other tables,
+ * which is the reason this is the default backend. It also works well
+ * on multi-server setup without needing for shared file storage.
+ *
+ * @since 2012-04-12
+ */
+class DatabaseMessageIndex extends MessageIndex {
+ /// @var array
+ protected $index;
+
+ /** @return array */
+ public function retrieve() {
+ if ( $this->index !== null ) {
+ return $this->index;
+ }
+
+ wfProfileIn( __METHOD__ );
+ $dbr = wfGetDB( DB_SLAVE );
+ $res = $dbr->select( 'translate_messageindex', '*', array(), __METHOD__ );
+ $this->index = array();
+ foreach ( $res as $row ) {
+ $this->index[$row->tmi_key] = $this->unserialize( $row->tmi_value );
+ }
+ wfProfileOut( __METHOD__ );
+
+ return $this->index;
+ }
+
+ protected function get( $key ) {
+ wfProfileIn( __METHOD__ );
+ $dbr = wfGetDB( DB_SLAVE );
+ $value = $dbr->selectField(
+ 'translate_messageindex',
+ 'tmi_value',
+ array( 'tmi_key' => $key ),
+ __METHOD__
+ );
+
+ if ( is_string( $value ) ) {
+ $value = $this->unserialize( $value );
+ } else {
+ $value = null;
+ }
+
+ wfProfileOut( __METHOD__ );
+
+ return $value;
+ }
+
+ protected function store( array $array ) {
+ wfProfileIn( __METHOD__ );
+ $dbw = wfGetDB( DB_MASTER );
+ $rows = array();
+
+ foreach ( $array as $key => $value ) {
+ $value = $this->serialize( $value );
+ $rows[] = array( 'tmi_key' => $key, 'tmi_value' => $value );
+ }
+
+ // BC for <= MW 1.22
+ if ( method_exists( $dbw, 'startAtomic' ) ) {
+ $dbw->startAtomic( __METHOD__ );
+ }
+ $dbw->delete( 'translate_messageindex', '*', __METHOD__ );
+ $dbw->insert( 'translate_messageindex', $rows, __METHOD__ );
+ if ( method_exists( $dbw, 'endAtomic' ) ) {
+ $dbw->endAtomic( __METHOD__ );
+ }
+
+ $this->index = $array;
+ wfProfileOut( __METHOD__ );
+ }
+}
+
+/**
+ * Storage on the object cache.
+ *
+ * This can be faster than DatabaseMessageIndex, but it doesn't
+ * provide random access, and the data is not guaranteed to be persistent.
+ *
+ * This is unlikely to be the best backend for you, so don't use it.
+ */
+class CachedMessageIndex extends MessageIndex {
+ protected $key = 'translate-messageindex';
+ protected $cache;
+
+ /// @var array
+ protected $index;
+
+ protected function __construct( array $params ) {
+ $this->cache = wfGetCache( CACHE_ANYTHING );
+ }
+
+ /** @return array */
+ public function retrieve() {
+ if ( $this->index !== null ) {
+ return $this->index;
+ }
+
+ wfProfileIn( __METHOD__ );
+ $key = wfMemckey( $this->key );
+ $data = $this->cache->get( $key );
+ if ( is_array( $data ) ) {
+ $this->index = $data;
+ } else {
+ $this->index = $this->rebuild();
+ }
+ wfProfileOut( __METHOD__ );
+
+ return $this->index;
+ }
+
+ protected function store( array $array ) {
+ wfProfileIn( __METHOD__ );
+ $key = wfMemckey( $this->key );
+ $this->cache->set( $key, $array );
+
+ $this->index = $array;
+ wfProfileOut( __METHOD__ );
+ }
+}
+
+/**
+ * Storage on CDB files.
+ *
+ * This is improved version of SerializedMessageIndex. It uses CDB files
+ * for storage, which means it provides random access. The CDB files are
+ * about double the size of serialized files (~7M for 50000 keys).
+ *
+ * Loading the whole index is slower than serialized, but about the same
+ * as for database. Suitable for single-server setups where
+ * SerializedMessageIndex is too slow for sloading the whole index.
+ *
+ * @since 2012-04-10
+ */
+class CDBMessageIndex extends MessageIndex {
+ /// @var array
+ protected $index;
+
+ /// @var CdbReader
+ protected $reader;
+
+ /// @var string
+ protected $filename = 'translate_messageindex.cdb';
+
+ /** @return array */
+ public function retrieve() {
+ $reader = $this->getReader();
+ // This must be below the line above, which may fill the index
+ if ( $this->index !== null ) {
+ return $this->index;
+ }
+
+ wfProfileIn( __METHOD__ );
+ $keys = (array)$this->unserialize( $reader->get( '#keys' ) );
+ $this->index = array();
+ foreach ( $keys as $key ) {
+ $this->index[$key] = $this->unserialize( $reader->get( $key ) );
+ }
+ wfProfileOut( __METHOD__ );
+
+ return $this->index;
+ }
+
+ protected function get( $key ) {
+ $reader = $this->getReader();
+ // We might have the full cache loaded
+ if ( $this->index !== null ) {
+ if ( isset( $this->index[$key] ) ) {
+ return $this->index[$key];
+ } else {
+ return null;
+ }
+ }
+
+ $value = $reader->get( $key );
+ if ( !is_string( $value ) ) {
+ $value = null;
+ } else {
+ $value = $this->unserialize( $value );
+ }
+
+ return $value;
+ }
+
+ protected function store( array $array ) {
+ wfProfileIn( __METHOD__ );
+ $this->reader = null;
+
+ $file = TranslateUtils::cacheFile( $this->filename );
+ $cache = CdbWriter::open( $file );
+ $keys = array_keys( $array );
+ $cache->set( '#keys', $this->serialize( $keys ) );
+
+ foreach ( $array as $key => $value ) {
+ $value = $this->serialize( $value );
+ $cache->set( $key, $value );
+ }
+
+ $cache->close();
+
+ $this->index = $array;
+ wfProfileOut( __METHOD__ );
+ }
+
+ protected function getReader() {
+ if ( $this->reader ) {
+ return $this->reader;
+ }
+
+ $file = TranslateUtils::cacheFile( $this->filename );
+ if ( !file_exists( $file ) ) {
+ // Create an empty index to allow rebuild
+ $this->store( array() );
+ $this->index = $this->rebuild();
+ }
+
+ return $this->reader = CdbReader::open( $file );
+ }
+}
diff --git a/MLEB/Translate/utils/MessageIndexRebuildJob.php b/MLEB/Translate/utils/MessageIndexRebuildJob.php
new file mode 100644
index 00000000..413df987
--- /dev/null
+++ b/MLEB/Translate/utils/MessageIndexRebuildJob.php
@@ -0,0 +1,53 @@
+<?php
+/**
+ * Contains class with job for rebuilding message index.
+ *
+ * @file
+ * @author Niklas Laxström
+ * @copyright Copyright © 2011-2013, Niklas Laxström
+ * @license GPL-2.0+
+ */
+
+/**
+ * Job for rebuilding message index.
+ *
+ * @ingroup JobQueue
+ */
+class MessageIndexRebuildJob extends Job {
+
+ /**
+ * @return MessageIndexRebuildJob
+ */
+ public static function newJob() {
+ $job = new self( Title::newMainPage() );
+
+ return $job;
+ }
+
+ function __construct( $title, $params = array(), $id = 0 ) {
+ parent::__construct( __CLASS__, $title, $params, $id );
+ }
+
+ function run() {
+ MessageIndex::singleton()->rebuild();
+
+ return true;
+ }
+
+ /**
+ * Usually this job is fast enough to be executed immediately,
+ * in which case having it go through jobqueue only causes problems
+ * in installations with errant job queue processing.
+ * @override
+ */
+ public function insert() {
+ global $wgTranslateDelayedMessageIndexRebuild;
+ if ( $wgTranslateDelayedMessageIndexRebuild ) {
+ return JobQueueGroup::singleton()->push( $this );
+ } else {
+ $this->run();
+
+ return true;
+ }
+ }
+}
diff --git a/MLEB/Translate/utils/MessageTable.php b/MLEB/Translate/utils/MessageTable.php
new file mode 100644
index 00000000..d2dc83a7
--- /dev/null
+++ b/MLEB/Translate/utils/MessageTable.php
@@ -0,0 +1,418 @@
+<?php
+/**
+ * Contains classes to build tables for MessageCollection objects.
+ *
+ * @file
+ * @author Niklas Laxström
+ * @license GPL-2.0+
+ */
+
+/**
+ * Pretty formatter for MessageCollection objects.
+ */
+class MessageTable {
+ /*
+ * @var bool
+ */
+ protected $reviewMode = false;
+
+ /**
+ * @var MessageCollection
+ */
+ protected $collection;
+
+ /**
+ * @var MessageGroup
+ */
+ protected $group;
+
+ /**
+ * @var IContextSource
+ */
+ protected $context;
+
+ /**
+ * @var array
+ */
+ protected $headers = array(
+ 'table' => array( 'msg', 'allmessagesname' ),
+ 'current' => array( 'msg', 'allmessagescurrent' ),
+ 'default' => array( 'msg', 'allmessagesdefault' ),
+ );
+
+ /**
+ * Use this rather than the constructor directly
+ * to allow alternative implementations.
+ *
+ * @since 2012-11-29
+ */
+ public static function newFromContext(
+ IContextSource $context,
+ MessageCollection $collection,
+ MessageGroup $group
+ ) {
+ $table = new self( $collection, $group );
+ $table->setContext( $context );
+
+ wfRunHooks( 'TranslateMessageTableInit', array( &$table, $context, $collection, $group ) );
+
+ return $table;
+ }
+
+ public function setContext( IContextSource $context ) {
+ $this->context = $context;
+ }
+
+ /**
+ * Use the newFromContext() function rather than the constructor directly
+ * to construct the object to allow alternative implementations.
+ */
+ public function __construct( MessageCollection $collection, MessageGroup $group ) {
+ $this->collection = $collection;
+ $this->group = $group;
+ $this->setHeaderText( 'table', $group->getLabel() );
+ }
+
+ public function setReviewMode( $mode = true ) {
+ $this->reviewMode = $mode;
+ }
+
+ public function setHeaderTextMessage( $type, $value ) {
+ if ( !isset( $this->headers[$type] ) ) {
+ throw new MWException( "Unexpected type $type" );
+ }
+
+ $this->headers[$type] = array( 'msg', $value );
+ }
+
+ public function setHeaderText( $type, $value ) {
+ if ( !isset( $this->headers[$type] ) ) {
+ throw new MWException( "Unexpected type $type" );
+ }
+
+ $this->headers[$type] = array( 'raw', htmlspecialchars( $value ) );
+ }
+
+ public function includeAssets() {
+ TranslationHelpers::addModules( $this->context->getOutput() );
+ $pages = array();
+
+ foreach ( $this->collection->getTitles() as $title ) {
+ $pages[] = $title->getPrefixedDBKey();
+ }
+
+ $vars = array( 'trlKeys' => $pages );
+ $this->context->getOutput()->addScript( Skin::makeVariablesScript( $vars ) );
+ }
+
+ public function header() {
+ $tableheader = Xml::openElement( 'table', array(
+ 'class' => 'mw-sp-translate-table'
+ ) );
+
+ if ( $this->reviewMode ) {
+ $tableheader .= Xml::openElement( 'tr' );
+ $tableheader .= Xml::element( 'th',
+ array( 'rowspan' => '2' ),
+ $this->headerText( 'table' )
+ );
+ $tableheader .= Xml::tags( 'th', null, $this->headerText( 'default' ) );
+ $tableheader .= Xml::closeElement( 'tr' );
+
+ $tableheader .= Xml::openElement( 'tr' );
+ $tableheader .= Xml::tags( 'th', null, $this->headerText( 'current' ) );
+ $tableheader .= Xml::closeElement( 'tr' );
+ } else {
+ $tableheader .= Xml::openElement( 'tr' );
+ $tableheader .= Xml::tags( 'th', null, $this->headerText( 'table' ) );
+ $tableheader .= Xml::tags( 'th', null, $this->headerText( 'current' ) );
+ $tableheader .= Xml::closeElement( 'tr' );
+ }
+
+ return $tableheader . "\n";
+ }
+
+ public function contents() {
+ $optional = $this->context->msg( 'translate-optional' )->escaped();
+
+ $this->doLinkBatch();
+
+ $sourceLang = Language::factory( $this->group->getSourceLanguage() );
+ $targetLang = Language::factory( $this->collection->getLanguage() );
+ $titleMap = $this->collection->keys();
+
+ $output = '';
+
+ $this->collection->initMessages(); // Just to be sure
+
+ /**
+ * @var TMessage $m
+ */
+ foreach ( $this->collection as $key => $m ) {
+ $tools = array();
+ /**
+ * @var Title $title
+ */
+ $title = $titleMap[$key];
+
+ $original = $m->definition();
+ $translation = $m->translation();
+
+ $hasTranslation = $translation !== null;
+
+ if ( $hasTranslation ) {
+ $message = $translation;
+ $extraAttribs = self::getLanguageAttributes( $targetLang );
+ } else {
+ $message = $original;
+ $extraAttribs = self::getLanguageAttributes( $sourceLang );
+ }
+
+ wfRunHooks(
+ 'TranslateFormatMessageBeforeTable',
+ array( &$message, $m, $this->group, $targetLang, &$extraAttribs )
+ );
+
+ // Using Html::element( a ) because Linker::link is memory hog.
+ // It takes about 20 KiB per call, and that times 5000 is quite
+ // a lot of memory.
+ $niceTitle = htmlspecialchars( $this->context->getLanguage()->truncate(
+ $title->getPrefixedText(),
+ -35
+ ) );
+ $linkAttribs = array(
+ 'href' => $title->getLocalUrl( array( 'action' => 'edit' ) ),
+ );
+ $linkAttribs += TranslationEditPage::jsEdit( $title, $this->group->getId() );
+
+ $tools['edit'] = Html::element( 'a', $linkAttribs, $niceTitle );
+
+ $anchor = 'msg_' . $key;
+ $anchor = Xml::element( 'a', array( 'id' => $anchor, 'href' => "#$anchor" ), "↓" );
+
+ $extra = '';
+ if ( $m->hasTag( 'optional' ) ) {
+ $extra = '<br />' . $optional;
+ }
+
+ $tqeData = $extraAttribs + array(
+ 'data-title' => $title->getPrefixedText(),
+ 'data-group' => $this->group->getId(),
+ 'id' => 'tqe-anchor-' . substr( sha1( $title->getPrefixedText() ), 0, 12 ),
+ 'class' => 'tqe-inlineeditable ' . ( $hasTranslation ? 'translated' : 'untranslated' )
+ );
+
+ $button = $this->getReviewButton( $m );
+ $status = $this->getReviewStatus( $m );
+ $leftColumn = $button . $anchor . $tools['edit'] . $extra . $status;
+
+ if ( $this->reviewMode ) {
+ $output .= Xml::tags( 'tr', array( 'class' => 'orig' ),
+ Xml::tags( 'td', array( 'rowspan' => '2' ), $leftColumn ) .
+ Xml::tags( 'td', self::getLanguageAttributes( $sourceLang ),
+ TranslateUtils::convertWhiteSpaceToHTML( $original )
+ )
+ );
+
+ $output .= Xml::tags( 'tr', null,
+ Xml::tags( 'td', $tqeData, TranslateUtils::convertWhiteSpaceToHTML( $message ) )
+ );
+ } else {
+ $output .= Xml::tags( 'tr', array( 'class' => 'def' ),
+ Xml::tags( 'td', null, $leftColumn ) .
+ Xml::tags( 'td', $tqeData, TranslateUtils::convertWhiteSpaceToHTML( $message ) )
+ );
+ }
+
+ $output .= "\n";
+ }
+
+ return $output;
+ }
+
+ public function fullTable( $offsets, $nondefaults ) {
+ $this->includeAssets();
+
+ $content = $this->header() . $this->contents() . '</table>';
+ $pager = $this->doStupidLinks( $offsets, $nondefaults );
+
+ if ( $offsets['count'] === 0 ) {
+ return $pager;
+ } elseif ( $offsets['count'] === $offsets['total'] ) {
+ return $content . $pager;
+ } else {
+ return $pager . $content . $pager;
+ }
+ }
+
+ protected function headerText( $type ) {
+ if ( !isset( $this->headers[$type] ) ) {
+ throw new MWException( "Unexpected type $type" );
+ }
+
+ list( $format, $value ) = $this->headers[$type];
+ if ( $format === 'msg' ) {
+ return wfMessage( $value )->escaped();
+ } elseif ( $format === 'raw' ) {
+ return $value;
+ } else {
+ throw new MWException( "Unexcepted format $format" );
+ }
+ }
+
+ protected static function getLanguageAttributes( Language $language ) {
+ global $wgTranslateDocumentationLanguageCode;
+
+ $code = $language->getCode();
+ $dir = $language->getDir();
+
+ if ( $code === $wgTranslateDocumentationLanguageCode ) {
+ // Should be good enough for now
+ $code = 'en';
+ }
+
+ return array( 'lang' => $code, 'dir' => $dir );
+ }
+
+ protected function getReviewButton( TMessage $message ) {
+ $revision = $message->getProperty( 'revision' );
+ $user = $this->context->getUser();
+
+ if ( !$this->reviewMode || !$user->isAllowed( 'translate-messagereview' ) || !$revision ) {
+ return '';
+ }
+
+ $attribs = array(
+ 'type' => 'button',
+ 'class' => 'mw-translate-messagereviewbutton',
+ 'data-token' => ApiTranslationReview::getToken( 0, '' ),
+ 'data-revision' => $revision,
+ 'name' => 'acceptbutton-' . $revision, // Otherwise Firefox disables buttons on page load
+ );
+
+ $reviewers = (array)$message->getProperty( 'reviewers' );
+ if ( in_array( $user->getId(), $reviewers ) ) {
+ $attribs['value'] = wfMessage( 'translate-messagereview-done' )->text();
+ $attribs['disabled'] = 'disabled';
+ $attribs['title'] = wfMessage( 'translate-messagereview-doit' )->text();
+ } elseif ( $message->hasTag( 'fuzzy' ) ) {
+ $attribs['value'] = wfMessage( 'translate-messagereview-submit' )->text();
+ $attribs['disabled'] = 'disabled';
+ $attribs['title'] = wfMessage( 'translate-messagereview-no-fuzzy' )->text();
+ } elseif ( $user->getName() === $message->getProperty( 'last-translator-text' ) ) {
+ $attribs['value'] = wfMessage( 'translate-messagereview-submit' )->text();
+ $attribs['disabled'] = 'disabled';
+ $attribs['title'] = wfMessage( 'translate-messagereview-no-own' )->text();
+ } else {
+ $attribs['value'] = wfMessage( 'translate-messagereview-submit' )->text();
+ }
+
+ $review = Html::element( 'input', $attribs );
+
+ return $review;
+ }
+
+ /// For optimization
+ protected $reviewStatusCache = array();
+
+ protected function getReviewStatus( TMessage $message ) {
+ if ( !$this->reviewMode ) {
+ return '';
+ }
+
+ $reviewers = (array)$message->getProperty( 'reviewers' );
+ $count = count( $reviewers );
+
+ if ( $count === 0 ) {
+ return '';
+ }
+
+ $userId = $this->context->getUser()->getId();
+ $you = in_array( $userId, $reviewers );
+ $key = $you ? "y$count" : "n$count";
+
+ // ->text() (and ->parse()) invokes the parser. Each call takes
+ // about 70 KiB, so it makes sense to cache these messages which
+ // have high repetition.
+ if ( isset( $this->reviewStatusCache[$key] ) ) {
+ return $this->reviewStatusCache[$key];
+ } elseif ( $you ) {
+ $msg = wfMessage( 'translate-messagereview-reviewswithyou' )->numParams( $count )->text();
+ } else {
+ $msg = wfMessage( 'translate-messagereview-reviews' )->numParams( $count )->text();
+ }
+
+ $wrap = Html::rawElement( 'div', array( 'class' => 'mw-translate-messagereviewstatus' ), $msg );
+ $this->reviewStatusCache[$key] = $wrap;
+
+ return $wrap;
+ }
+
+ protected function doLinkBatch() {
+ $batch = new LinkBatch();
+ $batch->setCaller( __METHOD__ );
+
+ foreach ( $this->collection->getTitles() as $title ) {
+ $batch->addObj( $title );
+ }
+
+ $batch->execute();
+ }
+
+ protected function doStupidLinks( $info, $nondefaults ) {
+ // Total number of messages for this query
+ $total = $info['total'];
+ // Messages in this page
+ $count = $info['count'];
+
+ $allInThisPage = $info['start'] === 0 && $total === $count;
+
+ if ( $info['count'] === 0 ) {
+ $navigation = wfMessage( 'translate-page-showing-none' )->parse();
+ } elseif ( $allInThisPage ) {
+ $navigation = wfMessage( 'translate-page-showing-all' )->numParams( $total )->parse();
+ } else {
+ $previous = wfMessage( 'translate-prev' )->escaped();
+
+ if ( $info['backwardsOffset'] !== false ) {
+ $previous = $this->makeOffsetLink( $previous, $info['backwardsOffset'], $nondefaults );
+ }
+
+ $nextious = wfMessage( 'translate-next' )->escaped();
+ if ( $info['forwardsOffset'] !== false ) {
+ $nextious = $this->makeOffsetLink( $nextious, $info['forwardsOffset'], $nondefaults );
+ }
+
+ $start = $info['start'] + 1;
+ $stop = $start + $info['count'] - 1;
+ $total = $info['total'];
+
+ $navigation = wfMessage( 'translate-page-showing' )
+ ->numParams( $start, $stop, $total )->parse();
+ $navigation .= ' ';
+ $navigation .= wfMessage( 'translate-page-paging-links' )
+ ->rawParams( $previous, $nextious )->escaped();
+ }
+
+ return Html::openElement( 'fieldset' ) .
+ Html::element( 'legend', array(), wfMessage( 'translate-page-navigation-legend' )->text() ) .
+ $navigation .
+ Html::closeElement( 'fieldset' );
+ }
+
+ protected function makeOffsetLink( $label, $offset, $nondefaults ) {
+ $query = array_merge(
+ $nondefaults,
+ array( 'offset' => $offset )
+ );
+
+ $link = Linker::link(
+ $this->context->getTitle(),
+ $label,
+ array(),
+ $query
+ );
+
+ return $link;
+ }
+}
diff --git a/MLEB/Translate/utils/MessageUpdateJob.php b/MLEB/Translate/utils/MessageUpdateJob.php
new file mode 100644
index 00000000..a12972a3
--- /dev/null
+++ b/MLEB/Translate/utils/MessageUpdateJob.php
@@ -0,0 +1,91 @@
+<?php
+/**
+ * Job for updating translation pages.
+ *
+ * @file
+ * @author Niklas Laxström
+ * @copyright Copyright © 2008-2013, Niklas Laxström
+ * @license GPL-2.0+
+ */
+
+/**
+ * Job for updating translation pages when translation or message definition changes.
+ *
+ * @ingroup JobQueue
+ */
+class MessageUpdateJob extends Job {
+ public static function newJob( Title $target, $content, $fuzzy = false ) {
+ $params = array(
+ 'content' => $content,
+ 'fuzzy' => $fuzzy,
+ );
+ $job = new self( $target, $params );
+
+ return $job;
+ }
+
+ function __construct( $title, $params = array(), $id = 0 ) {
+ parent::__construct( __CLASS__, $title, $params, $id );
+ $this->params = $params;
+ }
+
+ function run() {
+ global $wgTranslateDocumentationLanguageCode;
+
+ $title = $this->title;
+ $params = $this->params;
+ $user = FuzzyBot::getUser();
+ $flags = EDIT_DEFER_UPDATES | EDIT_FORCE_BOT;
+
+ $wikiPage = WikiPage::factory( $title );
+ $summary = wfMessage( 'translate-manage-import-summary' )
+ ->inContentLanguage()->plain();
+ $content = ContentHandler::makeContent( $params['content'], $title );
+ $wikiPage->doEditContent( $content, $summary, $flags, false, $user );
+
+ // NOTE: message documentation is excluded from fuzzying!
+ if ( $params['fuzzy'] ) {
+ $handle = new MessageHandle( $title );
+ $key = $handle->getKey();
+
+ $languages = TranslateUtils::getLanguageNames( 'en' );
+ unset( $languages[$wgTranslateDocumentationLanguageCode] );
+ $languages = array_keys( $languages );
+
+ $dbw = wfGetDB( DB_MASTER );
+ $fields = array( 'page_id', 'page_latest' );
+ $conds = array( 'page_namespace' => $title->getNamespace() );
+
+ $pages = array();
+ foreach ( $languages as $code ) {
+ $otherTitle = Title::makeTitleSafe( $title->getNamespace(), "$key/$code" );
+ $pages[$otherTitle->getDBKey()] = true;
+ }
+ unset( $pages[$title->getDBKey()] );
+ if ( count( $pages ) === 0 ) {
+ return true;
+ }
+
+ $conds['page_title'] = array_keys( $pages );
+
+ $res = $dbw->select( 'page', $fields, $conds, __METHOD__ );
+ $inserts = array();
+ foreach ( $res as $row ) {
+ $inserts[] = array(
+ 'rt_type' => RevTag::getType( 'fuzzy' ),
+ 'rt_page' => $row->page_id,
+ 'rt_revision' => $row->page_latest,
+ );
+ }
+
+ $dbw->replace(
+ 'revtag',
+ array( array( 'rt_type', 'rt_page', 'rt_revision' ) ),
+ $inserts,
+ __METHOD__
+ );
+ }
+
+ return true;
+ }
+}
diff --git a/MLEB/Translate/utils/MessageWebImporter.php b/MLEB/Translate/utils/MessageWebImporter.php
new file mode 100644
index 00000000..ec9c968b
--- /dev/null
+++ b/MLEB/Translate/utils/MessageWebImporter.php
@@ -0,0 +1,577 @@
+<?php
+/**
+ * Class which encapsulates message importing. It scans for changes (new, changed, deleted),
+ * displays them in pretty way with diffs and finally executes the actions the user choices.
+ *
+ * @file
+ * @author Niklas Laxström
+ * @author Siebrand Mazeland
+ * @copyright Copyright © 2009-2013, Niklas Laxström, Siebrand Mazeland
+ * @license GPL-2.0+
+ */
+
+/**
+ * Class which encapsulates message importing. It scans for changes (new, changed, deleted),
+ * displays them in pretty way with diffs and finally executes the actions the user choices.
+ */
+class MessageWebImporter {
+ /**
+ * @var Title
+ */
+ protected $title;
+
+ /**
+ * @var User
+ */
+ protected $user;
+
+ /**
+ * @var MessageGroup
+ */
+ protected $group;
+ protected $code;
+ protected $time;
+
+ /**
+ * @var OutputPage
+ */
+ protected $out;
+
+ /**
+ * Maximum processing time in seconds.
+ */
+ protected $processingTime = 43;
+
+ public function __construct( Title $title = null, $group = null, $code = 'en' ) {
+ $this->setTitle( $title );
+ $this->setGroup( $group );
+ $this->setCode( $code );
+ }
+
+ /**
+ * Wrapper for consistency with SpecialPage
+ *
+ * @return Title
+ */
+ public function getTitle() {
+ return $this->title;
+ }
+
+ public function setTitle( Title $title ) {
+ $this->title = $title;
+ }
+
+ /**
+ * @return User
+ */
+ public function getUser() {
+ return $this->user ? $this->user : RequestContext::getMain()->getUser();
+ }
+
+ public function setUser( User $user ) {
+ $this->user = $user;
+ }
+
+ /**
+ * @return MessageGroup
+ */
+ public function getGroup() {
+ return $this->group;
+ }
+
+ /**
+ * Group is either MessageGroup object or group id.
+ * @param MessageGroup|string $group
+ */
+ public function setGroup( $group ) {
+ if ( $group instanceof MessageGroup ) {
+ $this->group = $group;
+ } else {
+ $this->group = MessageGroups::getGroup( $group );
+ }
+ }
+
+ public function getCode() {
+ return $this->code;
+ }
+
+ public function setCode( $code = 'en' ) {
+ $this->code = $code;
+ }
+
+ /**
+ * @return string
+ */
+ protected function getAction() {
+ return $this->getTitle()->getFullURL();
+ }
+
+ /**
+ * @return string
+ */
+ protected function doHeader() {
+ $formParams = array(
+ 'method' => 'post',
+ 'action' => $this->getAction(),
+ 'class' => 'mw-translate-manage'
+ );
+
+ return
+ Xml::openElement( 'form', $formParams ) .
+ Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) .
+ Html::hidden( 'token', $this->getUser()->getEditToken() ) .
+ Html::hidden( 'process', 1 );
+ }
+
+ /**
+ * @return string
+ */
+ protected function doFooter() {
+ return '</form>';
+ }
+
+ /**
+ * @return bool
+ */
+ protected function allowProcess() {
+ $request = RequestContext::getMain()->getRequest();
+
+ if ( $request->wasPosted() &&
+ $request->getBool( 'process', false ) &&
+ $this->getUser()->matchEditToken( $request->getVal( 'token' ) )
+ ) {
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * @return array
+ */
+ protected function getActions() {
+ if ( $this->code === 'en' ) {
+ return array( 'import', 'fuzzy', 'ignore' );
+ } else {
+ return array( 'import', 'conflict', 'ignore' );
+ }
+ }
+
+ /**
+ * @param bool $fuzzy
+ * @param string $action
+ * @return string
+ */
+ protected function getDefaultAction( $fuzzy, $action ) {
+ if ( $action ) {
+ return $action;
+ }
+
+ return $fuzzy ? 'conflict' : 'import';
+ }
+
+ public function execute( $messages ) {
+ $context = RequestContext::getMain();
+ $this->out = $context->getOutput();
+
+ // Set up diff engine
+ $diff = new DifferenceEngine;
+ $diff->showDiffStyle();
+ $diff->setReducedLineNumbers();
+
+ // Check whether we do processing
+ $process = $this->allowProcess();
+
+ // Initialise collection
+ $group = $this->getGroup();
+ $code = $this->getCode();
+ $collection = $group->initCollection( $code );
+ $collection->loadTranslations();
+
+ $this->out->addHTML( $this->doHeader() );
+
+ // Determine changes
+ $alldone = $process;
+ $changed = array();
+
+ foreach ( $messages as $key => $value ) {
+ $fuzzy = $old = false;
+
+ if ( isset( $collection[$key] ) ) {
+ $old = $collection[$key]->translation();
+ }
+
+ // No changes at all, ignore
+ if ( strval( $old ) === strval( $value ) ) {
+ continue;
+ }
+
+ if ( $old === false ) {
+ $para = '<code class="mw-tmi-new">' . htmlspecialchars( $key ) . '</code>';
+ $name = $context->msg( 'translate-manage-import-new' )->rawParams( $para )
+ ->escaped();
+ $text = TranslateUtils::convertWhiteSpaceToHTML( $value );
+ $changed[] = self::makeSectionElement( $name, 'new', $text );
+ } else {
+ $oldContent = ContentHandler::makeContent( $old, $diff->getTitle() );
+ $newContent = ContentHandler::makeContent( $value, $diff->getTitle() );
+
+ $diff->setContent( $oldContent, $newContent );
+
+ $text = $diff->getDiff( '', '' );
+ $type = 'changed';
+
+ $action = $context->getRequest()
+ ->getVal( self::escapeNameForPHP( "action-$type-$key" ) );
+
+ if ( $process ) {
+ if ( !count( $changed ) ) {
+ $changed[] = '<ul>';
+ }
+
+ if ( $action === null ) {
+ $message = $context->msg(
+ 'translate-manage-inconsistent',
+ wfEscapeWikiText( "action-$type-$key" )
+ )->parse();
+ $changed[] = "<li>$message</li></ul>";
+ $process = false;
+ } else {
+ // Check processing time
+ if ( !isset( $this->time ) ) {
+ $this->time = wfTimestamp();
+ }
+
+ $message = self::doAction(
+ $action,
+ $group,
+ $key,
+ $code,
+ $value
+ );
+
+ $key = array_shift( $message );
+ $params = $message;
+ $message = $context->msg( $key, $params )->parse();
+ $changed[] = "<li>$message</li>";
+
+ if ( $this->checkProcessTime() ) {
+ $process = false;
+ $message = $context->msg( 'translate-manage-toolong' )
+ ->numParams( $this->processingTime )->parse();
+ $changed[] = "<li>$message</li></ul>";
+ }
+ continue;
+ }
+ }
+
+ $alldone = false;
+
+ $actions = $this->getActions();
+ $defaction = $this->getDefaultAction( $fuzzy, $action );
+
+ $act = array();
+
+ // Give grep a chance to find the usages:
+ // translate-manage-action-import, translate-manage-action-conflict,
+ // translate-manage-action-ignore, translate-manage-action-fuzzy
+ foreach ( $actions as $action ) {
+ $label = $context->msg( "translate-manage-action-$action" )->text();
+ $name = self::escapeNameForPHP( "action-$type-$key" );
+ $id = Sanitizer::escapeId( "action-$key-$action" );
+ $act[] = Xml::radioLabel( $label, $name, $action, $id, $action === $defaction );
+ }
+
+ $param = '<code class="mw-tmi-diff">' . htmlspecialchars( $key ) . '</code>';
+ $name = $context->msg( 'translate-manage-import-diff', $param,
+ implode( ' ', $act )
+ )->text();
+
+ $changed[] = self::makeSectionElement( $name, $type, $text );
+ }
+ }
+
+ if ( !$process ) {
+ $collection->filter( 'hastranslation', false );
+ $keys = $collection->getMessageKeys();
+
+ $diff = array_diff( $keys, array_keys( $messages ) );
+
+ foreach ( $diff as $s ) {
+ $para = '<code class="mw-tmi-deleted">' . htmlspecialchars( $s ) . '</code>';
+ $name = $context->msg( 'translate-manage-import-deleted' )->rawParams( $para )->escaped();
+ $text = TranslateUtils::convertWhiteSpaceToHTML( $collection[$s]->translation() );
+ $changed[] = self::makeSectionElement( $name, 'deleted', $text );
+ }
+ }
+
+ if ( $process || ( !count( $changed ) && $code !== 'en' ) ) {
+ if ( !count( $changed ) ) {
+ $this->out->addWikiMsg( 'translate-manage-nochanges-other' );
+ }
+
+ if ( !count( $changed ) || strpos( $changed[count( $changed ) - 1], '<li>' ) !== 0 ) {
+ $changed[] = '<ul>';
+ }
+
+ $message = $context->msg( 'translate-manage-import-done' )->parse();
+ $changed[] = "<li>$message</li></ul>";
+ $this->out->addHTML( implode( "\n", $changed ) );
+ } else {
+ // END
+ if ( count( $changed ) ) {
+ if ( $code === 'en' ) {
+ $this->out->addWikiMsg( 'translate-manage-intro-en' );
+ } else {
+ $lang = TranslateUtils::getLanguageName(
+ $code,
+ $context->getLanguage()->getCode()
+ );
+ $this->out->addWikiMsg( 'translate-manage-intro-other', $lang );
+ }
+ $this->out->addHTML( Html::hidden( 'language', $code ) );
+ $this->out->addHTML( implode( "\n", $changed ) );
+ $this->out->addHTML( Xml::submitButton( $context->msg( 'translate-manage-submit' )->text() ) );
+ } else {
+ $this->out->addWikiMsg( 'translate-manage-nochanges' );
+ }
+ }
+
+ $this->out->addHTML( $this->doFooter() );
+
+ return $alldone;
+ }
+
+ /**
+ * Perform an action on a given group/key/code
+ *
+ * @param string $action Options: 'import', 'conflict' or 'ignore'
+ * @param MessageGroup $group Group object
+ * @param string $key Message key
+ * @param string $code Language code
+ * @param string $message Contents for the $key/code combination
+ * @param string $comment Edit summary (default: empty) - see Article::doEdit
+ * @param User $user User that will make the edit (default: null - RequestContext user).
+ * See Article::doEdit.
+ * @param int $editFlags Integer bitfield: see Article::doEdit
+ * @throws MWException
+ * @return string Action result
+ */
+ public static function doAction( $action, $group, $key, $code, $message, $comment = '',
+ $user = null, $editFlags = 0
+ ) {
+ global $wgTranslateDocumentationLanguageCode;
+
+ $title = self::makeTranslationTitle( $group, $key, $code );
+
+ if ( $action === 'import' || $action === 'conflict' ) {
+ if ( $action === 'import' ) {
+ $comment = wfMessage( 'translate-manage-import-summary' )->inContentLanguage()->plain();
+ } else {
+ $comment = wfMessage( 'translate-manage-conflict-summary' )->inContentLanguage()->plain();
+ $message = self::makeTextFuzzy( $message );
+ }
+
+ return self::doImport( $title, $message, $comment, $user, $editFlags );
+ } elseif ( $action === 'ignore' ) {
+ return array( 'translate-manage-import-ignore', $key );
+ } elseif ( $action === 'fuzzy' && $code !== 'en' &&
+ $code !== $wgTranslateDocumentationLanguageCode
+ ) {
+ $message = self::makeTextFuzzy( $message );
+
+ return self::doImport( $title, $message, $comment, $user, $editFlags );
+ } elseif ( $action === 'fuzzy' && $code == 'en' ) {
+ return self::doFuzzy( $title, $message, $comment, $user, $editFlags );
+ } else {
+ throw new MWException( "Unhandled action $action" );
+ }
+ }
+
+ protected function checkProcessTime() {
+ return wfTimestamp() - $this->time >= $this->processingTime;
+ }
+
+ /**
+ * @throws MWException
+ * @param Title $title
+ * @param $message
+ * @param $summary
+ * @param User $user
+ * @param $editFlags
+ * @return array
+ */
+ public static function doImport( $title, $message, $summary, $user = null, $editFlags = 0 ) {
+ $wikiPage = WikiPage::factory( $title );
+ $content = ContentHandler::makeContent( $message, $title );
+ $status = $wikiPage->doEditContent( $content, $summary, $editFlags, false, $user );
+ $success = $status->isOK();
+
+ if ( $success ) {
+ return array( 'translate-manage-import-ok',
+ wfEscapeWikiText( $title->getPrefixedText() )
+ );
+ } else {
+ $text = "Failed to import new version of page {$title->getPrefixedText()}\n";
+ $text .= "{$status->getWikiText()}";
+ throw new MWException( $text );
+ }
+ }
+
+ /**
+ * @param Title $title
+ * @param $message
+ * @param $comment
+ * @param $user
+ * @param int $editFlags
+ * @return array|String
+ */
+ public static function doFuzzy( $title, $message, $comment, $user, $editFlags = 0 ) {
+ $context = RequestContext::getMain();
+
+ if ( !$context->getUser()->isAllowed( 'translate-manage' ) ) {
+ return $context->msg( 'badaccess-group0' )->text();
+ }
+
+ $dbw = wfGetDB( DB_MASTER );
+
+ // Work on all subpages of base title.
+ $handle = new MessageHandle( $title );
+ $titleText = $handle->getKey();
+
+ $conds = array(
+ 'page_namespace' => $title->getNamespace(),
+ 'page_latest=rev_id',
+ 'rev_text_id=old_id',
+ 'page_title' . $dbw->buildLike( "$titleText/", $dbw->anyString() ),
+ );
+
+ $rows = $dbw->select(
+ array( 'page', 'revision', 'text' ),
+ array( 'page_title', 'page_namespace', 'old_text', 'old_flags' ),
+ $conds,
+ __METHOD__
+ );
+
+ // Edit with fuzzybot if there is no user.
+ if ( !$user ) {
+ $user = FuzzyBot::getUser();
+ }
+
+ // Process all rows.
+ $changed = array();
+ foreach ( $rows as $row ) {
+ global $wgTranslateDocumentationLanguageCode;
+
+ $ttitle = Title::makeTitle( $row->page_namespace, $row->page_title );
+
+ // No fuzzy for English original or documentation language code.
+ if ( $ttitle->getSubpageText() === 'en' ||
+ $ttitle->getSubpageText() === $wgTranslateDocumentationLanguageCode
+ ) {
+ // Use imported text, not database text.
+ $text = $message;
+ } else {
+ $text = Revision::getRevisionText( $row );
+ $text = self::makeTextFuzzy( $text );
+ }
+
+ // Do actual import
+ $changed[] = self::doImport(
+ $ttitle,
+ $text,
+ $comment,
+ $user,
+ $editFlags
+ );
+ }
+
+ // Format return text
+ $text = '';
+ foreach ( $changed as $c ) {
+ $key = array_shift( $c );
+ $text .= "* " . $context->msg( $key, $c )->plain() . "\n";
+ }
+
+ return array( 'translate-manage-import-fuzzy', "\n" . $text );
+ }
+
+ /**
+ * Given a group, message key and language code, creates a title for the
+ * translation page.
+ *
+ * @param MessageGroup $group
+ * @param string $key Message key
+ * @param string $code Language code
+ * @return Title
+ */
+ public static function makeTranslationTitle( $group, $key, $code ) {
+ $ns = $group->getNamespace();
+
+ return Title::makeTitleSafe( $ns, "$key/$code" );
+ }
+
+ /**
+ * Make section elements.
+ *
+ * @param string $legend Legend as raw html.
+ * @param string $type Contents of type class.
+ * @param string $content Contents as raw html.
+ * @param Language $lang The language in which the text is written.
+ * @return string Section element as html.
+ */
+ public static function makeSectionElement( $legend, $type, $content, $lang = null ) {
+ $containerParams = array( 'class' => "mw-tpt-sp-section mw-tpt-sp-section-type-{$type}" );
+ $legendParams = array( 'class' => 'mw-tpt-sp-legend' );
+ $contentParams = array( 'class' => 'mw-tpt-sp-content' );
+ if ( $lang ) {
+ $contentParams['dir'] = wfGetLangObj( $lang )->getDir();
+ $contentParams['lang'] = wfGetLangObj( $lang )->getCode();
+ }
+
+ $output = Html::rawElement( 'div', $containerParams,
+ Html::rawElement( 'div', $legendParams, $legend ) .
+ Html::rawElement( 'div', $contentParams, $content )
+ );
+
+ return $output;
+ }
+
+ /**
+ * Prepends translation with fuzzy tag and ensures there is only one of them.
+ *
+ * @param string $message Message content
+ * @return string Message prefixed with TRANSLATE_FUZZY tag
+ */
+ public static function makeTextFuzzy( $message ) {
+ $message = str_replace( TRANSLATE_FUZZY, '', $message );
+
+ return TRANSLATE_FUZZY . $message;
+ }
+
+ /**
+ * Escape name such that it validates as name and id parameter in html, and
+ * so that we can get it back with WebRequest::getVal(). Especially dot and
+ * spaces are difficult for the latter.
+ * @param string $name
+ * @return string
+ */
+ public static function escapeNameForPHP( $name ) {
+ $replacements = array(
+ "(" => '(OP)',
+ " " => '(SP)',
+ "\t" => '(TAB)',
+ "." => '(DOT)',
+ "'" => '(SQ)',
+ "\"" => '(DQ)',
+ "%" => '(PC)',
+ "&" => '(AMP)',
+ );
+
+ /* How nice of you PHP. No way to split array into keys and values in one
+ * function or have str_replace which takes one array? */
+
+ return str_replace( array_keys( $replacements ), array_values( $replacements ), $name );
+ }
+}
diff --git a/MLEB/Translate/utils/RcFilter.php b/MLEB/Translate/utils/RcFilter.php
new file mode 100644
index 00000000..8dbda197
--- /dev/null
+++ b/MLEB/Translate/utils/RcFilter.php
@@ -0,0 +1,91 @@
+<?php
+/**
+ * Contains class with filter to Special:RecentChanges to enable additional
+ * filtering.
+ *
+ * @file
+ * @author Niklas Laxström
+ * @copyright Copyright © 2010, Niklas Laxström
+ * @license GPL-2.0+
+ */
+
+/**
+ * Adds a new filter to Special:RecentChanges which makes it possible to filter
+ * translations away or show them only.
+ */
+class TranslateRcFilter {
+ /**
+ * Hooks SpecialRecentChangesQuery. See the hook documentation for
+ * documentation of the function parameters.
+ *
+ * Appends SQL filter conditions into $conds.
+ * @param array $conds
+ * @param array|string $tables
+ * @param array $join_conds
+ * @param FormOptions $opts
+ * @return bool true
+ */
+ public static function translationFilter( &$conds, &$tables, &$join_conds, $opts ) {
+ global $wgTranslateMessageNamespaces, $wgTranslateRcFilterDefault;
+
+ $request = RequestContext::getMain()->getRequest();
+ $translations = $request->getVal( 'translations', $wgTranslateRcFilterDefault );
+ $opts->add( 'translations', $wgTranslateRcFilterDefault );
+ $opts->setValue( 'translations', $translations );
+
+ $dbr = wfGetDB( DB_SLAVE );
+
+ $namespaces = array();
+
+ foreach ( $wgTranslateMessageNamespaces as $index ) {
+ $namespaces[] = $index;
+ $namespaces[] = $index + 1; // Talk too
+ }
+
+ if ( $translations === 'only' ) {
+ $conds[] = 'rc_namespace IN (' . $dbr->makeList( $namespaces ) . ')';
+ $conds[] = 'rc_title like \'%%/%%\'';
+ } elseif ( $translations === 'filter' ) {
+ $conds[] = 'rc_namespace NOT IN (' . $dbr->makeList( $namespaces ) . ')';
+ } elseif ( $translations === 'site' ) {
+ $conds[] = 'rc_namespace IN (' . $dbr->makeList( $namespaces ) . ')';
+ $conds[] = 'rc_title not like \'%%/%%\'';
+ }
+
+ return true;
+ }
+
+ /**
+ * Hooks SpecialRecentChangesPanel. See the hook documentation for
+ * documentation of the function parameters.
+ *
+ * Adds a HTMl selector into $items
+ * @param $items
+ * @param FormOptions $opts
+ * @return bool true
+ */
+ public static function translationFilterForm( &$items, $opts ) {
+ $opts->consumeValue( 'translations' );
+ $default = $opts->getValue( 'translations' );
+
+ $label = Xml::label(
+ wfMessage( 'translate-rc-translation-filter' )->text(),
+ 'mw-translation-filter'
+ );
+ $select = new XmlSelect( 'translations', 'mw-translation-filter', $default );
+ $select->addOption(
+ wfMessage( 'translate-rc-translation-filter-no' )->text(),
+ 'noaction'
+ );
+ $select->addOption( wfMessage( 'translate-rc-translation-filter-only' )->text(), 'only' );
+ $select->addOption(
+ wfMessage( 'translate-rc-translation-filter-filter' )->text(),
+ 'filter'
+ );
+ $select->addOption( wfMessage( 'translate-rc-translation-filter-site' )->text(), 'site' );
+
+ $items['translations'] = array( $label, $select->getHTML() );
+
+ return true;
+ }
+}
diff --git a/MLEB/Translate/utils/ResourceLoader.php b/MLEB/Translate/utils/ResourceLoader.php
new file mode 100644
index 00000000..9c9e1716
--- /dev/null
+++ b/MLEB/Translate/utils/ResourceLoader.php
@@ -0,0 +1,29 @@
+<?php
+/**
+ * Stuff for handling configuration files in PHP format.
+ * @file
+ * @author Niklas Laxström
+ * @copyright Copyright © 2010 Niklas Laxström
+ * @license GPL-2.0+
+ */
+
+/**
+ * Stuff for handling configuration files in PHP format.
+ */
+class PHPVariableLoader {
+ /**
+ * Returns a global variable from PHP file by executing the file.
+ * @param $_filename \string Path to the file.
+ * @param $_variable \string Name of the variable.
+ * @return \mixed The variable contents or null.
+ */
+ public static function loadVariableFromPHPFile( $_filename, $_variable ) {
+ if ( !file_exists( $_filename ) ) {
+ return null;
+ } else {
+ require $_filename;
+
+ return isset( $$_variable ) ? $$_variable : null;
+ }
+ }
+}
diff --git a/MLEB/Translate/utils/RevTag.php b/MLEB/Translate/utils/RevTag.php
new file mode 100644
index 00000000..c918e578
--- /dev/null
+++ b/MLEB/Translate/utils/RevTag.php
@@ -0,0 +1,102 @@
+<?php
+/**
+ * Code related to revtag database table
+ *
+ * @file
+ * @author Niklas Laxström
+ * @copyright Copyright © 2011 Niklas Laxström
+ * @license GPL-2.0+
+ */
+
+/**
+ * Abstraction for revtag table to handle new and old schemas during migration.
+ */
+class RevTag {
+ protected static $schema = false;
+
+ /**
+ * Determines the schema version.
+ *
+ * @return int
+ */
+ public static function checkSchema() {
+ if ( self::$schema !== false ) {
+ return self::$schema;
+ } else {
+ $dbr = wfGetDB( DB_SLAVE );
+ if ( $dbr->tableExists( 'revtag_type' ) ) {
+ return self::$schema = 1;
+ } else {
+ return self::$schema = 2;
+ }
+ }
+ }
+
+ /**
+ * Returns value suitable for rt_type field.
+ * @param string $tag Tag name
+ * @throws MWException
+ * @return int|string
+ */
+ public static function getType( $tag ) {
+ if ( self::checkSchema() === 2 ) {
+ return $tag;
+ }
+
+ $tags = self::loadTags();
+
+ if ( isset( $tags[$tag] ) ) {
+ return $tags[$tag];
+ } else {
+ $text = "Unknown revtag $tag. Known are " . implode( ', ', array_keys( $tags ) );
+ throw new MWException( $text );
+ }
+ }
+
+ /**
+ * Converts rt_type field back to the tag name.
+ * @param $tag int rt_type value
+ * @throws MWException
+ * @return string
+ */
+ public static function typeToTag( $tag ) {
+ if ( self::checkSchema() === 2 ) {
+ return $tag;
+ }
+
+ $tags = self::loadTags();
+ $tags = array_flip( $tags );
+
+ if ( isset( $tags[$tag] ) ) {
+ return $tags[$tag];
+ } else {
+ $text = "Unknown revtag type $tag. Known are " . implode( ', ', array_keys( $tags ) );
+ throw new MWException( $text );
+ }
+ }
+
+ /**
+ * Loads the list of tags from database using the old schema
+ * @return array tag names => tag id
+ */
+ protected static function loadTags() {
+ static $tags = null;
+ if ( $tags === null ) {
+ $tags = array();
+
+ $dbr = wfGetDB( DB_SLAVE );
+ $res = $dbr->select(
+ 'revtag_type',
+ array( 'rtt_name', 'rtt_id' ),
+ array(),
+ __METHOD__
+ );
+
+ foreach ( $res as $row ) {
+ $tags[$row->rtt_name] = $row->rtt_id;
+ }
+ }
+
+ return $tags;
+ }
+}
diff --git a/MLEB/Translate/utils/StatsBar.php b/MLEB/Translate/utils/StatsBar.php
new file mode 100644
index 00000000..39bd66a0
--- /dev/null
+++ b/MLEB/Translate/utils/StatsBar.php
@@ -0,0 +1,88 @@
+<?php
+/**
+ * Compact stats.
+ *
+ * @file
+ * @author Niklas Laxström
+ * @copyright Copyright © 2012-2013 Niklas Laxström
+ * @license GPL-2.0+
+ */
+
+/**
+ * Compact, colorful stats.
+ * @since 2012-11-30
+ */
+class StatsBar {
+ /**
+ * @see MessageGroupStats
+ * @var array
+ */
+ protected $stats;
+
+ /// @var string Message group id
+ protected $group;
+
+ /// @var string Language
+ protected $language;
+
+ public static function getNew( $group, $language, array $stats = null ) {
+ $self = new self();
+ $self->group = $group;
+ $self->language = $language;
+
+ if ( is_array( $stats ) ) {
+ $self->stats = $stats;
+ } else {
+ $self->stats = MessageGroupStats::forItem( $group, $language );
+ }
+
+ return $self;
+ }
+
+ public function getHtml( IContextSource $context ) {
+ $context->getOutput()->addModules( 'ext.translate.statsbar' );
+
+ $total = $this->stats[MessageGroupStats::TOTAL];
+ $proofread = $this->stats[MessageGroupStats::PROOFREAD];
+ $translated = $this->stats[MessageGroupStats::TRANSLATED];
+ $fuzzy = $this->stats[MessageGroupStats::FUZZY];
+
+ if ( !$total ) {
+ $untranslated = null;
+ $wproofread = $wtranslated = $wfuzzy = $wuntranslated = 0;
+ } else {
+ // Proofread is subset of translated
+ $untranslated = $total - $translated - $fuzzy;
+
+ $wproofread = round( 100 * $proofread / $total, 2 );
+ $wtranslated = round( 100 * ( $translated - $proofread ) / $total, 2 );
+ $wfuzzy = round( 100 * $fuzzy / $total, 2 );
+ $wuntranslated = round( 100 - $wproofread - $wtranslated - $wfuzzy, 2 );
+ }
+
+ return Html::rawElement( 'div', array(
+ 'class' => 'tux-statsbar',
+ 'data-total' => $total,
+ 'data-group' => $this->group,
+ 'data-language' => $this->language,
+ ),
+ Html::element( 'span', array(
+ 'class' => 'tux-proofread',
+ 'style' => "width: $wproofread%",
+ 'data-proofread' => $proofread,
+ ) ) . Html::element( 'span', array(
+ 'class' => 'tux-translated',
+ 'style' => "width: $wtranslated%",
+ 'data-translated' => $translated,
+ ) ) . Html::element( 'span', array(
+ 'class' => 'tux-fuzzy',
+ 'style' => "width: $wfuzzy%",
+ 'data-fuzzy' => $fuzzy,
+ ) ) . Html::element( 'span', array(
+ 'class' => 'tux-untranslated',
+ 'style' => "width: $wuntranslated%",
+ 'data-untranslated' => $untranslated,
+ ) )
+ );
+ }
+}
diff --git a/MLEB/Translate/utils/StatsTable.php b/MLEB/Translate/utils/StatsTable.php
new file mode 100644
index 00000000..52fd4cae
--- /dev/null
+++ b/MLEB/Translate/utils/StatsTable.php
@@ -0,0 +1,344 @@
+<?php
+/**
+ * Contains logic for special page Special:LanguageStats.
+ *
+ * @file
+ * @author Siebrand Mazeland
+ * @author Niklas Laxström
+ * @copyright Copyright © 2008-2013 Siebrand Mazeland, Niklas Laxström
+ * @license GPL-2.0+
+ */
+
+/**
+ * Implements includable special page Special:LanguageStats which provides
+ * translation statistics for all defined message groups.
+ *
+ * Loosely based on the statistics code in phase3/maintenance/language
+ *
+ * Use {{Special:LanguageStats/nl/1}} to show for 'nl' and suppres completely
+ * translated groups.
+ *
+ * @ingroup Stats
+ */
+class StatsTable {
+ /// @var Language
+ protected $lang;
+ /// @var SpecialPage
+ protected $translate;
+ /// @var string
+ protected $mainColumnHeader;
+ /// @var array
+ protected $extraColumns = array();
+
+ public function __construct() {
+ $this->lang = RequestContext::getMain()->getLanguage();
+ $this->translate = SpecialPage::getTitleFor( 'Translate' );
+ }
+
+ /**
+ * Statistics table element (heading or regular cell)
+ *
+ * @param $in \string Element contents.
+ * @param $bgcolor \string Backround color in ABABAB format.
+ * @param $sort \string Value used for sorting.
+ * @return \string Html td element.
+ */
+ public function element( $in, $bgcolor = '', $sort = '' ) {
+ $attributes = array();
+
+ if ( $sort ) {
+ $attributes['data-sort-value'] = $sort;
+ }
+
+ if ( $bgcolor ) {
+ $attributes['style'] = "background-color: #" . $bgcolor;
+ $attributes['class'] = 'hover-color';
+ }
+
+ $element = Html::element( 'td', $attributes, $in );
+
+ return $element;
+ }
+
+ public function getBackgroundColor( $subset, $total, $fuzzy = false ) {
+ wfSuppressWarnings();
+ $v = round( 255 * $subset / $total );
+ wfRestoreWarnings();
+
+ if ( $fuzzy ) {
+ // Weigh fuzzy with factor 20.
+ $v = $v * 20;
+
+ if ( $v > 255 ) {
+ $v = 255;
+ }
+
+ $v = 255 - $v;
+ }
+
+ if ( $v < 128 ) {
+ // Red to Yellow
+ $red = 'FF';
+ $green = sprintf( '%02X', 2 * $v );
+ } else {
+ // Yellow to Green
+ $red = sprintf( '%02X', 2 * ( 255 - $v ) );
+ $green = 'FF';
+ }
+ $blue = '00';
+
+ return $red . $green . $blue;
+ }
+
+ public function getMainColumnHeader() {
+ return $this->mainColumnHeader;
+ }
+
+ public function setMainColumnHeader( Message $msg ) {
+ $this->mainColumnHeader = $this->createColumnHeader( $msg );
+ }
+
+ public function createColumnHeader( Message $msg ) {
+ return Html::element( 'th', array(), $msg->text() );
+ }
+
+ public function addExtraColumn( Message $column ) {
+ $this->extraColumns[] = $column;
+ }
+
+ public function getOtherColumnHeaders() {
+ return array_merge( array(
+ wfMessage( 'translate-total' ),
+ wfMessage( 'translate-untranslated' ),
+ wfMessage( 'translate-percentage-complete' ),
+ wfMessage( 'translate-percentage-fuzzy' ),
+ ), $this->extraColumns );
+ }
+
+ public function createHeader() {
+ // Create table header
+ $out = Html::openElement(
+ 'table',
+ array( 'class' => "statstable wikitable mw-sp-translate-table" )
+ );
+
+ $out .= "\n\t" . Html::openElement( 'thead' );
+ $out .= "\n\t" . Html::openElement( 'tr' );
+
+ $out .= "\n\t\t" . $this->getMainColumnHeader();
+ foreach ( $this->getOtherColumnHeaders() as $label ) {
+ $out .= "\n\t\t" . $this->createColumnHeader( $label );
+ }
+ $out .= "\n\t" . Html::closeElement( 'tr' );
+ $out .= "\n\t" . Html::closeElement( 'thead' );
+ $out .= "\n\t" . Html::openElement( 'tbody' );
+
+ return $out;
+ }
+
+ /**
+ * Makes a row with aggregate numbers.
+ * @param Message $message
+ * @param array $stats ( total, translate, fuzzy )
+ * @return string Html
+ */
+ public function makeTotalRow( Message $message, $stats ) {
+ $out = "\t" . Html::openElement( 'tr' );
+ $out .= "\n\t\t" . Html::element( 'td', array(), $message->text() );
+ $out .= $this->makeNumberColumns( $stats );
+ $out .= "\n\t" . Xml::closeElement( 'tr' ) . "\n";
+
+ return $out;
+ }
+
+ /**
+ * Makes partial row from completion numbers
+ * @param array $stats
+ * @return string Html
+ */
+ public function makeNumberColumns( $stats ) {
+ $total = $stats[MessageGroupStats::TOTAL];
+ $translated = $stats[MessageGroupStats::TRANSLATED];
+ $fuzzy = $stats[MessageGroupStats::FUZZY];
+
+ if ( $total === null ) {
+ $na = "\n\t\t" . Html::element( 'td', array( 'data-sort-value' => -1 ), '...' );
+ $nap = "\n\t\t" . $this->element( '...', 'AFAFAF', -1 );
+ $out = $na . $na . $nap . $nap;
+
+ return $out;
+ }
+
+ $out = "\n\t\t" . Html::element( 'td',
+ array( 'data-sort-value' => $total ),
+ $this->lang->formatNum( $total ) );
+
+ $out .= "\n\t\t" . Html::element( 'td',
+ array( 'data-sort-value' => $total - $translated ),
+ $this->lang->formatNum( $total - $translated ) );
+
+ if ( $total === 0 ) {
+ $transRatio = 0;
+ $fuzzyRatio = 0;
+ } else {
+ $transRatio = $translated / $total;
+ $fuzzyRatio = $fuzzy / $total;
+ }
+
+ $out .= "\n\t\t" . $this->element( $this->formatPercentage( $transRatio, 'floor' ),
+ $this->getBackgroundColor( $translated, $total ),
+ sprintf( '%1.5f', $transRatio ) );
+
+ $out .= "\n\t\t" . $this->element( $this->formatPercentage( $fuzzyRatio, 'ceil' ),
+ $this->getBackgroundColor( $fuzzy, $total, true ),
+ sprintf( '%1.5f', $fuzzyRatio ) );
+
+ return $out;
+ }
+
+ /**
+ * Makes a nice print from plain float.
+ * @param $num float
+ * @param $to string floor or ceil
+ * @return string Plain text
+ */
+ public function formatPercentage( $num, $to = 'floor' ) {
+ $num = $to === 'floor' ? floor( 100 * $num ) : ceil( 100 * $num );
+ $fmt = $this->lang->formatNum( $num );
+
+ return wfMessage( 'percent', $fmt )->text();
+ }
+
+ /**
+ * Gets the name of group with some extra formatting.
+ * @param $group MessageGroup
+ * @return string Html
+ */
+ public function getGroupLabel( MessageGroup $group ) {
+ $groupLabel = htmlspecialchars( $group->getLabel() );
+
+ // Bold for meta groups.
+ if ( $group->isMeta() ) {
+ $groupLabel = Html::rawElement( 'b', array(), $groupLabel );
+ }
+
+ return $groupLabel;
+ }
+
+ /**
+ * Gets the name of group linked to translation tool.
+ * @param $group MessageGroup
+ * @param $code string Language code
+ * @param $params array Any extra query parameters.
+ * @return string Html
+ */
+ public function makeGroupLink( MessageGroup $group, $code, $params ) {
+ $queryParameters = $params + array(
+ 'group' => $group->getId(),
+ 'language' => $code
+ );
+
+ $attributes = array(
+ 'title' => $this->getGroupDescription( $group )
+ );
+
+ $translateGroupLink = Linker::link(
+ $this->translate, $this->getGroupLabel( $group ), $attributes, $queryParameters
+ );
+
+ return $translateGroupLink;
+ }
+
+ /**
+ * Gets the description of a group. This is a bit slow thing to do for
+ * thousand+ groups, so some caching is involved.
+ * @param $group MessageGroup
+ * @return string Plain text
+ */
+ public function getGroupDescription( MessageGroup $group ) {
+ $code = $this->lang->getCode();
+
+ $cache = wfGetCache( CACHE_ANYTHING );
+ $key = wfMemckey( "translate-groupdesc-$code-" . $group->getId() );
+ $desc = $cache->get( $key );
+
+ if ( is_string( $desc ) ) {
+ return $desc;
+ }
+
+ $realFunction = array( 'MessageCache', 'singleton' );
+
+ if ( is_callable( $realFunction ) ) {
+ $mc = MessageCache::singleton();
+ } else {
+ global $wgMessageCache;
+
+ $mc = $wgMessageCache;
+ }
+
+ $desc = $mc->transform( $group->getDescription(), true, $this->lang );
+ $cache->set( $key, $desc );
+
+ return $desc;
+ }
+
+ /**
+ * Check whether translations in given group in given language
+ * has been disabled.
+ * @param $groupId string Message group id
+ * @param $code string Language code
+ * @return bool
+ */
+ public function isBlacklisted( $groupId, $code ) {
+ global $wgTranslateBlacklist;
+
+ $blacklisted = null;
+
+ $checks = array(
+ $groupId,
+ strtok( $groupId, '-' ),
+ '*'
+ );
+
+ foreach ( $checks as $check ) {
+ if ( isset( $wgTranslateBlacklist[$check] ) && isset( $wgTranslateBlacklist[$check][$code] ) ) {
+ $blacklisted = $wgTranslateBlacklist[$check][$code];
+ }
+
+ if ( $blacklisted !== null ) {
+ break;
+ }
+ }
+
+ $group = MessageGroups::getGroup( $groupId );
+ $languages = $group->getTranslatableLanguages();
+ if ( $languages !== null && !isset( $languages[$code] ) ) {
+ $blacklisted = true;
+ }
+
+ $include = wfRunHooks( 'Translate:MessageGroupStats:isIncluded', array( $groupId, $code ) );
+ if ( !$include ) {
+ $blacklisted = true;
+ }
+
+ return $blacklisted;
+ }
+
+ /**
+ * Used to circumvent ugly tooltips when newlines are used in the
+ * message content ("x\ny" becomes "x y").
+ * @param $text
+ * @return string
+ */
+ public static function formatTooltip( $text ) {
+ $wordSeparator = wfMessage( 'word-separator' )->text();
+
+ $text = strtr( $text, array(
+ "\n" => $wordSeparator,
+ "\r" => $wordSeparator,
+ "\t" => $wordSeparator,
+ ) );
+
+ return $text;
+ }
+}
diff --git a/MLEB/Translate/utils/ToolBox.php b/MLEB/Translate/utils/ToolBox.php
new file mode 100644
index 00000000..ba04013e
--- /dev/null
+++ b/MLEB/Translate/utils/ToolBox.php
@@ -0,0 +1,41 @@
+<?php
+/**
+ * Classes for adding extension specific toolbox menu items.
+ *
+ * @file
+ * @author Siebrand Mazeland
+ * @author Niklas Laxström
+ * @copyright Copyright © 2008-2010, Siebrand Mazeland, Niklas Laxström
+ * @license GPL-2.0+
+ */
+
+/**
+ * Adds extension specific context aware toolbox menu items.
+ */
+class TranslateToolbox {
+ /**
+ * Adds link in toolbox to Special:Prefixindex to show all other
+ * available translations for a message. Only shown when it
+ * actually is a translatable/translated message.
+ *
+ * @param $quickTemplate QuickTemplate
+ *
+ * @return bool
+ */
+ static function toolboxAllTranslations( &$quickTemplate ) {
+ $title = $quickTemplate->getSkin()->getTitle();
+ $handle = new MessageHandle( $title );
+ if ( $handle->isValid() ) {
+ $message = $title->getNsText() . ':' . $handle->getKey();
+ $desc = wfMessage( 'translate-sidebar-alltrans' )->text();
+ $url = htmlspecialchars( SpecialPage::getTitleFor( 'Translations' )
+ ->getLocalURL( array ('message' => $message ) ) );
+
+ // Add the actual toolbox entry.
+ // Add newlines and tabs for nicer HTML output.
+ echo "\n\t\t\t\t<li id=\"t-alltrans\"><a href=\"$url\">$desc</a></li>\n";
+ }
+
+ return true;
+ }
+}
diff --git a/MLEB/Translate/utils/TranslateLogFormatter.php b/MLEB/Translate/utils/TranslateLogFormatter.php
new file mode 100644
index 00000000..019d6c5e
--- /dev/null
+++ b/MLEB/Translate/utils/TranslateLogFormatter.php
@@ -0,0 +1,79 @@
+<?php
+/**
+ * Class for formatting Translate logs.
+ *
+ * @file
+ * @author Niklas Laxström
+ * @copyright Copyright © 2013, Niklas Laxström
+ * @license GPL-2.0+
+ */
+
+/**
+ * Class for formatting Translate logs.
+ */
+class TranslateLogFormatter extends LogFormatter {
+
+ public function getMessageParameters() {
+ $params = parent::getMessageParameters();
+
+ $type = $this->entry->getFullType();
+
+ if ( $type === 'translationreview/message' ) {
+ $targetPage = $this->makePageLink(
+ $this->entry->getTarget(),
+ array( 'oldid' => $params[3] )
+ );
+
+ $params[2] = Message::rawParam( $targetPage );
+ } elseif ( $type === 'translationreview/group' ) {
+ /*
+ * - 3: language code
+ * - 4: label of the message group
+ * - 5: old state
+ * - 6: new state
+ */
+
+ $uiLanguage = $this->context->getLanguage();
+ $language = $params[3];
+
+ $targetPage = $this->makePageLinkWithText(
+ $this->entry->getTarget(),
+ $params[4],
+ array( 'language' => $language )
+ );
+
+ $params[2] = Message::rawParam( $targetPage );
+ $params[3] = TranslateUtils::getLanguageName( $language, $uiLanguage->getCode() );
+ $params[5] = $this->formatStateMessage( $params[5] );
+ $params[6] = $this->formatStateMessage( $params[6] );
+ } elseif ( $type === 'translatorsandbox/rejected' ) {
+ // No point linking to the user page which cannot have existed
+ $params[2] = $this->entry->getTarget()->getText();
+ } elseif ( $type === 'translatorsandbox/promoted' ) {
+ // Gender for the target
+ $params[3] = User::newFromId( $params[3] )->getName();
+ }
+
+ return $params;
+ }
+
+ protected function formatStateMessage( $value ) {
+ $message = $this->msg( "translate-workflow-state-$value" );
+
+ return $message->isBlank() ? $value : $message->text();
+ }
+
+ protected function makePageLinkWithText( Title $title = null, $text, $parameters = array() ) {
+ if ( !$this->plaintext ) {
+ $link = Linker::link( $title, htmlspecialchars( $text ), array(), $parameters );
+ } else {
+ $target = '***';
+ if ( $title instanceof Title ) {
+ $target = $title->getPrefixedText();
+ }
+ $link = "[[$target|$text]]";
+ }
+
+ return $link;
+ }
+}
diff --git a/MLEB/Translate/utils/TranslateMetadata.php b/MLEB/Translate/utils/TranslateMetadata.php
new file mode 100644
index 00000000..540128ce
--- /dev/null
+++ b/MLEB/Translate/utils/TranslateMetadata.php
@@ -0,0 +1,110 @@
+<?php
+/**
+ * Contains class which offers functionality for reading and updating Translate group
+ * related metadata
+ *
+ * @file
+ * @author Niklas Laxström
+ * @author Santhosh Thottingal
+ * @copyright Copyright © 2012-2013, Niklas Laxström, Santhosh Thottingal
+ * @license GPL-2.0+
+ */
+
+class TranslateMetadata {
+ protected static $cache = null;
+
+ /**
+ * Get a metadata value for the given group and key.
+ * @param $group string The group name
+ * @param $key string Metadata key
+ * @return String
+ */
+ public static function get( $group, $key ) {
+ if ( self::$cache === null ) {
+ $dbr = wfGetDB( DB_SLAVE );
+ $res = $dbr->select( 'translate_metadata', '*', array(), __METHOD__ );
+ foreach ( $res as $row ) {
+ self::$cache[$row->tmd_group][$row->tmd_key] = $row->tmd_value;
+ }
+ }
+
+ if ( isset( self::$cache[$group][$key] ) ) {
+ return self::$cache[$group][$key];
+ }
+
+ return false;
+ }
+
+ /**
+ * Set a metadata value for the given group and metadata key. Updates the
+ * value if already existing.
+ * @param $group string The group id
+ * @param $key string Metadata key
+ * @param $value string Metadata value
+ */
+ public static function set( $group, $key, $value ) {
+ $dbw = wfGetDB( DB_MASTER );
+ $data = array( 'tmd_group' => $group, 'tmd_key' => $key, 'tmd_value' => $value );
+ if ( $value === false ) {
+ unset( $data['tmd_value'] );
+ $dbw->delete( 'translate_metadata', $data );
+ } else {
+ $dbw->replace(
+ 'translate_metadata',
+ array( array( 'tmd_group', 'tmd_key' ) ),
+ $data,
+ __METHOD__
+ );
+ }
+
+ self::$cache = null;
+ }
+
+ /**
+ * Wrapper for getting subgroups.
+ * @param string $groupId
+ * @return array|String
+ * @since 2012-05-09
+ * return array|false
+ */
+ public static function getSubgroups( $groupId ) {
+ $groups = self::get( $groupId, 'subgroups' );
+ if ( $groups !== false ) {
+ if ( strpos( $groups, '|' ) !== false ) {
+ $groups = explode( '|', $groups );
+ } else {
+ $groups = array_map( 'trim', explode( ',', $groups ) );
+ }
+
+ foreach ( $groups as $index => $id ) {
+ if ( trim( $id ) === '' ) {
+ unset( $groups[$index] );
+ }
+ }
+ }
+
+ return $groups;
+ }
+
+ /**
+ * Wrapper for setting subgroups.
+ * @param string $groupId
+ * @param array $subgroupIds
+ * @since 2012-05-09
+ */
+ public static function setSubgroups( $groupId, $subgroupIds ) {
+ $subgroups = implode( '|', $subgroupIds );
+ self::set( $groupId, 'subgroups', $subgroups );
+ }
+
+ /**
+ * Wrapper for deleting one wiki aggregate group at once.
+ * @param string $groupId
+ * @since 2012-05-09
+ */
+ public static function deleteGroup( $groupId ) {
+ $dbw = wfGetDB( DB_MASTER );
+ $conds = array( 'tmd_group' => $groupId );
+ $dbw->delete( 'translate_metadata', $conds );
+ }
+}
diff --git a/MLEB/Translate/utils/TranslateSandbox.php b/MLEB/Translate/utils/TranslateSandbox.php
new file mode 100644
index 00000000..12d3c0d6
--- /dev/null
+++ b/MLEB/Translate/utils/TranslateSandbox.php
@@ -0,0 +1,255 @@
+<?php
+/**
+ * Utilities for the sandbox feature of Translate.
+ *
+ * @file
+ * @author Niklas Laxström
+ * @license GPL-2.0+
+ */
+
+/**
+ * Utility class for the sandbox feature of Translate.
+ */
+class TranslateSandbox {
+ /**
+ * Adds a new user without doing much validation.
+ * @param string $name User name.
+ * @param string $email Email address.
+ * @param string $password User provided password.
+ * @return User
+ * @throws MWException
+ */
+ public static function addUser( $name, $email, $password ) {
+ $user = User::newFromName( $name, 'creatable' );
+ if ( !$user instanceof User ) {
+ throw new MWException( "Invalid user name" );
+ }
+
+ $user->setEmail( $email );
+ $user->setPassword( $password );
+ $status = $user->addToDatabase();
+
+ if ( !$status->isOK() ) {
+ throw new MWException( $status->getWikiText() );
+ }
+
+ // Need to have an id first
+ $user->addGroup( 'translate-sandboxed' );
+ $user->clearInstanceCache( 'name' );
+ $user->sendConfirmationMail();
+
+ return $user;
+ }
+
+ /**
+ * Deletes a sandboxed user without doing much validation.
+ *
+ * @param User $user
+ * @param string $force If set to 'force' will skip the little validation we have.
+ * @throws MWException
+ */
+ public static function deleteUser( User $user, $force = '' ) {
+ $uid = $user->getId();
+
+ if ( $force !== 'force' && !self::isSandboxed( $user ) ) {
+ throw new MWException( "Not a sandboxed user" );
+ }
+
+ // Delete from database
+ $dbw = wfGetDB( DB_MASTER );
+ $dbw->delete( 'user', array( 'user_id' => $uid ), __METHOD__ );
+ $dbw->delete( 'user_groups', array( 'ug_user' => $uid ), __METHOD__ );
+
+ // If someone tries to access still object still, they will get anon user
+ // data.
+ $user->clearInstanceCache( 'defaults' );
+
+ // Nobody should access the user by id anymore, but in case they do, purge
+ // the cache so they wont get stale data
+ // @todo why the bunny is this private?!
+ // $user->clearSharedCache();
+ global $wgMemc;
+ $wgMemc->delete( wfMemcKey( 'user', 'id', $uid ) );
+
+ // In case we create an user with same name as was deleted during the same
+ // request, we must also reset this cache or the User class will try to load
+ // stuff for the old id, which is no longer present since we just deleted
+ // the cache above. But it would have the side effect or overwriting all
+ // member variables with null data. This used to manifest as a bug where
+ // inserting a new user fails because the mName properpty is set to null,
+ // which is then converted as the ip of the current user, and trying to
+ // add that twice results in a name conflict. It was fun to debug.
+ User::resetIdByNameCache();
+ }
+
+ /**
+ * Get all sandboxed users.
+ * @return UserArray List of users.
+ */
+ public static function getUsers() {
+ $dbw = wfGetDB( DB_MASTER );
+ $tables = array( 'user', 'user_groups' );
+ $fields = User::selectFields();
+ $conds = array(
+ 'ug_group' => 'translate-sandboxed',
+ 'ug_user = user_id',
+ );
+
+ $res = $dbw->select( $tables, $fields, $conds, __METHOD__ );
+
+ return UserArray::newFromResult( $res );
+ }
+
+ /**
+ * Removes the user from the sandbox.
+ * @param User $user
+ * @throws MWException
+ */
+ public static function promoteUser( User $user ) {
+ global $wgTranslateSandboxPromotedGroup;
+
+ if ( !self::isSandboxed( $user ) ) {
+ throw new MWException( "Not a sandboxed user" );
+ }
+
+ $user->removeGroup( 'translate-sandboxed' );
+ if ( $wgTranslateSandboxPromotedGroup ) {
+ $user->addGroup( $wgTranslateSandboxPromotedGroup );
+ }
+
+ $user->setOption( 'translate-sandbox-reminders', '' );
+ $user->saveSettings();
+ }
+
+ /**
+ * Sends a reminder to the user.
+ * @param User $sender
+ * @param User $target
+ * @param string $type 'reminder' or 'promotion'
+ * @throws MWException
+ * @since 2013.12
+ */
+ public static function sendEmail( User $sender, User $target, $type ) {
+ global $wgNoReplyAddress;
+
+ $targetLang = $target->getOption( 'language' );
+
+ switch ( $type ) {
+ case 'reminder':
+ if ( !self::isSandboxed( $target ) ) {
+ throw new MWException( 'Not a sandboxed user' );
+ }
+
+ $subjectMsg = 'tsb-reminder-title-generic';
+ $bodyMsg = 'tsb-reminder-content-generic';
+ $targetSpecialPage = 'TranslationStash';
+
+ break;
+ case 'promotion':
+ $subjectMsg = 'tsb-email-promoted-subject';
+ $bodyMsg = 'tsb-email-promoted-body';
+ $targetSpecialPage = 'Translate';
+
+ break;
+ case 'rejection':
+ $subjectMsg = 'tsb-email-rejected-subject';
+ $bodyMsg = 'tsb-email-rejected-body';
+ $targetSpecialPage = 'TwnMainPage';
+
+ break;
+ default:
+ throw new MWException( "'$type' is an invalid type of translate sandbox email" );
+ }
+
+ $subject = wfMessage( $subjectMsg )->inLanguage( $targetLang )->text();
+ $body = wfMessage(
+ $bodyMsg,
+ $target->getName(),
+ SpecialPage::getTitleFor( $targetSpecialPage )->getCanonicalUrl(),
+ $sender->getName()
+ )->inLanguage( $targetLang )->text();
+
+ $params = array(
+ 'user' => $target->getId(),
+ 'to' => new MailAddress( $target ),
+ 'from' => new MailAddress( $sender ),
+ 'replyto' => new MailAddress( $wgNoReplyAddress ),
+ 'subj' => $subject,
+ 'body' => $body,
+ 'emailType' => $type,
+ );
+
+ JobQueueGroup::singleton()->push( TranslateSandboxEmailJob::newJob( $params ) );
+ }
+
+ /**
+ * Shortcut for checking if given user is in the sandbox.
+ * @param User $user
+ * @return bool
+ * @since 2013.06
+ */
+ public static function isSandboxed( User $user ) {
+ if ( in_array( 'translate-sandboxed', $user->getGroups(), true ) ) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /// Hook: UserGetRights
+ public static function enforcePermissions( User $user, array &$rights ) {
+ global $wgTranslateUseSandbox;
+
+ if ( !$wgTranslateUseSandbox ) {
+ return true;
+ }
+
+ if ( !self::isSandboxed( $user ) ) {
+ return true;
+ }
+
+ $rights = array(
+ 'editmyoptions',
+ 'editmyprivateinfo',
+ 'read',
+ 'readapi',
+ 'translate-sandboxaction',
+ 'viewmyprivateinfo',
+ 'writeapi',
+ );
+
+ // Do not let other hooks add more actions
+ return false;
+ }
+
+ /// Hook: onGetPreferences
+ public static function onGetPreferences( $user, &$preferences ) {
+ $preferences['translate-sandbox'] = $preferences['translate-sandbox-reminders'] =
+ array( 'type' => 'api' );
+
+ return true;
+ }
+
+ /**
+ * Whitelisting for certain API modules. See also enforcePermissions.
+ * Hook: ApiCheckCanExecute
+ */
+ public static function onApiCheckCanExecute( ApiBase $module, User $user, &$message ) {
+ $whitelist = array(
+ // Obviously this is needed to get out of the sandbox
+ 'ApiTranslationStash',
+ // Used by UniversalLanguageSelector for example
+ 'ApiOptions'
+ );
+
+ if ( TranslateSandbox::isSandboxed( $user ) ) {
+ $class = get_class( $module );
+ if ( $module->isWriteMode() && !in_array( $class, $whitelist, true ) ) {
+ $message = 'writerequired';
+ return false;
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/MLEB/Translate/utils/TranslateSandboxEmailJob.php b/MLEB/Translate/utils/TranslateSandboxEmailJob.php
new file mode 100644
index 00000000..1496bfda
--- /dev/null
+++ b/MLEB/Translate/utils/TranslateSandboxEmailJob.php
@@ -0,0 +1,38 @@
+<?php
+
+class TranslateSandboxEmailJob extends Job {
+ public static function newJob( array $params ) {
+ return new self( Title::newMainPage(), $params );
+ }
+
+ function __construct( $title, $params, $id = 0 ) {
+ parent::__construct( __CLASS__, $title, $params, $id );
+ }
+
+ function run() {
+ $status = UserMailer::send(
+ $this->params['to'],
+ $this->params['from'],
+ $this->params['subj'],
+ $this->params['body'],
+ $this->params['replyto']
+ );
+
+ $isOK = $status->isOK();
+
+ if ( $isOK && $this->params['emailType'] === 'reminder' ) {
+ $user = User::newFromId( $this->params['user'] );
+
+ $reminders = $user->getOption( 'translate-sandbox-reminders' );
+ $reminders = $reminders ? explode( '|', $reminders ) : array();
+ $reminders[] = wfTimestamp();
+ $user->setOption( 'translate-sandbox-reminders', implode( '|', $reminders ) );
+
+ $reminders = $user->getOption( 'translate-sandbox-reminders' );
+ $user->setOption( 'translate-sandbox-reminders', $reminders );
+ $user->saveSettings();
+ }
+
+ return $isOK;
+ }
+}
diff --git a/MLEB/Translate/utils/TranslateYaml.php b/MLEB/Translate/utils/TranslateYaml.php
new file mode 100644
index 00000000..d3ae3630
--- /dev/null
+++ b/MLEB/Translate/utils/TranslateYaml.php
@@ -0,0 +1,224 @@
+<?php
+/**
+ * Contains wrapper class for interface to parse and generate YAML files.
+ *
+ * @file
+ * @author Ævar Arnfjörð Bjarmason
+ * @author Niklas Laxström
+ * @copyright Copyright © 2009-2013, Niklas Laxström, Ævar Arnfjörð Bjarmason
+ * @license GPL-2.0+
+ */
+
+/**
+ * This class is a wrapper class to provide interface to parse
+ * and generate YAML files with syck or spyc backend.
+ */
+class TranslateYaml {
+ /**
+ * @param $filename string
+ * @return array
+ */
+ public static function parseGroupFile( $filename ) {
+ $data = file_get_contents( $filename );
+ $documents = preg_split( "/^---$/m", $data, -1, PREG_SPLIT_NO_EMPTY );
+ $groups = array();
+ $template = false;
+ foreach ( $documents as $document ) {
+ $document = self::loadString( $document );
+
+ if ( isset( $document['TEMPLATE'] ) ) {
+ $template = $document['TEMPLATE'];
+ } else {
+ if ( !isset( $document['BASIC']['id'] ) ) {
+ $error = "No path ./BASIC/id (group id not defined) ";
+ $error .= "in YAML document located in $filename";
+ trigger_error( $error );
+ continue;
+ }
+ $groups[$document['BASIC']['id']] = $document;
+ }
+ }
+
+ foreach ( $groups as $i => $group ) {
+ $groups[$i] = self::mergeTemplate( $template, $group );
+ }
+
+ return $groups;
+ }
+
+ /**
+ * Merges a document template (base) to actual definition (specific)
+ * @param $base
+ * @param $specific
+ * @return array
+ */
+ public static function mergeTemplate( $base, $specific ) {
+ foreach ( $specific as $key => $value ) {
+ if ( is_array( $value ) && isset( $base[$key] ) && is_array( $base[$key] ) ) {
+ $base[$key] = self::mergeTemplate( $base[$key], $value );
+ } else {
+ $base[$key] = $value;
+ }
+ }
+
+ return $base;
+ }
+
+ /**
+ * @param $text string
+ * @return array
+ * @throws MWException
+ */
+ public static function loadString( $text ) {
+ global $wgTranslateYamlLibrary;
+
+ switch ( $wgTranslateYamlLibrary ) {
+ case 'phpyaml':
+ return yaml_parse( $text );
+
+ case 'spyc':
+ // Load the bundled version if not otherwise available
+ if ( !class_exists( 'Spyc' ) ) {
+ require_once __DIR__ . '/../libs/spyc/spyc.php';
+ }
+ $yaml = spyc_load( $text );
+
+ return self::fixSpycSpaces( $yaml );
+ case 'syck':
+ $yaml = self::syckLoad( $text );
+
+ return self::fixSyckBooleans( $yaml );
+ default:
+ throw new MWException( "Unknown Yaml library" );
+ }
+ }
+
+ /**
+ * @param $yaml array
+ * @return array
+ */
+ public static function fixSyckBooleans( &$yaml ) {
+ foreach ( $yaml as &$value ) {
+ if ( is_array( $value ) ) {
+ self::fixSyckBooleans( $value );
+ } elseif ( $value === 'yes' ) {
+ $value = true;
+ }
+ }
+
+ return $yaml;
+ }
+
+ /**
+ * @param $yaml array
+ * @return array
+ */
+ public static function fixSpycSpaces( &$yaml ) {
+ foreach ( $yaml as $key => &$value ) {
+ if ( is_array( $value ) ) {
+ self::fixSpycSpaces( $value );
+ } elseif ( is_string( $value ) && $key === 'header' ) {
+ $value = preg_replace( '~^\*~m', ' *', $value ) . "\n";
+ }
+ }
+
+ return $yaml;
+ }
+
+ public static function load( $file ) {
+ $text = file_get_contents( $file );
+
+ return self::loadString( $text );
+ }
+
+ public static function dump( $text ) {
+ global $wgTranslateYamlLibrary;
+
+ switch ( $wgTranslateYamlLibrary ) {
+ case 'phpyaml':
+ return yaml_emit( $text, YAML_UTF8_ENCODING );
+
+ case 'spyc':
+ require_once __DIR__ . '/../libs/spyc/spyc.php';
+
+ return Spyc::YAMLDump( $text );
+ case 'syck':
+ return self::syckDump( $text );
+ default:
+ throw new MWException( "Unknown Yaml library" );
+ }
+ }
+
+ protected static function syckLoad( $data ) {
+ # Make temporary file
+ $td = wfTempDir();
+ $tf = tempnam( $td, 'yaml-load-' );
+
+ # Write to file
+ file_put_contents( $tf, $data );
+
+ $cmd = "perl -MYAML::Syck=LoadFile -MPHP::Serialization=serialize -wle '" .
+ 'my $tf = q[' . $tf . '];' .
+ 'my $yaml = LoadFile($tf);' .
+ 'open my $fh, ">", "$tf.serialized" or die qq[Can not open "$tf.serialized"];' .
+ 'print $fh serialize($yaml);' .
+ 'close($fh);' .
+ "' 2>&1";
+
+ $out = wfShellExec( $cmd, $ret );
+
+ if ( $ret != 0 ) {
+ throw new MWException( "The command '$cmd' died in execution with exit code '$ret': $out" );
+ }
+
+ $serialized = file_get_contents( "$tf.serialized" );
+ $php_data = unserialize( $serialized );
+
+ unlink( $tf );
+ unlink( "$tf.serialized" );
+
+ return $php_data;
+ }
+
+ protected static function syckDump( $data ) {
+ # Make temporary file
+ $td = wfTempDir();
+ $tf = tempnam( $td, 'yaml-load-' );
+
+ # Write to file
+ $sdata = serialize( $data );
+ file_put_contents( $tf, $sdata );
+
+ $cmd = "perl -MYAML::Syck=DumpFile -MPHP::Serialization=unserialize -MFile::Slurp=slurp -we '" .
+ '$YAML::Syck::Headless = 1;' .
+ '$YAML::Syck::SortKeys = 1;' .
+ 'my $tf = q[' . $tf . '];' .
+ 'my $serialized = slurp($tf);' .
+ 'my $unserialized = unserialize($serialized);' .
+ 'my $unserialized_utf8 = deutf8($unserialized);' .
+ 'DumpFile(qq[$tf.yaml], $unserialized_utf8);' .
+ 'sub deutf8 {' .
+ 'if(ref($_[0]) eq "HASH") {' .
+ 'return { map { deutf8($_) } %{$_[0]} };' .
+ '} elsif(ref($_[0]) eq "ARRAY") {' .
+ 'return [ map { deutf8($_) } @{$_[0]} ];' .
+ '} else {' .
+ 'my $s = $_[0];' .
+ 'utf8::decode($s);' .
+ 'return $s;' .
+ '}' .
+ '}' .
+ "' 2>&1";
+ $out = wfShellExec( $cmd, $ret );
+ if ( $ret != 0 ) {
+ throw new MWException( "The command '$cmd' died in execution with exit code '$ret': $out" );
+ }
+
+ $yaml = file_get_contents( "$tf.yaml" );
+
+ unlink( $tf );
+ unlink( "$tf.yaml" );
+
+ return $yaml;
+ }
+}
diff --git a/MLEB/Translate/utils/TranslationEditPage.php b/MLEB/Translate/utils/TranslationEditPage.php
new file mode 100644
index 00000000..ffcbd8ca
--- /dev/null
+++ b/MLEB/Translate/utils/TranslationEditPage.php
@@ -0,0 +1,307 @@
+<?php
+/**
+ * Contains classes that imeplement the server side component of AJAX
+ * translation page.
+ *
+ * @file
+ * @author Niklas Laxström
+ * @license GPL-2.0+
+ */
+
+/**
+ * This class together with some JavaScript implements the AJAX translation
+ * page.
+ */
+class TranslationEditPage {
+ // Instance of an Title object
+ protected $title;
+ protected $suggestions = 'sync';
+
+ /**
+ * Constructor.
+ * @param $title Title A title object
+ */
+ public function __construct( Title $title ) {
+ $this->setTitle( $title );
+ }
+
+ /**
+ * Constructs a page from WebRequest.
+ * This interface is a big klunky.
+ * @param $request WebRequest
+ * @return TranslationEditPage
+ */
+ public static function newFromRequest( WebRequest $request ) {
+ $title = Title::newFromText( $request->getText( 'page' ) );
+
+ if ( !$title ) {
+ return null;
+ }
+
+ $obj = new self( $title );
+ $obj->suggestions = $request->getText( 'suggestions' );
+
+ return $obj;
+ }
+
+ /**
+ * Change the title of the page we are working on.
+ * @param $title Title
+ */
+ public function setTitle( Title $title ) {
+ $this->title = $title;
+ }
+
+ /**
+ * Get the title of the page we are working on.
+ * @return Title
+ */
+ public function getTitle() {
+ return $this->title;
+ }
+
+ /**
+ * Generates the html snippet for ajax edit. Echoes it to the output and
+ * disabled all other output.
+ */
+ public function execute() {
+ global $wgServer, $wgScriptPath;
+
+ $context = RequestContext::getMain();
+
+ $context->getOutput()->disable();
+
+ $data = $this->getEditInfo();
+ $helpers = new TranslationHelpers( $this->getTitle(), '' );
+
+ $id = "tm-target-{$helpers->dialogID()}";
+ $helpers->setTextareaId( $id );
+
+ if ( $this->suggestions === 'only' ) {
+ echo $helpers->getBoxes( $this->suggestions );
+
+ return;
+ }
+
+ if ( $this->suggestions === 'checks' ) {
+ echo $helpers->getBoxes( $this->suggestions );
+
+ return;
+ }
+
+ $handle = new MessageHandle( $this->getTitle() );
+ $groupId = MessageIndex::getPrimaryGroupId( $handle );
+
+ $translation = '';
+ if ( $groupId ) {
+ $translation = $helpers->getTranslation();
+ }
+
+ $targetLang = Language::factory( $helpers->getTargetLanguage() );
+ $textareaParams = array(
+ 'name' => 'text',
+ 'class' => 'mw-translate-edit-area',
+ 'id' => $id,
+ /* Target language might differ from interface language. Set
+ * a suitable default direction */
+ 'lang' => $targetLang->getCode(),
+ 'dir' => $targetLang->getDir(),
+ );
+
+ if ( !$groupId || !$context->getUser()->isAllowed( 'translate' ) ) {
+ $textareaParams['readonly'] = 'readonly';
+ }
+
+ $extraInputs = '';
+ wfRunHooks( 'TranslateGetExtraInputs', array( &$translation, &$extraInputs ) );
+
+ $textarea = Html::element( 'textarea', $textareaParams, $translation );
+
+ $hidden = array();
+ $hidden[] = Html::hidden( 'title', $this->getTitle()->getPrefixedDbKey() );
+
+ if ( isset( $data['revisions'][0]['timestamp'] ) ) {
+ $hidden[] = Html::hidden( 'basetimestamp', $data['revisions'][0]['timestamp'] );
+ }
+
+ $hidden[] = Html::hidden( 'starttimestamp', $data['starttimestamp'] );
+ if ( isset( $data['edittoken'] ) ) {
+ $hidden[] = Html::hidden( 'token', $data['edittoken'] );
+ }
+ $hidden[] = Html::hidden( 'format', 'json' );
+ $hidden[] = Html::hidden( 'action', 'edit' );
+
+ $summary = Xml::inputLabel(
+ $context->msg( 'translate-js-summary' )->text(),
+ 'summary',
+ 'summary',
+ 40
+ );
+ $save = Xml::submitButton(
+ $context->msg( 'translate-js-save' )->text(),
+ array( 'class' => 'mw-translate-save' )
+ );
+ $saveAndNext = Xml::submitButton(
+ $context->msg( 'translate-js-next' )->text(),
+ array( 'class' => 'mw-translate-next' )
+ );
+ $skip = Html::element( 'input', array(
+ 'class' => 'mw-translate-skip',
+ 'type' => 'button',
+ 'value' => $context->msg( 'translate-js-skip' )->text()
+ ) );
+
+ if ( $this->getTitle()->exists() ) {
+ $history = Html::element(
+ 'input',
+ array(
+ 'class' => 'mw-translate-history',
+ 'type' => 'button',
+ 'value' => $context->msg( 'translate-js-history' )->text()
+ )
+ );
+ } else {
+ $history = '';
+ }
+
+ $support = $this->getSupportButton( $this->getTitle() );
+
+ if ( $context->getUser()->isAllowed( 'translate' ) ) {
+ $bottom = "$summary$save$saveAndNext$skip$history$support";
+ } else {
+ $text = $context->msg( 'translate-edit-nopermission' )->escaped();
+ $button = $this->getPermissionPageButton();
+ $bottom = "$text $button$skip$history$support";
+ }
+
+ // Use the api to submit edits
+ $formParams = array(
+ 'action' => "{$wgServer}{$wgScriptPath}/api.php",
+ 'method' => 'post',
+ );
+
+ $form = Html::rawElement( 'form', $formParams,
+ implode( "\n", $hidden ) . "\n" .
+ $helpers->getBoxes( $this->suggestions ) . "\n" .
+ Html::rawElement(
+ 'div',
+ array( 'class' => 'mw-translate-inputs' ),
+ "$textarea\n$extraInputs"
+ ) . "\n" .
+ Html::rawElement( 'div', array( 'class' => 'mw-translate-bottom' ), $bottom )
+ );
+
+ echo Html::rawElement( 'div', array( 'class' => 'mw-ajax-dialog' ), $form );
+ }
+
+ /**
+ * Gets the edit token and timestamps in some ugly array structure. Needs to
+ * be cleaned up.
+ * @throws MWException
+ * @return \array
+ */
+ protected function getEditInfo() {
+ $params = new FauxRequest( array(
+ 'action' => 'query',
+ 'prop' => 'info|revisions',
+ 'intoken' => 'edit',
+ 'titles' => $this->getTitle(),
+ 'rvprop' => 'timestamp',
+ ) );
+
+ $api = new ApiMain( $params );
+ $api->execute();
+ $data = $api->getResultData();
+
+ if ( !isset( $data['query']['pages'] ) ) {
+ throw new MWException( 'Api query failed' );
+ }
+ $data = $data['query']['pages'];
+ $data = array_shift( $data );
+
+ return $data;
+ }
+
+ /**
+ * Returns link attributes that enable javascript translation dialog.
+ * Will degrade gracefully if user does not have permissions or JavaScript
+ * is not enabled.
+ * @param $title Title Title object for the translatable message.
+ * @param $group \string The group in which this message belongs to.
+ * Optional, but avoids a lookup later if provided.
+ * @param $type \string Force the type of editor to be used. Use dialog
+ * where embedded editor is no applicable.
+ * @return \array
+ */
+ public static function jsEdit( Title $title, $group = "", $type = 'default' ) {
+ $context = RequestContext::getMain();
+
+ if ( $type === 'default' ) {
+ $text = 'tqe-anchor-' . substr( sha1( $title->getPrefixedText() ), 0, 12 );
+ $onclick = "jQuery( '#$text' ).dblclick(); return false;";
+ } else {
+ $onclick = Xml::encodeJsCall(
+ 'return mw.translate.openDialog', array( $title->getPrefixedDbKey(), $group )
+ );
+ }
+
+ return array(
+ 'onclick' => $onclick,
+ 'title' => $context->msg( 'translate-edit-title', $title->getPrefixedText() )->text()
+ );
+ }
+
+ protected function getSupportButton( $title ) {
+ global $wgTranslateSupportUrl;
+ if ( !$wgTranslateSupportUrl ) {
+ return '';
+ }
+
+ $supportTitle = Title::newFromText( $wgTranslateSupportUrl['page'] );
+ if ( !$supportTitle ) {
+ return '';
+ }
+
+ $supportParams = $wgTranslateSupportUrl['params'];
+ foreach ( $supportParams as &$value ) {
+ $value = str_replace( '%MESSAGE%', $title->getPrefixedText(), $value );
+ }
+
+ $support = Html::element(
+ 'input',
+ array(
+ 'class' => 'mw-translate-support',
+ 'type' => 'button',
+ 'value' => wfMessage( 'translate-js-support' )->text(),
+ 'title' => wfMessage( 'translate-js-support-title' )->text(),
+ 'data-load-url' => $supportTitle->getLocalUrl( $supportParams ),
+ )
+ );
+
+ return $support;
+ }
+
+ protected function getPermissionPageButton() {
+ global $wgTranslatePermissionUrl;
+ if ( !$wgTranslatePermissionUrl ) {
+ return '';
+ }
+
+ $title = Title::newFromText( $wgTranslatePermissionUrl );
+ if ( !$title ) {
+ return '';
+ }
+
+ $button = Html::element(
+ 'input',
+ array(
+ 'class' => 'mw-translate-askpermission',
+ 'type' => 'button',
+ 'value' => wfMessage( 'translate-edit-askpermission' )->text(),
+ 'data-load-url' => $title->getLocalUrl(),
+ )
+ );
+
+ return $button;
+ }
+}
diff --git a/MLEB/Translate/utils/TranslationHelpers.php b/MLEB/Translate/utils/TranslationHelpers.php
new file mode 100644
index 00000000..e33e7e86
--- /dev/null
+++ b/MLEB/Translate/utils/TranslationHelpers.php
@@ -0,0 +1,1473 @@
+<?php
+/**
+ * Contains helper class for interface parts that aid translations in doing
+ * their thing.
+ *
+ * @file
+ * @author Niklas Laxström
+ * @license GPL-2.0+
+ */
+
+/**
+ * Provides the nice boxes that aid the translators to do their job.
+ * Boxes contain definition, documentation, other languages, translation memory
+ * suggestions, highlighted changes etc.
+ */
+class TranslationHelpers {
+ /**
+ * @var MessageHandle
+ * @since 2012-01-04
+ */
+ protected $handle;
+ /**
+ * The group object of the message (or null if there isn't any)
+ * @var MessageGroup
+ */
+ protected $group;
+
+ /**
+ * The current translation as a string.
+ */
+ protected $translation;
+ /**
+ * The message definition as a string.
+ */
+ protected $definition;
+ /**
+ * HTML id to the text area that contains the translation. Used to insert
+ * suggestion directly into the text area, for example.
+ */
+ protected $textareaId = 'wpTextbox1';
+ /**
+ * Whether to include extra tools to aid translating.
+ */
+ protected $editMode = 'true';
+
+ /**
+ * @param Title $title Title of a page that holds a translation.
+ * @param string $groupId Group id that should be used, otherwise autodetected from title.
+ */
+ public function __construct( Title $title, $groupId ) {
+ $this->handle = new MessageHandle( $title );
+ $this->group = $this->getMessageGroup( $this->handle, $groupId );
+ }
+
+ /**
+ * Tries to determine to which group this message belongs. Falls back to the
+ * message index if valid group id was not supplied.
+ *
+ * @param MessageHandle $handle
+ * @param string $groupId
+ * @return MessageGroup|null Group the key belongs to, or null.
+ */
+ protected function getMessageGroup( MessageHandle $handle, $groupId ) {
+ $mg = MessageGroups::getGroup( $groupId );
+
+ # If we were not given (a valid) group
+ if ( $mg === null ) {
+ $groupId = MessageIndex::getPrimaryGroupId( $handle );
+ $mg = MessageGroups::getGroup( $groupId );
+ }
+
+ return $mg;
+ }
+
+ /**
+ * Gets the HTML id of the text area that contains the translation.
+ * @return String
+ */
+ public function getTextareaId() {
+ return $this->textareaId;
+ }
+
+ /**
+ * Sets the HTML id of the text area that contains the translation.
+ * @param $id String
+ */
+ public function setTextareaId( $id ) {
+ $this->textareaId = $id;
+ }
+
+ /**
+ * Enable or disable extra help for editing.
+ * @param $mode Boolean
+ */
+ public function setEditMode( $mode = true ) {
+ $this->editMode = $mode;
+ }
+
+ /**
+ * Gets the message definition.
+ * @return String
+ */
+ public function getDefinition() {
+ if ( $this->definition !== null ) {
+ return $this->definition;
+ }
+
+ $this->mustBeKnownMessage();
+
+ if ( method_exists( $this->group, 'getMessageContent' ) ) {
+ $this->definition = $this->group->getMessageContent( $this->handle );
+ } else {
+ $this->definition = $this->group->getMessage(
+ $this->handle->getKey(),
+ $this->group->getSourceLanguage()
+ );
+ }
+
+ return $this->definition;
+ }
+
+ /**
+ * Gets the current message translation. Fuzzy messages will be marked as
+ * such unless translation is provided manually.
+ * @return string
+ */
+ public function getTranslation() {
+ if ( $this->translation === null ) {
+ $obj = new CurrentTranslationAid( $this->group, $this->handle, RequestContext::getMain() );
+ $aid = $obj->getData();
+ $this->translation = $aid['value'];
+
+ if ( $aid['fuzzy'] ) {
+ $this->translation = TRANSLATE_FUZZY . $this->translation;
+ }
+ }
+
+ return $this->translation;
+ }
+
+ /**
+ * Manual override for the translation. If not given or it is null, the code
+ * will try to fetch it automatically.
+ * @param string|null $translation
+ */
+ public function setTranslation( $translation ) {
+ $this->translation = $translation;
+ }
+
+ /**
+ * Gets the linguistically correct language code for translation
+ */
+ public function getTargetLanguage() {
+ global $wgLanguageCode, $wgTranslateDocumentationLanguageCode;
+
+ $code = $this->handle->getCode();
+ if ( !$code ) {
+ $this->mustBeKnownMessage();
+ $code = $this->group->getSourceLanguage();
+ }
+ if ( $code === $wgTranslateDocumentationLanguageCode ) {
+ return $wgLanguageCode;
+ }
+
+ return $code;
+ }
+
+ /**
+ * Returns block element HTML snippet that contains the translation aids.
+ * Not all boxes are shown all the time depending on whether they have
+ * any information to show and on configuration variables.
+ * @param $suggestions string
+ * @return String. Block level HTML snippet or empty string.
+ */
+ public function getBoxes( $suggestions = 'sync' ) {
+ // Box filter
+ $all = $this->getBoxNames();
+
+ if ( $suggestions === 'async' ) {
+ $all['translation-memory'] = array( $this, 'getLazySuggestionBox' );
+ } elseif ( $suggestions === 'only' ) {
+ return (string)$this->callBox(
+ 'translation-memory',
+ $all['translation-memory'],
+ array( 'lazy' )
+ );
+ } elseif ( $suggestions === 'checks' ) {
+ $request = RequestContext::getMain()->getRequest();
+ $this->translation = $request->getText( 'translation' );
+
+ return (string)$this->callBox( 'check', $all['check'] );
+ }
+
+ if ( $this->group instanceof RecentMessageGroup ) {
+ $all['last-diff'] = array( $this, 'getLastDiff' );
+ }
+
+ $boxes = array();
+ foreach ( $all as $type => $cb ) {
+ $box = $this->callBox( $type, $cb );
+ if ( $box ) {
+ $boxes[$type] = $box;
+ }
+ }
+
+ wfRunHooks( 'TranslateGetBoxes', array( $this->group, $this->handle, &$boxes ) );
+
+ if ( count( $boxes ) ) {
+ return Html::rawElement(
+ 'div',
+ array( 'class' => 'mw-sp-translate-edit-fields' ),
+ implode( "\n\n", $boxes )
+ );
+ } else {
+ return '';
+ }
+ }
+
+ /**
+ * Public since 2012-06-26
+ * @since 2012-01-04
+ */
+ public function callBox( $type, $cb, $params = array() ) {
+ try {
+ return call_user_func_array( $cb, $params );
+ } catch ( TranslationHelperException $e ) {
+ return "<!-- Box $type not available: {$e->getMessage()} -->";
+ }
+ }
+
+ /**
+ * @return array
+ */
+ public function getBoxNames() {
+ return array(
+ 'other-languages' => array( $this, 'getOtherLanguagesBox' ),
+ 'translation-memory' => array( $this, 'getSuggestionBox' ),
+ 'translation-diff' => array( $this, 'getPageDiff' ),
+ 'separator' => array( $this, 'getSeparatorBox' ),
+ 'documentation' => array( $this, 'getDocumentationBox' ),
+ 'definition' => array( $this, 'getDefinitionBox' ),
+ 'check' => array( $this, 'getCheckBox' ),
+ );
+ }
+
+ /**
+ * Returns suggestions from a translation memory.
+ * @param $serviceName
+ * @param $config
+ * @throws TranslationHelperException
+ * @return string Html snippet which contains the suggestions.
+ */
+ protected function getTTMServerBox( $serviceName, $config ) {
+ $this->mustHaveDefinition();
+ $this->mustBeTranslation();
+
+ $source = $this->group->getSourceLanguage();
+ $code = $this->handle->getCode();
+ $definition = $this->getDefinition();
+ $TTMServer = TTMServer::factory( $config );
+ $suggestions = $TTMServer->query( $source, $code, $definition );
+ if ( count( $suggestions ) === 0 ) {
+ throw new TranslationHelperException( 'No suggestions' );
+ }
+
+ return $suggestions;
+ }
+
+ /**
+ * Returns suggestions from a translation memory.
+ * @param $serviceName
+ * @param $config
+ * @throws TranslationHelperException
+ * @return string Html snippet which contains the suggestions.
+ */
+ protected function getRemoteTTMServerBox( $serviceName, $config ) {
+ $this->mustHaveDefinition();
+ $this->mustBeTranslation();
+
+ self::checkTranslationServiceFailure( $serviceName );
+
+ $source = $this->group->getSourceLanguage();
+ $code = $this->handle->getCode();
+ $definition = $this->getDefinition();
+ $params = array(
+ 'format' => 'json',
+ 'action' => 'ttmserver',
+ 'sourcelanguage' => $source,
+ 'targetlanguage' => $code,
+ 'text' => $definition,
+ '*', // Because we hate IE
+ );
+
+ wfProfileIn( 'TranslateWebServiceRequest-' . $serviceName );
+ $json = Http::get( wfAppendQuery( $config['url'], $params ) );
+ wfProfileOut( 'TranslateWebServiceRequest-' . $serviceName );
+
+ $response = FormatJson::decode( $json, true );
+
+ if ( $json === false ) {
+ // Most likely a timeout or other general error
+ self::reportTranslationServiceFailure( $serviceName );
+ throw new TranslationHelperException( 'No reply from remote server' );
+ } elseif ( !is_array( $response ) ) {
+ error_log( __METHOD__ . ': Unable to parse reply: ' . strval( $json ) );
+ throw new TranslationHelperException( 'Malformed reply from remote server' );
+ }
+
+ if ( !isset( $response['ttmserver'] ) ) {
+ self::reportTranslationServiceFailure( $serviceName );
+ throw new TranslationHelperException( 'Unexpected reply from remote server' );
+ }
+
+ $suggestions = $response['ttmserver'];
+ if ( count( $suggestions ) === 0 ) {
+ throw new TranslationHelperException( 'No suggestions' );
+ }
+
+ return $suggestions;
+ }
+
+ /// Since 2012-03-05
+ protected function formatTTMServerSuggestions( $data ) {
+ $sugFields = array();
+
+ foreach ( $data as $serviceWrapper ) {
+ $config = $serviceWrapper['config'];
+ $suggestions = $serviceWrapper['suggestions'];
+
+ foreach ( $suggestions as $s ) {
+ $tooltip = wfMessage( 'translate-edit-tmmatch-source', $s['source'] )->plain();
+ $text = wfMessage(
+ 'translate-edit-tmmatch',
+ sprintf( '%.2f', $s['quality'] * 100 )
+ )->plain();
+ $accuracy = Html::element( 'span', array( 'title' => $tooltip ), $text );
+ $legend = array( $accuracy => array() );
+
+ $TTMServer = TTMServer::factory( $config );
+ if ( $TTMServer->isLocalSuggestion( $s ) ) {
+ $title = Title::newFromText( $s['location'] );
+ $symbol = isset( $config['symbol'] ) ? $config['symbol'] : '•';
+ $legend[$accuracy][] = self::ajaxEditLink( $title, $symbol );
+ } else {
+ if ( $TTMServer instanceof RemoteTTMServer ) {
+ $displayName = $config['displayname'];
+ } else {
+ $wiki = WikiMap::getWiki( $s['wiki'] );
+ $displayName = $wiki->getDisplayName() . ': ' . $s['location'];
+ }
+
+ $params = array(
+ 'href' => $TTMServer->expandLocation( $s ),
+ 'target' => '_blank',
+ 'title' => $displayName,
+ );
+
+ $symbol = isset( $config['symbol'] ) ? $config['symbol'] : '‣';
+ $legend[$accuracy][] = Html::element( 'a', $params, $symbol );
+ }
+
+ $suggestion = $s['target'];
+ $text = $this->suggestionField( $suggestion );
+ $params = array( 'class' => 'mw-sp-translate-edit-tmsug' );
+
+ // Group identical suggestions together
+ if ( isset( $sugFields[$suggestion] ) ) {
+ $sugFields[$suggestion][2] = array_merge_recursive( $sugFields[$suggestion][2], $legend );
+ } else {
+ $sugFields[$suggestion] = array( $text, $params, $legend );
+ }
+ }
+ }
+
+ $boxes = array();
+ foreach ( $sugFields as $field ) {
+ list( $text, $params, $label ) = $field;
+ $legend = array();
+
+ foreach ( $label as $acc => $links ) {
+ $legend[] = $acc . ' ' . implode( " ", $links );
+ }
+
+ $legend = implode( ' | ', $legend );
+ $boxes[] = Html::rawElement(
+ 'div',
+ $params,
+ self::legend( $legend ) . $text . self::clear()
+ ) . "\n";
+ }
+
+ // Limit to three best
+ $boxes = array_slice( $boxes, 0, 3 );
+ $result = implode( "\n", $boxes );
+
+ return $result;
+ }
+
+ /**
+ * @return string
+ * @throws MWException
+ */
+ public function getSuggestionBox() {
+ global $wgTranslateTranslationServices;
+
+ $handlers = array(
+ 'microsoft' => 'getMicrosoftSuggestion',
+ 'apertium' => 'getApertiumSuggestion',
+ 'yandex' => 'getYandexSuggestion',
+ );
+
+ $errors = '';
+ $boxes = array();
+ $TTMSSug = array();
+ foreach ( $wgTranslateTranslationServices as $name => $config ) {
+ $type = $config['type'];
+
+ if ( !isset( $config['timeout'] ) ) {
+ $config['timeout'] = 3;
+ }
+
+ $method = null;
+ if ( isset( $handlers[$type] ) ) {
+ $method = $handlers[$type];
+
+ try {
+ $boxes[] = $this->$method( $name, $config );
+ } catch ( TranslationHelperException $e ) {
+ $errors .= "<!-- Box $name not available: {$e->getMessage()} -->\n";
+ }
+ continue;
+ }
+
+ $server = TTMServer::factory( $config );
+ if ( $server instanceof RemoteTTMServer ) {
+ $method = 'getRemoteTTMServerBox';
+ } elseif ( $server instanceof ReadableTTMServer ) {
+ $method = 'getTTMServerBox';
+ }
+
+ if ( !$method ) {
+ throw new MWException( __METHOD__ . ": Unsupported type {$config['type']}" );
+ }
+
+ try {
+ $TTMSSug[$name] = array(
+ 'config' => $config,
+ 'suggestions' => $this->$method( $name, $config ),
+ );
+ } catch ( TranslationHelperException $e ) {
+ $errors .= "<!-- Box $name not available: {$e->getMessage()} -->\n";
+ }
+ }
+
+ if ( count( $TTMSSug ) ) {
+ array_unshift( $boxes, $this->formatTTMServerSuggestions( $TTMSSug ) );
+ }
+
+ // Remove nulls and falses
+ $boxes = array_filter( $boxes );
+
+ // Enclose if there is more than one box
+ if ( count( $boxes ) ) {
+ $sep = Html::element( 'hr', array( 'class' => 'mw-translate-sep' ) );
+
+ return $errors . TranslateUtils::fieldset(
+ wfMessage( 'translate-edit-tmsugs' )->escaped(),
+ implode( "$sep\n", $boxes ),
+ array( 'class' => 'mw-translate-edit-tmsugs' )
+ );
+ } else {
+ return $errors;
+ }
+ }
+
+ protected static function makeGoogleQueryParams( $definition, $pair, $config ) {
+ global $wgSitename, $wgVersion, $wgSecretKey;
+
+ $app = "$wgSitename (MediaWiki $wgVersion; Translate " . TRANSLATE_VERSION . ")";
+ $context = RequestContext::getMain();
+ $options = array();
+ $options['timeout'] = $config['timeout'];
+
+ $options['postData'] = array(
+ 'q' => $definition,
+ 'v' => '1.0',
+ 'langpair' => $pair,
+ // Unique but not identifiable
+ 'userip' => sha1( $wgSecretKey . $context->getUser()->getName() ),
+ 'x-application' => $app,
+ );
+
+ if ( $config['key'] ) {
+ $options['postData']['key'] = $config['key'];
+ }
+
+ return $options;
+ }
+
+ protected function getMicrosoftSuggestion( $serviceName, $config ) {
+ $this->mustHaveDefinition();
+ self::checkTranslationServiceFailure( $serviceName );
+
+ $code = $this->handle->getCode();
+ $definition = trim( strval( $this->getDefinition() ) );
+ $definition = self::wrapUntranslatable( $definition );
+
+ $memckey = wfMemckey( 'translate-tmsug-badcodes-' . $serviceName );
+ $unsupported = wfGetCache( CACHE_ANYTHING )->get( $memckey );
+
+ if ( isset( $unsupported[$code] ) ) {
+ throw new TranslationHelperException( 'Unsupported language' );
+ }
+
+ $options = array();
+ $options['timeout'] = $config['timeout'];
+
+ $params = array(
+ 'text' => $definition,
+ 'to' => $code,
+ );
+
+ if ( isset( $config['key'] ) ) {
+ $params['appId'] = $config['key'];
+ } else {
+ throw new TranslationHelperException( 'API key is not set' );
+ }
+
+ $url = $config['url'] . '?' . wfArrayToCgi( $params );
+ $url = wfExpandUrl( $url );
+
+ $options['method'] = 'GET';
+
+ $req = MWHttpRequest::factory( $url, $options );
+
+ wfProfileIn( 'TranslateWebServiceRequest-' . $serviceName );
+ $status = $req->execute();
+ wfProfileOut( 'TranslateWebServiceRequest-' . $serviceName );
+
+ if ( !$status->isOK() ) {
+ $error = $req->getContent();
+ if ( strpos( $error, 'must be a valid language' ) !== false ) {
+ $unsupported[$code] = true;
+ wfGetCache( CACHE_ANYTHING )->set( $memckey, $unsupported, 60 * 60 * 8 );
+ throw new TranslationHelperException( 'Unsupported language code' );
+ }
+
+ if ( $error ) {
+ error_log( __METHOD__ . ': Http::get failed:' . $error );
+ } else {
+ error_log( __METHOD__ . ': Unknown error, grr' );
+ }
+ // Most likely a timeout or other general error
+ self::reportTranslationServiceFailure( $serviceName );
+ }
+
+ $ret = $req->getContent();
+ $text = preg_replace( '~<string.*>(.*)</string>~', '\\1', $ret );
+ $text = Sanitizer::decodeCharReferences( $text );
+ $text = self::unwrapUntranslatable( $text );
+ $text = $this->suggestionField( $text );
+
+ return Html::rawElement( 'div', array(), self::legend( $serviceName ) . $text . self::clear() );
+ }
+
+ protected static function wrapUntranslatable( $text ) {
+ $text = str_replace( "\n", "!N!", $text );
+ $wrap = '<span class="notranslate">\0</span>';
+ $pattern = '~%[^% ]+%|\$\d|{VAR:[^}]+}|{?{(PLURAL|GRAMMAR|GENDER):[^|]+\||%(\d\$)?[sd]~';
+ $text = preg_replace( $pattern, $wrap, $text );
+
+ return $text;
+ }
+
+ protected static function unwrapUntranslatable( $text ) {
+ $text = str_replace( '!N!', "\n", $text );
+ $text = preg_replace( '~<span class="notranslate">(.*?)</span>~', '\1', $text );
+
+ return $text;
+ }
+
+ protected function getApertiumSuggestion( $serviceName, $config ) {
+ self::checkTranslationServiceFailure( $serviceName );
+
+ $page = $this->handle->getKey();
+ $code = $this->handle->getCode();
+ $ns = $this->handle->getTitle()->getNamespace();
+
+ $memckey = wfMemckey( 'translate-tmsug-pairs-' . $serviceName );
+ $pairs = wfGetCache( CACHE_ANYTHING )->get( $memckey );
+
+ if ( !$pairs ) {
+
+ $pairs = array();
+ $json = Http::get( $config['pairs'], 5 );
+ $response = FormatJson::decode( $json );
+
+ if ( $json === false ) {
+ self::reportTranslationServiceFailure( $serviceName );
+ } elseif ( !is_object( $response ) ) {
+ error_log( __METHOD__ . ': Unable to parse reply: ' . strval( $json ) );
+ throw new TranslationHelperException( 'Malformed reply from remote server' );
+ }
+
+ foreach ( $response->responseData as $pair ) {
+ $source = $pair->sourceLanguage;
+ $target = $pair->targetLanguage;
+ if ( !isset( $pairs[$target] ) ) {
+ $pairs[$target] = array();
+ }
+ $pairs[$target][$source] = true;
+ }
+
+ wfGetCache( CACHE_ANYTHING )->set( $memckey, $pairs, 60 * 60 * 24 );
+ }
+
+ if ( isset( $config['codemap'][$code] ) ) {
+ $code = $config['codemap'][$code];
+ }
+
+ $code = str_replace( '-', '_', wfBCP47( $code ) );
+
+ if ( !isset( $pairs[$code] ) ) {
+ throw new TranslationHelperException( 'Unsupported language' );
+ }
+
+ $suggestions = array();
+
+ $codemap = array_flip( $config['codemap'] );
+ foreach ( $pairs[$code] as $candidate => $unused ) {
+ $mwcode = str_replace( '_', '-', strtolower( $candidate ) );
+
+ if ( isset( $codemap[$mwcode] ) ) {
+ $mwcode = $codemap[$mwcode];
+ }
+
+ $text = TranslateUtils::getMessageContent( $page, $mwcode, $ns );
+ if ( $text === null || MessageHandle::hasFuzzyString( $text ) ) {
+ continue;
+ }
+
+ $title = Title::makeTitleSafe( $ns, "$page/$mwcode" );
+ $handle = new MessageHandle( $title );
+ if ( $handle->isFuzzy() ) {
+ continue;
+ }
+
+ $options = self::makeGoogleQueryParams( $text, "$candidate|$code", $config );
+ $options['postData']['format'] = 'html';
+
+ wfProfileIn( 'TranslateWebServiceRequest-' . $serviceName );
+ $json = Http::post( $config['url'], $options );
+ wfProfileOut( 'TranslateWebServiceRequest-' . $serviceName );
+
+ $response = FormatJson::decode( $json );
+ if ( $json === false || !is_object( $response ) ) {
+ self::reportTranslationServiceFailure( $serviceName );
+ } elseif ( $response->responseStatus !== 200 ) {
+ error_log( __METHOD__ .
+ " (HTTP {$response->responseStatus}) with ($serviceName ($candidate|$code)): " .
+ $response->responseDetails
+ );
+ } else {
+ $sug = Sanitizer::decodeCharReferences( $response->responseData->translatedText );
+ $sug = trim( $sug );
+ $sug = $this->suggestionField( $sug );
+ $suggestions[] = Html::rawElement( 'div',
+ array( 'title' => $text ),
+ self::legend( "$serviceName ($candidate)" ) . $sug . self::clear()
+ );
+ }
+ }
+
+ if ( !count( $suggestions ) ) {
+ throw new TranslationHelperException( 'No suggestions' );
+ }
+
+ $divider = Html::element( 'div', array( 'style' => 'margin-bottom: 0.5ex' ) );
+
+ return implode( "$divider\n", $suggestions );
+ }
+
+ protected function getYandexSuggestion( $serviceName, $config ) {
+ self::checkTranslationServiceFailure( $serviceName );
+
+ $page = $this->handle->getKey();
+ $code = $this->handle->getCode();
+ $ns = $this->handle->getTitle()->getNamespace();
+
+ $memckey = wfMemckey( 'translate-tmsug-pairs-' . $serviceName );
+ $pairs = wfGetCache( CACHE_ANYTHING )->get( $memckey );
+
+ if ( !$pairs ) {
+ $pairs = array();
+ $json = Http::get( $config['pairs'], $config['timeout'] );
+ $response = FormatJson::decode( $json );
+
+ if ( $json === false ) {
+ self::reportTranslationServiceFailure( $serviceName );
+ } elseif ( !is_object( $response ) ) {
+ error_log( __METHOD__ . ': Unable to parse reply: ' . strval( $json ) );
+ throw new TranslationHelperException( 'Malformed reply from remote server' );
+ }
+
+ foreach ( $response->dirs as $pair ) {
+ list( $source, $target ) = explode( '-', $pair );
+ if ( !isset( $pairs[$target] ) ) {
+ $pairs[$target] = array();
+ }
+ $pairs[$target][$source] = true;
+ }
+
+ $weights = array_flip( $config['langorder'] );
+ $cmpLangs = function ( $lang1, $lang2 ) use ( $weights ) {
+ $weight1 = isset( $weights[$lang1] ) ? $weights[$lang1] : PHP_INT_MAX;
+ $weight2 = isset( $weights[$lang2] ) ? $weights[$lang2] : PHP_INT_MAX;
+
+ if ( $weight1 === $weight2 ) {
+ return 0;
+ }
+
+ return ( $weight1 < $weight2 ) ? -1 : 1;
+ };
+
+ foreach ( $pairs as &$langs ) {
+ uksort( $langs, $cmpLangs );
+ }
+
+ wfGetCache( CACHE_ANYTHING )->set( $memckey, $pairs, 60 * 60 * 24 );
+ }
+
+ if ( !isset( $pairs[$code] ) ) {
+ throw new TranslationHelperException( 'Unsupported language' );
+ }
+
+ $suggestions = array();
+
+ foreach ( $pairs[$code] as $candidate => $unused ) {
+ $text = TranslateUtils::getMessageContent( $page, $candidate, $ns );
+ if ( $text === null || MessageHandle::hasFuzzyString( $text ) ) {
+ continue;
+ }
+
+ $title = Title::makeTitleSafe( $ns, "$page/$candidate" );
+ $handle = new MessageHandle( $title );
+ if ( $handle->isFuzzy() ) {
+ continue;
+ }
+
+ $options = array(
+ 'timeout' => $config['timeout'],
+ 'postData' => array(
+ 'lang' => "$candidate-$code",
+ 'text' => $text,
+ )
+ );
+ wfProfileIn( 'TranslateWebServiceRequest-' . $serviceName );
+ $json = Http::post( $config['url'], $options );
+ wfProfileOut( 'TranslateWebServiceRequest-' . $serviceName );
+ $response = FormatJson::decode( $json );
+
+ if ( $json === false || !is_object( $response ) ) {
+ self::reportTranslationServiceFailure( $serviceName );
+ } elseif ( $response->code !== 200 ) {
+ error_log( __METHOD__ . " (HTTP {$response->code}) with ($serviceName ($candidate|$code))" );
+ } else {
+ $sug = Sanitizer::decodeCharReferences( $response->text[0] );
+ $sug = $this->suggestionField( $sug );
+ $suggestions[] = Html::rawElement( 'div',
+ array( 'title' => $text ),
+ self::legend( "$serviceName ($candidate)" ) . $sug . self::clear()
+ );
+ if ( count( $suggestions ) === $config['langlimit'] ) {
+ break;
+ }
+ }
+ }
+
+ if ( $suggestions === array() ) {
+ throw new TranslationHelperException( 'No suggestions' );
+ }
+
+ $divider = Html::element( 'div', array( 'style' => 'margin-bottom: 0.5ex' ) );
+
+ return implode( "$divider\n", $suggestions );
+ }
+
+ public function getDefinitionBox() {
+ $this->mustHaveDefinition();
+ $en = $this->getDefinition();
+
+ $title = Linker::link(
+ SpecialPage::getTitleFor( 'Translate' ),
+ htmlspecialchars( $this->group->getLabel() ),
+ array(),
+ array(
+ 'group' => $this->group->getId(),
+ 'language' => $this->handle->getCode()
+ )
+ );
+
+ $label =
+ wfMessage( 'translate-edit-definition' )->text() .
+ wfMessage( 'word-separator' )->text() .
+ wfMessage( 'parentheses', $title )->text();
+
+ // Source language object
+ $sl = Language::factory( $this->group->getSourceLanguage() );
+
+ $dialogID = $this->dialogID();
+ $id = Sanitizer::escapeId( "def-$dialogID" );
+ $msg = $this->adder( $id, $sl ) . "\n" . Html::rawElement( 'div',
+ array(
+ 'class' => 'mw-translate-edit-deftext',
+ 'dir' => $sl->getDir(),
+ 'lang' => $sl->getCode(),
+ ),
+ TranslateUtils::convertWhiteSpaceToHTML( $en )
+ );
+
+ $msg .= $this->wrapInsert( $id, $en );
+
+ $class = array( 'class' => 'mw-sp-translate-edit-definition mw-translate-edit-definition' );
+
+ return TranslateUtils::fieldset( $label, $msg, $class );
+ }
+
+ public function getTranslationDisplayBox() {
+ $en = $this->getTranslation();
+ if ( $en === null ) {
+ return null;
+ }
+ $label = wfMessage( 'translate-edit-translation' )->text();
+ $class = array( 'class' => 'mw-translate-edit-translation' );
+ $msg = Html::rawElement( 'span',
+ array( 'class' => 'mw-translate-edit-translationtext' ),
+ TranslateUtils::convertWhiteSpaceToHTML( $en )
+ );
+
+ return TranslateUtils::fieldset( $label, $msg, $class );
+ }
+
+ public function getCheckBox() {
+ $this->mustBeKnownMessage();
+
+ global $wgTranslateDocumentationLanguageCode;
+
+ $context = RequestContext::getMain();
+ $title = $context->getOutput()->getTitle();
+ list( $alias, ) = SpecialPageFactory::resolveAlias( $title->getText() );
+
+ $tux = SpecialTranslate::isBeta( $context->getRequest() )
+ && $title->isSpecialPage()
+ && ( $alias === 'Translate' );
+
+ $formattedChecks = $tux ?
+ FormatJson::encode( array() ) :
+ Html::element( 'div', array( 'class' => 'mw-translate-messagechecks' ) );
+
+ $page = $this->handle->getKey();
+ $translation = $this->getTranslation();
+ $code = $this->handle->getCode();
+ $en = $this->getDefinition();
+
+ if ( strval( $translation ) === '' ) {
+ return $formattedChecks;
+ }
+
+ if ( $code === $wgTranslateDocumentationLanguageCode ) {
+ return $formattedChecks;
+ }
+
+ // We need to get the primary group of the message. It may differ from
+ // the supplied group (aggregate groups, dynamic groups).
+ $checker = $this->handle->getGroup()->getChecker();
+ if ( !$checker ) {
+ return $formattedChecks;
+ }
+
+ $message = new FatMessage( $page, $en );
+ // Take the contents from edit field as a translation
+ $message->setTranslation( $translation );
+
+ $checks = $checker->checkMessage( $message, $code );
+ if ( !count( $checks ) ) {
+ return $formattedChecks;
+ }
+
+ $checkMessages = array();
+
+ foreach ( $checks as $checkParams ) {
+ $key = array_shift( $checkParams );
+ $checkMessages[] = $context->msg( $key, $checkParams )->parse();
+ }
+
+ if ( $tux ) {
+ $formattedChecks = FormatJson::encode( $checkMessages );
+ } else {
+ $formattedChecks = Html::rawElement(
+ 'div',
+ array( 'class' => 'mw-translate-messagechecks' ),
+ TranslateUtils::fieldset(
+ $context->msg( 'translate-edit-warnings' )->escaped(),
+ implode( '<hr />', $checkMessages ),
+ array( 'class' => 'mw-sp-translate-edit-warnings' )
+ )
+ );
+ }
+
+ return $formattedChecks;
+ }
+
+ public function getOtherLanguagesBox() {
+ $code = $this->handle->getCode();
+ $page = $this->handle->getKey();
+ $ns = $this->handle->getTitle()->getNamespace();
+
+ $boxes = array();
+ foreach ( self::getFallbacks( $code ) as $fbcode ) {
+ $text = TranslateUtils::getMessageContent( $page, $fbcode, $ns );
+ if ( $text === null ) {
+ continue;
+ }
+
+ $context = RequestContext::getMain();
+ $label = TranslateUtils::getLanguageName( $fbcode, $context->getLanguage()->getCode() ) .
+ $context->msg( 'word-separator' )->text() .
+ $context->msg( 'parentheses', wfBCP47( $fbcode ) )->text();
+
+ $target = Title::makeTitleSafe( $ns, "$page/$fbcode" );
+ if ( $target ) {
+ $label = self::ajaxEditLink( $target, htmlspecialchars( $label ) );
+ }
+
+ $dialogID = $this->dialogID();
+ $id = Sanitizer::escapeId( "other-$fbcode-$dialogID" );
+
+ $params = array( 'class' => 'mw-translate-edit-item' );
+
+ $display = TranslateUtils::convertWhiteSpaceToHTML( $text );
+ $display = Html::rawElement( 'div', array(
+ 'lang' => $fbcode,
+ 'dir' => Language::factory( $fbcode )->getDir() ),
+ $display
+ );
+
+ $contents = self::legend( $label ) . "\n" . $this->adder( $id, $fbcode ) .
+ $display . self::clear();
+
+ $boxes[] = Html::rawElement( 'div', $params, $contents ) .
+ $this->wrapInsert( $id, $text );
+ }
+
+ if ( count( $boxes ) ) {
+ $sep = Html::element( 'hr', array( 'class' => 'mw-translate-sep' ) );
+
+ return TranslateUtils::fieldset(
+ wfMessage(
+ 'translate-edit-in-other-languages',
+ $page
+ )->escaped(),
+ implode( "$sep\n", $boxes ),
+ array( 'class' => 'mw-sp-translate-edit-inother' )
+ );
+ }
+
+ return null;
+ }
+
+ public function getSeparatorBox() {
+ return Html::element( 'div', array( 'class' => 'mw-translate-edit-extra' ) );
+ }
+
+ public function getDocumentationBox() {
+ global $wgTranslateDocumentationLanguageCode;
+
+ if ( !$wgTranslateDocumentationLanguageCode ) {
+ throw new TranslationHelperException( 'Message documentation language code is not defined' );
+ }
+
+ $context = RequestContext::getMain();
+ $page = $this->handle->getKey();
+ $ns = $this->handle->getTitle()->getNamespace();
+
+ $title = Title::makeTitle( $ns, $page . '/' . $wgTranslateDocumentationLanguageCode );
+ $edit = self::ajaxEditLink(
+ $title,
+ $context->msg( 'translate-edit-contribute' )->escaped()
+ );
+ $info = TranslateUtils::getMessageContent( $page, $wgTranslateDocumentationLanguageCode, $ns );
+
+ $class = 'mw-sp-translate-edit-info';
+
+ $gettext = $this->formatGettextComments();
+ if ( $info !== null && $gettext ) {
+ $info .= Html::element( 'hr' );
+ }
+ $info .= $gettext;
+
+ // The information is most likely in English
+ $divAttribs = array( 'dir' => 'ltr', 'lang' => 'en', 'class' => 'mw-content-ltr' );
+
+ if ( strval( $info ) === '' ) {
+ $info = $context->msg( 'translate-edit-no-information' )->text();
+ $class = 'mw-sp-translate-edit-noinfo';
+ $lang = $context->getLanguage();
+ // The message saying that there's no info, should be translated
+ $divAttribs = array( 'dir' => $lang->getDir(), 'lang' => $lang->getCode() );
+ }
+ $class .= ' mw-sp-translate-message-documentation';
+
+ $contents = $context->getOutput()->parse( $info );
+ // Remove whatever block element wrapup the parser likes to add
+ $contents = preg_replace( '~^<([a-z]+)>(.*)</\1>$~us', '\2', $contents );
+
+ return TranslateUtils::fieldset(
+ $context->msg( 'translate-edit-information' )->rawParams( $edit )->escaped(),
+ Html::rawElement( 'div', $divAttribs, $contents ), array( 'class' => $class )
+ );
+ }
+
+ protected function formatGettextComments() {
+ if ( !$this->handle->isValid() ) {
+ return '';
+ }
+
+ // We need to get the primary group to get the correct file
+ // So $group can be different from $this->group
+ $group = $this->handle->getGroup();
+ if ( !$group instanceof FileBasedMessageGroup ) {
+ return '';
+ }
+
+ $ffs = $group->getFFS();
+ if ( $ffs instanceof GettextFFS ) {
+ global $wgContLang;
+ $mykey = $wgContLang->lcfirst( $this->handle->getKey() );
+ $mykey = str_replace( ' ', '_', $mykey );
+ $data = $ffs->read( $group->getSourceLanguage() );
+ $help = $data['TEMPLATE'][$mykey]['comments'];
+ // Do not display an empty comment. That's no help and takes up unnecessary space.
+ $conf = $group->getConfiguration();
+ if ( isset( $conf['BASIC']['codeBrowser'] ) ) {
+ $out = '';
+ $pattern = $conf['BASIC']['codeBrowser'];
+ $pattern = str_replace( '%FILE%', '\1', $pattern );
+ $pattern = str_replace( '%LINE%', '\2', $pattern );
+ $pattern = "[$pattern \\1:\\2]";
+ foreach ( $help as $type => $lines ) {
+ if ( $type === ':' ) {
+ $files = '';
+ foreach ( $lines as $line ) {
+ $files .= ' ' . preg_replace( '/([^ :]+):(\d+)/', $pattern, $line );
+ }
+ $out .= "<nowiki>#:</nowiki> $files<br />";
+ } else {
+ foreach ( $lines as $line ) {
+ $out .= "<nowiki>#$type</nowiki> $line<br />";
+ }
+ }
+ }
+
+ return "$out";
+ }
+ }
+
+ return '';
+ }
+
+ protected function getPageDiff() {
+ $this->mustBeKnownMessage();
+
+ $title = $this->handle->getTitle();
+ $key = $this->handle->getKey();
+
+ if ( !$title->exists() ) {
+ return null;
+ }
+
+ $definitionTitle = Title::makeTitleSafe( $title->getNamespace(), "$key/en" );
+ if ( !$definitionTitle || !$definitionTitle->exists() ) {
+ return null;
+ }
+
+ $db = wfGetDB( DB_MASTER );
+ $conds = array(
+ 'rt_page' => $title->getArticleID(),
+ 'rt_type' => RevTag::getType( 'tp:transver' ),
+ );
+ $options = array(
+ 'ORDER BY' => 'rt_revision DESC',
+ );
+
+ $latestRevision = $definitionTitle->getLatestRevID();
+
+ $translationRevision = $db->selectField( 'revtag', 'rt_value', $conds, __METHOD__, $options );
+ if ( $translationRevision === false ) {
+ return null;
+ }
+
+ // Using newFromId instead of newFromTitle, because the page might have been renamed
+ $oldrev = Revision::newFromId( $translationRevision );
+ if ( !$oldrev ) {
+ // And someone might still have deleted it
+ return null;
+ }
+
+ $oldtext = ContentHandler::getContentText( $oldrev->getContent() );
+ $newContent = Revision::newFromTitle( $definitionTitle, $latestRevision )->getContent();
+ $newtext = ContentHandler::getContentText( $newContent );
+
+ if ( $oldtext === $newtext ) {
+ return null;
+ }
+
+ $diff = new DifferenceEngine;
+ if ( method_exists( 'DifferenceEngine', 'setTextLanguage' ) ) {
+ $diff->setTextLanguage( $this->group->getSourceLanguage() );
+ }
+
+ $oldContent = ContentHandler::makeContent( $oldtext, $diff->getTitle() );
+ $newContent = ContentHandler::makeContent( $newtext, $diff->getTitle() );
+
+ $diff->setContent( $oldContent, $newContent );
+ $diff->setReducedLineNumbers();
+ $diff->showDiffStyle();
+
+ return $diff->getDiff(
+ wfMessage( 'tpt-diff-old' )->escaped(),
+ wfMessage( 'tpt-diff-new' )->escaped()
+ );
+ }
+
+ protected function getLastDiff() {
+ // Shortcuts
+ $title = $this->handle->getTitle();
+ $latestRevId = $title->getLatestRevID();
+ $previousRevId = $title->getPreviousRevisionID( $latestRevId );
+
+ $latestRev = Revision::newFromTitle( $title, $latestRevId );
+ $previousRev = Revision::newFromTitle( $title, $previousRevId );
+
+ $diffText = '';
+
+ if ( $latestRev && $previousRev ) {
+ $latest = ContentHandler::getContentText( $latestRev->getContent() );
+ $previous = ContentHandler::getContentText( $previousRev->getContent() );
+
+ if ( $previous !== $latest ) {
+ $diff = new DifferenceEngine;
+
+ if ( method_exists( 'DifferenceEngine', 'setTextLanguage' ) ) {
+ $diff->setTextLanguage( $this->getTargetLanguage() );
+ }
+
+ $oldContent = ContentHandler::makeContent( $previous, $diff->getTitle() );
+ $newContent = ContentHandler::makeContent( $latest, $diff->getTitle() );
+
+ $diff->setContent( $oldContent, $newContent );
+ $diff->setReducedLineNumbers();
+ $diff->showDiffStyle();
+ $diffText = $diff->getDiff( false, false );
+ }
+ }
+
+ if ( !$latestRev ) {
+ return null;
+ }
+
+ $context = RequestContext::getMain();
+ $user = $latestRev->getUserText( Revision::FOR_THIS_USER, $context->getUser() );
+ $comment = $latestRev->getComment();
+
+ if ( $diffText === '' ) {
+ if ( strval( $comment ) !== '' ) {
+ $text = $context->msg( 'translate-dynagroup-byc', $user, $comment )->escaped();
+ } else {
+ $text = $context->msg( 'translate-dynagroup-by', $user )->escaped();
+ }
+ } else {
+ if ( strval( $comment ) !== '' ) {
+ $text = $context->msg( 'translate-dynagroup-lastc', $user, $comment )->escaped();
+ } else {
+ $text = $context->msg( 'translate-dynagroup-last', $user )->escaped();
+ }
+ }
+
+ return TranslateUtils::fieldset(
+ $text,
+ $diffText,
+ array( 'class' => 'mw-sp-translate-latestchange' )
+ );
+ }
+
+ /**
+ * @param $label string
+ * @return string
+ */
+ protected static function legend( $label ) {
+ # Float it to the opposite direction
+ return Html::rawElement( 'div', array( 'class' => 'mw-translate-legend' ), $label );
+ }
+
+ /**
+ * @return string
+ */
+ protected static function clear() {
+ return Html::element( 'div', array( 'style' => 'clear:both;' ) );
+ }
+
+ /**
+ * @param $code string
+ * @return array
+ */
+ protected static function getFallbacks( $code ) {
+ global $wgTranslateLanguageFallbacks;
+
+ // User preference has the final say
+ $user = RequestContext::getMain()->getUser();
+ $preference = $user->getOption( 'translate-editlangs' );
+ if ( $preference !== 'default' ) {
+ $fallbacks = array_map( 'trim', explode( ',', $preference ) );
+ foreach ( $fallbacks as $k => $v ) {
+ if ( $v === $code ) {
+ unset( $fallbacks[$k] );
+ }
+ }
+
+ return $fallbacks;
+ }
+
+ // Global configuration settings
+ $fallbacks = array();
+ if ( isset( $wgTranslateLanguageFallbacks[$code] ) ) {
+ $fallbacks = (array)$wgTranslateLanguageFallbacks[$code];
+ }
+
+ $list = Language::getFallbacksFor( $code );
+ array_pop( $list ); // Get 'en' away from the end
+ $fallbacks = array_merge( $list, $fallbacks );
+
+ return array_unique( $fallbacks );
+ }
+
+ /**
+ * @return null|string
+ */
+ public function getLazySuggestionBox() {
+ $this->mustBeKnownMessage();
+ if ( !$this->handle->getCode() ) {
+ return null;
+ }
+
+ $url = SpecialPage::getTitleFor( 'Translate', 'editpage' )->getLocalUrl( array(
+ 'suggestions' => 'only',
+ 'page' => $this->handle->getTitle()->getPrefixedDbKey(),
+ 'loadgroup' => $this->group->getId(),
+ ) );
+ $url = Xml::encodeJsVar( $url );
+
+ $id = Sanitizer::escapeId( 'tm-lazysug-' . $this->dialogID() );
+ $target = self::jQueryPathId( $id );
+
+ $script = Html::inlineScript( "jQuery($target).load($url)" );
+ $spinner = Html::element( 'div', array( 'class' => 'mw-ajax-loader' ) );
+
+ return Html::rawElement( 'div', array( 'id' => $id ), $script . $spinner );
+ }
+
+ /**
+ * @return string
+ */
+ public function dialogID() {
+ $hash = sha1( $this->handle->getTitle()->getPrefixedDbKey() );
+
+ return substr( $hash, 0, 4 );
+ }
+
+ /**
+ * @param string $source jQuery selector for element containing the source
+ * @param string|Language $lang Language code or object
+ * @return string
+ */
+ public function adder( $source, $lang ) {
+ if ( !$this->editMode ) {
+ return '';
+ }
+ $target = self::jQueryPathId( $this->getTextareaId() );
+ $source = self::jQueryPathId( $source );
+ $dir = wfGetLangObj( $lang )->getDir();
+ $params = array(
+ 'onclick' => "jQuery($target).val(jQuery($source).text()).focus(); return false;",
+ 'href' => '#',
+ 'title' => wfMessage( 'translate-use-suggestion' )->text(),
+ 'class' => 'mw-translate-adder mw-translate-adder-' . $dir,
+ );
+
+ return Html::element( 'a', $params, '↓' );
+ }
+
+ /**
+ * @param $id string|int
+ * @param $text string
+ * @return string
+ */
+ public function wrapInsert( $id, $text ) {
+ return Html::element( 'pre', array( 'id' => $id, 'style' => 'display: none;' ), $text );
+ }
+
+ /**
+ * @param $text string
+ * @return string
+ */
+ public function suggestionField( $text ) {
+ static $counter = 0;
+
+ $code = $this->getTargetLanguage();
+
+ $counter++;
+ $dialogID = $this->dialogID();
+ $id = Sanitizer::escapeId( "tmsug-$dialogID-$counter" );
+ $contents = Html::rawElement( 'div', array( 'lang' => $code,
+ 'dir' => Language::factory( $code )->getDir() ),
+ TranslateUtils::convertWhiteSpaceToHTML( $text ) );
+ $contents .= $this->wrapInsert( $id, $text );
+
+ return $this->adder( $id, $code ) . "\n" . $contents;
+ }
+
+ /**
+ * Ajax-enabled message editing link.
+ * @param $target Title: Title of the target message.
+ * @param $text String: Link text for Linker::link()
+ * @return string HTML link
+ */
+ public static function ajaxEditLink( $target, $text ) {
+ $handle = new MessageHandle( $target );
+ $groupId = MessageIndex::getPrimaryGroupId( $handle );
+
+ $params = array();
+ $params['action'] = 'edit';
+ $params['loadgroup'] = $groupId;
+
+ $jsEdit = TranslationEditPage::jsEdit( $target, $groupId, 'dialog' );
+
+ return Linker::link( $target, $text, $jsEdit, $params );
+ }
+
+ /**
+ * Escapes $id such that it can be used in jQuery selector.
+ * @param $id string
+ * @return string
+ */
+ public static function jQueryPathId( $id ) {
+ $id = preg_replace( '/[^A-Za-z0-9_-]/', '\\\\$0', $id );
+
+ return Xml::encodeJsVar( "#$id" );
+ }
+
+ /**
+ * How many failures during failure period need to happen to consider
+ * the service being temporarily off-line. */
+ protected static $serviceFailureCount = 5;
+ /**
+ * How long after the last detected failure we clear the status and
+ * try again.
+ */
+ protected static $serviceFailurePeriod = 900;
+
+ /**
+ * Checks whether the given service has exceeded failure count
+ * @param $service string
+ * @throws TranslationHelperException
+ */
+ public static function checkTranslationServiceFailure( $service ) {
+ $key = wfMemckey( "translate-service-$service" );
+ $value = wfGetCache( CACHE_ANYTHING )->get( $key );
+ if ( !is_string( $value ) ) {
+ return;
+ }
+ list( $count, $failed ) = explode( '|', $value, 2 );
+
+ if ( $failed + ( 2 * self::$serviceFailurePeriod ) < wfTimestamp() ) {
+ if ( $count >= self::$serviceFailureCount ) {
+ error_log( "Translation service $service (was) restored" );
+ }
+ wfGetCache( CACHE_ANYTHING )->delete( $key );
+
+ return;
+ } elseif ( $failed + self::$serviceFailurePeriod < wfTimestamp() ) {
+ /* We are in suspicious mode and one failure is enough to update
+ * failed timestamp. If the service works however, let's use it.
+ * Previous failures are forgotten after another failure period
+ * has passed */
+ return;
+ }
+
+ if ( $count >= self::$serviceFailureCount ) {
+ throw new TranslationHelperException( "web service $service is temporarily disabled" );
+ }
+ }
+
+ /**
+ * Increases the failure count for a given service
+ * @param $service
+ * @throws TranslationHelperException
+ */
+ public static function reportTranslationServiceFailure( $service ) {
+ $key = wfMemckey( "translate-service-$service" );
+ $value = wfGetCache( CACHE_ANYTHING )->get( $key );
+ if ( !is_string( $value ) ) {
+ $count = 0;
+ } else {
+ list( $count, ) = explode( '|', $value, 2 );
+ }
+
+ $count += 1;
+ $failed = wfTimestamp();
+ wfGetCache( CACHE_ANYTHING )->set( $key, "$count|$failed", self::$serviceFailurePeriod * 5 );
+
+ if ( $count == self::$serviceFailureCount ) {
+ error_log( "Translation service $service suspended" );
+ } elseif ( $count > self::$serviceFailureCount ) {
+ error_log( "Translation service $service still suspended" );
+ }
+
+ throw new TranslationHelperException( "web service $service failed to provide valid response" );
+ }
+
+ public static function addModules( OutputPage $out ) {
+ $modules = array( 'ext.translate.quickedit' );
+ wfRunHooks( 'TranslateBeforeAddModules', array( &$modules ) );
+ $out->addModules( $modules );
+
+ // Might be needed, but ajax doesn't load it
+ // Globals :(
+ $diff = new DifferenceEngine;
+ $diff->showDiffStyle();
+ }
+
+ /// @since 2012-01-04
+ protected function mustBeKnownMessage() {
+ if ( !$this->group ) {
+ throw new TranslationHelperException( 'unknown group' );
+ }
+ }
+
+ /// @since 2012-01-04
+ protected function mustBeTranslation() {
+ if ( !$this->handle->getCode() ) {
+ throw new TranslationHelperException( 'editing source language' );
+ }
+ }
+
+ /// @since 2012-01-04
+ protected function mustHaveDefinition() {
+ if ( strval( $this->getDefinition() ) === '' ) {
+ throw new TranslationHelperException( 'message does not have definition' );
+ }
+ }
+}
+
+/**
+ * Translation helpers can throw this exception when they cannot do
+ * anything useful with the current message. This helps in debugging
+ * why some fields are not shown. See also helpers in TranslationHelpers:
+ * - mustBeKnownMessage()
+ * - mustBeTranslation()
+ * - mustHaveDefinition()
+ * @since 2012-01-04 (Renamed in 2012-07-24 to fix typo in name)
+ */
+class TranslationHelperException extends MWException {
+}
diff --git a/MLEB/Translate/utils/TranslationStats.php b/MLEB/Translate/utils/TranslationStats.php
new file mode 100644
index 00000000..0a918f47
--- /dev/null
+++ b/MLEB/Translate/utils/TranslationStats.php
@@ -0,0 +1,61 @@
+<?php
+/**
+ * Contains class which offers functionality for statistics reporting.
+ *
+ * @file
+ * @author Niklas Laxström
+ * @author Siebrand Mazeland
+ * @copyright Copyright © 2010-2013, Niklas Laxström, Siebrand Mazeland
+ * @license GPL-2.0+
+ */
+
+/**
+ * Contains methods that provide statistics for message groups.
+ *
+ * @ingroup Stats
+ */
+class TranslationStats {
+ /**
+ * Returns translated percentage for message group in given
+ * languages
+ *
+ * @param $group \string Unique key identifying the group
+ * @param $languages \array List of language codes
+ * @param bool|int $threshold \int Minimum required percentage translated to
+ * return. Other given language codes will not be returned.
+ * @param $simple \bool Return only codes or code/pecentage pairs
+ *
+ * @return \array Array of key value pairs code (string)/percentage
+ * (float) or array of codes, depending on $simple
+ */
+ public static function getPercentageTranslated( $group, $languages, $threshold = false,
+ $simple = false
+ ) {
+ $stats = array();
+
+ $g = MessageGroups::singleton()->getGroup( $group );
+
+ $collection = $g->initCollection( 'en' );
+ foreach ( $languages as $code ) {
+ $collection->resetForNewLanguage( $code );
+ // Initialise messages
+ $collection->filter( 'ignored' );
+ $collection->filter( 'optional' );
+ // Store the count of real messages for later calculation.
+ $total = count( $collection );
+ $collection->filter( 'translated', false );
+ $translated = count( $collection );
+
+ $translatedPercentage = ( $translated * 100 ) / $total;
+ if ( $translatedPercentage >= $threshold ) {
+ if ( $simple ) {
+ $stats[] = $code;
+ } else {
+ $stats[$code] = $translatedPercentage;
+ }
+ }
+ }
+
+ return $stats;
+ }
+}
diff --git a/MLEB/Translate/utils/TuxMessageTable.php b/MLEB/Translate/utils/TuxMessageTable.php
new file mode 100644
index 00000000..bc77fa68
--- /dev/null
+++ b/MLEB/Translate/utils/TuxMessageTable.php
@@ -0,0 +1,72 @@
+<?php
+
+class TuxMessageTable extends ContextSource {
+ protected $group;
+ protected $language;
+
+ public function __construct( IContextSource $context, MessageGroup $group, $language ) {
+ $this->setContext( $context );
+ $this->group = $group;
+ $this->language = $language;
+ }
+
+ public function fullTable() {
+ $modules = array( 'ext.translate.editor' );
+ wfRunHooks( 'TranslateBeforeAddModules', array( &$modules ) );
+ $this->getOutput()->addModules( $modules );
+
+ $sourceLang = Language::factory( $this->group->getSourceLanguage() );
+ $targetLang = Language::factory( $this->language );
+ $batchSize = 100;
+
+ $list = Html::element( 'div', array(
+ 'class' => 'row tux-messagelist',
+ 'data-grouptype' => get_class( $this->group ),
+ 'data-sourcelangcode' => $sourceLang->getCode(),
+ 'data-sourcelangdir' => $sourceLang->getDir(),
+ 'data-targetlangcode' => $targetLang->getCode(),
+ 'data-targetlangdir' => $targetLang->getDir(),
+ ) );
+
+ $groupId = htmlspecialchars( $this->group->getId() );
+ $msg = $this->msg( 'tux-messagetable-loading-messages' )
+ ->numParams( $batchSize )
+ ->escaped();
+
+ $loader = <<<HTML
+<div class="tux-messagetable-loader hide" data-messagegroup="$groupId" data-pagesize="$batchSize">
+ <span class="tux-loading-indicator"></span>
+ <div class="tux-messagetable-loader-count"></div>
+ <div class="tux-messagetable-loader-more">$msg</div>
+</div>
+HTML;
+
+ $hideOwn = $this->msg( 'tux-editor-proofreading-hide-own-translations' )->escaped();
+ $clearTranslated = $this->msg( 'tux-editor-clear-translated' )->escaped();
+ $modeTranslate = $this->msg( 'tux-editor-translate-mode' )->escaped();
+ $modePage = $this->msg( 'tux-editor-page-mode' )->escaped();
+ $modeProofread = $this->msg( 'tux-editor-proofreading-mode' )->escaped();
+
+ $actionbar = <<<HTML
+<div class="tux-action-bar row">
+ <div class="three columns tux-message-list-statsbar" data-messagegroup="$groupId"></div>
+ <div class="three columns text-center">
+ <button class="toggle button tux-proofread-own-translations-button hide-own hide">
+ $hideOwn
+ </button>
+ <button class="toggle button tux-editor-clear-translated hide">$clearTranslated</button>
+ </div>
+ <div class="six columns tux-view-switcher text-center">
+ <button class="toggle button down translate-mode-button">$modeTranslate
+ </button><button class="toggle button down page-mode-button">$modePage
+ </button><button class="toggle button hide proofread-mode-button">$modeProofread
+ </button>
+ </div>
+</div>
+HTML;
+
+ // Actual message table is fetched and rendered at client side. This just provides
+ // the loader and action bar.
+ return $list . $loader . $actionbar;
+ }
+}
diff --git a/MLEB/Translate/utils/UserToggles.php b/MLEB/Translate/utils/UserToggles.php
new file mode 100644
index 00000000..030a3024
--- /dev/null
+++ b/MLEB/Translate/utils/UserToggles.php
@@ -0,0 +1,109 @@
+<?php
+/**
+ * Contains classes for addition of extension specific preference settings.
+ *
+ * @file
+ * @author Siebrand Mazeland
+ * @author Niklas Laxström
+ * @copyright Copyright © 2008-2010 Siebrand Mazeland, Niklas Laxström
+ * @license GPL-2.0+
+ */
+
+/**
+ * Class to add Translate specific preference settings.
+ */
+class TranslatePreferences {
+ /**
+ * Add 'translate-pref-nonewsletter' preference.
+ * This is most probably specific to translatewiki.net. Can be enabled
+ * with $wgTranslateNewsletterPreference.
+ *
+ * @param $user User
+ * @param $preferences array
+ * @return bool true
+ */
+ public static function onGetPreferences( $user, &$preferences ) {
+ global $wgTranslateNewsletterPreference;
+
+ if ( !$wgTranslateNewsletterPreference ) {
+ return true;
+ }
+
+ global $wgEnableEmail, $wgEnotifRevealEditorAddress;
+
+ // Only show if email is enabled and user has a confirmed email address.
+ if ( $wgEnableEmail && $user->isEmailConfirmed() ) {
+ // 'translate-pref-nonewsletter' is used as opt-out for
+ // users with a confirmed email address
+ $prefs = array(
+ 'translate-nonewsletter' => array(
+ 'type' => 'toggle',
+ 'section' => 'personal/email',
+ 'label-message' => 'translate-pref-nonewsletter'
+ )
+ );
+
+ // Add setting after 'enotifrevealaddr'.
+ $preferences = wfArrayInsertAfter( $preferences, $prefs,
+ $wgEnotifRevealEditorAddress ? 'enotifrevealaddr' : 'enotifminoredits' );
+ }
+
+ return true;
+ }
+
+ /**
+ * Add 'translate-editlangs' preference.
+ * These are the languages also shown when translating.
+ *
+ * @param User $user
+ * @param array $preferences
+ * @return bool true
+ */
+ public static function translationAssistLanguages( User $user, &$preferences ) {
+ // Get selector.
+ $select = self::languageSelector();
+ // Set target ID.
+ $select->setTargetId( 'mw-input-translate-editlangs' );
+ // Get available languages.
+ $languages = Language::fetchLanguageNames();
+
+ $preferences['translate-editlangs'] = array(
+ 'class' => 'HTMLJsSelectToInputField',
+ 'section' => 'editing/translate',
+ 'label-message' => 'translate-pref-editassistlang',
+ 'help-message' => 'translate-pref-editassistlang-help',
+ 'select' => $select,
+ 'valid-values' => array_keys( $languages ),
+ 'name' => 'translate-editlangs',
+ );
+
+ return true;
+ }
+
+ /**
+ * JavsScript selector for language codes.
+ * @return JsSelectToInput
+ */
+ protected static function languageSelector() {
+ if ( is_callable( array( 'LanguageNames', 'getNames' ) ) ) {
+ $lang = RequestContext::getMain()->getLanguage();
+ $languages = LanguageNames::getNames( $lang->getCode(),
+ LanguageNames::FALLBACK_NORMAL
+ );
+ } else {
+ $languages = Language::fetchLanguageNames();
+ }
+
+ ksort( $languages );
+
+ $selector = new XmlSelect( 'mw-language-selector', 'mw-language-selector' );
+ foreach ( $languages as $code => $name ) {
+ $selector->addOption( "$code - $name", $code );
+ }
+
+ $jsSelect = new JsSelectToInput( $selector );
+ $jsSelect->setSourceId( 'mw-language-selector' );
+
+ return $jsSelect;
+ }
+}