summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'MLEB/Translate/utils/TranslationHelpers.php')
-rw-r--r--MLEB/Translate/utils/TranslationHelpers.php1473
1 files changed, 1473 insertions, 0 deletions
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 {
+}