Nikerabbit has uploaded a new change for review. ( 
https://gerrit.wikimedia.org/r/351894 )

Change subject: CX2: Integrate MT with SectionTranslationUnit
......................................................................

CX2: Integrate MT with SectionTranslationUnit

Now works:
* Adapting sections with MT and the special providers reset, source, scratch
* Switching between prodivers, remembering the last state for the section
* Setting default provider for new sections

Known issues:
* Does not work restore things properly when restoring from draft
* If MT provider fails, it falls back to source, overwriting any
  possible previous value for source. Probably not worth fixing.

Unrelated changes:
* Add oojs to model dependecy to avoid intermittent JS errors
* Fix token expiration check in MTService, ended up in infinite loop
* Change a bit the way how placeholder and highlighting is removed
  to make it more robust

Bug: T162110
Change-Id: Id11f3de91562cab80e52bc00f06b2f41e74188bd
---
M extension.json
M modules/dm/translationunits/mw.cx.dm.SectionTranslationUnit.js
M modules/mw.cx.MachineTranslationManager.js
M modules/mw.cx.MachineTranslationService.js
M modules/tools/mw.cx.tools.MachineTranslationTool.js
M modules/tools/styles/mw.cx.tools.MachineTranslationTool.less
M modules/ui/translationunits/mw.cx.ui.SectionTranslationUnit.js
7 files changed, 237 insertions(+), 53 deletions(-)


  git pull 
ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/ContentTranslation 
refs/changes/94/351894/1

diff --git a/extension.json b/extension.json
index 25f7ac7..952b967 100644
--- a/extension.json
+++ b/extension.json
@@ -1209,7 +1209,8 @@
                                "dm/mw.cx.dm.ModelFactory.js"
                        ],
                        "dependencies": [
-                               "mw.cx.dm"
+                               "mw.cx.dm",
+                               "oojs"
                        ]
                },
                "mw.cx.dm.Translation": {
diff --git a/modules/dm/translationunits/mw.cx.dm.SectionTranslationUnit.js 
b/modules/dm/translationunits/mw.cx.dm.SectionTranslationUnit.js
index f51aa14..7b189a2 100644
--- a/modules/dm/translationunits/mw.cx.dm.SectionTranslationUnit.js
+++ b/modules/dm/translationunits/mw.cx.dm.SectionTranslationUnit.js
@@ -11,6 +11,14 @@
  */
 mw.cx.dm.SectionTranslationUnit = function SectionTranslationUnit( config, 
translation, sourceDocument, targetDocument ) {
        mw.cx.dm.SectionTranslationUnit.super.call( this, config, translation, 
sourceDocument, targetDocument );
+
+       this.MTService = config.MTService;
+       this.MTManager = config.MTManager;
+       // XXX: When restoring, set this properly
+       this.MTProvider = null;
+
+       this.documentsPerProvider = {};
+
        this.init();
 };
 
@@ -25,23 +33,111 @@
 };
 
 /**
- * Translate the section using machine translation if available
+ * Translate the section using machine translation if available, with 
appropriate fallback.
  *
+ * @private
  * @param {Element} sourceDocument Source section DOM Element
- * @return {Element} Translated document
+ * @return {jQuery.Promise} Element as first param. Should never reject.
  */
 mw.cx.dm.SectionTranslationUnit.prototype.translate = function ( 
sourceDocument ) {
-       // TODO: Use Machine translation
-       return sourceDocument.cloneNode( true );
+       return this.getMTProvider().then( function ( provider ) {
+               var deferred = $.Deferred();
+
+               if ( provider === 'source' ) {
+                       deferred.resolve( sourceDocument.cloneNode( true ) );
+               } else if ( provider === 'scratch' ) {
+                       deferred.resolve( sourceDocument.cloneNode( false ) );
+               } else {
+                       deferred = this.MTService.translate( 
sourceDocument.outerHTML, provider ).then(
+                               this.translateSuccess.bind( this ),
+                               this.translateError.bind( this, sourceDocument )
+                       );
+               }
+
+               return deferred;
+       }.bind( this ) );
 };
 
 /**
- * Adapt the section for target language
+ * Convert MT string result from HTML string to Element.
+ *
+ * @private
+ * @param {string} html
+ * @return {Element} Similar as for this.translate.
  */
