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