summaryrefslogtreecommitdiff
path: root/www/wiki/extensions/Translate/resources/js/ext.translate.editor.js
diff options
context:
space:
mode:
Diffstat (limited to 'www/wiki/extensions/Translate/resources/js/ext.translate.editor.js')
-rw-r--r--www/wiki/extensions/Translate/resources/js/ext.translate.editor.js1324
1 files changed, 1324 insertions, 0 deletions
diff --git a/www/wiki/extensions/Translate/resources/js/ext.translate.editor.js b/www/wiki/extensions/Translate/resources/js/ext.translate.editor.js
new file mode 100644
index 00000000..b5e3637d
--- /dev/null
+++ b/www/wiki/extensions/Translate/resources/js/ext.translate.editor.js
@@ -0,0 +1,1324 @@
+/* global autosize */
+
+( function () {
+ '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.hook( 'mw.translate.editor.beforeSubmit' ).fire( 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.hook( 'mw.translate.editor.afterSubmit' ).fire( 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.hook( 'mw.translate.editor.afterEditorShown' ).fire( 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 );
+ };
+ }() );
+ }
+}() );