jenkins-bot has submitted this change and it was merged.

Change subject: Parallel corpora: Implement storage
......................................................................


Parallel corpora: Implement storage

* Save the source sections translated
* Save the inital translation of a section - Inital MT
* Save the improvised version of a section
* API to accept the contents to save.
* Add boolean config ContentTranslationCorpora to enable this.
  Not planned for keeping it once we have tables in production.
* Section data is send as compressed.

Bug: T120062
Change-Id: I96af0fe181747c75d2c8251a8af29f1430ca1857
---
M ContentTranslation.hooks.php
A api/ApiContentTranslationSave.php
M extension.json
M i18n/api/en.json
M i18n/api/qqq.json
A includes/TranslationStorageManager.php
A includes/TranslationUnit.php
M modules/tools/ext.cx.tools.mt.js
M modules/translation/ext.cx.translation.draft.js
A modules/translation/ext.cx.translation.storage.init.js
A modules/translation/ext.cx.translation.storage.js
M modules/translationview/ext.cx.translationview.js
12 files changed, 441 insertions(+), 31 deletions(-)

Approvals:
  Nikerabbit: Looks good to me, approved
  jenkins-bot: Verified



diff --git a/ContentTranslation.hooks.php b/ContentTranslation.hooks.php
index 1bf5ba6..a96bb9d 100644
--- a/ContentTranslation.hooks.php
+++ b/ContentTranslation.hooks.php
@@ -171,7 +171,8 @@
                        $wgContentTranslationCampaigns,
                        $wgContentTranslationBrowserBlacklist,
                        $wgContentTranslationDefaultSourceLanguage,
-                       $wgContentTranslationTargetNamespace;
+                       $wgContentTranslationTargetNamespace,
+                       $wgContentTranslationCorpora;
 
                $vars['wgContentTranslationTranslateInTarget'] = 
$wgContentTranslationTranslateInTarget;
                $vars['wgContentTranslationDomainCodeMapping'] = 
$wgContentTranslationDomainCodeMapping;
@@ -183,6 +184,7 @@
                $vars['wgContentTranslationBrowserBlacklist'] = 
$wgContentTranslationBrowserBlacklist;
                $vars['wgContentTranslationDefaultSourceLanguage'] = 
$wgContentTranslationDefaultSourceLanguage;
                $vars['wgContentTranslationTargetNamespace'] = 
$wgContentTranslationTargetNamespace;
+               $vars['wgContentTranslationCorpora'] = 
$wgContentTranslationCorpora;
        }
 
        /**
diff --git a/api/ApiContentTranslationSave.php 
b/api/ApiContentTranslationSave.php
new file mode 100644
index 0000000..bcb09bc
--- /dev/null
+++ b/api/ApiContentTranslationSave.php
@@ -0,0 +1,84 @@
+<?php
+/**
+ *
+ * @file
+ * @copyright See AUTHORS.txt
+ * @license GPL-2.0+
+ */
+
+use ContentTranslation\Translation;
+use ContentTranslation\Database;
+use ContentTranslation\Translator;
+use ContentTranslation\TranslationUnit;
+use ContentTranslation\TranslationStorageManager;
+
+class ApiContentTranslationSave extends ApiBase {
+
+       public function execute() {
+               $params = $this->extractRequestParams();
+               $user = $this->getUser();
+               $content = null;
+               if ( $this->getUser()->isBlocked() ) {
+                       $this->dieUsageMsg( 'blockedtext' );
+               }
+
+               if ( trim( $params['content'] ) === '' ) {
+                       $this->dieUsage( 'content cannot be empty', 
'invalidcontent' );
+               }
+
+               if ( substr( $params['content'], 0, 11 ) === 'rawdeflate,' ) {
+                       $content = gzinflate( base64_decode( substr( $params[ 
'content' ], 11 ) ) );
+                       // gzinflate returns false on error.
+                       if ( $content === false ) {
+                               $this->dieUsage( 'Invalid section content' );
+                       }
+               }
+               $translationUnits = json_decode( $content, true );
+               if ( !is_array( $translationUnits ) ) {
+                       $this->dieUsage( 'content must be valid json array', 
'invalidjson' );
+               }
+
+               $translationId = $params['translationid'];
+               $translator = new Translator( $user );
+               $translation = $translator->getTranslation( $translationId );
+               $translation = $translation->translation;
+               if ( $translationId === null ||
+                       $translator->getGlobalUserId() !== intval( 
$translation['lastUpdatedTranslator'] ) ) {
+                       // Translation does not exist or belong to another 
translator
+                       $this->dieUsage( 'Invalid translation ID: ' . 
$params['translationid'] );
+               }
+               foreach ( $translationUnits as $tuData ) {
+                       $tuData['translationId'] = $translationId;
+                       if ( !isset( $tuData['sectionId'] ) || !isset( 
$tuData['origin'] ) ) {
+                               $this->dieUsage( 'Invalid section data' );
+                       }
+                       $translationUnit = new TranslationUnit( $tuData );
+                       TranslationStorageManager::save( $translationUnit );
+               }
+
+               $result = array(
+                       'result' => 'success'
+               );
+               $this->getResult()->addValue( null, $this->getModuleName(), 
$result );
+       }
+
+       public function getAllowedParams() {
+               return array(
+                       'translationid' => array(
+                               ApiBase::PARAM_TYPE => 'integer',
+                               ApiBase::PARAM_REQUIRED => true,
+                       ),
+                       'content' => array(
+                               ApiBase::PARAM_REQUIRED => true,
+                       ),
+               );
+       }
+
+       public function needsToken() {
+               return 'csrf';
+       }
+
+       public function isWriteMode() {
+               return true;
+       }
+}
diff --git a/extension.json b/extension.json
index 823e6bd..9b56517 100644
--- a/extension.json
+++ b/extension.json
@@ -37,11 +37,12 @@
                "EchoGetDefaultNotifiedUsers": 
