resources/js/ext.translate.special.translate.js
( function () {
'use strict';
var state, hideOptionalMessages = '!optional';
state = {
group: null,
language: null,
messageList: null
};
mw.translate = mw.translate || {};
var logger = require( 'ext.translate.eventlogginghelpers' );
mw.translate = $.extend( mw.translate, {
/**
* Change the group that is currently displayed
* in the TUX translation editor.
*
* @private
* @param {Object} group a message group object.
*/
changeGroup: function ( group ) {
if ( !checkDirty() ) {
return;
}
showAggregateSubgroupCount();
state.group = group.id;
var changes = {
group: group.id,
showMessage: null
};
mw.translate.changeUrl( changes );
mw.translate.updateTabLinks( changes );
removeGroupWarnings();
state.messageList.changeSettings( changes );
updateGroupInformation( state );
},
/**
* @private
* @param {string} language
*/
changeLanguage: function ( language ) {
var changes = {
language: language,
showMessage: null
};
state.language = language;
mw.translate.changeUrl( changes );
mw.translate.updateTabLinks( changes );
removeGroupWarnings();
state.messageList.changeSettings( changes );
state.groupSelector.updateTargetLanguage( language );
updateGroupInformation( state );
getTranslationStats( language ).done( function ( stats ) {
logger.logClickEvent(
'change_target_lang',
'language_selector',
{
// eslint-disable-next-line camelcase
target_lang: language,
// eslint-disable-next-line camelcase
translatable_count: stats.total,
// eslint-disable-next-line camelcase
translated_count: stats.translated
}
);
} );
},
/**
* @internal
* @param {string} filter
*/
changeFilter: function ( filter ) {
if ( !checkDirty() ) {
return;
}
mw.translate.changeUrl( { filter: filter, showMessage: null } );
state.messageList.changeSettings( { filter: getActualFilter( filter ) } );
},
/**
* @internal
* @param {Object} params
*/
changeUrl: function ( params ) {
var uri = new mw.Uri( window.location.href );
uri.extend( params );
// Support removing keys from the query
Object.keys( params ).forEach( function ( key ) {
if ( params[ key ] === null || params[ key ] === undefined ) {
delete uri.query[ key ];
}
} );
mw.hook( 'mw.translate.translationView.stateChange' ).fire( state );
if ( uri.toString() === window.location.href ) {
return;
}
if ( $( '.tux-messagelist' ).length ) {
history.replaceState( uri, null, uri.toString() );
} else {
window.location.href = uri.toString();
}
},
/**
* Updates the navigation tabs.
*
* @private
* @param {Object} params Url parameters to update.
* @since 2013.05
*/
updateTabLinks: function ( params ) {
$( '.tux-tab a' ).each( function () {
var $a = $( this );
var uri = new mw.Uri( $a.prop( 'href' ) );
uri.extend( params );
$a.prop( 'href', uri.toString() );
} );
}
} );
function getTranslationStats( language ) {
return mw.translate.loadMessageGroupStatsForItem( state.language, state.group ).then(
function () {
var statsData = mw.translate.languagestats[ language ] || [];
for ( var i = 0; i < statsData.length; i++ ) {
if ( statsData[ i ].group === state.group ) {
return statsData[ i ];
}
}
return {
total: 0,
translated: 0
};
}
);
}
function getActionSource() {
var uri = new mw.Uri( window.location.href );
return uri.query.action_source || 'direct_open';
}
function getActualFilter( filter ) {
var realFilters = [ '!ignored' ];
var uri = new mw.Uri( window.location.href );
if ( uri.query.optional !== '1' ) {
realFilters.push( hideOptionalMessages );
}
if ( filter ) {
realFilters.push( filter );
}
return realFilters.join( '|' );
}
function checkDirty() {
if ( mw.translate.isDirty() ) {
// eslint-disable-next-line no-alert
return confirm( mw.msg( 'translate-js-support-unsaved-warning' ) );
}
return true;
}
// Returns an array of jQuery objects of rows of translated
// and proofread messages in the TUX editors.
// Used several times.
function getTranslatedMessages( $translateContainer ) {
$translateContainer = $translateContainer || $( '.ext-translate-container' );
return $translateContainer.find( '.tux-message-item' )
.filter( '.translated, .proofread' );
}
/**
* Updates all group specific stuff on the page.
*
* @param {Object} stateInfo Information about current group and language.
* @param {string} stateInfo.group Message group id.
* @param {string} stateInfo.language Language.
*/
function updateGroupInformation( stateInfo ) {
var props = 'id|priority|prioritylangs|priorityforce|description|label|sourcelanguage|class|subscription';
mw.translate.recentGroups.append( stateInfo.group );
mw.translate.getMessageGroup( stateInfo.group, props ).done( function ( group ) {
updateDescription( group );
updateGroupPriorityWarnings( group, stateInfo.language );
updateGroupSubscription( group );
} );
}
function updateDescription( group ) {
var
api = new mw.Api(),
$description = $( '.tux-editor-header .description' );
if ( group.description === null ) {
$description.empty();
return;
}
var description = group.description;
if (
group.class === 'WikiPageMessageGroup' &&
group.sourcelanguage !== state.language &&
// Message documentation does not have a translation page
state.language !== mw.config.get( 'wgTranslateDocumentationLanguageCode' )
) {
description = mw.msg(
'translate-tag-page-wikipage-desc',
':' + group.label + '/' + state.language,
':' + group.label,
$.uls.data.getAutonym( group.sourcelanguage ),
group.sourcelanguage,
$.uls.data.getAutonym( state.language ),
state.language );
}
api.parse( description ).done( function ( parsedDescription ) {
// The parsed text is returned in a <p> tag,
// so it's removed here.
$description.html( parsedDescription );
} ).fail( function () {
$description.empty();
mw.log( 'Error parsing description for group ' + group.id );
} );
}
function updateGroupPriorityWarnings( group, language ) {
var $groupWarning = $( '.tux-editor-header .tux-group-warning' );
if ( group.priority === 'discouraged' ) {
$groupWarning.append(
$( '<p>' ).append( $( '<strong>' )
.text( mw.message( 'tpt-discouraged-translation-header' ).text() )
),
$( '<p>' ).append( mw.message( 'tpt-discouraged-translation-content' ).parseDom() )
);
}
var headerMessage, languagesMessage;
if ( !group.prioritylangs && group.priorityforce ) {
headerMessage = mw.message(
'tpt-discouraged-language-force-header',
$.uls.data.getAutonym( language )
);
languagesMessage = mw.message( 'tpt-translation-restricted-no-priority-languages-no-reason' );
$groupWarning.append(
$( '<p>' ).append( $( '<strong>' ).text( headerMessage.text() ) ),
$( '<p>' ).text( languagesMessage.text() )
);
return;
}
if ( !group.prioritylangs || isPriorityLanguage( language, group.prioritylangs ) ) {
return;
}
// Make a comma-separated list of preferred languages
var $preferredLanguages = $( '<span>' );
group.prioritylangs.forEach( function ( languageCode, index ) {
// bidi isolation for language names
$preferredLanguages.append(
$( '<bdi>' ).text( $.uls.data.getAutonym( languageCode ) )
);
// Add comma between languages
if ( index + 1 !== group.prioritylangs.length ) {
$preferredLanguages.append( ', ' );
}
} );
if ( group.priorityforce ) {
headerMessage = mw.message(
'tpt-discouraged-language-force-header',
$.uls.data.getAutonym( language )
);
languagesMessage = mw.message(
'tpt-discouraged-language-force-content',
$preferredLanguages
);
} else {
headerMessage = mw.message(
'tpt-discouraged-language-header',
$.uls.data.getAutonym( language )
);
languagesMessage = mw.message(
'tpt-discouraged-language-content',
$preferredLanguages
);
}
$groupWarning.append(
$( '<p>' ).append( $( '<strong>' ).text( headerMessage.text() ) ),
$( '<p>' ).append( languagesMessage.parseDom() )
);
}
function updateGroupSubscription( group ) {
if ( mw.config.get( 'wgTranslateEnableMessageGroupSubscription' ) !== true ) {
return;
}
var $tuxBreadcrumb = $( '.tux-breadcrumb' );
if ( group.subscription === undefined ) {
$tuxBreadcrumb.find( '.tux-watch-button' ).remove();
return;
}
var buttonMessage = group.subscription ? 'tux-unwatch-group' : 'tux-watch-group';
var watchClass = group.subscription ? 'tux-watch-group--watch' : 'tux-watch-group--unwatch';
var $subscribeButton = $( '<button>' )
// CSS Classes:
// * tux-watch-group--watch
// * tux-watch-group--unwatch
.addClass( 'mw-ui-button tux-watch-button ' + watchClass )
.data( 'subscribed', group.subscription )
.on( 'click', toggleSubscription );
$subscribeButton.append(
$( '<span>' ).addClass( 'tux-watch-icon' ),
$( '<span>' )
.addClass( 'tux-watch-label' )
// * tux-watch-group
// * tux-unwatch-group
.text( mw.msg( buttonMessage ) )
);
$tuxBreadcrumb.find( '.tux-watch-button' ).remove();
$tuxBreadcrumb.append( $subscribeButton );
}
function removeGroupWarnings() {
var $tuxHeader = $( '.tux-editor-header' );
$tuxHeader.find( '.tux-group-warning' ).empty();
}
function isPriorityLanguage( language, priorityLanguages ) {
// Don't show priority notice if the language is message documentation.
if ( language === mw.config.get( 'wgTranslateDocumentationLanguageCode' ) ) {
return true;
}
// If no priority language is set, return early.
if ( !priorityLanguages ) {
return true;
}
return priorityLanguages.indexOf( language ) !== -1;
}
function setupLanguageSelector( $element ) {
var ulsOptions = {
languages: mw.config.get( 'wgTranslateLanguages' ),
showRegions: [ 'SP' ].concat( $.fn.lcd.defaults.showRegions ),
onSelect: function ( languageCode ) {
var languageDetails = mw.translate.getLanguageDetailsForHtml( languageCode );
mw.translate.changeLanguage( languageCode );
$element
.find( '.ext-translate-target-language' )
.text( languageDetails.autonym )
.prop( {
lang: languageDetails.code,
dir: languageDetails.direction
} );
},
ulsPurpose: 'translate-special-translate',
quickList: function () {
return mw.uls.getFrequentLanguageList();
}
};
mw.translate.addExtraLanguagesToLanguageData( ulsOptions.languages, [ 'SP' ] );
$element.uls( ulsOptions );
}
function addTuxGroupWarningContainer() {
var $tuxEditorHeader = $( '.tux-editor-header' );
var $tuxWarning = $tuxEditorHeader.find( 'tux-group-warning' );
if ( !$tuxWarning.length ) {
$tuxWarning = $( '<div>' )
.addClass( 'mw-message-box-warning mw-message-box tux-group-warning twelve column' );
$tuxEditorHeader.append( $tuxWarning );
}
}
function toggleSubscription() {
var api = new mw.Api();
var $button = $( this );
$button.prop( 'disabled', true );
var subscriptionStatus = $button.data( 'subscribed' );
var params = {
action: 'messagegroupsubscription',
groupId: state.group,
operation: subscriptionStatus ? 'unsubscribe' : 'subscribe',
assert: 'user',
formatversion: 2
};
return api.postWithToken( 'csrf', params ).then(
function ( response ) {
var oldSubscriptionStatus = subscriptionStatus;
if ( response.messagegroupsubscription && response.messagegroupsubscription.success === 1 ) {
var buttonMessage = oldSubscriptionStatus ? 'tux-watch-group' : 'tux-unwatch-group';
var watchClass = oldSubscriptionStatus ?
'tux-watch-group--unwatch' : 'tux-watch-group--watch';
$button
.removeClass( [ 'tux-watch-group--watch', 'tux-watch-group--unwatch' ] )
// CSS Classes:
// * tux-watch-group--watch
// * tux-watch-group--unwatch
.addClass( watchClass )
.data( 'subscribed', !oldSubscriptionStatus );
$button.find( '.tux-watch-label' )
// * tux-watch-group
// * tux-unwatch-group
.text( mw.msg( buttonMessage ) );
loadWatchedMessageGroups();
} else {
mw.notify( mw.msg( 'tux-subscription-error' ) );
}
},
function ( error ) {
mw.notify( mw.msg( 'tux-subscription-error' ) );
mw.log.error( 'messagegroupsubscription: Failed', error, params );
}
).always( function () {
$button.prop( 'disabled', false );
} );
}
function loadWatchedMessageGroups() {
if ( mw.config.get( 'wgTranslateEnableMessageGroupSubscription' ) !== true ) {
return;
}
var api = new mw.Api();
var params = {
action: 'query',
list: 'messagegroupsubscription',
formatversion: 2
};
return api.get( params ).then(
function ( response ) {
state.groupSelector.setWatchedGroups( response.query.messagegroupsubscription );
},
function ( error ) {
mw.log.error( 'messagegroupsubscription: Failed to fetch user subscriptions', error, params );
}
);
}
function showAggregateSubgroupCount() {
var $groupBreadcrumbs = $( '.tux-breadcrumb .grouptitle' );
var $selectedGroup = $groupBreadcrumbs.last();
var groupCounts = $selectedGroup.data( 'msggroup-subgroup-count' );
if ( groupCounts === undefined ) {
$groupBreadcrumbs
.find( '.tux-breadcrumb__item--aggregate-count' )
.remove();
} else {
var subGroups = mw.msg( 'translate-msggroupselector-view-subprojects', groupCounts );
$selectedGroup.append(
$( '<span>' ).append( mw.message( 'parentheses', $( '<span>' ).text( subGroups ) ).parse() )
.addClass( 'tux-breadcrumb__item--aggregate-count' )
);
}
}
$( function () {
var $messageList = $( '.tux-messagelist' );
state.group = $( '.tux-messagetable-loader' ).data( 'messagegroup' );
state.language = $messageList.data( 'targetlangcode' );
var uri = new mw.Uri( window.location.href );
if ( $messageList.length ) {
$messageList.messagetable();
state.messageList = $messageList.data( 'messagetable' );
var filter = uri.query.filter;
var offset = uri.query.showMessage;
var limit;
if ( offset ) {
limit = uri.query.limit || 1;
// Default to no filters
filter = filter || '';
}
if ( filter === undefined ) {
filter = '!translated';
}
$( '.tux-message-selector li' ).each( function () {
var $this = $( this );
if ( $this.data( 'filter' ) === filter ) {
$this.addClass( 'selected' );
}
} );
mw.translate.changeUrl( {
group: state.group,
language: state.language,
filter: filter,
showMessage: offset,
optional: offset ? 1 : undefined
} );
// Start loading messages
var actualFilter = getActualFilter( filter );
state.messageList.changeSettings( {
group: state.group,
language: state.language,
offset: offset,
limit: limit,
filter: actualFilter
} );
if ( actualFilter.indexOf( hideOptionalMessages ) === -1 ) {
$( '#tux-option-optional' ).prop( 'checked', true );
}
}
addTuxGroupWarningContainer();
var position = {
my: 'left top',
at: 'left-10 bottom+5'
};
if ( $( document.body ).hasClass( 'rtl' ) ) {
position = {
my: 'right top',
at: 'right+10 bottom+5'
};
}
$( '.tux-breadcrumb__item--aggregate' ).msggroupselector( {
onSelect: mw.translate.changeGroup,
language: state.language,
position: position,
recent: mw.translate.recentGroups.get(),
showWatched: mw.config.get( 'wgTranslateEnableMessageGroupSubscription' ) || false,
menuClass: 'tux-groupselector-tpt'
} );
state.groupSelector = $( '.tux-breadcrumb__item--aggregate' ).data( 'msggroupselector' );
loadWatchedMessageGroups();
updateGroupInformation( state );
showAggregateSubgroupCount();
$( '.ext-translate-language-selector .uls' ).one( 'click', function () {
var $target = $( this );
mw.loader.using( 'ext.uls.mediawiki' ).done( function () {
setupLanguageSelector( $target );
$target.trigger( 'click' );
} );
} ).on( 'keypress', function () {
$( this ).trigger( 'click' );
} );
if ( $.fn.translateeditor ) {
// New translation editor
$( '.tux-message' ).translateeditor();
}
var $translateContainer = $( '.ext-translate-container' );
if ( mw.translate.canProofread() ) {
$translateContainer.find( '.proofread-mode-button' ).removeClass( 'hide' );
}
var $hideTranslatedButton = $translateContainer.find( '.tux-editor-clear-translated' );
$hideTranslatedButton
.prop( 'disabled', !getTranslatedMessages( $translateContainer ).length )
.on( 'click', function () {
getTranslatedMessages( $translateContainer ).remove();
$( this ).prop( 'disabled', true );
} );
// Message filter click handler
$translateContainer.find( '.row.tux-message-selector > li' ).on( 'click', function () {
var $this = $( this );
if ( $this.hasClass( 'more' ) ) {
return false;
}
var newFilter = $this.data( 'filter' );
// Remove the 'selected' class from all the items.
// Some of them could have been moved to under the "more" menu,
// so everything under .row.tux-message-selector is searched.
$translateContainer.find( '.row.tux-message-selector .selected' )
.removeClass( 'selected' );
mw.translate.changeFilter( newFilter );
$this.addClass( 'selected' );
var translated = newFilter !== '!translated';
// TODO: this could should be in messagetable
$hideTranslatedButton.toggleClass( 'hide', translated )
.prop( 'disabled', !translated && !getTranslatedMessages( $translateContainer ).length );
return false;
} );
// TODO: this could should be in messagetable
$hideTranslatedButton
.toggleClass( 'hide', $( '.tux-messagetable-loader' ).data( 'filter' ) !== '!translated' );
// Don't let clicking the items in the "more" menu
// affect the rest of it.
$( '.row.tux-message-selector .more ul' )
.on( 'click', function ( e ) {
e.stopPropagation();
} );
$( '#tux-option-optional' ).on( 'change', function () {
var currentUri = new mw.Uri( window.location.href ),
checked = $( this ).prop( 'checked' );
mw.translate.changeUrl( { optional: checked ? 1 : 0 } );
mw.translate.changeFilter( currentUri.query.filter );
} );
getTranslationStats( state.language ).done( function ( stats ) {
logger.logEvent(
'interface_open',
null,
getActionSource(),
{
// eslint-disable-next-line camelcase
source_lang: $messageList.data( 'sourcelangcode' ),
// eslint-disable-next-line camelcase
target_lang: state.language,
// eslint-disable-next-line camelcase
source_id: uri.query.group,
// eslint-disable-next-line camelcase
source_type: 'page',
// eslint-disable-next-line camelcase
translatable_count: stats.total,
// eslint-disable-next-line camelcase
translated_count: stats.translated
}
);
} );
} );
}() );