jenkins-bot has submitted this change and it was merged. (
https://gerrit.wikimedia.org/r/372537 )
Change subject: Add article languages count
......................................................................
Add article languages count
Add langlinkscount API.
Add languages count while searching and selecting.
Add missing in target language information.
Bug: T111094
Change-Id: I8fffe5b2056b977f1524b1978789266c281239d9
---
A api/ApiQueryLangLinksCount.php
M extension.json
M i18n/api/en.json
M i18n/api/qqq.json
M i18n/en.json
M i18n/qqq.json
M modules/source/ext.cx.source.selector.js
A modules/source/images/languages.png
A modules/source/images/languages.svg
M modules/source/styles/ext.cx.source.selector.less
A modules/ui/styles/widgets/mw.cx.ui.TitleOptionWidget.less
M modules/ui/widgets/mw.cx.ui.PageSelectorWidget.js
A modules/ui/widgets/mw.cx.ui.TitleOptionWidget.js
13 files changed, 339 insertions(+), 5 deletions(-)
Approvals:
jenkins-bot: Verified
Nikerabbit: Checked; Looks good to me, approved
diff --git a/api/ApiQueryLangLinksCount.php b/api/ApiQueryLangLinksCount.php
new file mode 100644
index 0000000..f75e744
--- /dev/null
+++ b/api/ApiQueryLangLinksCount.php
@@ -0,0 +1,44 @@
+<?php
+/**
+ * A query module to get total number of langlinks (links to corresponding
foreign language pages).
+ *
+ * @file
+ * @copyright See AUTHORS.txt
+ * @license GPL-2.0+
+ */
+
+class ApiQueryLangLinksCount extends ApiQueryBase {
+ public function execute() {
+ if ( $this->getPageSet()->getGoodTitleCount() < 1 ) {
+ return;
+ }
+
+ $this->addTables( 'langlinks' );
+ $this->addFields( [
+ 'll_from',
+ 'COUNT(*) AS ll_count'
+ ] );
+ $this->addWhereFld( 'll_from', array_keys(
$this->getPageSet()->getGoodTitles() ) );
+ $this->addOption( 'GROUP BY', [ 'll_from' ] );
+
+ // Generated SQL query example
+ // SELECT /* ApiQueryLangLinksCount::execute */
ll_from,COUNT(*) AS ll_count
+ // FROM `langlinks` WHERE ll_from IN ('16','22') GROUP BY
ll_from
+ $res = $this->select( __METHOD__ );
+
+ foreach ( $res as $row ) {
+ $this->getResult()->addValue(
+ [ 'query', 'pages', $row->ll_from ],
+ $this->getModuleName(),
+ (int)$row->ll_count
+ );
+ }
+ }
+
+ protected function getExamplesMessages() {
+ return [
+
'action=query&prop=langlinkscount&titles=Dog&redirects=1'
+ => 'apihelp-query+langlinkscount-example-1',
+ ];
+ }
+}
diff --git a/extension.json b/extension.json
index e1dc515..6a9ff0c 100644
--- a/extension.json
+++ b/extension.json
@@ -56,6 +56,9 @@
"cxpublishedtranslations": "ApiQueryPublishedTranslations",
"cxtranslatorstats": "ApiQueryTranslatorStats"
},
+ "APIPropModules": {
+ "langlinkscount": "ApiQueryLangLinksCount"
+ },
"MessagesDirs": {
"ContentTranslation": "i18n",
"ContentTranslationApi": "i18n/api"
@@ -76,6 +79,7 @@
"ApiQueryContentTranslationLanguageTrend":
"api/ApiQueryContentTranslationLanguageTrend.php",
"ApiQueryContentTranslationStats":
"api/ApiQueryContentTranslationStats.php",
"ApiQueryContentTranslationSuggestions":
"api/ApiQueryContentTranslationSuggestions.php",
+ "ApiQueryLangLinksCount": "api/ApiQueryLangLinksCount.php",
"ApiQueryPublishedTranslations":
"api/ApiQueryPublishedTranslations.php",
"ApiQueryTranslatorStats": "api/ApiQueryTranslatorStats.php",
"ContentTranslationHooks": "ContentTranslation.hooks.php",
@@ -2050,8 +2054,23 @@
],
"dependencies": [
"mediawiki.widgets",
+ "mw.cx.ui.TitleOptionWidget",
"oojs-ui.styles.icons-interactions"
]
+ },
+ "mw.cx.ui.TitleOptionWidget": {
+ "scripts": [
+ "ui/widgets/mw.cx.ui.TitleOptionWidget.js"
+ ],
+ "styles": [
+
"ui/styles/widgets/mw.cx.ui.TitleOptionWidget.less"
+ ],
+ "messages": [
+ "cx-sourceselector-missing-in-target-language"
+ ],
+ "dependencies": [
+ "mediawiki.widgets"
+ ]
}
},
"ResourceFileModulePaths": {
diff --git a/i18n/api/en.json b/i18n/api/en.json
index dd7f545..9ca9768 100644
--- a/i18n/api/en.json
+++ b/i18n/api/en.json
@@ -116,5 +116,8 @@
"apierror-cx-mustbeloggedin-viewtranslations": "To view your
translations, you must log in.",
"apierror-cx-samelanguages": "Source and target languages cannot be the
same.",
"apierror-cx-suggestionsdisabled": "Suggestions not enabled for this
wiki.",
- "apierror-cx-translationnotfound": "Translation not found."
+ "apierror-cx-translationnotfound": "Translation not found.",
+ "apihelp-query+langlinkscount-description": "Get the number of other
languages that article exists in.",
+ "apihelp-query+langlinkscount-summary": "Get the number of other
language versions.",
+ "apihelp-query+langlinkscount-example-1": "Get the number of other
language versions for 'Dog' page"
}
diff --git a/i18n/api/qqq.json b/i18n/api/qqq.json
index a095870..f9bcd7b 100644
--- a/i18n/api/qqq.json
+++ b/i18n/api/qqq.json
@@ -113,5 +113,8 @@
"apierror-cx-mustbeloggedin-viewtranslations": "{{doc-apierror}}",
"apierror-cx-samelanguages": "{{doc-apierror}}",
"apierror-cx-suggestionsdisabled": "{{doc-apierror}}",
- "apierror-cx-translationnotfound": "{{doc-apierror}}"
+ "apierror-cx-translationnotfound": "{{doc-apierror}}",
+ "apihelp-query+langlinkscount-description":
"{{doc-apihelp-description|query+langlinkscount}}",
+ "apihelp-query+langlinkscount-summary":
"{{doc-apihelp-summary|query+langlinkscount}}",
+ "apihelp-query+langlinkscount-example-1":
"{{doc-apihelp-example|query+langlinkscount}}"
}
diff --git a/i18n/en.json b/i18n/en.json
index 6e39db4..449d4af 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -97,6 +97,7 @@
"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-sourceselector-missing-in-target-language": "Missing in $1",
"cx-mt-abuse-warning-title": "Your translation {{PLURAL:$1|contains}}
$1% of unmodified machine-translated text",
"cx-mt-abuse-warning-text": "Machine translation is provided only as a
starting point. 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 7f0a630..17430c7 100644
--- a/i18n/qqq.json
+++ b/i18n/qqq.json
@@ -106,6 +106,7 @@
"cx-sourceselector-dialog-error-page-exists": "An error message 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 page.\n*
$2 - target language name.",
"cx-sourceselector-dialog-error-title-in-use": "Error that indicates
there is already a page in the target wiki with the same title as the proposed
target title.\n\nParameters:\n* $1 - link to target page 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-sourceselector-missing-in-target-language": "Label appended to
search result in Special:ContentTranslation when using the 'Start new article'
feature, which indicates that matching article is missing in target
language.\n\nParameters:\n* $1 - Autonym name of the 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 752800f..f500149 100644
--- a/modules/source/ext.cx.source.selector.js
+++ b/modules/source/ext.cx.source.selector.js
@@ -740,7 +740,7 @@
};
CXSourceSelector.prototype.setSelectedItem = function ( item ) {
- var itemImage;
+ var itemImage, numOfLanguages;
this.sourcePageSelector.setValue( item.getData() );
@@ -755,6 +755,7 @@
} );
}
+ numOfLanguages = item.initialConfig.numOfLanguages;
this.$selectedItemInfo.append(
$( '<a>' ).prop( {
href: item.$label.prop( 'href' ),
@@ -763,6 +764,14 @@
text: item.$label.text()
} )
);
+ if ( numOfLanguages ) {
+ this.$selectedItemInfo.append(
+ $( '<span>' )
+ .addClass(
'cx-sourceselector-embedded-selected-item__language-count' )
+ .text( mw.language.convertNumber(
numOfLanguages ) )
+ );
+ }
+
this.$container
.toggleClass( 'cx-sourceselector-embedded--selected' );
@@ -995,7 +1004,8 @@
siteMapper: this.siteMapper,
value: this.options.sourceTitle,
validateTitle: true,
- placeholder: mw.msg(
'cx-sourceselector-dialog-source-title-placeholder' )
+ placeholder: mw.msg(
'cx-sourceselector-dialog-source-title-placeholder' ),
+ showRedirectTargets: true
} );
this.sourcePageSelector.onLookupMenuItemChoose =
this.setSelectedItem.bind( this );
diff --git a/modules/source/images/languages.png
b/modules/source/images/languages.png
new file mode 100644
index 0000000..b378cf9
--- /dev/null
+++ b/modules/source/images/languages.png
Binary files differ
diff --git a/modules/source/images/languages.svg
b/modules/source/images/languages.svg
new file mode 100644
index 0000000..7fed73c
--- /dev/null
+++ b/modules/source/images/languages.svg
@@ -0,0 +1 @@
+<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg"
width="18" height="18" viewBox="0 0 24 24"><path d="M13 19l.8-3h5.3l.9
3h2.2l-4.2-13h-3l-4.2 13h2.2zm3.5-11l2 6h-4l2-6z" fill="#707070"/><path d="M5
4l.938 1.906h-4.938v2.094h1.594c.6 1.8 1.406 3.206 2.406 4.406-1.1.7-4.313
1.781-4.313 1.781l1.313 1.813s3.487-1.387 4.688-2.188c1 .7 2.319 1.188 3.719
1.688l.594-2c-1-.3-1.988-.688-2.688-1.188 1.1-1.1 1.9-2.506
2.5-4.406h2.188l.5-2h-5.563l-.938-1.906h-2zm-.188 4h3.781c-.4 1.3-.906 2-1.906
3-1.1-1-1.475-1.7-1.875-3z" fill="#707070"/></svg>
diff --git a/modules/source/styles/ext.cx.source.selector.less
b/modules/source/styles/ext.cx.source.selector.less
index cbef4c5..a2fc8a5 100644
--- a/modules/source/styles/ext.cx.source.selector.less
+++ b/modules/source/styles/ext.cx.source.selector.less
@@ -94,14 +94,25 @@
padding-left: 0.7em;
font-size: 1.2em;
- font-weight: bold;
// Needs to be overridden for specificity
> a {
color: @colorGray1;
+ font-weight: bold;
}
}
+ &__language-count {
+ display: block;
+
+ .background-image-svg('../images/languages.svg',
'../images/languages.png');
+ background-size: 24px;
+ background-repeat: no-repeat;
+ background-position: left center;
+ padding-left: 26px;
+ font-size: 0.8em;
+ }
+
.cx-sourceselector-embedded-discard {
.background-image-svg('../../tools/images/clear.svg',
'../../tools/images/clear.png');
background-position: center center;
diff --git a/modules/ui/styles/widgets/mw.cx.ui.TitleOptionWidget.less
b/modules/ui/styles/widgets/mw.cx.ui.TitleOptionWidget.less
new file mode 100644
index 0000000..3a47643
--- /dev/null
+++ b/modules/ui/styles/widgets/mw.cx.ui.TitleOptionWidget.less
@@ -0,0 +1,10 @@
+.mw-widget-titleOptionWidget {
+ &-numOfLanguages {
+ padding-right: 1em;
+ }
+
+ &-missing {
+ color: #00af89;
+ font-weight: 500;
+ }
+}
diff --git a/modules/ui/widgets/mw.cx.ui.PageSelectorWidget.js
b/modules/ui/widgets/mw.cx.ui.PageSelectorWidget.js
index 6a7390c..305cca2 100644
--- a/modules/ui/widgets/mw.cx.ui.PageSelectorWidget.js
+++ b/modules/ui/widgets/mw.cx.ui.PageSelectorWidget.js
@@ -69,6 +69,174 @@
optionWidgetData =
mw.widgets.TitleWidget.prototype.getOptionWidgetData.call( this, title, data );
// Correct the URL so that it can point to the source language wiki.
optionWidgetData.url = this.siteMapper.getPageUrl( this.language, title
);
+ optionWidgetData.numOfLanguages = data.langlinkscount + 1; // One
language is added to get actual total number of languages
+ optionWidgetData.missingInTargetLanguage = !data.langlinks;
+ optionWidgetData.targetLanguage = this.targetLanguage;
return optionWidgetData;
};
+
+mw.cx.ui.PageSelectorWidget.prototype.getSuggestionsPromise = function () {
+ var req,
+ api = this.getApi(),
+ query = this.getQueryValue(),
+ widget = this,
+ promiseAbortObject = { abort: function () {
+ // Do nothing. This is just so OOUI doesn't break due
to abort being undefined.
+ } };
+
+ // Set API URL to localhost for local testing of langlinks API
+ // api.apiUrl = this.api.defaults.ajax.url;
+ // api.defaults.ajax.url = this.api.defaults.ajax.url;
+
+ if ( !mw.Title.newFromText( query ) ) {
+ // Don't send invalid titles to the API.
+ // Just pretend it returned nothing so we can show the 'invalid
title' section
+ return $.Deferred().resolve( {} ).promise( promiseAbortObject );
+ }
+
+ return this.getInterwikiPrefixesPromise().then( function (
interwikiPrefixes ) {
+ var params,
+ interwiki = query.substring( 0, query.indexOf( ':' ) );
+ if (
+ interwiki && interwiki !== '' &&
+ interwikiPrefixes.indexOf( interwiki ) !== -1
+ ) {
+ return $.Deferred().resolve( { query: {
+ pages: [ {
+ title: query
+ } ]
+ } } ).promise( promiseAbortObject );
+ }
+ params = {
+ action: 'query',
+ prop: [ 'info', 'pageprops', 'langlinks',
'langlinkscount' ],
+ generator: 'prefixsearch',
+ gpssearch: query,
+ gpsnamespace: widget.namespace !== null ?
widget.namespace : undefined,
+ gpslimit: widget.limit,
+ ppprop: 'disambiguation',
+ lllang: widget.targetLanguage
+ };
+ if ( widget.showRedirectTargets ) {
+ params.redirects = 1;
+ }
+ if ( widget.showImages ) {
+ params.prop.push( 'pageimages' );
+ params.pithumbsize = 80;
+ params.pilimit = widget.limit;
+ }
+ if ( widget.showDescriptions ) {
+ params.prop.push( 'pageterms' );
+ params.wbptterms = 'description';
+ }
+ req = api.get( params );
+ promiseAbortObject.abort = req.abort.bind( req );
+ return req.then( function ( ret ) {
+ if ( ret.query === undefined ) {
+ ret = api.get( { action: 'query', titles: query
} );
+ promiseAbortObject.abort = ret.abort.bind( ret
);
+ }
+ return ret;
+ } );
+ } ).promise( promiseAbortObject );
+};
+
+mw.cx.ui.PageSelectorWidget.prototype.getOptionsFromData = function ( data ) {
+ var i, len, index, pageExists, pageExistsExact, suggestionPage, page,
redirect, redirects,
+ currentPageName = new mw.Title( mw.config.get(
'wgRelevantPageName' ) ).getPrefixedText(),
+ items = [],
+ titles = [],
+ titleObj = mw.Title.newFromText( this.getQueryValue() ),
+ redirectsTo = {},
+ pageData = {};
+
+ if ( data.redirects ) {
+ for ( i = 0, len = data.redirects.length; i < len; i++ ) {
+ redirect = data.redirects[ i ];
+ redirectsTo[ redirect.to ] = redirectsTo[ redirect.to ]
|| [];
+ redirectsTo[ redirect.to ].push( redirect.from );
+ }
+ }
+
+ for ( index in data.pages ) {
+ suggestionPage = data.pages[ index ];
+ // When excludeCurrentPage is set, don't list the current page
unless the user has type the full title
+ if ( this.excludeCurrentPage && suggestionPage.title ===
currentPageName && suggestionPage.title !== titleObj.getPrefixedText() ) {
+ continue;
+ }
+ pageData[ suggestionPage.title ] = {
+ known: suggestionPage.known !== undefined,
+ missing: suggestionPage.missing !== undefined,
+ redirect: suggestionPage.redirect !== undefined,
+ disambiguation: OO.getProp( suggestionPage,
'pageprops', 'disambiguation' ) !== undefined,
+ imageUrl: OO.getProp( suggestionPage, 'thumbnail',
'source' ),
+ description: OO.getProp( suggestionPage, 'terms',
'description' ),
+ langlinkscount: suggestionPage.langlinkscount,
+ langlinks: suggestionPage.langlinks,
+ // Sort index
+ index: suggestionPage.index
+ };
+
+ // Throw away pages from wrong namespaces. This can happen when
'showRedirectTargets' is true
+ // and we encounter a cross-namespace redirect.
+ if ( this.namespace === null || this.namespace ===
suggestionPage.ns ) {
+ titles.push( suggestionPage.title );
+ }
+
+ redirects = redirectsTo[ suggestionPage.title ] || [];
+ for ( i = 0, len = redirects.length; i < len; i++ ) {
+ pageData[ redirects[ i ] ] = {
+ missing: false,
+ known: true,
+ redirect: true,
+ disambiguation: false,
+ description: mw.msg(
'mw-widgets-titleinput-description-redirect', suggestionPage.title ),
+ langlinks: suggestionPage.langlinks,
+ // Sort index, just below its target
+ index: suggestionPage.index + 0.5
+ };
+ titles.push( redirects[ i ] );
+ }
+ }
+
+ titles.sort( function ( a, b ) {
+ return pageData[ a ].index - pageData[ b ].index;
+ } );
+
+ // If not found, run value through mw.Title to avoid treating a match
as a
+ // mismatch where normalisation would make them matching (T50476)
+
+ pageExistsExact = (
+ Object.prototype.hasOwnProperty.call( pageData,
this.getQueryValue() ) &&
+ (
+ !pageData[ this.getQueryValue() ].missing ||
+ pageData[ this.getQueryValue() ].known
+ )
+ );
+ pageExists = pageExistsExact || (
+ titleObj &&
+ Object.prototype.hasOwnProperty.call( pageData,
titleObj.getPrefixedText() ) &&
+ (
+ !pageData[ titleObj.getPrefixedText() ].missing ||
+ pageData[ titleObj.getPrefixedText() ].known
+ )
+ );
+
+ if ( this.cache ) {
+ this.cache.set( pageData );
+ }
+
+ // Offer the exact text as a suggestion if the page exists
+ if ( pageExists && !pageExistsExact ) {
+ titles.unshift( this.getQueryValue() );
+ pageData[ this.getQueryValue() ] = pageData[
titleObj.getPrefixedText() ];
+ }
+
+ for ( i = 0, len = titles.length; i < len; i++ ) {
+ page = pageData[ titles[ i ] ] || {};
+ items.push( new mw.cx.ui.TitleOptionWidget(
this.getOptionWidgetData( titles[ i ], page ) ) );
+ }
+
+ return items;
+};
diff --git a/modules/ui/widgets/mw.cx.ui.TitleOptionWidget.js
b/modules/ui/widgets/mw.cx.ui.TitleOptionWidget.js
new file mode 100644
index 0000000..3c9d140
--- /dev/null
+++ b/modules/ui/widgets/mw.cx.ui.TitleOptionWidget.js
@@ -0,0 +1,63 @@
+/*!
+* Content Translation UserInterface adaptation of MediaWiki Widget
TitleOptionWidget.
+*
+* @ingroup Extensions
+* @copyright See AUTHORS.txt
+* @license GPL-2.0+
+*/
+
+'use strict';
+
+( function ( $, mw ) {
+
+ /**
+ * Creates an mw.cx.ui.TitleOptionWidget object.
+ *
+ * @class
+ * @extends mw.widgets.TitleOptionWidget
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @cfg {number} [numOfLanguages] Number of languages matched article
exists in.
+ * @cfg {boolean} [missingInTargetLanguage] Article is missing in
target language
+ */
+ mw.cx.ui.TitleOptionWidget = function MwCxTitleOptionWidget( config ) {
+ var languageIcon, languageLabel;
+
+ // Parent constructor
+ mw.cx.ui.TitleOptionWidget.parent.call( this, config );
+
+ if ( config.numOfLanguages ) {
+ languageIcon = new OO.ui.IconWidget( {
+ icon: 'language',
+ iconTitle: 'Number of languages'
+ } );
+ languageLabel = new OO.ui.LabelWidget( {
+ label: mw.language.convertNumber(
config.numOfLanguages )
+ } );
+ this.$element.append(
+ $( '<span>' )
+ .addClass(
'mw-widget-titleOptionWidget-numOfLanguages' )
+ .append(
+ languageIcon.$element,
+ languageLabel.$element
+ )
+ );
+ }
+
+ if ( config.missingInTargetLanguage ) {
+ this.$element.append(
+ $( '<span>' )
+ .addClass(
'mw-widget-titleOptionWidget-missing' )
+ .text( mw.msg(
'cx-sourceselector-missing-in-target-language',
+ $.uls.data.getAutonym(
config.targetLanguage ) )
+ )
+ );
+ }
+ };
+
+ /* Setup */
+
+ OO.inheritClass( mw.cx.ui.TitleOptionWidget,
mw.widgets.TitleOptionWidget );
+
+}( jQuery, mediaWiki ) );
--
To view, visit https://gerrit.wikimedia.org/r/372537
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: merged
Gerrit-Change-Id: I8fffe5b2056b977f1524b1978789266c281239d9
Gerrit-PatchSet: 7
Gerrit-Project: mediawiki/extensions/ContentTranslation
Gerrit-Branch: master
Gerrit-Owner: Petar.petkovic <[email protected]>
Gerrit-Reviewer: Nikerabbit <[email protected]>
Gerrit-Reviewer: Petar.petkovic <[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