jenkins-bot has submitted this change and it was merged. Change subject: Source Selector: Refactor source selector ......................................................................
Source Selector: Refactor source selector * Updates error message logic for when: * Source page does not exist * An equivalent target page exists * An equivalent target page exists and the chosen target title is in use * The chosen target title is in use * Start translation button only enabled after entering a source title and choosing languages; disabled if source page does not exist * New labels and message strings: From, To, placeholders * Styling improvements to match design mocks * Start translation on enter key if necessary fields are filled in Change-Id: I6dbf7bfe79d5885e5507f3b3a22a488f8fbab6c0 --- M Resources.php M i18n/en.json M i18n/qqq.json M modules/source/ext.cx.source.selector.js M modules/source/styles/ext.cx.source.selector.less 5 files changed, 409 insertions(+), 32 deletions(-) Approvals: Santhosh: Looks good to me, approved jenkins-bot: Verified diff --git a/Resources.php b/Resources.php index 328a56d..61cad91 100644 --- a/Resources.php +++ b/Resources.php @@ -148,6 +148,12 @@ 'cx-sourceselector-dialog-button-start-translation', 'cx-sourceselector-dialog-source-language-label', 'cx-sourceselector-dialog-target-language-label', + 'cx-sourceselector-dialog-source-title-placeholder', + 'cx-sourceselector-dialog-target-title-placeholder', + 'cx-sourceselector-dialog-error-page-and-title-exist', + 'cx-sourceselector-dialog-error-page-exists', + 'cx-sourceselector-dialog-error-title-in-use', + 'cx-sourceselector-dialog-error-no-source-article', ), ) + $resourcePaths; diff --git a/i18n/en.json b/i18n/en.json index c34d025..7d0e982 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -79,8 +79,14 @@ "cx-error-page-not-found": "The \"$1\" page could not be found in $2 Wikipedia", "cx-sourceselector-dialog-new-translation": "New translation", "cx-sourceselector-dialog-button-start-translation": "Start translation", - "cx-sourceselector-dialog-source-language-label": "Source:", - "cx-sourceselector-dialog-target-language-label": "Target:", + "cx-sourceselector-dialog-source-language-label": "From:", + "cx-sourceselector-dialog-target-language-label": "To:", + "cx-sourceselector-dialog-source-title-placeholder": "Search for source article", + "cx-sourceselector-dialog-target-title-placeholder": "Translation title (if different from source)", + "cx-sourceselector-dialog-error-page-and-title-exist": "The page already exists in [$1 $2] and the title is used by [$3 a different article]", + "cx-sourceselector-dialog-error-page-exists": "The page [$1 already exists] in $2", + "cx-sourceselector-dialog-error-title-in-use": "The title for the new page is [$1 already in use]", + "cx-sourceselector-dialog-error-no-source-article": "The page to translate does not exist in $1", "cx-mt-abuse-warning-title": "Your translation contains $1% of unmodified machine-translated text", "cx-mt-abuse-warning-text": "Machine translations are provided only as a template. You need to make sure that the content is accurate and reads naturally in your language.", "cx-publish-captcha-title": "Security question", diff --git a/i18n/qqq.json b/i18n/qqq.json index a3d413f..455086d 100644 --- a/i18n/qqq.json +++ b/i18n/qqq.json @@ -85,6 +85,12 @@ "cx-sourceselector-dialog-button-start-translation": "Button label for translation selector. Clicking on it starts a new translation in Special:ContentTranslation.", "cx-sourceselector-dialog-source-language-label": "Label text for source language and title selector.\n{{Identical|Source}}", "cx-sourceselector-dialog-target-language-label": "Label text for target language and title selector.\n{{Identical|Target}}", + "cx-sourceselector-dialog-source-title-placeholder": "Placeholder for the source title input. Provides prompt to search for source title.", + "cx-sourceselector-dialog-target-title-placeholder": "Placeholder for the target title input. Provides prompt to enter translation title (optional).", + "cx-sourceselector-dialog-error-page-and-title-exist": "Error that indicates there is page in the target wiki with the same title as the proposed source title and the target title is used elsewhere.\n\nParameters:\n* $1 - link to existing target article.\n* $2 - target language name.\n* $3 - link to article using proposed target title.", + "cx-sourceselector-dialog-error-page-exists": "Error that indicates there is a page in the target wiki with the same title as the proposed source title.\n\nParameters:\n* $1 - link to existing target article.\n* $2 - target language name.", + "cx-sourceselector-dialog-error-title-in-use": "Error that indicates there is already an article in the target wiki with the same title as the proposed target title.\n\nParameters:\n* $1 - link to target article with same title", + "cx-sourceselector-dialog-error-no-source-article": "Error that indicates there is no page with the specified title in the source language to translate.\n\nParameters:\n* $1 - the source language", "cx-mt-abuse-warning-title": "Title text shown in machine translation abuse card.\n* $1: Percentage of machine translation", "cx-mt-abuse-warning-text": "Detailed explanation of machine translation abuse.", "cx-publish-captcha-title": "Title of captcha form while publishing the translation", diff --git a/modules/source/ext.cx.source.selector.js b/modules/source/ext.cx.source.selector.js index 956ac59..a561f21 100644 --- a/modules/source/ext.cx.source.selector.js +++ b/modules/source/ext.cx.source.selector.js @@ -29,6 +29,9 @@ this.$targetLanguage = null; this.sourceLanguage = null; this.targetLanguage = null; + this.$messageBar = null; + this.$sourceTitleInput = null; + this.$targetTitleInput = null; this.init(); } @@ -126,26 +129,334 @@ * Listen for events. */ CXSourceSelector.prototype.listen = function () { - var selector = this; - // Open or close the dialog when clicking the link. // The dialog will be unitialized until the first click. this.$trigger.click( $.proxy( this.show, this ) ); - this.$sourceTitleInput.on( 'input', $.debounce( 100, false, function () { - selector.sourceLanguage = selector.$sourceLanguage.val(); - selector.searchTitles( selector.sourceLanguage, $( this ).val() ).done( function ( response ) { - var i, len, suggestions = response[ 1 ]; - selector.$titleList.empty(); - if ( suggestions.length ) { - for ( i = 0, len = suggestions.length; i < len; i++ ) { - selector.$titleList.append( $( '<option>' ).attr( 'value', suggestions[ i ] ) ); + // Source title input input (search titles) + this.$sourceTitleInput.on( + 'input', + $.debounce( 100, false, $.proxy( this.searchHandler, this ) ) + ); + + // Source language selector change (fill target languages, localStorage, check) + this.$sourceLanguage.on( + 'change', + $.proxy( this.sourceLanguageChangeHandler, this ) + ); + + // Target language selector change (localStorage, check) + this.$targetLanguage.on( + 'change', + $.proxy( this.targetLanguageChangeHandler, this ) + ); + + // Source title input or target title input, blur or search (check) + this.$dialog.on( + 'blur search', + '.cx-sourceselector-dialog__title', + $.proxy( this.check, this ) + ); + + // Keypress (start translation on enter key) + this.$dialog.on( + 'keypress', + '.cx-sourceselector-dialog__title', + $.proxy( this.enterKeyHandler, this ) + ); + }; + + /** + * Handles searching for titles based on source title input + */ + CXSourceSelector.prototype.searchHandler = function () { + var selector = this; + + this.sourceLanguage = this.$sourceLanguage.val(); + this.searchTitles( this.sourceLanguage, this.$sourceTitleInput.val() ).done( function ( response ) { + var i, len, suggestions = response[ 1 ]; + selector.$titleList.empty(); + if ( suggestions.length ) { + for ( i = 0, len = suggestions.length; i < len; i++ ) { + selector.$titleList.append( $( '<option>' ).attr( 'value', suggestions[ i ] ) ); + } + } + } ); + }; + + /** + * Handles source language change + */ + CXSourceSelector.prototype.sourceLanguageChangeHandler = function () { + this.fillTargetLanguages(); + if ( localStorage ) { + localStorage.cxSourceLanguage = this.$sourceLanguage.val(); + localStorage.cxTargetLanguage = this.$targetLanguage.val(); + } + this.check(); + }; + + /** + * Handles target language change + */ + CXSourceSelector.prototype.targetLanguageChangeHandler = function () { + if ( localStorage ) { + localStorage.cxTargetLanguage = this.$targetLanguage.val(); + } + this.check(); + }; + + /** + * Handles enter keypress + */ + CXSourceSelector.prototype.enterKeyHandler = function ( e ) { + var sourceLanguage = this.$sourceLanguage.val(), + sourceTitle = this.$sourceTitleInput.val().trim(), + selector = this; + + if ( e.which === 13 && sourceTitle !== '' ) { + this.checkForTitle( sourceLanguage, sourceTitle ) + .done( function ( response ) { + if ( response !== false ) { + selector.startPageInCX(); } + } ); + } + }; + + /** + * Checks source and target inputs for errors. + */ + CXSourceSelector.prototype.check = function () { + var sourceLanguage = this.$sourceLanguage.val(), + targetLanguage = this.$targetLanguage.val(), + sourceTitle = this.$sourceTitleInput.val().trim(), + targetTitle = this.$targetTitleInput.val().trim(), + selector = this; + + this.$messageBar.hide(); + + // if source title is blank, disable button and skip validation + if ( sourceTitle === '' ) { + selector.$translateFromButton.prop( 'disabled', true ); + return; + } + + // check to see if the specified source article exists + this.checkForTitle( sourceLanguage, sourceTitle ) + .done( function ( sourcePage ) { + // if no source page to translate disable button and show error + // skip rest of validation checks + if ( !sourcePage && sourceTitle !== '' ) { + selector.$translateFromButton.prop( 'disabled', true ); + selector.showSourceTitleError( sourceLanguage ); + } else { + selector.$translateFromButton.prop( 'disabled', false ); + // check to see if there is a matching article in the target wiki + // the matching article may or may not have the same title + selector.checkForEquivalentTargetPage( + sourceLanguage, + targetLanguage, + sourceTitle + ) + .done( function ( equivalentTargetPage ) { + // check to see if the specified target title is in use + // must be nested inside check for matching target article + // because first possible error requires results of both api calls + selector.checkForTitle( targetLanguage, targetTitle ) + .done( function ( existingTargetTitle ) { + // if there is a matching target page and + // the specified target title is in use + if ( equivalentTargetPage && existingTargetTitle ) { + selector.showPageExistsAndTitleInUseError( + equivalentTargetPage, + existingTargetTitle + ); + // if there is just an matching target page + } else if ( equivalentTargetPage ) { + selector.showPageExistsError( equivalentTargetPage ); + // if the specified target title is in use + } else if ( existingTargetTitle ) { + selector.showTitleInUseError( existingTargetTitle ); + } + } ); + } ); } } ); - } ) ); + }; - this.$sourceLanguage.on( 'change', $.proxy( this.fillTargetLanguages, this ) ); + /** + * Checks to see if a title exists in the specified language wiki + * @param {string} language the language of the wiki to check + * @param {string} title the title to look for + * return {jQuery.promise} + */ + CXSourceSelector.prototype.checkForTitle = function ( language, title ) { + var api = this.siteMapper.getApi( language ), + $deferred = $.Deferred(); + + api.get( { + action: 'opensearch', + search: title, + namespace: 0, + format: 'json', + limit: 1 + }, { + dataType: 'jsonp', + // This prevents warnings about the unrecognized parameter "_" + cache: true + } ) + .done( function ( response ) { + if ( response[ 1 ][ 0 ] === title ) { + $deferred.resolve( title ); + } else { + $deferred.resolve( false ); + } + } ) + .fail( function () { + $deferred.resolve( false ); + } ); + return $deferred.promise(); + }; + + /** + * Checks for an equivalent page in the target wiki based on sourceTitle + * @param {string} sourceLanguage the source language + * @param {string} targetLanguage the target language + * @param {string} sourceTitle the title to check + * @return {jQuery.promise} + */ + CXSourceSelector.prototype.checkForEquivalentTargetPage = function ( + sourceLanguage, + targetLanguage, + sourceTitle + ) { + var api = this.siteMapper.getApi( sourceLanguage ), + $deferred = $.Deferred(); + + api.get( { + action: 'query', + prop: 'langlinks', + titles: sourceTitle, + lllang: targetLanguage, + lllimit: 1, + redirects: true, + format: 'json' + }, { + dataType: 'jsonp', + cache: true + } ).done( function ( response ) { + var equivalentTargetPage = false; + if ( response.query && response.query.pages ) { + $.each( response.query.pages, function ( pageId, page ) { + if ( page.langlinks ) { + equivalentTargetPage = page.langlinks[ 0 ][ '*' ]; + } + } ); + } + $deferred.resolve( equivalentTargetPage ); + } ).fail( function () { + $deferred.resolve( false ); + } ); + + return $deferred.promise(); + }; + + /** + * Shows error for source page not existing + * @param {string} sourceLanguage the source language language code + */ + CXSourceSelector.prototype.showSourceTitleError = function ( sourceLanguage ) { + var sourceLanguageDisplay, message; + + sourceLanguageDisplay = $.uls.data.getAutonym( sourceLanguage ); + message = mw.message( + 'cx-sourceselector-dialog-error-no-source-article', + sourceLanguageDisplay + ); + this.showMessage( message ); + }; + + /** + * Shows error for target page existing and target title in use + * @param {string} equivalentTargetPage the title of the existing page + * @param {string} existingTargetTitle the title already in use + */ + CXSourceSelector.prototype.showPageExistsAndTitleInUseError = function ( + equivalentTargetPage, + existingTargetTitle + ) { + var targetLanguage, equivalentTargetPageLink, targetLanguageDisplay, + existingTargetTitleLink, message; + + targetLanguage = this.$targetLanguage.val(); + equivalentTargetPageLink = this.siteMapper + .getPageUrl( targetLanguage, equivalentTargetPage ); + targetLanguageDisplay = $.uls.data.getAutonym( targetLanguage ); + existingTargetTitleLink = this.siteMapper + .getPageUrl( targetLanguage, existingTargetTitle ); + message = mw.message( + 'cx-sourceselector-dialog-error-page-and-title-exist', + equivalentTargetPageLink, + targetLanguageDisplay, + existingTargetTitleLink + ); + this.showMessage( message ); + }; + + /** + * Shows error for page already existing in target + * @param {string} equivalentTargetPage the title of the existing page + */ + CXSourceSelector.prototype.showPageExistsError = function ( equivalentTargetPage ) { + var targetLanguage, equivalentTargetPageLink, + targetLanguageDisplay, message; + + targetLanguage = this.$targetLanguage.val(); + equivalentTargetPageLink = this.siteMapper + .getPageUrl( targetLanguage, equivalentTargetPage ); + targetLanguageDisplay = $.uls.data.getAutonym( targetLanguage ); + message = mw.message( + 'cx-sourceselector-dialog-error-page-exists', + equivalentTargetPageLink, targetLanguageDisplay + ); + this.showMessage( message ); + }; + + /** + * Shows error for title already in use in target wiki + * @param {string} existingTargetTitle the title already in use + */ + CXSourceSelector.prototype.showTitleInUseError = function ( existingTargetTitle ) { + var targetLanguage, existingTargetTitleLink, message; + + targetLanguage = this.$targetLanguage.val(); + existingTargetTitleLink = this.siteMapper + .getPageUrl( targetLanguage, existingTargetTitle ); + message = mw.message( + 'cx-sourceselector-dialog-error-title-in-use', + existingTargetTitleLink + ); + this.showMessage( message ); + }; + + /** + * Shows error message for dialog + * @param {mw.Message|text} message the message to show + */ + CXSourceSelector.prototype.showMessage = function ( message ) { + var $messageText = $( '.cx-sourceselector-dialog__messagebar-text' ); + + if ( message instanceof mw.Message ) { + $messageText.html( message.parse() ); + } else { + $messageText.text( message ); + } + + this.$messageBar.find( 'a' ) + .attr( 'target', '_blank' ); + + this.$messageBar.show(); }; /** @@ -210,9 +521,16 @@ * Start a new page translation in Special:CX */ CXSourceSelector.prototype.startPageInCX = function () { + var targetTitle; + + if ( this.$targetTitleInput.val() === '' ) { + targetTitle = this.$sourceTitleInput.val(); + } else { + targetTitle = this.$targetTitleInput.val(); + } location.href = this.siteMapper.getCXUrl( this.$sourceTitleInput.val(), - this.$targetTitleInput.val(), + targetTitle, this.$sourceLanguage.val(), this.$targetLanguage.val() ); @@ -226,6 +544,7 @@ $sourceLanguageLabel, $heading, $targetLanguageLabel, $sourceInputs, $targetInputs, + $messageText, index; this.$dialog = $( '<div>' ) @@ -260,12 +579,15 @@ .attr( { name: 'sourceTitle', type: 'search', - list: 'searchresults' + list: 'searchresults', + placeholder: mw.msg( 'cx-sourceselector-dialog-source-title-placeholder' ) } ); + this.$targetTitleInput = $( '<input>' ) .addClass( 'cx-sourceselector-dialog__title' ) .attr( { - name: 'targetTitle' + name: 'targetTitle', + placeholder: mw.msg( 'cx-sourceselector-dialog-target-title-placeholder' ) } ); this.$titleList = $( '<datalist>' ).prop( 'id', 'searchresults' ); @@ -282,10 +604,18 @@ this.$targetLanguage, this.$targetTitleInput ); + this.$messageBar = $( '<div>' ) + .addClass( 'cx-sourceselector-dialog__messagebar' ); + $messageText = $( '<span>' ) + .addClass( 'cx-sourceselector-dialog__messagebar-text' ); + this.$messageBar + .append( $messageText ) + .hide(); this.$translateFromButton = $( '<button>' ) .addClass( 'mw-ui-button mw-ui-progressive cx-sourceselector-dialog__button-translate' ) .text( mw.msg( 'cx-sourceselector-dialog-button-start-translation' ) ) + .prop( 'disabled', true ) .click( $.proxy( this.startPageInCX, this ) ); $actions = $( '<div>' ).addClass( 'cx-sourceselector-dialog__actions' ) @@ -294,9 +624,25 @@ this.$dialog.append( $heading, $sourceInputs, $targetInputs, + this.$messageBar, $actions, this.$titleList ); + + if ( localStorage && localStorage.cxSourceLanguage ) { + this.$sourceLanguage.children().filter( function () { + return this.getAttribute( 'value' ) === localStorage.cxSourceLanguage; + } ) + .prop( 'selected', true ); + this.fillTargetLanguages(); + } + + if ( localStorage && localStorage.cxTargetLanguage ) { + this.$targetLanguage.children().filter( function () { + return this.getAttribute( 'value' ) === localStorage.cxTargetLanguage; + } ) + .prop( 'selected', true ); + } $( 'body' ).append( this.$dialog ); }; @@ -321,7 +667,7 @@ $container.empty().cxSourceSelector( { top: '150px', - left: '33%' + left: '30%' } ).click(); } ); } ); diff --git a/modules/source/styles/ext.cx.source.selector.less b/modules/source/styles/ext.cx.source.selector.less index eb71247..a788989 100644 --- a/modules/source/styles/ext.cx.source.selector.less +++ b/modules/source/styles/ext.cx.source.selector.less @@ -7,7 +7,8 @@ .cx-sourceselector-dialog { .mw-ui-grid; - .mw-ui-one-third; + .mw-ui-item; + .mw-ui-four-tenths; color: #333; position: absolute; min-width: 500px; @@ -17,6 +18,7 @@ border-bottom-width: 3px; border-radius: 3px; z-index: 100; + padding: 0; } .cx-sourceselector-dialog__heading { @@ -25,37 +27,44 @@ .mw-ui-item; .mw-ui-one-whole; padding: @vertical-margin @horizontal-margin; - font-size: larger; + font-size: 2em; line-height: 1.5em; - background-color: #fbfbfb; + border-bottom: 1px solid #ccc; + font-weight: normal; } .cx-sourceselector-dialog__target-inputs, .cx-sourceselector-dialog__source-inputs { .mw-ui-item; .mw-ui-one-whole; + height: 50px; + padding: 10px; + border-bottom: 1px solid #ccc; } .cx-sourceselector-dialog__language-label { .mw-ui-item; - .mw-ui-one-sixth; + .mw-ui-one-eighth; padding: 5px; font-size: large; } .cx-sourceselector-dialog__language { .mw-ui-item; - .mw-ui-one-third; - margin: 5px 0; - font-size: large; + .mw-ui-two-tenths; + font-size: medium; + height: 30px; + margin-top: 5px; } .cx-sourceselector-dialog__title { .mw-ui-item; - .mw-ui-one-half; - margin: 5px 0; - font-size: large; + .mw-ui-six-tenths; + margin: 5px 10px 0; + font-size: medium; -webkit-appearance: none; + border: none; + padding: 2px; &[type=search] { .background-image-svg('../../tools/images/search.svg', '../../tools/images/search.png'); @@ -65,16 +74,20 @@ padding-left: 30px; } - border: 1px solid #ccc; + } + + .cx-sourceselector-dialog__messagebar { + .mw-ui-item; + .mw-ui-one-whole; + padding: 10px; + background-color: #F7D358; } .cx-sourceselector-dialog__actions { - .mw-ui-item; .mw-ui-one-whole; padding: 10px 15px 15px 15px; text-align: right; - border-top: 1px solid #aaa; - margin-top: 20px; + margin-top: 10px; button { margin-left: 10px; -- To view, visit https://gerrit.wikimedia.org/r/170657 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: merged Gerrit-Change-Id: I6dbf7bfe79d5885e5507f3b3a22a488f8fbab6c0 Gerrit-PatchSet: 12 Gerrit-Project: mediawiki/extensions/ContentTranslation Gerrit-Branch: master Gerrit-Owner: Jsahleen <jsahl...@wikimedia.org> Gerrit-Reviewer: Amire80 <amir.ahar...@mail.huji.ac.il> Gerrit-Reviewer: Jsahleen <jsahl...@wikimedia.org> Gerrit-Reviewer: Nikerabbit <niklas.laxst...@gmail.com> Gerrit-Reviewer: Pginer <pgi...@wikimedia.org> Gerrit-Reviewer: Santhosh <santhosh.thottin...@gmail.com> Gerrit-Reviewer: Siebrand <siebr...@kitano.nl> Gerrit-Reviewer: jenkins-bot <> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits