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 <[email protected]>
Gerrit-Reviewer: Amire80 <[email protected]>
Gerrit-Reviewer: Jsahleen <[email protected]>
Gerrit-Reviewer: Nikerabbit <[email protected]>
Gerrit-Reviewer: Pginer <[email protected]>
Gerrit-Reviewer: Santhosh <[email protected]>
Gerrit-Reviewer: Siebrand <[email protected]>
Gerrit-Reviewer: jenkins-bot <>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits