%PDF- %PDF-
| Direktori : /www/varak.net/wiki.varak.net/extensions/Translate/resources/js/ |
| Current File : //www/varak.net/wiki.varak.net/extensions/Translate/resources/js/ext.translate.editor.js |
/* global autosize */
( function ( $, mw, autosize ) {
'use strict';
/**
* TranslateEditor Plugin
* Prepare the translation editor UI for a translation unit (message).
* This is mainly used with the messagetable plugin,
* but it is independent of messagetable.
* Example usage:
*
* $( 'div.messageRow' ).translateeditor( {
* message: messageObject // Mandatory message object
* } );
*
* Assumptions: The jquery element to which translateeditor is applied will
* internally contain the editor's generated UI. So it is going to have the same width
* and inherited properies of the container.
* The container can mark the message item with class 'message'. This is not
* mandatory, but if found, when the editor is opened, the message item will be hidden
* and the editor will appear as if the message is replaced by the editor.
* See the UI of Translate messagetable for a demo.
*
* @param {HTMLElement} element
* @param {Object} options
* @param {Function} [options.beforeSave] Callback to call when translation is going to be saved.
* @param {Function} [options.onReady] Callback to call when the editor is ready.
* @param {Function} [options.onSave] Callback to call when translation has been saved.
* @param {Function} [options.onSkip] Callback to call when a message is skipped.
* @param {Object} options.message Object as returned by messagecollection api.
* @param {TranslationApiStorage} [options.storage]
*/
function TranslateEditor( element, options ) {
this.$editTrigger = $( element );
this.$editor = null;
this.options = options;
this.message = this.options.message;
this.$messageItem = this.$editTrigger.find( '.message' );
this.shown = false;
this.dirty = false;
this.saving = false;
this.expanded = false;
this.listen();
this.storage = this.options.storage || new mw.translate.TranslationApiStorage();
this.canDelete = mw.translate.canDelete();
this.delayValidation = delayer();
}
TranslateEditor.prototype = {
/**
* Initialize the plugin
*/
init: function () {
// In case we have already created the editor earlier,
// don't add a new one. The existing one may have unsaved
// changes.
if ( this.$editor ) {
return;
}
this.render();
// onReady callback
if ( this.options.onReady ) {
this.options.onReady.call( this );
}
},
/**
* Render the editor UI
*/
render: function () {
this.$editor = $( '<div>' )
.addClass( 'row tux-message-editor hide' )
.append(
this.prepareEditorColumn(),
this.prepareInfoColumn()
);
this.expanded = false;
this.$editTrigger.append( this.$editor );
if ( this.message.properties && this.message.properties.status === 'fuzzy' ) {
this.addWarning(
mw.message( 'tux-editor-outdated-warning' ).escaped(),
'fuzzy'
);
}
this.showTranslationHelpers();
},
/**
* Mark the message as unsaved because of edits, can be resumed later
*
* @param {string} [highlightClass] Class for background highlighting
*/
markUnsaved: function ( highlightClass ) {
var $tuxListStatus = this.$editTrigger.find( '.tux-list-status' );
highlightClass = highlightClass || 'tux-highlight';
$tuxListStatus.children( '.tux-status-unsaved' ).remove();
$tuxListStatus.children().addClass( 'hide' );
$( '<span>' )
.addClass( 'tux-status-unsaved ' + highlightClass )
.text( mw.msg( 'tux-status-unsaved' ) )
.appendTo( $tuxListStatus );
},
/**
* Mark the message as unsaved because of saving failure.
*/
markUnsavedFailure: function () {
this.markUnsaved( 'tux-warning' );
},
/**
* Mark the message as no longer unsaved
*/
markUnunsaved: function () {
var $tuxListStatus = this.$editTrigger.find( '.tux-list-status' );
$tuxListStatus.children( '.tux-status-unsaved' ).remove();
$tuxListStatus.children().removeClass( 'hide' );
this.dirty = false;
mw.translate.dirty = false;
},
/**
* Mark the message as being saved
*/
markSaving: function () {
var $tuxListStatus = this.$editTrigger.find( '.tux-list-status' );
// Disable the save button
this.$editor.find( '.tux-editor-save-button' )
.prop( 'disabled', true );
// Add a "Saving" indicator
$tuxListStatus.empty();
$( '<span>' )
.addClass( 'tux-status-unsaved' )
.text( mw.msg( 'tux-status-saving' ) )
.appendTo( $tuxListStatus );
},
/**
* Mark the message as translated and successfully saved.
*/
markTranslated: function () {
this.$editTrigger.find( '.tux-list-status' )
.empty()
.append( $( '<span>' )
.addClass( 'tux-status-translated' )
.text( mw.msg( 'tux-status-translated' ) )
);
this.$messageItem
.removeClass( 'untranslated translated fuzzy proofread' )
.addClass( 'translated' );
this.dirty = false;
if ( this.message.properties ) {
$( '.tux-action-bar .tux-statsbar' ).trigger(
'change',
[ 'translated', this.message.properties.status ]
);
this.message.properties.status = 'translated';
// TODO: Update any other statsbar for the same group in the page.
}
},
/**
* Save the translation
*/
save: function () {
var translation, editSummary,
translateEditor = this;
mw.translateHooks.run( 'beforeSubmit', translateEditor.$editor );
translation = translateEditor.$editor.find( '.editcolumn textarea' ).val();
editSummary = translateEditor.$editor.find( '.tux-input-editsummary' ).val() || '';
translateEditor.saving = true;
// beforeSave callback
if ( translateEditor.options.beforeSave ) {
translateEditor.options.beforeSave( translation );
}
// For responsiveness and efficiency,
// immediately move to the next message.
translateEditor.next();
// Now the message definitely has a history,
// so make sure the history menu item is shown
translateEditor.$editor.find( '.message-tools-history' )
.removeClass( 'hide' );
// Show the delete menu item if the user can delete
if ( this.canDelete ) {
translateEditor.$editor.find( '.message-tools-delete' )
.removeClass( 'hide' );
}
this.storage.save(
translateEditor.message.title,
translation,
editSummary
).done( function ( response, xhr ) {
var editResp = response.edit;
if ( editResp.result === 'Success' ) {
translateEditor.message.translation = translation;
translateEditor.onSaveSuccess();
// Handle errors
} else if ( editResp.spamblacklist ) {
// @todo Show exactly which blacklisted URL triggered it
translateEditor.onSaveFail( mw.msg( 'spamprotectiontext' ) );
} else if ( editResp.info &&
editResp.info.indexOf( 'Hit AbuseFilter:' ) === 0 &&
editResp.warning
) {
translateEditor.onSaveFail( editResp.warning );
} else {
translateEditor.onSaveFail( mw.msg( 'tux-save-unknown-error' ) );
mw.log( response, xhr );
}
} ).fail( function ( errorCode, response ) {
translateEditor.onSaveFail(
response.error && response.error.info || mw.msg( 'tux-save-unknown-error' )
);
if ( errorCode === 'assertuserfailed' ) {
// eslint-disable-next-line no-alert
alert( mw.msg( 'tux-session-expired' ) );
}
} );
},
/**
* Success handler for the translation saving.
*/
onSaveSuccess: function () {
this.markTranslated();
this.$editTrigger.find( '.tux-list-translation' )
.text( this.message.translation );
this.saving = false;
// remove warnings if any.
this.removeWarning( 'diff' );
this.removeWarning( 'fuzzy' );
this.removeWarning( 'validation' );
this.$editor.find( '.tux-warning' ).empty();
this.$editor.find( '.tux-more-warnings' )
.addClass( 'hide' )
.empty();
$( '.tux-editor-clear-translated' )
.removeClass( 'hide' )
.prop( 'disabled', false );
this.$editor.find( '.tux-input-editsummary' )
.val( '' )
.prop( 'disabled', true );
// Save callback
if ( this.options.onSave ) {
this.options.onSave( this.message.translation );
}
mw.translate.dirty = false;
mw.translateHooks.run( 'afterSubmit', this.$editor );
if ( mw.track ) {
mw.track( 'ext.translate.event.translation', this.message );
}
},
/**
* Marks that there was a problem saving a translation.
*
* @param {string} error Strings of warnings to display.
*/
onSaveFail: function ( error ) {
this.addWarning(
mw.msg( 'tux-editor-save-failed', error ),
'translation-saving'
);
this.saving = false;
this.markUnsavedFailure();
},
/**
* Skip the current message.
* Record it to mark as hard.
*/
skip: function () {
// @TODO devise good algorithm for identifying hard to translate messages
},
/**
* Jump to the next translation editor row.
*/
next: function () {
var $next = this.$editTrigger.next( '.tux-message' );
// Skip if the message is hidden. For example in a filter result.
if ( $next.length && $next.hasClass( 'hide' ) ) {
this.$editTrigger = $next;
this.next();
return;
}
// If this is the last message, just hide it
if ( !$next.length ) {
this.hide();
return;
}
$next.data( 'translateeditor' ).show();
// Scroll the page a little bit up, slowly.
if ( $( document ).height() -
( $( window ).height() + window.pageYOffset + $next.height() ) > 0
) {
$( 'html, body' ).stop().animate( {
scrollTop: $( '.tux-message-editor:visible' ).offset().top - 85
}, 500 );
}
},
/**
* Creates a menu element for the message tools.
*
* @param {string} className Used as the element's CSS class
* @param {Object} query Used as the query in the mw.Uri object
* @param {string} message The message of the label of the menu item
* @return {jQuery} The new menu item element
*/
createMessageToolsItem: function ( className, query, message ) {
var uri = new mw.Uri();
uri.path = mw.config.get( 'wgScript' );
uri.query = query;
return $( '<li>' )
.addClass( className )
.append( $( '<a>' )
.attr( {
href: uri.toString(),
target: '_blank'
} )
.text( mw.msg( message ) )
);
},
/**
* Creates an element with a dropdown menu including
* tools for the translators.
*
* @return {jQuery} The new message tools menu element
*/
createMessageTools: function () {
var $editItem, $historyItem, $deleteItem, $translationsItem, $linkToThisItem;
$editItem = this.createMessageToolsItem(
'message-tools-edit',
{
title: this.message.title,
action: 'edit'
},
'tux-editor-message-tools-show-editor'
);
if ( !mw.translate.canTranslate() ) {
$editItem.addClass( 'hide' );
}
$historyItem = this.createMessageToolsItem(
'message-tools-history',
{
title: this.message.title,
action: 'history'
},
'tux-editor-message-tools-history'
);
$deleteItem = this.createMessageToolsItem(
'message-tools-delete',
{
title: this.message.title,
action: 'delete'
},
'tux-editor-message-tools-delete'
);
// Hide these links if the translation doesn't actually exist.
// They will be shown when a translation will be created.
if ( this.message.translation === null ) {
$historyItem.addClass( 'hide' );
$deleteItem.addClass( 'hide' );
} else if ( !this.canDelete ) {
$deleteItem.addClass( 'hide' );
}
// A link to Special:Translations,
// with translations of this message to other languages
$translationsItem = this.createMessageToolsItem(
'message-tools-translations',
{
title: 'Special:Translations',
message: this.message.title
},
'tux-editor-message-tools-translations'
);
$linkToThisItem = this.createMessageToolsItem(
'message-tools-linktothis',
{
title: 'Special:Translate',
showMessage: this.message.key,
group: this.message.primaryGroup
},
'tux-editor-message-tools-linktothis'
);
return $( '<ul>' )
.addClass( 'tux-dropdown-menu tux-message-tools-menu hide' )
.append( $editItem, $historyItem, $deleteItem, $translationsItem, $linkToThisItem );
},
prepareEditorColumn: function () {
var translateEditor = this,
sourceString,
originalTranslation,
$editorColumn,
$messageKeyLabel,
$moreWarningsTab,
$warnings,
$warningsBlock,
$editAreaBlock,
$textarea,
$controlButtonBlock,
$editingButtonBlock,
$pasteOriginalButton,
$editSummary,
$editSummaryBlock,
$discardChangesButton = $( [] ),
$saveButton,
$requestRight,
$skipButton,
$cancelButton,
$sourceString,
$closeIcon,
$layoutActions,
$infoToggleIcon,
$messageList,
targetLangAttrib, targetLangDir, targetLangCode, prefix,
$messageTools = translateEditor.createMessageTools(),
canTranslate = mw.translate.canTranslate();
$editorColumn = $( '<div>' )
.addClass( 'seven columns editcolumn' );
$messageKeyLabel = $( '<div>' )
.addClass( 'ten columns messagekey' )
.text( this.message.title )
.append(
$( '<span>' ).addClass( 'caret' ),
$messageTools
)
.on( 'click', function ( e ) {
$messageTools.toggleClass( 'hide' );
e.stopPropagation();
} );
$closeIcon = $( '<span>' )
.addClass( 'one column close' )
.attr( 'title', mw.msg( 'tux-editor-close-tooltip' ) )
.on( 'click', function ( e ) {
translateEditor.hide();
e.stopPropagation();
} );
$infoToggleIcon = $( '<span>' )
// Initially the editor column is contracted,
// so show the expand button first
.addClass( 'one column editor-info-toggle editor-expand' )
.attr( 'title', mw.msg( 'tux-editor-expand-tooltip' ) )
.on( 'click', function ( e ) {
translateEditor.infoToggle( $( this ) );
e.stopPropagation();
} );
$layoutActions = $( '<div>' )
.addClass( 'two columns layout-actions' )
.append( $closeIcon, $infoToggleIcon );
$editorColumn.append( $( '<div>' )
.addClass( 'row tux-editor-titletools' )
.append( $messageKeyLabel, $layoutActions )
);
$messageList = $( '.tux-messagelist' );
originalTranslation = this.message.translation;
sourceString = this.message.definition;
$sourceString = $( '<span>' )
.addClass( 'twelve columns sourcemessage' )
.attr( {
lang: $messageList.data( 'sourcelangcode' ),
dir: $messageList.data( 'sourcelangdir' )
} )
.text( sourceString );
// Adjust the font size for the message string based on the length
if ( sourceString.length > 100 && sourceString.length < 200 ) {
$sourceString.addClass( 'long' );
}
if ( sourceString.length > 200 ) {
$sourceString.addClass( 'longer' );
}
$editorColumn.append( $( '<div>' )
.addClass( 'row' )
.append( $sourceString )
);
$warnings = $( '<div>' )
.addClass( 'tux-warning hide' );
$moreWarningsTab = $( '<div>' )
.addClass( 'tux-more-warnings hide' )
.on( 'click', function () {
var $this = $( this ),
$moreWarnings = $warnings.children(),
lastWarningIndex = $moreWarnings.length - 1;
// If the warning list is not open, only one warning is shown
if ( $this.hasClass( 'open' ) ) {
$moreWarnings.each( function ( index, element ) {
// The first element must always be shown
if ( index ) {
$( element ).addClass( 'hide' );
}
} );
$this
.removeClass( 'open' )
.text( mw.msg( 'tux-warnings-more', lastWarningIndex ) );
} else {
$moreWarnings.each( function ( index, element ) {
// The first element must always be shown
if ( index ) {
$( element ).removeClass( 'hide' );
}
} );
$this
.addClass( 'open' )
.text( mw.msg( 'tux-warnings-hide' ) );
}
} );
targetLangCode = $messageList.data( 'targetlangcode' );
if ( targetLangCode === mw.config.get( 'wgTranslateDocumentationLanguageCode' ) ) {
targetLangAttrib = mw.config.get( 'wgContentLanguage' );
targetLangDir = $.uls.data.getDir( targetLangAttrib );
} else {
targetLangAttrib = targetLangCode;
targetLangDir = $messageList.data( 'targetlangdir' );
}
$textarea = $( '<textarea>' )
.addClass( 'tux-textarea-translation' )
.attr( {
lang: targetLangAttrib,
dir: targetLangDir
} )
.val( this.message.translation || '' );
if ( mw.translate.isPlaceholderSupported( $textarea ) ) {
$textarea.prop( 'placeholder', mw.msg( 'tux-editor-placeholder' ) );
}
// Shortcuts for various insertable things
$textarea.on( 'keyup keydown', function ( e ) {
var index, info, direction;
if ( e.type === 'keydown' && e.altKey === true ) {
// Up and down arrows
if ( e.keyCode === 38 || e.keyCode === 40 ) {
direction = e.keyCode === 40 ? 1 : -1;
info = translateEditor.$editor.find( '.infocolumn' );
info.scrollTop( info.scrollTop() + 100 * direction );
translateEditor.showShortcuts();
}
}
// Move zero to last
index = e.keyCode - 49;
if ( index === -1 ) {
index = 9;
}
// 0..9 ~ 48..57
if (
e.type === 'keydown' &&
e.altKey === true &&
e.ctrlKey === false &&
e.shiftKey === false &&
index >= 0 && index < 10
) {
e.preventDefault();
e.stopPropagation();
translateEditor.$editor.find( '.shortcut-activated:visible' ).eq( index ).trigger( 'click' );
// Update numbers and locations after trigger should be completed
window.setTimeout( function () { translateEditor.showShortcuts(); }, 100 );
}
if ( e.which === 18 && e.type === 'keyup' ) {
translateEditor.hideShortcuts();
} else if ( e.which === 18 && e.type === 'keydown' ) {
translateEditor.showShortcuts();
}
} );
$textarea.on( 'textchange', function () {
var $textarea = $( this ),
$saveButton = translateEditor.$editor.find( '.tux-editor-save-button' ),
$pasteSourceButton = translateEditor.$editor.find( '.tux-editor-paste-original-button' ),
original = translateEditor.message.translation || '',
current = $textarea.val() || '';
if ( original !== '' ) {
$discardChangesButton.removeClass( 'hide' );
}
/* Avoid Unsaved marking when translated message is not changed in content.
* - translateEditor.dirty: internal book keeping
* - mw.translate.dirty: "you have unchanged edits" warning
*/
if ( original === current ) {
translateEditor.markUnunsaved();
} else {
translateEditor.dirty = true;
mw.translate.dirty = true;
}
translateEditor.makeSaveButtonJustSave( $saveButton );
// When there is content in the editor enable the button.
// But do not enable when some saving is not finished yet.
if ( current.trim() && !translateEditor.saving ) {
$pasteSourceButton.addClass( 'hide' );
$saveButton.prop( 'disabled', false );
} else {
$saveButton.prop( 'disabled', true );
$pasteSourceButton.removeClass( 'hide' );
}
translateEditor.resizeInsertables( $textarea );
translateEditor.delayValidation( function () {
translateEditor.validateTranslation();
}, 500 );
} );
$warningsBlock = $( '<div>' )
.addClass( 'tux-warnings-block' )
.append( $moreWarningsTab, $warnings );
$editAreaBlock = $( '<div>' )
.addClass( 'row tux-editor-editarea-block' )
.append( $( '<div>' )
.addClass( 'editarea twelve columns' )
.append( $warningsBlock, $textarea )
);
$editorColumn.append( $editAreaBlock );
if ( canTranslate ) {
$pasteOriginalButton = $( '<button>' )
.addClass( 'tux-editor-paste-original-button' )
.text( mw.msg( 'tux-editor-paste-original-button-label' ) )
.on( 'click', function () {
$textarea
.focus()
.val( sourceString )
.trigger( 'input' );
$pasteOriginalButton.addClass( 'hide' );
} );
$editSummary = $( '<input>' )
.addClass( 'tux-input-editsummary' )
.attr( {
maxlength: 255,
disabled: true,
placeholder: mw.msg( 'tux-editor-editsummary-placeholder' )
} )
.val( '' );
// Enable edit summary if there was a change to translation area
// or disable if there is no text in translation area
$textarea.on( 'textchange', function () {
if ( $editSummary.prop( 'disabled' ) ) {
$editSummary.prop( 'disabled', false );
}
if ( $textarea.val().trim() === '' ) {
$editSummary.prop( 'disabled', true );
}
} ).on( 'keydown', function ( e ) {
if ( !e.ctrlKey || e.keyCode !== 13 ) {
return;
}
if ( !$saveButton.is( ':disabled' ) ) {
$saveButton.click();
return;
}
$skipButton.click();
} );
if ( originalTranslation !== null ) {
$discardChangesButton = $( '<button>' )
.addClass( 'tux-editor-discard-changes-button hide' ) // Initially hidden
.text( mw.msg( 'tux-editor-discard-changes-button-label' ) )
.on( 'click', function () {
// Restore the translation
$textarea
.focus()
.val( originalTranslation );
// and go back to hiding.
$discardChangesButton.addClass( 'hide' );
// There's nothing new to save...
$editSummary.val( '' ).prop( 'disabled', true );
$saveButton.prop( 'disabled', true );
// ...unless there is other action
translateEditor.makeSaveButtonContextSensitive( $saveButton );
translateEditor.markUnunsaved();
translateEditor.resizeInsertables( $textarea );
} );
}
if ( this.message.translation ) {
$pasteOriginalButton.addClass( 'hide' );
}
$editingButtonBlock = $( '<div>' )
.addClass( 'twelve columns tux-editor-insert-buttons' )
.append(
$pasteOriginalButton,
$discardChangesButton
);
$editSummaryBlock = $( '<div>' )
.addClass( 'row tux-editor-editsummary-block' )
.append(
$( '<div>' )
.addClass( 'twelve columns' )
.append( $editSummary )
);
$requestRight = $( [] );
$saveButton = $( '<button>' )
.prop( 'disabled', true )
.addClass( 'tux-editor-save-button mw-ui-button mw-ui-progressive' )
.text( mw.msg( 'tux-editor-save-button-label' ) )
.on( 'click', function ( e ) {
translateEditor.save();
e.stopPropagation();
} );
this.makeSaveButtonContextSensitive( $saveButton, this.$messageItem );
} else {
$editingButtonBlock = $( [] );
$editSummaryBlock = $( [] );
$requestRight = $( '<span>' )
.addClass( 'tux-editor-request-right' )
.text( mw.msg( 'translate-edit-nopermission' ) );
// Make sure wgTranslatePermissionUrl setting is not 'false'
if ( mw.config.get( 'wgTranslatePermissionUrl' ) !== false ) {
$requestRight
.append( $( '<a>' )
.text( mw.msg( 'translate-edit-askpermission' ) )
.addClass( 'tux-editor-ask-permission' )
.attr( {
href: mw.util.getUrl(
mw.config.get( 'wgTranslateUseSandbox' ) ?
'Special:TranslationStash' :
mw.config.get( 'wgTranslatePermissionUrl' )
)
} )
);
}
// Disable the text area if user has no translation rights.
// Use readonly to allow copy-pasting (except for placeholders)
$textarea.prop( 'readonly', true );
$saveButton = $( [] );
}
$skipButton = $( '<button>' )
.addClass( 'tux-editor-skip-button mw-ui-button mw-ui-quiet' )
.text( mw.msg( 'tux-editor-skip-button-label' ) )
.on( 'click', function ( e ) {
translateEditor.skip();
translateEditor.next();
if ( translateEditor.options.onSkip ) {
translateEditor.options.onSkip.call( translateEditor );
}
e.stopPropagation();
} );
// This appears instead of "Skip" on the last message on the page
$cancelButton = $( '<button>' )
.addClass( 'tux-editor-cancel-button mw-ui-button mw-ui-quiet' )
.text( mw.msg( 'tux-editor-cancel-button-label' ) )
.on( 'click', function ( e ) {
translateEditor.skip();
translateEditor.hide();
e.stopPropagation();
} );
$controlButtonBlock = $( '<div>' )
.addClass( 'twelve columns tux-editor-control-buttons' )
.append( $requestRight, $saveButton, $skipButton, $cancelButton );
$editorColumn.append( $( '<div>' )
.addClass( 'row tux-editor-actions-block' )
.append( $editingButtonBlock )
);
$editorColumn.append( $editSummaryBlock );
$editorColumn.append( $( '<div>' )
.addClass( 'row tux-editor-actions-block' )
.append( $controlButtonBlock )
);
if ( canTranslate ) {
prefix = $.fn.updateTooltipAccessKeys.getAccessKeyPrefix();
$editorColumn.append( $( '<div>' )
.addClass( 'row shortcutinfo' )
.text( mw.msg(
'tux-editor-shortcut-info',
'CTRL-ENTER',
( prefix + 'd' ).toUpperCase(),
'ALT',
( prefix + 'b' ).toUpperCase()
) )
);
}
return $editorColumn;
},
/**
* Modifies the save button to provide suitable default action for *unchanged*
* message. It will revert back to normal save button if the text is changed.
*
* @param {jQuery} $button The save button.
*/
makeSaveButtonContextSensitive: function ( $button ) {
var self = this;
if ( this.message.properties.status === 'fuzzy' ) {
$button.prop( 'disabled', false );
$button.text( mw.msg( 'tux-editor-confirm-button-label' ) );
$button.off( 'click' );
$button.on( 'click', function ( e ) {
self.save();
e.stopPropagation();
} );
} else if ( this.message.proofreadable ) {
$button.prop( 'disabled', false );
$button.text( mw.msg( 'tux-editor-proofread-button-label' ) );
$button.off( 'click' );
$button.on( 'click', function ( e ) {
$button.prop( 'disabled', true );
self.message.proofreadAction();
self.next();
e.stopPropagation();
} );
}
},
/**
* Modifies the save button to just save the translation as usual. Whether the
* button is enabled or not is controlled elsewhere.
*
* @param {jQuery} $button The save button.
*/
makeSaveButtonJustSave: function ( $button ) {
var self = this;
$button.text( mw.msg( 'tux-editor-save-button-label' ) );
$button.off( 'click' );
$button.on( 'click', function ( e ) {
self.save();
e.stopPropagation();
} );
},
/**
* Validate the current translation using the API
* and show the warnings if necessary.
*/
validateTranslation: function () {
var translateEditor = this,
api,
$textarea = translateEditor.$editor.find( '.tux-textarea-translation' );
api = new mw.Api();
api.post( {
action: 'translationcheck',
title: this.message.title,
translation: $textarea.val()
} ).done( function ( data ) {
var warningIndex,
warnings = data.warnings;
translateEditor.removeWarning( 'validation' );
if ( !warnings || !warnings.length ) {
return;
}
// Remove useless fuzzy warning if we have more details
translateEditor.removeWarning( 'fuzzy' );
// Disable confirm translation button, since fuzzy translations
// cannot be confirmed. The check for dirty state can be removed
// to prevent translations with warnings.
if ( !translateEditor.dirty ) {
translateEditor.$editor.find( '.tux-editor-save-button' )
.prop( 'disabled', true );
}
for ( warningIndex = 0; warningIndex < warnings.length; warningIndex++ ) {
translateEditor.addWarning( warnings[ warningIndex ], 'validation' );
}
} );
},
/**
* Remove all warning of given type
*
* @param {string} type
*/
removeWarning: function ( type ) {
var $tuxWarning = this.$editor.find( '.tux-warning' );
$tuxWarning.find( '.' + type ).remove();
if ( !$tuxWarning.children().length ) {
this.$editor.find( '.tux-more-warnings' ).addClass( 'hide' );
}
},
/**
* Displays the supplied warning above the translation edit area.
* Newer warnings are added to the top while older warnings are
* added to the bottom. This also means that older warnings will
* not be shown by default unless the user clicks "more warnings" tab.
*
* @param {string} warning used as html for the warning display
* @param {string} type used to group the warnings.eg: validation, diff, error
* @return {jQuery} the new warning element
*/
addWarning: function ( warning, type ) {
var warningCount,
$warnings = this.$editor.find( '.tux-warning' ),
$moreWarningsTab = this.$editor.find( '.tux-more-warnings' ),
$newWarning = $( '<div>' )
.addClass( 'tux-warning-message ' + type )
.html( warning );
this.$editor.find( '.tux-warning-message' ).addClass( 'hide' );
$warnings
.removeClass( 'hide' )
.prepend( $newWarning );
warningCount = $warnings.find( '.tux-warning-message' ).length;
if ( warningCount > 1 ) {
$moreWarningsTab
.text( mw.msg( 'tux-warnings-more', warningCount - 1 ) )
.removeClass( 'hide open' );
} else {
$moreWarningsTab.addClass( 'hide' );
}
return $newWarning;
},
prepareInfoColumn: function () {
var $messageDescEditor, $messageDescTextarea,
$messageDescSaveButton, $messageDescCancelButton,
$messageDescViewer,
$infoColumn = $( '<div>' ).addClass( 'infocolumn' ),
translateEditor = this;
$infoColumn.append( $( '<div>' )
.addClass( 'row loading' )
.text( mw.msg( 'tux-editor-loading' ) )
);
if ( mw.config.get( 'wgTranslateDocumentationLanguageCode' ) ) {
$messageDescSaveButton = $( '<button>' )
.addClass( 'tux-editor-savedoc-button mw-ui-button mw-ui-progressive' )
.prop( 'disabled', true )
.text( mw.msg( 'tux-editor-doc-editor-save' ) )
.on( 'click', function () {
translateEditor.saveDocumentation()
.done( function () {
var $descEditLink = $messageDescViewer.find( '.message-desc-edit' );
$descEditLink.text( mw.msg( 'tux-editor-edit-desc' ) );
} );
} );
$messageDescCancelButton = $( '<button>' )
.addClass( 'tux-editor-skipdoc-button mw-ui-button mw-ui-quiet' )
.text( mw.msg( 'tux-editor-doc-editor-cancel' ) )
.on( 'click', function () {
translateEditor.hideDocumentationEditor();
} );
$messageDescTextarea = $( '<textarea>' )
.addClass( 'tux-textarea-documentation' )
.on( 'textchange', function () {
$messageDescSaveButton.prop( 'disabled', false );
} );
if ( mw.translate.isPlaceholderSupported( $messageDescTextarea ) ) {
$messageDescTextarea.prop( 'placeholder', mw.msg( 'tux-editor-doc-editor-placeholder' ) );
}
$messageDescEditor = $( '<div>' )
.addClass( 'row message-desc-editor hide' )
.append(
$messageDescTextarea,
$( '<div>' )
.addClass( 'row' )
.append(
$messageDescSaveButton,
$messageDescCancelButton
)
);
$messageDescViewer = $( '<div>' )
.addClass( 'message-desc-viewer hide' )
.append(
$( '<div>' )
.addClass( 'row message-desc' ),
$( '<div>' )
.addClass( 'row message-desc-control' )
.append( $( '<a>' )
.attr( {
href: mw.translate.getDocumentationEditURL(
this.message.title.replace( /\/[a-z-]+$/, '' )
),
target: '_blank'
} )
.addClass( 'message-desc-edit' )
.on( 'click', this.showDocumentationEditor.bind( this ) )
)
);
if ( !mw.translate.canTranslate() ) {
$messageDescViewer.find( '.message-desc-control' ).addClass( 'hide' );
}
$infoColumn.append(
$messageDescEditor,
$messageDescViewer
);
}
$infoColumn.append( $( '<div>' )
.addClass( 'row uneditable-documentation hide' )
);
$infoColumn.append( $( '<div>' )
.addClass( 'row tm-suggestions-title hide' )
.text( mw.msg( 'tux-editor-suggestions-title' ) )
);
$infoColumn.append( $( '<div>' )
.addClass( 'row in-other-languages-title hide' )
.text( mw.msg( 'tux-editor-in-other-languages' ) )
);
// The actual href is set when translationhelpers are loaded
$infoColumn.append( $( '<div>' )
.addClass( 'row help hide' )
.append(
$( '<span>' )
.text( mw.msg( 'tux-editor-need-more-help' ) ),
$( '<a>' )
.attr( {
href: '#',
target: '_blank'
} )
.text( mw.msg( 'tux-editor-ask-help' ) )
)
);
return $( '<div>' )
.addClass( 'five columns infocolumn-block' )
.append(
$( '<span>' ).addClass( 'tux-message-editor__caret' ),
$infoColumn
);
},
show: function () {
var $next, $textarea;
if ( !this.$editor ) {
this.init();
}
$textarea = this.$editor.find( '.editcolumn textarea' );
// Hide all other open editors in the page
$( '.tux-message.open' ).each( function () {
$( this ).data( 'translateeditor' ).hide();
} );
// The access keys need to be shifted to the editor currently active
$( '.tux-editor-save-button, .tux-editor-save-button' ).removeAttr( 'accesskey' );
this.$editor.find( '.tux-editor-save-button' ).attr( 'accesskey', 's' );
this.$editor.find( '.tux-editor-skip-button' ).attr( 'accesskey', 'd' );
this.$editor.find( '.tux-input-editsummary' ).attr( 'accesskey', 'b' );
// @todo access key for the cancel button
this.$messageItem.addClass( 'hide' );
this.$editor.removeClass( 'hide' );
$textarea.focus();
autosize( $textarea );
this.resizeInsertables( $textarea );
this.shown = true;
this.$editTrigger.addClass( 'open' );
// don't waste time, get ready with next message
$next = this.$editTrigger.next( '.tux-message' );
if ( $next.length ) {
$next.data( 'translateeditor' ).init();
}
mw.translateHooks.run( 'afterEditorShown', this.$editor );
return false;
},
hide: function () {
// If the user has made changes, make sure they are either
// in process of being saved or highlighted as unsaved.
if ( this.dirty ) {
if ( this.saving ) {
this.markSaving();
} else {
this.markUnsaved();
}
}
if ( this.$editor ) {
this.$editor.addClass( 'hide' );
}
this.hideShortcuts();
this.$editTrigger.removeClass( 'open' );
this.$messageItem.removeClass( 'hide' );
this.shown = false;
return false;
},
infoToggle: function ( toggleIcon ) {
if ( this.expanded ) {
this.contract( toggleIcon );
} else {
this.expand( toggleIcon );
}
},
contract: function ( toggleIcon ) {
// Change the icon image
toggleIcon
.removeClass( 'editor-contract' )
.addClass( 'editor-expand' )
.attr( 'title', mw.msg( 'tux-editor-expand-tooltip' ) );
this.$editor.removeClass( 'tux-message-editor--expanded' );
this.expanded = false;
},
expand: function ( toggleIcon ) {
// Change the icon image
toggleIcon
.removeClass( 'editor-expand' )
.addClass( 'editor-contract' )
.attr( 'title', mw.msg( 'tux-editor-collapse-tooltip' ) );
this.$editor.addClass( 'tux-message-editor--expanded' );
this.expanded = true;
},
/**
* Adds the diff between old and current definitions to the view.
*
* @param {Object} definitiondiff A definitiondiff object as returned by API.
*/
addDefinitionDiff: function ( definitiondiff ) {
var $trigger;
if ( !definitiondiff || definitiondiff.error ) {
mw.log( 'Error loading translation diff ' + definitiondiff && definitiondiff.error );
return;
}
// Load the diff styles
mw.loader.load( 'mediawiki.diff.styles' );
$trigger = $( '<span>' )
.addClass( 'show-diff-link' )
.text( mw.msg( 'tux-editor-outdated-warning-diff-link' ) )
.on( 'click', function () {
$( this ).parent().html( definitiondiff.html );
} );
this.removeWarning( 'fuzzy' );
this.addWarning(
mw.message( 'tux-editor-outdated-warning' ).escaped(),
'diff'
).append( $trigger );
},
/**
* Attach event listeners
*/
listen: function () {
var translateEditor = this;
this.$editTrigger.find( '.tux-message-item' ).click( function () {
translateEditor.show();
return false;
} );
},
/**
* Makes the textare large enough for insertables and positions the insertables.
*
* @param {jQuery} $textarea Text area.
*/
resizeInsertables: function ( $textarea ) {
var $buttonArea, buttonAreaHeight;
$buttonArea = this.$editor.find( '.tux-editor-insert-buttons' );
buttonAreaHeight = $buttonArea.height();
$textarea.css( 'padding-bottom', buttonAreaHeight + 5 );
$buttonArea.css( 'top', -buttonAreaHeight );
autosize.update( $textarea );
}
};
/*
* translateeditor PLUGIN DEFINITION
*/
$.fn.translateeditor = function ( options ) {
return this.each( function () {
var $this = $( this ),
data = $this.data( 'translateeditor' );
if ( !data ) {
$this.data( 'translateeditor',
( data = new TranslateEditor( this, options ) )
);
}
if ( typeof options === 'string' ) {
data[ options ].call( $this );
}
} );
};
mw.translate.editor = mw.translate.editor || {};
mw.translate.editor = $.extend( TranslateEditor.prototype, mw.translate.editor );
function delayer() {
return ( function () {
var timer = 0;
return function ( callback, milliseconds ) {
clearTimeout( timer );
timer = setTimeout( callback, milliseconds );
};
}() );
}
}( jQuery, mediaWiki, autosize ) );