"ContentTranslationHooks::onEchoGetDefaultNotifiedUsers"
        },
        "APIModules": {
-               "cxpublish": "ApiContentTranslationPublish",
-               "cxdelete": "ApiContentTranslationDelete",
                "cxconfiguration": "ApiContentTranslationConfiguration",
-               "cxtoken": "ApiContentTranslationToken",
-               "cxsuggestionlist": "ApiContentTranslationSuggestionList"
+               "cxdelete": "ApiContentTranslationDelete",
+               "cxpublish": "ApiContentTranslationPublish",
+               "cxsave": "ApiContentTranslationSave",
+               "cxsuggestionlist": "ApiContentTranslationSuggestionList",
+               "cxtoken": "ApiContentTranslationToken"
        },
        "APIListModules": {
                "contenttranslation": "ApiQueryContentTranslation",
@@ -62,6 +63,7 @@
                "ApiContentTranslationConfiguration": 
"api/ApiContentTranslationConfiguration.php",
                "ApiContentTranslationDelete": 
"api/ApiContentTranslationDelete.php",
                "ApiContentTranslationPublish": 
"api/ApiContentTranslationPublish.php",
+               "ApiContentTranslationSave": 
"api/ApiContentTranslationSave.php",
                "ApiContentTranslationSuggestionList": 
"api/ApiContentTranslationSuggestionList.php",
                "ApiContentTranslationToken": 
"api/ApiContentTranslationToken.php",
                "ApiQueryContentTranslation": 
"api/ApiQueryContentTranslation.php",
@@ -72,16 +74,18 @@
                "ContentTranslationHooks": "ContentTranslation.hooks.php",
                "ContentTranslation\\Database": "includes/Database.php",
                "ContentTranslation\\Draft": "includes/Draft.php",
+               "ContentTranslation\\EchoNotificationPresentationModel": 
"includes/EchoNotificationPresentationModel.php",
                "ContentTranslation\\GlobalUser": "includes/GlobalUser.php",
                "ContentTranslation\\Notification": "includes/Notification.php",
                "ContentTranslation\\SiteMapper": "includes/SiteMapper.php",
                "ContentTranslation\\Stats": "includes/Stats.php",
-               "ContentTranslation\\Translation": "includes/Translation.php",
                "ContentTranslation\\Suggestion": "includes/Suggestion.php",
                "ContentTranslation\\SuggestionList": 
"includes/SuggestionList.php",
                "ContentTranslation\\SuggestionListManager": 
"includes/SuggestionListManager.php",
+               "ContentTranslation\\Translation": "includes/Translation.php",
+               "ContentTranslation\\TranslationUnit": 
"includes/TranslationUnit.php",
+               "ContentTranslation\\TranslationStorageManager": 
"includes/TranslationStorageManager.php",
                "ContentTranslation\\Translator": "includes/Translator.php",
-               "ContentTranslation\\EchoNotificationPresentationModel": 
"includes/EchoNotificationPresentationModel.php",
                "SpecialContentTranslation": 
"specials/SpecialContentTranslation.php",
                "SpecialContentTranslationStats": 
"specials/SpecialContentTranslationStats.php"
        },
