jenkins-bot has submitted this change and it was merged. (
https://gerrit.wikimedia.org/r/343030 )
Change subject: CX2: mw.cx.Translation class
......................................................................
CX2: mw.cx.Translation class
Handles save, fetch and restore for translations
Going to replace ext.cx.translation.loader and ext.cx.translation.storage
modules in CX1
Uses dummy progress value. Translation units still don't track the change,
saved status. To be worked on next iterations
Change-Id: I3d43d775eedf2f2133accf0c62dbccd62190f538
---
M api/ApiContentTranslationSave.php
M extension.json
M modules/dm/mw.cx.dm.Translation.js
M modules/dm/translationunits/mw.cx.dm.SectionTranslationUnit.js
M modules/dm/translationunits/mw.cx.dm.TranslationUnit.js
A modules/mw.cx.Translation.js
M modules/mw.cx.init.Translation.js
M modules/ui/mw.cx.ui.Header.js
M modules/ui/mw.cx.ui.TranslationView.js
M modules/ui/styles/mw.cx.ui.Header.less
M modules/ui/translationunits/mw.cx.ui.SectionTranslationUnit.js
M modules/ui/translationunits/mw.cx.ui.TranslationUnit.js
12 files changed, 561 insertions(+), 150 deletions(-)
Approvals:
jenkins-bot: Verified
Nikerabbit: Looks good to me, approved
diff --git a/api/ApiContentTranslationSave.php
b/api/ApiContentTranslationSave.php
index 0efe9ef..bfe567c 100644
--- a/api/ApiContentTranslationSave.php
+++ b/api/ApiContentTranslationSave.php
@@ -26,36 +26,20 @@
}
if ( !Language::isKnownLanguageTag( $params['from'] ) ) {
- if ( is_callable( [ $this, 'dieWithError' ] ) ) {
- $this->dieWithError(
'apierror-cx-invalidsourcelanguage', 'invalidsourcelanguage' );
- } else {
- $this->dieUsage( 'Invalid source language',
'invalidsourcelanguage' );
- }
+ $this->dieWithError(
'apierror-cx-invalidsourcelanguage', 'invalidsourcelanguage' );
}
if ( !Language::isKnownLanguageTag( $params['to'] ) ) {
- if ( is_callable( [ $this, 'dieWithError' ] ) ) {
- $this->dieWithError(
'apierror-cx-invalidtargetlanguage', 'invalidtargetlanguage' );
- } else {
- $this->dieUsage( 'Invalid target language',
'invalidtargetlanguage' );
- }
+ $this->dieWithError(
'apierror-cx-invalidtargetlanguage', 'invalidtargetlanguage' );
}
$progress = FormatJson::decode( $params['progress'], true );
if ( !is_array( $progress ) ) {
- if ( is_callable( [ $this, 'dieWithError' ] ) ) {
- $this->dieWithError(
'apierror-cx-invalidprogress', 'invalidprogress' );
- } else {
- $this->dieUsage( 'Invalid progress',
'invalidprogress' );
- }
+ $this->dieWithError( 'apierror-cx-invalidprogress',
'invalidprogress' );
}
if ( $user->pingLimiter( 'cxsave' ) ) {
- if ( is_callable( [ $this, 'dieWithError' ] ) ) {
- $this->dieWithError( 'apierror-ratelimited' );
- } else {
- $this->dieUsageMsg( 'actionthrottledtext' );
- }
+ $this->dieWithError( 'apierror-ratelimited' );
}
$translator = new Translator( $user );
@@ -182,33 +166,21 @@
protected function getTranslationUnits( $content, $translationId ) {
$translationUnits = [];
if ( trim( $content ) === '' ) {
- if ( is_callable( [ $this, 'dieWithError' ] ) ) {
- $this->dieWithError( [ 'apierror-paramempty',
'content' ], 'invalidcontent' );
- } else {
- $this->dieUsage( 'content cannot be empty',
'invalidcontent' );
- }
+ $this->dieWithError( [ 'apierror-paramempty', 'content'
], 'invalidcontent' );
}
if ( substr( $content, 0, 11 ) === 'rawdeflate,' ) {
$content = gzinflate( base64_decode( substr( $content,
11 ) ) );
// gzinflate returns false on error.
if ( $content === false ) {
- if ( is_callable( [ $this, 'dieWithError' ] ) )
{
- $this->dieWithError(
'apierror-cx-invalidsectioncontent', 'invalidcontent' );
- } else {
- $this->dieUsage( 'Invalid section
content', 'invalidcontent' );
- }
+ $this->dieWithError(
'apierror-cx-invalidsectioncontent', 'invalidcontent' );
}
}
$units = json_decode( $content, true );
foreach ( $units as $tuData ) {
if ( !isset( $tuData['sectionId'] ) || !isset(
$tuData['origin'] ) ) {
- if ( is_callable( [ $this, 'dieWithError' ] ) )
{
- $this->dieWithError(
'apierror-cx-invalidsectiondata', 'invalidcontent' );
- } else {
- $this->dieUsage( 'Invalid section
data', 'invalidcontent' );
- }
+ $this->dieWithError(
'apierror-cx-invalidsectiondata', 'invalidcontent' );
}
// Make sure all translation unit fields are defined.
diff --git a/extension.json b/extension.json
index 3a39f3d..194a465 100644
--- a/extension.json
+++ b/extension.json
@@ -1361,7 +1361,8 @@
"dependencies": [
"mw.cx.dm.Translation",
"mw.cx.ui.TranslationView",
- "mw.cx.TargetArticle"
+ "mw.cx.TargetArticle",
+ "mw.cx.Translation"
]
},
"mw.cx.TargetArticle": {
@@ -1375,6 +1376,31 @@
"mediawiki.api.edit"
]
},
+ "mw.cx.Translation": {
+ "scripts": [
+ "mw.cx.Translation.js"
+ ],
+ "messages": [
+ "cx-lost-session-draft",
+ "cx-save-draft-error",
+ "cx-save-draft-save-success",
+ "cx-save-draft-saving",
+ "cx-save-draft-tooltip",
+ "cx-draft-restoring",
+ "cx-draft-restored",
+ "cx-draft-restore-failed",
+ "cx-page-old-revision-loaded"
+ ],
+ "dependencies": [
+ "easy-deflate.deflate",
+ "ext.cx.model",
+ "mediawiki.api",
+ "mediawiki.api.edit",
+ "mediawiki.user",
+ "mw.cx.dm.Translation",
+ "mw.cx.ui.TranslationView"
+ ]
+ },
"mw.cx.MwApiRequestManager": {
"scripts": [
"mw.cx.MwApiRequestManager.js"
diff --git a/modules/dm/mw.cx.dm.Translation.js
b/modules/dm/mw.cx.dm.Translation.js
index ffc65a0..723d71b 100644
--- a/modules/dm/mw.cx.dm.Translation.js
+++ b/modules/dm/mw.cx.dm.Translation.js
@@ -9,7 +9,7 @@
* @constructor
* @param {Object} config
*/
-mw.cx.dm.Translation = function mwcxTranslation( config ) {
+mw.cx.dm.Translation = function MwCxDmTranslation( config ) {
// Mixin constructor
OO.EventEmitter.call( this );
this.config = config;
@@ -24,7 +24,12 @@
this.revisionId = config.sourceRevision;
this.startDate = null;
this.status = 'draft';
- this.progress = null;
+ this.progress = {
+ any: 0,
+ human: 0,
+ mt: 0,
+ mtSectionsCount: 0
+ };
this.translationUnits = [];
};
@@ -108,12 +113,16 @@
this.targetPage = targetPage;
};
+mw.cx.dm.Translation.prototype.setTargetURL = function ( targetURL ) {
+ this.targetURL = targetURL;
+};
+
/**
* Get revision id
*
* @return {string} revision Id
*/
-mw.cx.dm.Translation.prototype.getSourceRevision = function () {
+mw.cx.dm.Translation.prototype.getSourceRevisionId = function () {
return this.revisionId;
};
@@ -122,8 +131,17 @@
*
* @param {string} revisionId revision Id
*/
-mw.cx.dm.Translation.prototype.setSourceRevision = function ( revisionId ) {
+mw.cx.dm.Translation.prototype.setSourceRevisionId = function ( revisionId ) {
this.revisionId = revisionId;
+};
+
+/**
+ * Set target revision id
+ *
+ * @param {string} revisionId revision Id
+ */
+mw.cx.dm.Translation.prototype.setTargetRevisionId = function ( revisionId ) {
+ this.targetRevisionId = revisionId;
};
/**
@@ -132,7 +150,21 @@
* @param {string} title Translation Id
*/
mw.cx.dm.Translation.prototype.setTargetTitle = function ( title ) {
+ if ( title === this.targetTitle ) {
+ // No title change
+ return;
+ }
this.targetTitle = title;
+ // Translation title change is a change trigger for translation.
+ this.emit( 'change' );
+};
+
+/**
+ * Get Translation title
+ * @return {string} Target title
+ */
+mw.cx.dm.Translation.prototype.getTargetTitle = function () {
+ return this.targetTitle;
};
mw.cx.dm.Translation.prototype.setStatus = function ( status ) {
@@ -142,3 +174,7 @@
mw.cx.dm.Translation.prototype.setProgress = function ( progress ) {
this.progress = progress;
};
+
+mw.cx.dm.Translation.prototype.getProgress = function () {
+ return this.progress;
+};
diff --git a/modules/dm/translationunits/mw.cx.dm.SectionTranslationUnit.js
b/modules/dm/translationunits/mw.cx.dm.SectionTranslationUnit.js
index 6886723..cf07978 100644
--- a/modules/dm/translationunits/mw.cx.dm.SectionTranslationUnit.js
+++ b/modules/dm/translationunits/mw.cx.dm.SectionTranslationUnit.js
@@ -76,13 +76,5 @@
this.setTargetId();
};
-mw.cx.dm.TranslationUnit.prototype.onChange = function () {
- this.save();
-};
-
-mw.cx.dm.SectionTranslationUnit.prototype.save = function () {
- // TODO: use the storage manager instance to save the sections.
-};
-
/* Register */
mw.cx.dm.modelRegistry.register( mw.cx.dm.SectionTranslationUnit );
diff --git a/modules/dm/translationunits/mw.cx.dm.TranslationUnit.js
b/modules/dm/translationunits/mw.cx.dm.TranslationUnit.js
index 7d5fe3d..051fbf5 100644
--- a/modules/dm/translationunits/mw.cx.dm.TranslationUnit.js
+++ b/modules/dm/translationunits/mw.cx.dm.TranslationUnit.js
@@ -38,6 +38,8 @@
this.sourceLanguage = config.sourceLanguage;
this.targetLanguage = config.targetLanguage;
this.parentTranslationUnit = null;
+ // Source of the translation. Can be source, user, Apertium, Yandex etc
+ this.translationSource = null;
// Parent constructor
mw.cx.dm.TranslationUnit.super.call( this );
this.connect( this, {
@@ -55,22 +57,19 @@
mw.cx.dm.TranslationUnit.prototype.adapt = null;
/**
- * Save this translation unit in backend database.
- * Section level translation units will be saved. Sub translation units
- * trigger saving of its parent units.
- * @inheritable
+ * Change handler
*/
-mw.cx.dm.TranslationUnit.prototype.save = function() {
+mw.cx.dm.TranslationUnit.prototype.onChange = function() {
var parentTranslationUnit;
parentTranslationUnit = this.getParentTranslationUnit();
if ( parentTranslationUnit ) {
- parentTranslationUnit.save();
+ parentTranslationUnit.emit( 'change' );
+ } else {
+ this.translation.emit( 'change', this );
}
};
-
-mw.cx.dm.TranslationUnit.prototype.onChange = null;
/**
* Change the element id of the translation copy. We follow a format where
@@ -90,6 +89,14 @@
*/
mw.cx.dm.TranslationUnit.prototype.getTranslationUnits = function() {
return this.translationUnits;
+};
+
+mw.cx.dm.TranslationUnit.prototype.getSourceDocument = function () {
+ return this.sourceDocument;
+};
+
+mw.cx.dm.TranslationUnit.prototype.setTargetDocument = function (
targetDocument ) {
+ this.targetDocument = targetDocument;
};
mw.cx.dm.TranslationUnit.prototype.getTargetDocument = function () {
@@ -128,7 +135,23 @@
* @return {string}
*/
mw.cx.dm.TranslationUnit.prototype.getId = function () {
- return this.constructor.static.name + '::' + this.sourceDocument.id;
+ return this.constructor.static.name + '::' + this.getSectionId();
+};
+
+/**
+ * Get the id of the section
+ * @return {string}
+ */
+mw.cx.dm.TranslationUnit.prototype.getSectionId = function () {
+ return this.sourceDocument.id;
+};
+
+mw.cx.dm.TranslationUnit.prototype.getTranslationSource = function () {
+ return this.translationSource;
+};
+
+mw.cx.dm.TranslationUnit.prototype.setTranslationSource = function (
translationSource ) {
+ this.translationSource = translationSource;
};
/**
diff --git a/modules/mw.cx.Translation.js b/modules/mw.cx.Translation.js
new file mode 100644
index 0000000..c9ccab5
--- /dev/null
+++ b/modules/mw.cx.Translation.js
@@ -0,0 +1,348 @@
+/**
+ * CX Translation - save, fetch and restore controller
+ *
+ * @param {mw.cx.dm.Translation} translation
+ * @param {mw.cx.ui.TranslationView} translationView
+ * @param {object} config Translation configuration
+ */
+mw.cx.Translation = function MwCxTranslation( translation, translationView,
config ) {
+ this.translation = translation;
+ this.view = translationView;
+ this.config = config;
+ this.siteMapper = config.siteMapper;
+ this.sourceTitle = config.sourceTitle;
+ this.targetTitle = config.targetTitle;
+ this.sourceLanguage = config.sourceLanguage;
+ this.targetLanguage = config.targetLanguage;
+ // Mixin constructors
+ OO.EventEmitter.call( this );
+ // Properties
+ this.translationId = null;
+ this.saveRequest = null;
+ this.failCounter = 0;
+ // Associative array of translation units queued to be saved
+ this.saveQueue = {};
+ this.saveTimer = null;
+ this.schedule = OO.ui.debounce( this.processSaveQueue.bind( this ), 3 *
1000 );
+ this.listen();
+};
+
+/* Inheritance */
+
+OO.mixinClass( mw.cx.Translation, OO.EventEmitter );
+
+mw.cx.Translation.prototype.listen = function () {
+ this.translation.connect( this, {
+ change: 'save'
+ } );
+
+ // Save when CTRL+S is pressed.
+ document.onkeydown = function ( e ) {
+ // See
https://medium.com/medium-eng/the-curious-case-of-disappearing-polish-s-fa398313d4df
+ if ( ( e.metaKey || e.ctrlKey && !e.altKey ) && e.which === 83
) {
+ this.processSaveQueue();
+ return false;
+ }
+ }.bind( this );
+ window.onbeforeunload = this.onPageUnload.bind( this );
+};
+
+/**
+ * Save the translation to database
+ * @param {mw.cx.dm.translationUnit} translationUnit
+ */
+mw.cx.Translation.prototype.save = function ( translationUnit ) {
+ if ( !translationUnit ) {
+ return;
+ }
+ // Keep records keyed by section id and origin to avoid duplicates.
+ // When more than one changes to a single translation unit comes, only
+ // the last one need to consider for saving.
+ this.getTranslationUnitData( translationUnit ).forEach( function( data
) {
+ this.saveQueue[ data.sectionId + '-' + data.origin ] = data;
+ }.bind( this ) );
+ this.schedule();
+};
+
+/**
+ * Process the save queue. Save the changed translation units.
+ * @fires savestart
+ * @fires saveerror
+ */
+mw.cx.Translation.prototype.processSaveQueue = function () {
+ var params,
+ api = new mw.Api();
+
+ if ( !this.saveQueue || !Object.keys( this.saveQueue ).length ) {
+ return;
+ }
+
+ // Starting the real save API call. Fire event so that we can show a
progress
+ // indicator in UI.
+ this.emit( 'savestart' );
+ this.view.setStatusMessage( mw.msg( 'cx-save-draft-saving' ) );
+ if ( this.saveRequest ) {
+ this.saveRequest.abort();
+ }
+
+ params = {
+ action: 'cxsave',
+ assert: 'user',
+ content: this.getContentToSave( this.saveQueue ),
+ from: this.sourceLanguage,
+ to: this.targetLanguage,
+ sourcetitle: this.sourceTitle,
+ title: this.translation.getTargetTitle(),
+ sourcerevision: this.config.sourceRevision,
+ progress: JSON.stringify( this.translation.getProgress() )
+ };
+
+ this.saveRequest = api.postWithToken( 'csrf', params )
+ .done( this.onSaveComplete.bind( this ) )
+ .fail( this.onSaveFailure.bind( this ) )
+ .always( function () {
+ this.saveRequest = null;
+ if ( this.failCounter > 5 ) {
+ // If there are more than 5 save errors, stop
autosave at timer triggers.
+ // It will get restarted on further translation
edits.
+ // Show a bigger error message at this point.
+ this.emit( 'saveerror' );
+ this.onSaveFailure();
+ return;
+ }
+ // Irrespective of success or fail, schedule next
autosave
+ this.schedule();
+ }.bind( this ) );
+};
+
+/**
+ * Find out if there is any "dirty" section translation units
+ * Inform about sections not saved to the user.
+ * @return {string|undefined} The message to be shown to user
+ */
+mw.cx.Translation.prototype.onPageUnload = function () {
+ if ( this.saveQueue.length ) {
+ this.schedule();
+ return mw.msg( 'cx-warning-unsaved-translation' );
+ }
+};
+
+mw.cx.Translation.prototype.onSaveComplete = function ( saveResult ) {
+ var i, sectionId, minutes = 0;
+
+ this.translationId = saveResult.cxsave.translationid;
+
+ for ( i = 0; i < this.saveQueue.length; i++ ) {
+ sectionId = this.saveQueue[ i ].sectionId;
+ mw.log( '[CX] Section ' + sectionId + ' saved.' );
+ // Annotate the section with errors.
+ // if ( validations[ sectionId ] && Object.keys( validations[
sectionId ] ).length ) {
+ // cxsave API will return errors from abusefilter validations,
if any.
+ // We need to set this in translation unit model. A tool
attached to UI model
+ // can query the model to see if there is any error and show in
tools(on focus
+ // of translation unit)
+ }
+
+ this.emit( 'savesuccess' );
+ // Show saved status with a time after last save.
+ clearTimeout( this.saveTimer );
+ this.view.setStatusMessage( mw.msg( 'cx-save-draft-save-success', 0 ) );
+ this.saveTimer = setInterval( function () {
+ minutes++;
+ this.view.setStatusMessage(
+ mw.msg( 'cx-save-draft-save-success',
mw.language.convertNumber( minutes ) )
+ );
+ }.bind( this ), 60 * 1000 );
+
+ // Reset fail counter.
+ this.failCounter = 0;
+ // Reset the queue
+ this.saveQueue = [];
+};
+
+mw.cx.Translation.prototype.onSaveFailure = function ( errorCode, details ) {
+ if ( errorCode === 'assertuserfailed' ) {
+ this.view.showMessage( 'error', mw.msg( 'cx-lost-session-draft'
) );
+ }
+
+ if ( details && details.exception instanceof Error ) {
+ details.exception = details.exception.toString();
+ details.errorCode = errorCode;
+ }
+ this.emit( 'saveerror' );
+ this.view.setStatusMessage( mw.msg( 'cx-save-draft-error' ) );
+ this.failCounter++;
+};
+
+/**
+ * Get the deflated content to save from save queue
+ * @param {Object[]} saveQueue
+ * @return {string}
+ */
+mw.cx.Translation.prototype.getContentToSave = function ( saveQueue ) {
+ var records = [];
+
+ Object.keys( saveQueue ).forEach( function( key ) {
+ records.push( saveQueue[ key ] );
+ } );
+ // The cxsave api accept non-deflated content too.
+ // Sometimes it is helpful for testing:
+ // return JSON.stringify( records );
+ return EasyDeflate.deflate( JSON.stringify( records ) );
+};
+
+/**
+ * Get the records for saving the translation unit.
+ * @param {mw.cx.dm.translationUnit} translationUnit
+ * @return {Object[]} Objects to save
+ */
+mw.cx.Translation.prototype.getTranslationUnitData = function (
translationUnit ) {
+ var sequenceId, origin, translationSource, records = [],
+ validate;
+
+ // XXX Section validation for abusefilter
+ validate = false;
+
+ sequenceId = translationUnit.sourceDocument.getAttribute( 'data-seqid'
);
+ translationSource = translationUnit.getTranslationSource();
+ if ( translationSource === 'mt' ) {
+ origin = translationUnit.getMachineTranslationProvider();
+ } else {
+ origin = 'user';
+ }
+ records.push( {
+ content: translationUnit.getTargetDocument().outerHTML,
+ sectionId: translationUnit.sourceDocument.id, // source section
id is the canonical section id.
+ validate: validate,
+ sequenceId: sequenceId,
+ origin: origin
+ } );
+ // XXX: Source sections are saved only once.
+ records.push( {
+ content: translationUnit.getSourceDocument().outerHTML,
+ sectionId: translationUnit.sourceDocument.id,
+ validate: false,
+ sequenceId: sequenceId,
+ origin: 'source'
+ } );
+ return records;
+};
+
+mw.cx.Translation.prototype.getSavedTranslation = function () {
+ return this.find().then( function( translation ) {
+ if ( !translation ) {
+ return $.Deferred().reject( false ).resolve();
+ }
+
+ // Do not allow two users to start a draft at the same time.
The API only
+ // returns a translation with different translatorName if this
is the case.
+ if ( translation.translatorName !== mw.user.getName() ) {
+ this.view.showConflictWarning( translation );
+ return $.Deferred().reject( false ).resolve();
+ }
+
+ if ( translation.status === 'deleted' ) {
+ // Deleted translation
+ return $.Deferred().reject( false ).resolve();
+ }
+ // Fetch the translation content
+ return this.fetch( translation.id ).then( function(
fetchedTranslation ) {
+ return fetchedTranslation;
+ } );
+ }.bind( this ) );
+};
+
+/**
+ * Restore the translation from database to translation view to continue.
+ * @param {Object} savedTranslation
+ */
+mw.cx.Translation.prototype.restore = function ( savedTranslation ) {
+ var i, translationUnits, sectionId, savedTranslationUnits,
savedTranslationUnit, translationContent;
+
+ this.emit( 'restorestart' );
+ translationUnits = this.translation.getTranslationUnits();
+ savedTranslationUnits = savedTranslation.translationUnits;
+ for ( i = 0; i < translationUnits.length; i++ ) {
+ sectionId = translationUnits[ i ].getSectionId();
+ savedTranslationUnit = savedTranslationUnits[ sectionId ];
+
+ if ( !savedTranslationUnit ) {
+ continue;
+ }
+
+ translationContent = savedTranslationUnit.user ||
savedTranslationUnit.mt || savedTranslationUnit.source;
+ // TODO: translationContent is an object, it has MT service,
timestamp and content.
+ // Do we need to retain all these info to the model?
+ translationUnits[ i ].setTargetDocument(
translationContent.content );
+ }
+ // TODO: Find out orphan translation units and handle them.
+ this.view.setStatusMessage( mw.msg( 'cx-draft-restored' ) );
+};
+
+/**
+ * Fetch the translation from database to translation view to continue.
+ * @param {string} translationId Translation id
+ * @return {jQuery.Promise}
+ */
+mw.cx.Translation.prototype.fetch = function ( translationId ) {
+ var api = new mw.Api();
+
+ this.emit( 'fetchstart' );
+ this.view.setStatusMessage( mw.msg( 'cx-draft-restoring' ) );
+ return api.get( {
+ action: 'query',
+ list: 'contenttranslation',
+ translationid: translationId
+ } ).then(
+ // Success handler
+ this.onFetchSuccess.bind( this ),
+ // Failure handler
+ this.onFetchFail.bind( this )
+ );
+};
+
+mw.cx.Translation.prototype.onFetchSuccess = function ( response ) {
+ var fetchedTranslation;
+ fetchedTranslation = response.query.contenttranslation.translation;
+ this.emit( 'fetchsuccess' );
+ return fetchedTranslation;
+};
+
+mw.cx.Translation.prototype.onFetchFail = function ( errorCode, details ) {
+ if ( details.exception instanceof Error ) {
+ details.exception = details.exception.toString();
+ }
+ details.errorCode = errorCode;
+ this.emit( 'fetcherror', this.translation, details );
+ this.view.setStatusMessage( mw.msg( 'cx-draft-restore-failed' ) );
+};
+
+/**
+ * Find if there is a draft existing for the current title and language pair.
+ *
+ * @return {jQuery.Promise}
+ */
+mw.cx.Translation.prototype.find = function () {
+ var api = new mw.Api();
+
+ return api.get( {
+ action: 'query',
+ list: 'contenttranslation',
+ sourcetitle: this.sourceTitle,
+ from: this.sourceLanguage,
+ to: this.targetLanguage
+ } ).then( function ( response ) {
+ return response.query &&
response.query.contenttranslation.translation;
+ } );
+};
+
+mw.cx.Translation.prototype.showOldRevisionWarning = function () {
+ var diffUrl;
+
+ diffUrl = this.siteMapper.getPageUrl( this.sourceLanguage,
this.sourceTitle, {
+ type: 'revision',
+ diff: 'cur',
+ oldid: this.translation.sourceRevisionId
+ } );
+ this.view.showMessage( 'warning', mw.message(
'cx-page-old-revision-loaded', diffUrl ) );
+};
diff --git a/modules/mw.cx.init.Translation.js
b/modules/mw.cx.init.Translation.js
index 07d73e7..a5e404a 100644
--- a/modules/mw.cx.init.Translation.js
+++ b/modules/mw.cx.init.Translation.js
@@ -13,6 +13,7 @@
this.targetLanguage = config.targetLanguage;
this.sourceRevision = config.sourceRevision;
this.translationView = null;
+ this.translationModel = null;
this.translation = null;
};
@@ -38,9 +39,11 @@
mw.cx.init.Translation.prototype.initTranslation = function () {
this.sourcePage = new mw.cx.dm.SourcePage( this.config );
this.targetPage = new mw.cx.dm.TargetPage( this.config );
- this.translation = new mw.cx.dm.Translation( this.config );
- this.translation.setSourcePage( this.sourcePage );
- this.translation.setTargetPage( this.targetPage );
+ this.translationModel = new mw.cx.dm.Translation( this.config );
+ this.translationModel.setSourcePage( this.sourcePage );
+ this.translationModel.setTargetPage( this.targetPage );
+
+ this.translation = new mw.cx.Translation( this.translationModel,
this.translationView, this.config );
// Fetch the source page from cxserver. The content is segmented.
return this.fetchSourcePageContent(
this.sourceTitle, this.sourceLanguage, this.sourceRevision
@@ -57,10 +60,24 @@
mw.cx.init.Translation.prototype.loadTranslation = function (
segmentedSourcePage ) {
this.sourcePage.setSections( $.parseHTML(
segmentedSourcePage.segmentedContent ) );
this.sourcePage.setSourceRevision( segmentedSourcePage.revision );
- this.translation.setSourceRevision( this.sourcePage.getSourceRevision()
);
- this.translation.prepareTranslationUnits();
- this.translationView.setTranslation( this.translation );
- this.translationView.loadTranslation();
+ this.config.sourceRevision = segmentedSourcePage.revision;
+ this.translationModel.setSourceRevisionId(
this.sourcePage.getSourceRevision() );
+ this.translationModel.prepareTranslationUnits();
+
+ // Get the saved translation and set the properties in translation model
+ this.translation.getSavedTranslation().then( function( savedTranslation
) {
+ this.translationModel.setTargetURL( savedTranslation.targetURL
);
+ this.translationModel.setStatus( savedTranslation.status );
+ this.translationModel.setTargetRevisionId(
savedTranslation.targetRevisionId );
+ this.translationModel.setProgress( JSON.parse(
savedTranslation.progress ) );
+ this.translationModel.setId( savedTranslation.id );
+ this.translationModel.setTargetTitle(
savedTranslation.targetTitle );
+ // Restore each translation storage units against the source
sections.
+ this.translation.restore( savedTranslation );
+ }.bind( this ) ).always( function() {
+ this.translationView.setTranslation( this.translationModel );
+ this.translationView.loadTranslation();
+ }.bind( this ) );
};
/**
diff --git a/modules/ui/mw.cx.ui.Header.js b/modules/ui/mw.cx.ui.Header.js
index 534e0d7..917edb1 100644
--- a/modules/ui/mw.cx.ui.Header.js
+++ b/modules/ui/mw.cx.ui.Header.js
@@ -12,6 +12,7 @@
this.config = config || {};
this.$headerBar = null;
this.infobar = null;
+ this.statusbar = null;
// Parent constructor
mw.cx.ui.Header.parent.call( this, $.extend( {}, this.config, {
continuous: true,
@@ -51,56 +52,54 @@
.addClass( 'cx-header__translation-center' )
.append( $translationCenterLink );
- this.$draftStatus = $( '<div>' )
- .addClass( 'cx-header__draft-status' );
+ this.statusbar = new OO.ui.LabelWidget( {
+ classes: [ 'cx-header-draft-status' ],
+ title: mw.msg( 'cx-save-draft-tooltip' )
+ } );
this.$headerBar = $( '<div>' )
.addClass( 'cx-header__bar' )
- .append( $translationCenter, this.$draftStatus );
+ .append( $translationCenter, this.statusbar.$element );
this.infobar = new mw.cx.ui.Infobar( this.config );
return $( '<div>' ).append( $headerTitle, this.$headerBar,
this.infobar.$element );
};
mw.cx.ui.Header.prototype.listen = function () {
- var self = this;
- mw.hook( 'mw.cx.translation.save-started' ).add( $.proxy(
this.updateSaveStatus, this, 'progress' ) );
- mw.hook( 'mw.cx.translation.saved' ).add( $.proxy(
this.updateSaveStatus, this, 'success' ) );
- mw.hook( 'mw.cx.translation.save-failed' ).add( $.proxy(
this.updateSaveStatus, this, 'fail' ) );
- mw.hook( 'mw.cx.translation.title.change' ).add( $.proxy(
this.clearMessages, this ) );
+ mw.hook( 'mw.cx.translation.save-started' ).add(
+ this.setStatusMessage.bind( this, mw.msg(
'cx-save-draft-saving' ) )
+ );
+ mw.hook( 'mw.cx.translation.saved' ).add( function() {
+ var minutes = 0;
- mw.hook( 'mw.cx.draft.restoring' ).add( function () {
- self.$draftStatus.text( mw.msg( 'cx-draft-restoring' ) );
- } );
- mw.hook( 'mw.cx.draft.restored' ).add( function () {
- self.$draftStatus.text( mw.msg( 'cx-draft-restored' ) );
- } );
+ clearTimeout( timer );
+ this.setStatusMessage( mw.msg( 'cx-save-draft-save-success', 0
) );
+ timer = setInterval( function () {
+ minutes++;
+ this.setStatusMessage(
+ mw.msg( 'cx-save-draft-save-success',
mw.language.convertNumber( minutes ) )
+ );
+ }.bind( this ), 60 * 1000 );
+ }.bind( this ) );
+ mw.hook( 'mw.cx.translation.save-failed' ).add(
+ this.setStatusMessage.bind( this, mw.msg( 'cx-save-draft-error'
) )
+ );
+
+ mw.hook( 'mw.cx.draft.restoring' ).add(
+ this.setStatusMessage.bind( this, mw.msg( 'cx-draft-restoring'
) )
+ );
+ mw.hook( 'mw.cx.draft.restored' ).add(
+ this.setStatusMessage.bind( this, mw.msg( 'cx-draft-restored' )
)
+ );
mw.hook( 'mw.cx.draft.restore-failed' ).add( function () {
- self.$draftStatus.text( mw.msg( 'cx-draft-restore-failed' ) );
+ this.setStatusMessage( mw.msg( 'cx-draft-restore-failed' ) );
$( '.cx-widget__columns' ).addClass( 'disabled' );
- } );
+ }.bind( this ) );
$( window ).on( 'scroll resize', this.onWindowScroll.bind( this ) );
};
-mw.cx.ui.Header.prototype.updateSaveStatus = function ( status ) {
- var $status = this.$draftStatus,
- minutes = 0;
-
- $status.attr( 'title', mw.msg( 'cx-save-draft-tooltip' ) );
- clearTimeout( timer );
- if ( status === 'progress' ) {
- $status.text( mw.msg( 'cx-save-draft-saving' ) );
- } else if ( status === 'success' ) {
- $status.text( mw.msg( 'cx-save-draft-save-success', 0 ) );
- timer = setInterval( function () {
- minutes++;
- $status.text(
- mw.msg( 'cx-save-draft-save-success',
mw.language.convertNumber( minutes ) )
- );
- }, 60 * 1000 );
- } else if ( status === 'fail' ) {
- $status.text( mw.msg( 'cx-save-draft-error' ) );
- }
+mw.cx.ui.Header.prototype.setStatusMessage = function ( message ) {
+ this.statusbar.setLabel( message );
};
mw.cx.ui.Header.prototype.onWindowScroll = function () {
diff --git a/modules/ui/mw.cx.ui.TranslationView.js
b/modules/ui/mw.cx.ui.TranslationView.js
index fa50ba3..fa7a8c0 100644
--- a/modules/ui/mw.cx.ui.TranslationView.js
+++ b/modules/ui/mw.cx.ui.TranslationView.js
@@ -208,8 +208,6 @@
this.columns.sourceColumn.titleWidget.$element,
this.columns.translationColumn.titleWidget.$element
);
- // Translation title change is a change trigger for translation.
- this.emit( 'change' );
};
/**
@@ -221,3 +219,13 @@
mw.cx.ui.TranslationView.prototype.showMessage = function ( type, message,
details ) {
this.header.infobar.showMessage( type, message, details );
};
+
+mw.cx.ui.TranslationView.prototype.setStatusMessage = function ( message ) {
+ this.header.setStatusMessage( message );
+};
+
+mw.cx.ui.TranslationView.prototype.showConflictWarning = function (
translation ) {
+ mw.loader.using( 'ext.cx.translation.conflict' ).then( function () {
+ mw.hook( 'mw.cx.translation.conflict' ).fire( translation );
+ } );
+};
diff --git a/modules/ui/styles/mw.cx.ui.Header.less
b/modules/ui/styles/mw.cx.ui.Header.less
index d03d77e..3e5d4c8 100644
--- a/modules/ui/styles/mw.cx.ui.Header.less
+++ b/modules/ui/styles/mw.cx.ui.Header.less
@@ -54,7 +54,7 @@
}
}
-.cx-header__draft-status {
+.cx-header-draft-status {
.mw-ui-item;
.mw-ui-one-third;
color: @gray-dark;
diff --git a/modules/ui/translationunits/mw.cx.ui.SectionTranslationUnit.js
b/modules/ui/translationunits/mw.cx.ui.SectionTranslationUnit.js
index 6af4af7..848ccad 100644
--- a/modules/ui/translationunits/mw.cx.ui.SectionTranslationUnit.js
+++ b/modules/ui/translationunits/mw.cx.ui.SectionTranslationUnit.js
@@ -65,8 +65,8 @@
* @return {jQuery}
*/
mw.cx.ui.SectionTranslationUnit.prototype.createTranslationSection = function
( model ) {
- if ( model.translationDocument ) {
- return $( '<section>' ).html( model.translationDocument );
+ if ( model.targetDocument ) {
+ return $( '<section>' ).html( model.targetDocument );
} else {
return this.getPlaceholderSection();
}
@@ -140,51 +140,6 @@
if ( !this.translated ) {
this.removeHighlight();
}
-};
-
-/**
- * @inheritDoc
- */
-mw.cx.ui.SectionTranslationUnit.prototype.onChange = function () {
- this.view.emit( 'change' );
- this.translationUnits = this.buildSubTranslationUnits( this.model );
-};
-
-/**
- * Build sub translation units.
- * We might require to add and remove translation units as they get added or
removed.
- * For example, links can get added to section and the corresponding
translation unit
- * should reflect here.
- *
- * @param {mw.cx.dm.TranslationUnit} model
- * @return {mw.cx.dm.TranslationUnit[]} Array of sub translation units
- */
-mw.cx.ui.SectionTranslationUnit.prototype.buildSubTranslationUnits = function
( model ) {
- var submodels, name, translationUnits = [], translationUnit, i;
-
- submodels = model.getTranslationUnits();
-
- if ( !submodels ) {
- return translationUnits;
- }
-
- // XXX have a way to avoid creating translation units for unchanged
units
- for ( i = 0; i < submodels.length; i++ ) {
- name = submodels[ i ].constructor.static.name;
-
- translationUnit = mw.cx.ui.translationUnitFactory.create(
- name,
- submodels[ i ],
- this.view,
- this.toolFactory,
- this.config
- );
- translationUnit.setParentTranslationUnit( this );
- this.emit( 'subunit', translationUnit );
- translationUnits.push( translationUnit );
- }
-
- return translationUnits;
};
/* Register */
diff --git a/modules/ui/translationunits/mw.cx.ui.TranslationUnit.js
b/modules/ui/translationunits/mw.cx.ui.TranslationUnit.js
index ddd38e7..a7e9e7b 100644
--- a/modules/ui/translationunits/mw.cx.ui.TranslationUnit.js
+++ b/modules/ui/translationunits/mw.cx.ui.TranslationUnit.js
@@ -167,7 +167,9 @@
};
mw.cx.ui.TranslationUnit.prototype.onChange = function () {
+ this.model.emit( 'change' );
this.view.emit( 'change' );
+ this.buildSubTranslationUnits( this.model );
};
mw.cx.ui.TranslationUnit.prototype.remove = function () {
@@ -176,7 +178,7 @@
// that undo/redo wont work. Use a contenteditable element removal by
selecting
// the range for this element and remove
this.$translationSection.remove();
- this.view.emit( 'change' );
+ this.emit( 'change' );
};
mw.cx.ui.TranslationUnit.prototype.onParentTranslationStarted = function () {};
@@ -188,6 +190,39 @@
} );
};
+/**
+ * Build sub translation units.
+ * We might require to add and remove translation units as they get added or
removed.
+ * For example, links can get added to section and the corresponding
translation unit
+ * should reflect here.
+ *
+ * @param {mw.cx.dm.TranslationUnit} model
+ */
+mw.cx.ui.TranslationUnit.prototype.buildSubTranslationUnits = function ( model
) {
+ var submodels, name, translationUnit, i;
+
+ submodels = model.getTranslationUnits();
+
+ if ( !submodels ) {
+ return;
+ }
+
+ // XXX have a way to avoid creating translation units for unchanged
units
+ for ( i = 0; i < submodels.length; i++ ) {
+ name = submodels[ i ].constructor.static.name;
+
+ translationUnit = mw.cx.ui.translationUnitFactory.create(
+ name,
+ submodels[ i ],
+ this.view,
+ this.toolFactory,
+ this.config
+ );
+ translationUnit.setParentTranslationUnit( this );
+ this.emit( 'subunit', translationUnit );
+ }
+};
+
mw.cx.ui.TranslationUnit.prototype.toString = function() {
return this.constructor.name + '::' + this.constructor.static.name +
'::' + this.sourceDocument.id;
};
--
To view, visit https://gerrit.wikimedia.org/r/343030
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: merged
Gerrit-Change-Id: I3d43d775eedf2f2133accf0c62dbccd62190f538
Gerrit-PatchSet: 12
Gerrit-Project: mediawiki/extensions/ContentTranslation
Gerrit-Branch: master
Gerrit-Owner: Santhosh <[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