-mw.cx.dm.SectionTranslationUnit.prototype.adapt = function () {
-       this.targetDocument = this.translate( this.sourceDocument );
-       // Udapte target id attributes
-       this.setTargetId();
+mw.cx.dm.SectionTranslationUnit.prototype.translateSuccess = function ( html ) 
{
+       return $( html )[ 0 ];
+};
+
+/**
+ * Handle errors for translate. Will fall back to non-MT provider.
+ *
+ * @private
+ * @param {Element} sourceDocument Similar as for this.translate.
+ * @return {jQuery.Promise} Similar as for this.translate.
+ */
+mw.cx.dm.SectionTranslationUnit.prototype.translateError = function ( 
sourceDocument ) {
+       // Translation with non-MT provider must never fail or we will have 
infinite loop
+       return this.MTManager.getDefaultNonMTProvider().then( function ( 
provider ) {
+               var deferred = $.Deferred();
+
+               // XXX i18n
+               mw.hook( 'mw.cx.error' ).fire( 'Unable to fetch machine 
translation.' );
+               this.MTProvider = provider;
+               // Convert failure to success
+               this.translate( sourceDocument ).done( deferred.resolve );
+               return deferred;
+       }.bind( this ) );
+};
+
+/**
+ * Adapt the section for target language. Adapt can mean one of three things:
+ * - revert any modifications done with current provider (reset)
+ * - use source text (source)
+ * - use empty text (scratch)
+ * - use machine translation (and fallback to either of above if it fails)
+ *
+ * The UI SectionTranslationUnit calls this without parameter, while the
+ * MachineTranslationTool will call with the provider chosen by the user.
+ *
+ * @param {string} [requestedProvider]
+ * @fires adapt
+ */
+mw.cx.dm.SectionTranslationUnit.prototype.adapt = function ( requestedProvider 
) {
+       if ( this.MTProvider && this.targetDocument ) {
+               // Store the current version so that it can be restored
+               this.documentsPerProvider[ this.MTProvider ] = 
this.targetDocument;
+       }
+
+       if ( requestedProvider !== 'reset' ) {
+               if ( requestedProvider ) {
+                       this.MTProvider = requestedProvider;
+               }
+
+               // Use the cached version
+               if ( this.documentsPerProvider[ requestedProvider ] ) {
+                       this.targetDocument = this.documentsPerProvider[ 
requestedProvider ];
+                       this.emit( 'adapt', this.targetDocument, 
requestedProvider );
+                       return;
+               }
+       }
+
+       this.translate( this.sourceDocument ).then( function ( document ) {
+               this.targetDocument = document;
+               this.setTargetId();
+               // Note that this.MTProvider might have changed from 
requestedProvider
+               this.emit( 'adapt', document, this.MTProvider );
+       }.bind( this ) );
+};
+
+mw.cx.dm.SectionTranslationUnit.prototype.getMTProvider = function () {
+       if ( this.MTProvider ) {
+               return $.Deferred().resolve( this.MTProvider );
+       }
+
+       return this.MTManager.getPreferredProvider().done( function ( provider 
) {
+               this.MTProvider = provider;
+       }.bind( this ) );
 };
 
 /* Register */
diff --git a/modules/mw.cx.MachineTranslationManager.js 
b/modules/mw.cx.MachineTranslationManager.js
index c49fc25..a9f5e85 100644
--- a/modules/mw.cx.MachineTranslationManager.js
+++ b/modules/mw.cx.MachineTranslationManager.js
@@ -84,7 +84,8 @@
                        return sourceDir === targetDir ? 'source' : 'scratch';
                }.bind( this ),
                function () {
-                       return 'source';
+                       // Convert failure to success
+                       return $.Deferred().resolve( 'source' ).promise();
                }
        );
 };
