diff options
Diffstat (limited to 'www/wiki/extensions/Translate/resources/js/ext.translate.special.pagemigration.js')
-rw-r--r-- | www/wiki/extensions/Translate/resources/js/ext.translate.special.pagemigration.js | 523 |
1 files changed, 523 insertions, 0 deletions
diff --git a/www/wiki/extensions/Translate/resources/js/ext.translate.special.pagemigration.js b/www/wiki/extensions/Translate/resources/js/ext.translate.special.pagemigration.js new file mode 100644 index 00000000..3713ac56 --- /dev/null +++ b/www/wiki/extensions/Translate/resources/js/ext.translate.special.pagemigration.js @@ -0,0 +1,523 @@ +( function () { + 'use strict'; + var noOfSourceUnits, noOfTranslationUnits, + pageName = '', + langCode = '', + sourceUnits = []; + + /** + * Create translation pages using content of right hand side blocks + * and identifiers from left hand side blocks. Create pages only if + * content is not empty. + * + * @param {number} i Array index to sourceUnits. + * @param {string} content + * @return {Function} Returns a function which returns a jQuery.Promise + */ + function createTranslationPage( i, content ) { + + return function () { + var identifier, title, summary, + api = new mw.Api(); + + identifier = sourceUnits[ i ].identifier; + title = 'Translations:' + pageName + '/' + identifier + '/' + langCode; + summary = $( '#pm-summary' ).val(); + + return api.postWithToken( 'csrf', { + action: 'edit', + watchlist: 'nochange', + title: title, + text: content, + summary: summary + } ); + }; + } + + /** + * Get the old translations of a given page at given time. + * + * @param {string} fuzzyTimestamp Timestamp in MediaWiki format + * @param {string} pageTitle + * @return {jQuery.Promise} + * @return {Function} return.done + * @return {Array} return.done.data Array of old translations + */ + function splitTranslationPage( fuzzyTimestamp, pageTitle ) { + var api = new mw.Api(); + + return api.get( { + action: 'query', + prop: 'revisions', + rvprop: 'content', + rvstart: fuzzyTimestamp, + titles: pageTitle + } ).then( function ( data ) { + var pageContent, oldTranslationUnits, obj, page, + errorBox = $( '.mw-tpm-sp-error__message' ); + for ( page in data.query.pages ) { + obj = data.query.pages[ page ]; + } + if ( typeof obj === undefined ) { + // obj was not initialized + errorBox.text( mw.msg( 'pm-page-does-not-exist', pageTitle ) ).show( 'fast' ); + return $.Deferred().reject(); + } + if ( obj.revisions === undefined ) { + // the case of /en subpage where first edit is by FuzzyBot + errorBox.text( mw.msg( 'pm-old-translations-missing', pageTitle ) ).show( 'fast' ); + return $.Deferred().reject(); + } + pageContent = obj.revisions[ 0 ][ '*' ]; + oldTranslationUnits = pageContent.split( '\n\n' ); + return oldTranslationUnits; + } ); + } + + /** + * Get the timestamp before FuzzyBot's first edit on page. + * + * @param {string} pageTitle + * @return {jQuery.Promise} + * @return {Function} return.done + * @return {string} return.done.data + */ + function getFuzzyTimestamp( pageTitle ) { + var api = new mw.Api(); + + // This api call returns the timestamp of FuzzyBot's edit + return api.get( { + action: 'query', + prop: 'revisions', + rvprop: 'timestamp', + rvuser: 'FuzzyBot', + rvdir: 'newer', + titles: pageTitle + } ).then( function ( data ) { + var timestampFB, dateFB, timestampOld, + page, obj, + errorBox = $( '.mw-tpm-sp-error__message' ); + for ( page in data.query.pages ) { + obj = data.query.pages[ page ]; + } + // Page does not exist if missing field is present + if ( obj.missing === '' ) { + errorBox.text( mw.msg( 'pm-page-does-not-exist', pageTitle ) ).show( 'fast' ); + return $.Deferred().reject(); + } + // Page exists, but no edit by FuzzyBot + if ( obj.revisions === undefined ) { + errorBox.text( mw.msg( 'pm-old-translations-missing', pageTitle ) ).show( 'fast' ); + return $.Deferred().reject(); + } else { + // FB over here refers to FuzzyBot + timestampFB = obj.revisions[ 0 ].timestamp; + dateFB = new Date( timestampFB ); + dateFB.setSeconds( dateFB.getSeconds() - 1 ); + timestampOld = dateFB.toISOString(); + mw.log( 'New Timestamp: ' + timestampOld ); + return timestampOld; + } + } ); + } + + /** + * Get the translation units created by Translate extension. + * + * @param {string} pageName + * @return {jQuery.Promise} + * @return {Function} return.done + * @return {Object[]} return.done.data Array of sUnit Objects + */ + function getSourceUnits( pageName ) { + var api = new mw.Api(); + + return api.get( { + action: 'query', + list: 'messagecollection', + mcgroup: 'page-' + pageName, + mclanguage: 'en', + mcprop: 'definition' + } ).then( function ( data ) { + var result, i, sUnit, key; + sourceUnits = []; + result = data.query.messagecollection; + for ( i = 1; i < result.length; i++ ) { + sUnit = {}; + key = result[ i ].key; + sUnit.identifier = key.slice( key.lastIndexOf( '/' ) + 1 ); + sUnit.definition = result[ i ].definition; + sourceUnits.push( sUnit ); + } + return sourceUnits; + } ); + } + + /** + * Shift rows up by one unit. This is called after a unit is deleted. + * + * @param {jQuery} $start The starting node + */ + function shiftRowsUp( $start ) { + var nextVal, + $current = $start, + $next = $start.next(); + + while ( $next.length ) { + nextVal = $next.find( '.mw-tpm-sp-unit__target' ).val(); + $current.find( '.mw-tpm-sp-unit__target' ).val( nextVal ); + $current = $next; + $next = $current.next(); + } + if ( $current.find( '.mw-tpm-sp-unit__source' ).val() ) { + $current.find( '.mw-tpm-sp-unit__target' ).val( '' ); + } else { + $current.remove(); + } + } + + /** + * Shift rows down by one unit. This is called after a new empty unit is + * added. + * + * @param {jQuery} $nextRow The next row to start with + * @param {string} text The text of the next row + * @return {string} text The text of the last row + */ + function shiftRowsDown( $nextRow, text ) { + var oldText; + + while ( $nextRow.length ) { + oldText = $nextRow.find( '.mw-tpm-sp-unit__target' ).val(); + $nextRow.find( '.mw-tpm-sp-unit__target' ).val( text ); + $nextRow = $nextRow.next(); + text = oldText; + } + return text; + } + + /** + * Create a new row of source text and target text with action icons. + * + * @param {string} sourceText + * @param {string} targetText + * @return {jQuery} newUnit The new row unit object + */ + + function createNewUnit( sourceText, targetText ) { + var newUnit, sourceUnit, targetUnit, actionUnit; + + newUnit = $( '<div>' ).addClass( 'mw-tpm-sp-unit row' ); + sourceUnit = $( '<textarea>' ).addClass( 'mw-tpm-sp-unit__source five columns' ) + .prop( 'readonly', true ).attr( 'tabindex', '-1' ).val( sourceText ); + targetUnit = $( '<textarea>' ).addClass( 'mw-tpm-sp-unit__target five columns' ) + .val( targetText ).prop( 'dir', $.uls.data.getDir( langCode ) ); + actionUnit = $( '<div>' ).addClass( 'mw-tpm-sp-unit__actions two columns' ); + actionUnit.append( + $( '<span>' ).addClass( 'mw-tpm-sp-action mw-tpm-sp-action--add' ) + .attr( 'title', mw.msg( 'pm-add-icon-hover-text' ) ), + $( '<span>' ).addClass( 'mw-tpm-sp-action mw-tpm-sp-action--swap' ) + .attr( 'title', mw.msg( 'pm-swap-icon-hover-text' ) ), + $( '<span>' ).addClass( 'mw-tpm-sp-action mw-tpm-sp-action--delete' ) + .attr( 'title', mw.msg( 'pm-delete-icon-hover-text' ) ) + ); + newUnit.append( sourceUnit, targetUnit, actionUnit ); + return newUnit; + } + + /** + * Display the source and target units alongwith the action icons. + * + * @param {Array} sourceUnits + * @param {Array} translations + */ + function displayUnits( sourceUnits, translations ) { + var i, totalUnits, newUnit, unitListing, + sourceText, targetText; + + noOfSourceUnits = sourceUnits.length; + noOfTranslationUnits = translations.length; + totalUnits = noOfSourceUnits > noOfTranslationUnits ? noOfSourceUnits : noOfTranslationUnits; + unitListing = $( '.mw-tpm-sp-unit-listing' ); + unitListing.html( '' ); + for ( i = 0; i < totalUnits; i++ ) { + sourceText = targetText = ''; + if ( sourceUnits[ i ] !== undefined ) { + sourceText = sourceUnits[ i ].definition; + } + if ( translations[ i ] !== undefined ) { + targetText = translations[ i ]; + } + newUnit = createNewUnit( sourceText, targetText ); + unitListing.append( newUnit ); + } + } + + /** + * Split headers from remaining text in each translation unit if present. + * + * @param {Array} translations Array of initial units obtained on splitting + * @return {string[]} Array having the headers split into new unit + */ + function splitHeaders( translations ) { + return $.map( translations, function ( elem ) { + // Check https://regex101.com/r/oT7fZ2 for details + return elem.match( /(^==.+$|(?:(?!^==).+\n?)+)/gm ); + } ); + } + + /** + * Get the index of next translation unit containing h2 header. + * + * @param {number} startIndex Index to start the scan from + * @param {string[]} translationUnits Segmented units. + * @return {number} Index of the next unit found, -1 if not. + */ + function getHeaderUnit( startIndex, translationUnits ) { + var i, regex; + regex = new RegExp( /^==[^=]+==$/m ); + for ( i = startIndex; i < translationUnits.length; i++ ) { + if ( regex.test( translationUnits[ i ] ) ) { + return i; + } + } + return -1; + } + + /** + * Align h2 headers in the order they appear. + * Assumption: The source headers and translation headers appear in + * the same order. + * + * @param {Object[]} sourceUnits + * @param {string[]} translationUnits + * @return {string[]} + */ + function alignHeaders( sourceUnits, translationUnits ) { + var i, regex, tIndex = 0, + matchText, emptyCount, mergeText; + + regex = new RegExp( /^==[^=]+==$/m ); + for ( i = 0; i < sourceUnits.length; i++ ) { + if ( regex.test( sourceUnits[ i ].definition ) ) { + tIndex = getHeaderUnit( tIndex, translationUnits ); + mergeText = ''; + // search is over + if ( tIndex === -1 ) { + break; + } + // remove the unit + matchText = translationUnits.splice( tIndex, 1 ).toString(); + emptyCount = i - tIndex; + if ( emptyCount > 0 ) { + // add empty units + while ( emptyCount !== 0 ) { + translationUnits.splice( tIndex, 0, '' ); + emptyCount -= 1; + } + } else if ( emptyCount < 0 ) { + // merge units until there is room for tIndex translation unit to + // align with ith source unit + while ( emptyCount !== 0 ) { + mergeText += translationUnits.splice( i, 1 ).toString() + '\n'; + emptyCount += 1; + } + if ( i !== 0 ) { + translationUnits[ i - 1 ] += '\n' + mergeText; + } else { + matchText = mergeText + matchText; + } + } + // add the unit back + translationUnits.splice( i, 0, matchText ); + tIndex = i + 1; + } + } + return translationUnits; + } + + /** + * Handler for 'Save' button click event. + */ + function saveHandler() { + var i, content, list = []; + + $( '.mw-tpm-sp-error__message' ).hide( 'fast' ); + if ( noOfSourceUnits < noOfTranslationUnits ) { + $( '.mw-tpm-sp-error__message' ).text( mw.msg( 'pm-extra-units-warning' ) ) + .show( 'fast' ); + return; + } else { + $( 'input' ).prop( 'disabled', true ); + $( '.mw-tpm-sp-instructions' ).hide( 'fast' ); + for ( i = 0; i < noOfSourceUnits; i++ ) { + content = $( '.mw-tpm-sp-unit__target' ).eq( i ).val(); + content = content.trim(); + if ( content !== '' ) { + list.push( createTranslationPage( i, content ) ); + } + } + + $.ajaxDispatcher( list, 1 ).done( function () { + $( '#action-import' ).removeClass( 'hide' ); + $( 'input' ).prop( 'disabled', false ); + $( '.mw-tpm-sp-instructions' ).text( mw.msg( 'pm-on-save-message-text' ) ).show( 'fast' ); + } ).fail( function ( errmsg ) { + $( 'input' ).prop( 'disabled', false ); + $( '.mw-tpm-sp-error__message' ).text( mw.msg( errmsg ) ).show( 'fast' ); + } ); + } + } + + /** + * Handler for 'Cancel' button click event. + */ + function cancelHandler() { + $( '.mw-tpm-sp-error__message' ).hide( 'fast' ); + $( '.mw-tpm-sp-instructions' ).hide( 'fast' ); + $( '#action-save, #action-cancel' ).addClass( 'hide' ); + $( '#action-import' ).removeClass( 'hide' ); + $( '.mw-tpm-sp-unit-listing' ).html( '' ); + } + + /** + * Handler for add new unit icon ('+') click event. Adds a translation unit + * below the current unit. + * + * @param {jQuery.Event} event + */ + function addHandler( event ) { + var nextRow, text, newUnit, targetUnit; + + nextRow = $( event.target ).closest( '.mw-tpm-sp-unit' ).next(); + targetUnit = nextRow.find( '.mw-tpm-sp-unit__target' ); + text = targetUnit.val(); + targetUnit.val( '' ); + nextRow = nextRow.next(); + text = shiftRowsDown( nextRow, text ); + if ( text ) { + newUnit = createNewUnit( '', text ); + $( '.mw-tpm-sp-unit-listing' ).append( newUnit ); + } + noOfTranslationUnits += 1; + } + + /** + * Handler for delete icon ('-') click event. Deletes the unit and shifts + * the units up by one. + * + * @param {jQuery.Event} event + */ + function deleteHandler( event ) { + var sourceText, rowUnit; + rowUnit = $( event.target ).closest( '.mw-tpm-sp-unit' ); + sourceText = rowUnit.find( '.mw-tpm-sp-unit__source' ).val(); + if ( !sourceText ) { + rowUnit.remove(); + } else { + rowUnit.find( '.mw-tpm-sp-unit__target' ).val( '' ); + shiftRowsUp( rowUnit ); + } + noOfTranslationUnits -= 1; + } + + /** + * Handler for swap icon click event. Swaps the text in the current unit + * with the text in the unit below. + * + * @param {jQuery.Event} event + */ + function swapHandler( event ) { + var rowUnit, tempText, nextVal; + rowUnit = $( event.target ).closest( '.mw-tpm-sp-unit' ); + tempText = rowUnit.find( '.mw-tpm-sp-unit__target' ).val(); + nextVal = rowUnit.next().find( '.mw-tpm-sp-unit__target' ).val(); + rowUnit.find( '.mw-tpm-sp-unit__target' ).val( nextVal ); + rowUnit.next().find( '.mw-tpm-sp-unit__target' ).val( tempText ); + } + + /** + * Handler for 'Import' button click event. Imports source and translation + * units and displays them. + * + * @param {jQuery.Event} e + */ + function importHandler( e ) { + var pageTitle, slashPos, titleObj, + errorBox = $( '.mw-tpm-sp-error__message' ), + messageBox = $( '.mw-tpm-sp-instructions' ); + + e.preventDefault(); + + pageTitle = $( '#title' ).val().trim(); + if ( pageTitle === '' ) { + errorBox.text( mw.msg( 'pm-pagetitle-missing' ) ).show( 'fast' ); + return; + } + + titleObj = mw.Title.newFromText( pageTitle ); + messageBox.hide( 'fast' ); + if ( titleObj === null ) { + errorBox.text( mw.msg( 'pm-pagetitle-invalid' ) ).show( 'fast' ); + return; + } + + pageTitle = titleObj.getPrefixedDb(); + slashPos = pageTitle.lastIndexOf( '/' ); + + if ( slashPos === -1 ) { + errorBox.text( mw.msg( 'pm-langcode-missing' ) ).show( 'fast' ); + return; + } + + pageName = pageTitle.substring( 0, slashPos ); + langCode = pageTitle.substring( slashPos + 1 ); + + if ( pageName === '' ) { + errorBox.text( mw.msg( 'pm-pagetitle-invalid' ) ).show( 'fast' ); + return; + } + + errorBox.hide( 'fast' ); + + $.when( getSourceUnits( pageName ), getFuzzyTimestamp( pageTitle ) ) + .then( function ( sourceUnits, fuzzyTimestamp ) { + noOfSourceUnits = sourceUnits.length; + splitTranslationPage( fuzzyTimestamp, pageTitle ).done( function ( translations ) { + var translationUnits = splitHeaders( translations ); + translationUnits = alignHeaders( sourceUnits, translationUnits ); + noOfTranslationUnits = translationUnits.length; + displayUnits( sourceUnits, translationUnits ); + $( '#action-save, #action-cancel' ).removeClass( 'hide' ); + $( '#action-import' ).addClass( 'hide' ); + messageBox.text( mw.msg( 'pm-on-import-message-text' ) ).show( 'fast' ); + } ); + } ); + } + + /** + * Listens to various click events + */ + function listen() { + var $listing = $( '.mw-tpm-sp-unit-listing' ); + + $( '#mw-tpm-sp-primary-form' ).submit( importHandler ); + $( '#action-import' ).click( importHandler ); + $( '#action-save' ).click( saveHandler ); + $( '#action-cancel' ).click( cancelHandler ); + $listing.on( 'click', '.mw-tpm-sp-action--swap', swapHandler ); + $listing.on( 'click', '.mw-tpm-sp-action--delete', deleteHandler ); + $listing.on( 'click', '.mw-tpm-sp-action--add', addHandler ); + } + + $( listen ); + + mw.translate = mw.translate || {}; + mw.translate = $.extend( mw.translate, { + getSourceUnits: getSourceUnits, + getFuzzyTimestamp: getFuzzyTimestamp, + splitTranslationPage: splitTranslationPage, + alignHeaders: alignHeaders + } ); + +}() ); |