@@ -140,7 +144,8 @@
                        "key": "",
                        "age": "3600"
                },
-               "ContentTranslationEnableSuggestions": false
+               "ContentTranslationEnableSuggestions": false,
+               "ContentTranslationCorpora": false
        },
        "ResourceModules": {
                "Base64.js": {
@@ -705,6 +710,19 @@
                                "cx-draft-restore-failed"
                        ]
                },
+               "ext.cx.translation.storage.init": {
+                       "scripts": [
+                               "translation/ext.cx.translation.storage.init.js"
+                       ]
+               },
+               "ext.cx.translation.storage": {
+                       "scripts": [
+                               "translation/ext.cx.translation.storage.js"
+                       ],
+                       "dependencies": [
+                               "easy-deflate.deflate"
+                       ]
+               },
                "ext.cx.publish": {
                        "scripts": [
                                "publish/ext.cx.publish.js"
diff --git a/i18n/api/en.json b/i18n/api/en.json
index a6a8ca5..0ceab83 100644
--- a/i18n/api/en.json
+++ b/i18n/api/en.json
@@ -70,6 +70,8 @@
        "apihelp-cxsuggestionlist-param-listaction": "Action to be performed on 
the list.",
        "apihelp-cxsuggestionlist-param-titles": "Page titles.",
        "apihelp-cxsuggestionlist-param-from": "The source language code.",
-       "apihelp-cxsuggestionlist-param-to": "The target language code."
-
+       "apihelp-cxsuggestionlist-param-to": "The target language code.",
+       "apihelp-cxsave-example-1": "Save the source and translation sections. 
The content must be JSON encoded string.",
+       "apihelp-cxsave-param-translationId": "The translation ID",
+       "apihelp-cxsave-param-content": "JSON encoded section data. Each 
section is an object and has the following keys: content, sectionId, 
sequenceId, sequenceId, origin"
 }
diff --git a/i18n/api/qqq.json b/i18n/api/qqq.json
index 2589de9..652cd76 100644
--- a/i18n/api/qqq.json
+++ b/i18n/api/qqq.json
@@ -65,5 +65,8 @@
        "apihelp-cxsuggestionlist-param-listaction": 
"{{doc-apihelp-param|cxsuggestionlist|listaction}}",
        "apihelp-cxsuggestionlist-param-titles": 
"{{doc-apihelp-param|cxsuggestionlist|titles}}",
        "apihelp-cxsuggestionlist-param-from": 
"{{doc-apihelp-param|cxsuggestionlist|from}}",
-       "apihelp-cxsuggestionlist-param-to": 
"{{doc-apihelp-param|cxsuggestionlist|to}}"
+       "apihelp-cxsuggestionlist-param-to": 
"{{doc-apihelp-param|cxsuggestionlist|to}}",
+       "apihelp-cxsave-example-1": "{{doc-apihelp-description|cxsave}}",
+       "apihelp-cxsave-param-translationId": 
"{{doc-apihelp-param|cxsave|translationId}}",
+       "apihelp-cxsave-param-content": "{{doc-apihelp-param|cxsave|content}}"
 }