diff --git a/modules/mw.cx.MachineTranslationService.js 
b/modules/mw.cx.MachineTranslationService.js
index c82028d..f06ac06 100644
--- a/modules/mw.cx.MachineTranslationService.js
+++ b/modules/mw.cx.MachineTranslationService.js
@@ -34,7 +34,12 @@
 mw.cx.MachineTranslationService.prototype.translate = function ( text, 
provider ) {
        return this.validateProvider( provider )
                .then( this.getCXServerToken.bind( this ) )
-               .then( this.fetchTranslation.bind( this, provider, text ) );
+               .then( this.fetchTranslation.bind( this, text, provider ) )
+               .then( function ( output ) {
+                       // Some hacks around the fact that cxserver always wraps
+                       // everything in a div (it shouldn't)
+                       return $( output )[ 0 ].innerHTML;
+               } );
 };
 
 /**
@@ -106,7 +111,7 @@
 mw.cx.MachineTranslationService.prototype.validateProvider = function ( 
provider ) {
        return this.getProviders().then( function ( providers ) {
                if ( providers.indexOf( provider ) === -1 ) {
-                       return $.Deferred.reject( 'Unknown provider' 
).promise();
+                       return $.Deferred().reject( 'Unknown provider', 
provider ).promise();
                }
        } );
 };
@@ -136,7 +141,7 @@
 
        return this.tokenPromise.then( function ( token ) {
                var now = Math.floor( Date.now() / 1000 );
-               if ( 'refreshAt' in token && token.refreshAt >= now ) {
+               if ( 'refreshAt' in token && token.refreshAt <= now ) {
                        this.tokenPromise = undefined;
                        return this.getCXServerToken();
                }
diff --git a/modules/tools/mw.cx.tools.MachineTranslationTool.js 
b/modules/tools/mw.cx.tools.MachineTranslationTool.js
index a161a8e..73315fd 100644
--- a/modules/tools/mw.cx.tools.MachineTranslationTool.js
+++ b/modules/tools/mw.cx.tools.MachineTranslationTool.js
@@ -44,53 +44,123 @@
 };
 
 mw.cx.tools.MachineTranslationTool.prototype.getContent = function () {
-       // XXX: implement reset functionality
+       var menu, model = this.model;
 
        this.mtProviderSelector = new OO.ui.DropdownWidget( {
                classes: [ 'card-mt-providers-menu' ]
        } );
 
+       menu = this.mtProviderSelector.getMenu();
+
        this.MTManager.getAvailableProviders().then( function ( providers ) {
+               var reset = new OO.ui.MenuOptionWidget( {
+                       data: 'reset',
+                       label: this.getProviderLabel( 'reset' ),
+                       classes: [ 'card-mt-providers-menu-reset' ]
+               } );
+               menu.addItems( reset );
+
                providers.forEach( function ( id ) {
                        var item = new OO.ui.MenuOptionWidget( {
                                data: id,
                                label: this.getProviderLabel( id )
                        } );
-                       this.mtProviderSelector.getMenu().addItems( item );
-
-                       // XXX
-                       // model.getActiveMTProvider()
-
-                       this.MTManager.getPreferredProvider().then( function ( 
provider ) {
-                               
this.mtProviderSelector.getMenu().selectItemByData( provider );
-                       }.bind( this ) );
+                       menu.addItems( item );
                }.bind( this ) );
+
+               // This also sets the listener for 'select' event
+               model.getMTProvider().done( this.selectProvider.bind( this ) );
+               model.on( 'adapt', this.onModelAdapted, [], this );
        }.bind( this ) );
 
        return this.mtProviderSelector.$element;
 };
 
-mw.cx.tools.MachineTranslationTool.prototype.changeProvider = function() {
-       // XXX
-       // mode.setMTProvider( value );
-       // model.update( ...new content... );
+/* Private methods */
+
+/**
+ * Maps provider id to human readable label.
+ * @private
+ * @param {string} provider Id of the provider.
+ * @return {string} Translated label
+ */
+mw.cx.tools.MachineTranslationTool.prototype.getProviderLabel = function ( 
provider ) {
+       var labels = {
+               Yandex: mw.msg( 'cx-tools-mt-provider-title', 
'Yandex.Translate' ),
+               scratch: mw.msg( 'cx-tools-mt-dont-use' ),
+               source: mw.msg( 'cx-tools-mt-use-source' ),
+               reset: mw.msg( 'cx-tools-mt-reset' )
+       };
+
+       return labels[ provider ] || mw.msg( 'cx-tools-mt-provider-title', 
provider );
 };
 
+/**
+ * Callback when the user selects the 'set as default' action. This does not 
affect
+ * current section in any way, because it already has the selected provider in 
user.
+ * @private
+ */
 mw.cx.tools.MachineTranslationTool.prototype.setDefaultProvider = function () {
        var provider = 
this.mtProviderSelector.getMenu().getSelectedItem().getData();
        this.MTManager.setPreferredProvider( provider );
 };
 
-/* Private methods */
+/**
+ * Callback when the user selects a provider from the menu. To avoid recursion 
and
+ * unnecessary updates, this function will return early until the model has 
signaled
+ * it has finished adapting AND we have updated the selected item in the menu 
to match
+ * what was actually the used provider (which may not be what the user 
requested, if
+ * an MT provider fails and we fallback to something else).
+ *
+ * @private
+ * @param {OO.ui.MenuOptionWidget} item
+ */
+mw.cx.tools.MachineTranslationTool.prototype.changeProvider = function ( item 
) {
+       this.model.adapt( item.getData() );
+};
 
-mw.cx.tools.MachineTranslationTool.prototype.getProviderLabel = function ( 
provider ) {
-       var labels = {
-               Yandex: mw.msg( 'cx-tools-mt-provider-title', 
'Yandex.Translate' ),
-               scratch: mw.msg( 'cx-tools-mt-dont-use' ),
-               source: mw.msg( 'cx-tools-mt-use-source' )
-       };
+/**
+ * Callback when the model has updated itself. Wraps this.selectProvider.
+ *
+ * @private
+ * @param {Element} document Ignored
+ * @param {string} provider The actually used provider
+ */
+mw.cx.tools.MachineTranslationTool.prototype.onModelAdapted = function ( 
document, provider ) {
+       this.selectProvider( provider );
+};
 
-       return labels[ provider ] || mw.msg( 'cx-tools-mt-provider-title', 
provider );
+/**
+ * Mark given provider as selected.
+ *
+ * @private
+ * @param {string} provider
+ */
+mw.cx.tools.MachineTranslationTool.prototype.selectProvider = function ( 
provider ) {
+       var item, selectedProvider, menu;
+
+       menu = this.mtProviderSelector.getMenu();
+       selectedProvider = menu.getSelectedItem() && 
menu.getSelectedItem().getData();
+
+       // Yay, nothing to do
+       if ( provider === selectedProvider ) {
+               return;
+       }
+
+       // Validate and fix the given provider if required
+       item = menu.getItemFromData( provider );
+       if ( provider === undefined || !item ) {
+               // Fallback to something that always exists
+               provider = 'source';
+       }
+
+       // Hack to avoid calling changeProvider which this method is called 
from onModelAdapted
+       menu.off( 'select', this.changeProvider, this );
+       menu.once( 'select', function () {
+               menu.on( 'select', this.changeProvider, [], this );
+       }.bind( this ) );
+
+       menu.selectItemByData( provider );
 };
 
 /* Register */
diff --git a/modules/tools/styles/mw.cx.tools.MachineTranslationTool.less 
b/modules/tools/styles/mw.cx.tools.MachineTranslationTool.less
index 3e0d6ad..962fe3a 100644
--- a/modules/tools/styles/mw.cx.tools.MachineTranslationTool.less
+++ b/modules/tools/styles/mw.cx.tools.MachineTranslationTool.less
@@ -11,3 +11,7 @@
                }
        }
 }
+
+.card-mt-providers-menu-reset {
+       border-bottom: 4px solid #eaecf0;
+}
diff --git a/modules/ui/translationunits/mw.cx.ui.SectionTranslationUnit.js 
b/modules/ui/translationunits/mw.cx.ui.SectionTranslationUnit.js
index 0064ccb..84726f4 100644
--- a/modules/ui/translationunits/mw.cx.ui.SectionTranslationUnit.js
+++ b/modules/ui/translationunits/mw.cx.ui.SectionTranslationUnit.js
@@ -13,6 +13,8 @@
        mw.cx.ui.mixin.AlignableTranslationUnit.call( this );
        // Click handler to translate need to execute only once.
        this.once( 'click', this.translate.bind( this ) );
+
+       model.on( 'adapt', this.onModelAdapted.bind( this ) );
 };
 
 /* Setup */
@@ -99,21 +101,17 @@
 };
 
 /**
- * Translate the section.
- * Either copy the source or use an MT tool to apply the initial translation
+ * Fill in the contents of the target section if it does not exist at all.
  */
 mw.cx.ui.SectionTranslationUnit.prototype.translate = function () {
        this.removeHighlight();
+       this.removePlaceholder();
 
        if ( !this.isTranslated() ) {
-               mw.log( '[CX] Translating section: ' + this );
-               // Adapt in general will be asynchronous operation
+               mw.log( '[CX] Adapting to replace placeholder: ' + this, 
this.$sourceSection[ 0 ] );
+               // Adapt is usually an async operation. The model emits 'adapt' 
event when it is
+               // ready, which is then handled in this.onModelAdapted.
                this.model.adapt();
-               this.setContent( this.model.targetDocument );
-               this.buildSubTranslationUnits( this.model );
-               this.emit( 'change' );
-       } else {
-               this.buildSubTranslationUnits( this.model );
        }
 
        if ( this.isEditable() ) {
@@ -121,16 +119,27 @@
        }
 };
 
+mw.cx.ui.SectionTranslationUnit.prototype.onModelAdapted = function ( 
document, provider ) {
+       mw.log( '[CX] Adapted section: ' + this, document, provider );
+       this.setContent( document );
+       this.buildSubTranslationUnits( this.model );
+       this.emit( 'change' );
+};
+
+/**
+ * Sets the translation section html content.
+ *
+ * @private
+ * @param {mixed} content Accepts similar values as $.fn.append().
+ */
 mw.cx.ui.SectionTranslationUnit.prototype.setContent = function ( content ) {
-       if ( !content ) {
-               return;
-       }
-       this.removePlaceholder();
-       this.$translationSection.html( content );
+       this.$translationSection.empty().append( content );
 };
 
 mw.cx.ui.SectionTranslationUnit.prototype.removePlaceholder = function () {
-       this.$translationSection.removeClass( 'cx-placeholder' );
+       if ( this.$translationSection.hasClass( 'cx-placeholder' ) ) {
+               this.$translationSection.removeClass( 'cx-placeholder' 
).empty();
+       }
 };
 
 mw.cx.ui.SectionTranslationUnit.prototype.isTranslated = function () {
@@ -150,9 +159,7 @@
  * @inheritDoc
  */
 mw.cx.ui.SectionTranslationUnit.prototype.onMouseLeave = function () {
-       if ( !this.isTranslated() ) {
-               this.removeHighlight();
-       }
+       this.removeHighlight();
 };
 
 mw.cx.ui.SectionTranslationUnit.prototype.addLink = function ( selection, 
title ) {

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

Gerrit-MessageType: newchange
Gerrit-Change-Id: Id11f3de91562cab80e52bc00f06b2f41e74188bd
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/ContentTranslation
Gerrit-Branch: master
Gerrit-Owner: Nikerabbit <niklas.laxst...@gmail.com>

_______________________________________________
MediaWiki-commits mailing list
MediaWiki-commits@lists.wikimedia.org
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to