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

Reply via email to