diff --git a/includes/TranslationStorageManager.php 
b/includes/TranslationStorageManager.php
new file mode 100644
index 0000000..3ce9793
--- /dev/null
+++ b/includes/TranslationStorageManager.php
@@ -0,0 +1,95 @@
+<?php
+/**
+ *
+ * @file
+ * @copyright See AUTHORS.txt
+ * @license GPL-2.0+
+ */
+
+namespace ContentTranslation;
+
+use ContentTranslation\TranslationUnit;
+
+class TranslationStorageManager {
+
+       /**
+        * Update a translation unit.
+        *
+        * @param TranslationUnit $translationUnit
+        */
+       public static function update( TranslationUnit $translationUnit ) {
+               $dbw = Database::getConnection( DB_MASTER );
+               $values = array(
+                       'cxc_sequence_id' => $translationUnit->getSequenceId(),
+                       'cxc_timestamp' => $dbw->timestamp(),
+                       'cxc_content' => $translationUnit->getContent()
+               );
+               $conditions = array(
+                       'cxc_translation_id' =>  
$translationUnit->getTranslationId(),
+                       'cxc_section_id' =>  $translationUnit->getSectionId(),
+                       'cxc_origin' =>  $translationUnit->getOrigin()
+               );
+
+               $dbw->update( 'cx_corpora', $values, $conditions, __METHOD__ );
+
+               return $dbw->insertId();
+       }
+
+       /**
+        * Insert a translation unit.
+        *
+        * @param TranslationUnit $translationUnit
+        */
+       public static function create( TranslationUnit $translationUnit ) {
+               $dbw = Database::getConnection( DB_MASTER );
+               $values = array(
+                       'cxc_translation_id' => 
$translationUnit->getTranslationId(),
+                       'cxc_section_id' => $translationUnit->getSectionId(),
+                       'cxc_origin' => $translationUnit->getOrigin(),
+                       'cxc_sequence_id' => $translationUnit->getSequenceId(),
+                       'cxc_timestamp' => $dbw->timestamp(),
+                       'cxc_content' => $translationUnit->getContent()
+               );
+               $dbw->insert( 'cx_corpora', $values, __METHOD__ );
+       }
+
+       /**
+        * Save the translation unit.
+        * If the record exist, update it, otherwise create.
+        * @param TranslationUnit $translationUnit
+        */
+       public static function save( TranslationUnit $translationUnit ) {
+               if ( TranslationStorageManager::find(
+                       $translationUnit->getTranslationId(),
+                       $translationUnit->getSectionId(),
+                       $translationUnit->getOrigin()
+               ) !== null ) {
+                       TranslationStorageManager::update( $translationUnit );
+               } else {
+                       TranslationStorageManager::create( $translationUnit );
+               }
+       }
+
+       /**
+        * Find the translation unit.
+        * @param int $translationId Translation Id
+        * @param string $sectionId Section id
+        * @param string $origin Origin of translation unit
+        * @return TranslationUnit|null
+        */
+       public static function find( $translationId, $sectionId, $origin ) {
+               $dbr = Database::getConnection( DB_SLAVE );
+               $conditions = array(
+                       'cxc_translation_id' => $translationId,
+                       'cxc_section_id' => $sectionId,
+                       'cxc_origin' => $origin
+               );
+               $row = $dbr->selectRow( 'cx_corpora', '*', $conditions, 
__METHOD__ );
+
+               if ( $row ) {
+                       return TranslationUnit::newFromRow( $row );
+               }
+
+               return null;
+       }
+}
diff --git a/includes/TranslationUnit.php b/includes/TranslationUnit.php
new file mode 100644
index 0000000..b587b5b
--- /dev/null
+++ b/includes/TranslationUnit.php
@@ -0,0 +1,73 @@
+<?php
+/**
+ * Value object for translation section.
+ *
+ * @file
+ * @copyright See AUTHORS.txt
+ * @license GPL-2.0+
+ */
+
+namespace ContentTranslation;
+
+class TranslationUnit {
+       protected $translationId;
+       protected $sectionId;
+       protected $origin;
+       protected $sequenceId;
+       protected $content;
+       protected $timestamp;
+
+       public function __construct( array $params ) {
+               $this->translationId = (int)$params['translationId'];
+               $this->sectionId = (string)$params['sectionId'];
+               $this->origin = (string)$params['origin'];
+               $this->sequenceId = (int)$params['sequenceId'];
+               $this->content = (string)$params['content'];
+               if ( isset( $params['timestamp'] ) ) {
+                       $this->timestamp = (int)$params['timestamp'];
+               } else {
+                       $this->timestamp = wfTimestamp();
+               }
+       }
+
+       /**
+        * @param stdClass $row
+        * @return TranslationUnit
+        */
+       public static function newFromRow( $row ) {
+               $params = array(
+                       'translationId' => $row->cxc_translation_id,
+                       'sectionId' => $row->cxc_section_id,
+                       'origin' => $row->cxc_origin,
+                       'sequenceId' => $row->cxc_sequence_id,
+                       'content' => $row->cxc_content,
+                       'timestamp' => $row->cxc_timestamp,
+               );
+
+               return new self( $params );
+       }
+
+       public function getTranslationId() {
+               return $this->translationId;
+       }
+
+       public function getSectionId() {
+               return $this->sectionId;
+       }
+
+       public function getSequenceId() {
+               return $this->sequenceId;
+       }
+
+       public function getOrigin() {
+               return $this->origin;
+       }
+
+       public function getTimestamp() {
+               return $this->timestamp;
+       }
+
+       public function getContent() {
+               return $this->content;
+       }
+}
diff --git a/modules/tools/ext.cx.tools.mt.js b/modules/tools/ext.cx.tools.mt.js
index e71eecb..86737dd 100644
--- a/modules/tools/ext.cx.tools.mt.js
+++ b/modules/tools/ext.cx.tools.mt.js
@@ -247,7 +247,8 @@
                                                .attr( {
                                                        id: 'cx' + sourceId,
                                                        'data-source': sourceId,
-                                                       'data-cx-state': 'mt'
+                                                       'data-cx-state': 'mt',
+                                                       'data-cx-mt-provider': 
MTControlCard.provider
                                                } )
                                        );
                                        // $section was replaced. Get the 
updated instance.
diff --git a/modules/translation/ext.cx.translation.draft.js 
b/modules/translation/ext.cx.translation.draft.js
index 9f6800b..98f0766 100644
--- a/modules/translation/ext.cx.translation.draft.js
+++ b/modules/translation/ext.cx.translation.draft.js
@@ -430,19 +430,5 @@
                        );
                } );
        };
-
        mw.cx.ContentTranslationDraft = ContentTranslationDraft;
-       $( function () {
-               var draft,
-                       query = new mw.Uri().query;
-
-               if ( mw.config.get( 'wgContentTranslationDatabase' ) === null ) 
{
-                       mw.log( 'The ext.cx.translation.draft module can only 
work if CX Database configured.' );
-                       return;
-               }
-               draft = new ContentTranslationDraft();
-               if ( query.to && query.from && query.page ) {
-                       draft.init();
-               }
-       } );
 }( jQuery, mediaWiki ) );
diff --git a/modules/translation/ext.cx.translation.storage.init.js 
b/modules/translation/ext.cx.translation.storage.init.js
new file mode 100644
index 0000000..9e6ab8f
--- /dev/null
+++ b/modules/translation/ext.cx.translation.storage.init.js
@@ -0,0 +1,35 @@
+/*!
+ * Intialize CX storage modules
+ *
+ * @copyright See AUTHORS.txt
+ * @license GPL-2.0+
+ */
+( function ( $, mw ) {
+       'use strict';
+
+       $( function () {
+               var storageModules = [
+                       'ext.cx.translation.draft'
+               ];
+
+               if ( mw.config.get( 'wgContentTranslationDatabase' ) === null ) 
{
+                       mw.log( 'CX Database not configured' );
+                       return;
+               }
+               if ( mw.config.get( 'wgContentTranslationCorpora' ) ) {
+                       storageModules.push( 'ext.cx.translation.storage' );
+               }
+               // CX Database configured.
+               mw.loader.using( storageModules ).then( function () {
+                       var storage, draft;
+
+                       draft = new mw.cx.ContentTranslationDraft();
+                       draft.init();
+                       if ( mw.cx.ContentTranslationStorage ) {
+                               storage = new mw.cx.ContentTranslationStorage();
+                               storage.init();
+                       }
+               } );
+       } );
+
+}( jQuery, mediaWiki ) );
diff --git a/modules/translation/ext.cx.translation.storage.js 
b/modules/translation/ext.cx.translation.storage.js
new file mode 100644
index 0000000..2ab36d8
--- /dev/null
+++ b/modules/translation/ext.cx.translation.storage.js
@@ -0,0 +1,114 @@
+/*!
+ * Client side interface for storing translations
+ *
+ * @copyright See AUTHORS.txt
+ * @license GPL-2.0+
+ */
+( function ( $, mw ) {
+       'use strict';
+
+       /**
+        * @class
+        */
+       function ContentTranslationStorage() {
+               this.sections = null;
+       }
+
+       ContentTranslationStorage.prototype.init = function () {
+               this.sections = {};
+               this.listen();
+       };
+
+       /**
+        * Get the content to save. Clean up the content by removing
+        * all unwanted classes and placeholders.
+        *
+        * @return {string} HTML to save
+        */
+       ContentTranslationStorage.prototype.getContent = function ( $section ) {
+               var $content;
+
+               $content = $section.clone();
+               // Remove all highlighting before saving
+               $content
+                       .find( '.cx-highlight, .cx-highlight--blue, 
.cx-highlight--lightblue' )
+                       .removeClass( 'cx-highlight cx-highlight--blue 
cx-highlight--lightblue' );
+
+               return $content.html();
+       };
+
+       ContentTranslationStorage.prototype.listen = function () {
+               var self = this;
+               mw.hook( 'mw.cx.translation.change' ).add( function ( 
$targetSection ) {
+                       self.markForSave( $targetSection );
+               } );
+
+               mw.hook( 'mw.cx.translation.save' ).add( function () {
+                       var sectionId, sections = [];
+
+                       for ( sectionId in self.sections ) {
+                               if ( !self.sections[ sectionId ].saved ) {
+                                       sections.push( self.sections[ sectionId 
] );
+                               }
+                       }
+                       self.saveSections( sections );
+               } );
+       };
+
+       ContentTranslationStorage.prototype.saveSections = function ( sections 
) {
+               var api = new mw.Api();
+
+               if ( !mw.cx.translationId ) {
+                       // A translation id is must to save translations. This 
must be set by
+                       // the ext.cx.translation.draft module. And that module 
to be eventually
+                       // merged to this module.
+                       return;
+               }
+
+               return api.postWithToken( 'csrf', {
+                       action: 'cxsave',
+                       translationid: mw.cx.translationId,
+                       content: EasyDeflate.deflate( JSON.stringify( sections 
) )
+               } ).done( function () {
+                       var i;
+                       // Mark the sections saved
+                       for ( i = 0; i < sections.length; i++ ) {
+                               sections[ i ].saved = true;
+                       }
+               } );
+       };
+
+       ContentTranslationStorage.prototype.markForSave = function ( 
$targetSection ) {
+               var $sourceSection, sourceSectionId, targetSectionId, 
sequenceId, state, origin;
+
+               targetSectionId = $targetSection.attr( 'id' );
+               state = $targetSection.data( 'cx-state' );
+               sourceSectionId = $targetSection.data( 'source' );
+               $sourceSection = mw.cx.getSourceSection( sourceSectionId );
+
+               if ( state === 'mt' ) {
+                       origin = $targetSection.data( 'cx-mt-provider' ) || 
'user';
+               } else {
+                       origin = 'user';
+               }
+               sequenceId = $sourceSection.data( 'seqid' );
+               this.sections[ targetSectionId ] = {
+                       content: this.getContent( $targetSection ),
+                       sectionId: sourceSectionId, // source section id is the 
canonical section id.
+                       saved: false,
+                       sequenceId: sequenceId,
+                       origin: origin
+               };
+
+               // Source sections are saved only once.
+               this.sections[ sourceSectionId ] = this.sections[ 
sourceSectionId ] || {
+                       content: this.getContent( $sourceSection ),
+                       sectionId: sourceSectionId,
+                       saved: false,
+                       sequenceId: sequenceId,
+                       origin: 'source'
+               };
+       };
+
+       mw.cx.ContentTranslationStorage = ContentTranslationStorage;
+}( jQuery, mediaWiki ) );
diff --git a/modules/translationview/ext.cx.translationview.js 
b/modules/translationview/ext.cx.translationview.js
index da0954a..a033a2e 100644
--- a/modules/translationview/ext.cx.translationview.js
+++ b/modules/translationview/ext.cx.translationview.js
@@ -48,12 +48,9 @@
                        'ext.cx.tools',
                        'ext.cx.translation',
                        'ext.cx.translation.progress',
-                       'ext.cx.publish'
+                       'ext.cx.publish',
+                       'ext.cx.translation.storage.init'
                ];
-               if ( mw.config.get( 'wgContentTranslationDatabase' ) !== null ) 
{
-                       // CX Database configured. Load 
ext.cx.translation.draft module.
-                       modules.push( 'ext.cx.translation.draft' );
-               }
 
                if ( mw.cx.sourceTitle ) {
                        mw.loader.using( modules ).then( function () {

-- 
To view, visit https://gerrit.wikimedia.org/r/257283
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: I96af0fe181747c75d2c8251a8af29f1430ca1857
Gerrit-PatchSet: 18
Gerrit-Project: mediawiki/extensions/ContentTranslation
Gerrit-Branch: master
Gerrit-Owner: Santhosh <[email protected]>
Gerrit-Reviewer: KartikMistry <[email protected]>
Gerrit-Reviewer: Nikerabbit <[email protected]>
Gerrit-Reviewer: Santhosh <[email protected]>
Gerrit-Reviewer: jenkins-bot <>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to