MediaWiki:Gadget-translation-editor.js

Wersja z dnia 21:29, 17 sty 2016 autorstwa Peter Bowman (dyskusja | edycje) (z Wikipedysta:Peter Bowman/add-translations.js)
(różn.) ← poprzednia wersja | przejdź do aktualnej wersji (różn.) | następna wersja → (różn.)

Uwaga: aby zobaczyć zmiany po opublikowaniu, może zajść potrzeba wyczyszczenia pamięci podręcznej przeglądarki.

  • Firefox / Safari: Przytrzymaj Shift podczas klikania Odśwież bieżącą stronę, lub naciśnij klawisze Ctrl+F5, lub Ctrl+R (⌘-R na komputerze Mac)
  • Google Chrome: Naciśnij Ctrl-Shift-R (⌘-Shift-R na komputerze Mac)
  • Internet Explorer / Edge: Przytrzymaj Ctrl, jednocześnie klikając Odśwież, lub naciśnij klawisze Ctrl+F5
  • Opera: Naciśnij klawisze Ctrl+F5.
var gadget = mw.libs.translationEditor = {};

var pageContent, pageDraft, translations, api;
var starttimestamp, basetimestamp;
var startIndex, endIndex;

var messages = {
	'transl-add-translation': '(dodaj)',
	'transl-api-error':       'Prawdopodobnie nastąpił konflikt edycji. Przeładuj stronę i spróbuj ponownie.',
	'transl-edit-error':      'Możliwość edytowania istniejących tlumaczeń zostanie wdrożona w przyszłości.',
	'transl-parsing-error':   'Nie udało się odczytać kodu strony. Przejdź na widok edycji i sprawdź, czy nie ma błędów w formacie.',
	'transl-select-lang':     'Wybierz język:',
	'transl-lang-label':      'Tłumaczysz na<br><strong>$1</strong>',
	'transl-lang-label-iso':  'Tłumaczysz na<br><strong>$1</strong> <small>(<a href="/wiki/Wikis%C5%82ownik:Kody_j%C4%99zyk%C3%B3w" target="_blank">$2</a>)</small>',
	'transl-load-button':     'Załaduj',
	'transl-preview-label':   'Podgląd zmian',
	'transl-apply-button':    'Pokaż zmiany',
	'transl-submit-button':   'Zapisz',
	'transl-watch-article':   'Obserwuj',
	'transl-submitting':      'Zapisywanie',
	'transl-reloading':       'Przeładowywanie',
	'transl-draft-change':    '(przejdź)',
	'transl-draft-remove':    '(usuń)',
	'transl-keyboard':        '(klawiatura&nbsp;ekranowa)',
	'transl-keyboard-close':  'Zamknij',
	'transl-report-error':    '(<a href="//pl.wiktionary.org/w/index.php?title=Dyskusja_wikipedysty:Peter_Bowman&action=edit&section=new&preloadtitle=add-translations.js%20%28b%C5%82%C4%85d%29&nosummary=" target="_blank">zgłoś&nbsp;błąd</a>)'
};

function initialize( $content ) {
	var $transl, $defs, $buttonHolder;
	
	gadget.activeLang = '';
	gadget.defn = [];
	gadget.langs = [];
	gadget.drafts = {};
	
	$transl = $content.find( 'dt.lang-pl.fldt-tlumaczenia' ).first();
	$defs   = $content.find( 'dd.lang-pl.fldt-znaczenia' );
	
	if ( !$transl.length || !$defs.length ) {
		return;
	}
	
	$transl
		.attr( 'id', 'transl-field' )
		.children( '#transl' )
		.css( 'display', 'inline' );
	
	$buttonHolder = $( '<div>' )
		.attr( 'id', 'transl-buttons' )
		.appendTo( $transl );
	
	if ( window.customLang && typeof hideTranslations === 'function' ) {
		hideTranslations( customLang );
	}
	
	extractDefinitions( $defs );
	
	if ( gadget.defn.length !== $defs.length ) {
		return;
	}
	
	$( '<small>' )
		.text( mw.msg( 'transl-add-translation' ) )
		.attr( 'id', 'transl-addbutton' )
		.appendTo( $buttonHolder )
		.on( 'click', function () {
			var $spinner;
			
			if ( !pageContent ) {
				$spinner = $.createSpinner( {
					size: 'large',
					type: 'block'
				} ).appendTo( $transl );
				
				makeRequest( function () {
					$spinner.remove();
					$content.find( '#transl-editbox' ).show();
				} );
			}
			
			$content.find( '#transl-editbox' ).toggle();
		} );
	
	$transl.parent().show();
}

function makeRequest( callback ) {
	$.when(
		api.get( {
			prop:        'revisions',
			rvprop:      [ 'timestamp', 'content' ],
			titles:       mw.config.get( 'wgPageName' ),
			indexpageids: true,
			curtimestamp: true
		} ),
		mw.loader.using( [
			'mediawiki.api.edit',
			'mediawiki.cookie',
			'mediawiki.language.specialCharacters',
			'mediawiki.RegExp',
			'jquery.suggestions',
			'jquery.byteLength',
			'jquery.tipsy',
			'jquery.textSelection',
			'ext.gadget.langdata'
		] )
	)
	.done( function ( res ) {
		var data = res[ 0 ];
		var id   = data.query.pageids[ 0 ];
		var page = data.query.pages[ id ];
		
		pageContent    = page.revisions[ 0 ][ '*' ];
		starttimestamp = data.curtimestamp;
		basetimestamp  = page.revisions[ 0 ].timestamp;
	} )
	.done( function () {
		if ( analyzePage() ) {
			createMenu();
		} else {
			alert( mw.msg( 'transl-parsing-error' ) );
		}
	} )
	.done( callback );
}

function extractDefinitions( $defs ) {
	$defs.each( function () {
		var m, text = $( this ).children( 'dfn' ).text();
		
		if ( !text ) {
			text = $( this ).text();
		}
		
		m = text.match( /^(\(.+?\)) (.+)/ );
		
		if ( m && m[ 1 ] ) {
			gadget.defn.push( {
				num: m[ 1 ],
				text: m[ 2 ].replace( /\[\d+\]/g, '' )
			} );
		} else {
			return false;
		}
	} );
}

function analyzePage() {
	var a2, a3, a4, b, langSection, langs, targetLang, targetRow;
	var a = pageContent.indexOf( ' ({' + '{język polski' );
	
	if ( a === -1 ) {
		a = pageContent.indexOf( ' ({' + '{termin obcy w języku polskim' );
	}
	
	if ( a === -1 ) {
		return false;
	}
	
	b = pageContent.indexOf( '\n== ', a );
	b = ( b !== -1 ) ? b : pageContent.length;
	langSection = pageContent.slice( 0, b );
	a2 = langSection.indexOf( '{' + '{tłumaczenia}}\n', a );
	b = langSection.indexOf( '{' + '{źródła}}', a2 );
	
	if ( a2 === -1 || b === -1 ) {
		return false;
	}
	
	translations = langSection.slice( 0, b );
	a3 = translations.indexOf( '\n*', a2 );
	
	if (
		a3 !== -1 &&
		translations.slice( a3 ).indexOf( '{{zobtłum' ) !== -1
	) {
		a3 = translations.lastIndexOf( '{{zobtłum' );
		a3 = translations.indexOf( '\n*', a3 );
	}
	
	if ( a3 !== -1 ) {
		langs = [];
		
		$.each(
			translations.slice( a3 + 1, b ).split( '\n' ),
			function ( i, line ) {
				var res = line.match( /^\* +([^:]+):/ );
				
				if ( res && res[ 1 ] ) {
					langs.push( res[ 1 ] );
				}
			} );
		
		if ( !langs.length ) {
			return false;
		}
		
		gadget.langs = langs;
		startIndex = a3 + 1;
	} else {
		startIndex = b;
	}
	
	endIndex = b;
	translations = pageContent.substring( startIndex, endIndex );
	
	return true;
}

function getLangPos( target ) {
	var n, lang, _char, p1, p2;
	var abc = 'aąbcćdeęfghijklłmnńoópqrsśtuvwxyzźż';
	var index = -1;
	
	outer:
	for ( n = 0; n < gadget.langs.length; n++ ) {
		lang = gadget.langs[ n ];
		
		inner:
		for ( _char = 0; _char < lang.length; _char++ ) {
			p1 = abc.indexOf( target[ _char ] );
			p2 = abc.indexOf( lang[ _char ] );
			
			if ( p1 > p2 ) {
				index++;
				continue outer;
			} else if ( p1 === p2 ) {
				continue inner;
			} else {
				break outer;
			}
		}
	}
	
	return gadget.langs[ index ];
}

function prepareDraft() {
	$.each( gadget.drafts, function ( lang, draft ) {
		var targetLang, targetRow, pos, translation;
		
		if ( gadget.langs.length ) {
			targetLang = getLangPos( lang );
		}
		
		if ( targetLang ) {
			targetRow = mw.format( '\\* +$1:', mw.RegExp.escape( targetLang ) );
			pos = translations.search( targetRow );
			pos = translations.indexOf( '\n', pos ) + 1;
		} else {
			pos = 0;
		}
		
		translation = mw.format( '$1: $2\n', lang, serializeDraft( draft ) );
		
		translations =
			translations.slice( 0, pos ) +
			'* ' + translation +
			translations.slice( pos, translations.length );
		
		gadget.langs.splice( $.inArray( targetLang, gadget.langs ) + 1, 0, lang );
	} );
	
	pageDraft =
		pageContent.slice( 0, startIndex ) +
		translations +
		pageContent.slice( endIndex, pageContent.length );
}

function resetForms( gui ) {
	gadget.activeLang = null;
	gui.$langLabel.html( mw.msg( 'transl-select-lang' ) ),
	gui.$langSelector.val( '' );
	gui.$textInputs.find( 'input' ).val( '' );
	gui.$apply.prop( 'disabled', true );
	gui.$textInputs.find( 'input' ).prop( 'disabled', true );
	
	if ( $.isEmptyObject( gadget.drafts ) ) {
		gui.$watch.prop( 'disabled', true );
		gui.$submit.prop( 'disabled', true );
	}
}

function onLoadLang( gui, targetLang, evt ) {
	var lang = targetLang || gui.$langSelector.val();
	var draft = gadget.drafts[ lang ];
	evt && evt.preventDefault();
	
	if ( !lang ) {
		return;
	}
	
	resetForms( gui );
	
	if (
		!draft &&
		$.inArray( lang, gadget.langs ) !== -1
	) {
		alert( mw.msg( 'transl-edit-error' ) );
		return;
	}
	
	gadget.activeLang = lang;
	gui.$langLabel.html( lang in mw.config.get( 'lang2code' )
		? mw.msg( 'transl-lang-label-iso', lang, mw.config.get( 'lang2code' )[ lang ] )
		: mw.msg( 'transl-lang-label', lang )
	);
	gui.$textInputs.find( 'input' ).prop( 'disabled', false );
	
	if ( draft ) {
		gui.$textInputs.children().each( function () {
			var $this = $( this );
			var defn = $this.data( 'defn' );
			var $inputs = $this.children( 'input' );
			var $text = $inputs.first();
			var $tmpl = $inputs.last();
			var text = [], tmpl = [];
			
			if ( !draft[ defn ] ) {
				return true;
			}
			
			$.each( draft[ defn ], function ( i, obj ) {
				text.push( stripBrackets( obj.base ) );
				tmpl.push( stripBrackets( obj.template || '' ) );
			} );
			
			$text.val( text.join( ', ' ) );
			
			if ( tmpl.join( '' ) !== '' ) {
				$tmpl.val( tmpl.join( ',' ) );
			}
		} );
	}
	
	refreshPreview( gui, lang );
	gui.$apply.prop( 'disabled', false );
}

function stripBrackets( s ) {
	return s.replace( /^[\[\{]{2}([^\|\[\]\{\}]+?)[\]\}]{2}$/, '$1' );
}

function onApplyChanges( gui, evt ) {
	var preview;
	var draft = {};
	evt.preventDefault();
	
	gui.$textInputs.children()
		.each( function () {
			var terms, tmpls, temp;
			var $this = $( this );
			var $inputs = $this.children( 'input' );
			var text = $.trim( $inputs.first().val() );
			var tmpl = $.trim( $inputs.last().val() );
			
			if ( !text ) {
				return true;
			}
			
			terms = text.split( / *, */ );
			tmpls = tmpl
				? tmpl.split( / *, */ )
				: null;
			arr = [];
			
			$.each( terms, function ( j, term ) {
				var base, template;

				if ( term.indexOf( '[' ) > -1 ) {
					base = term;
				} else {
					base = '[' + '[' + term + ']]';
				}
				
				if ( tmpls && tmpls[ j ] ) {
					template = '{' + '{' + tmpls[ j ] + '}}';
				}
				
				arr.push( {
					base:     base,
					template: template
				} );
			} );
			
			draft[ $this.data( 'defn' ) ] = arr;
		} );
	
	if ( $.isEmptyObject( draft ) ) {
		return;
	}
	
	gadget.drafts[ gadget.activeLang ] = draft;
	refreshPreview( gui, gadget.activeLang );
	
	gui.$watch.prop( 'disabled', false );
	gui.$submit.prop( 'disabled', false );
}

function serializeDraft( obj ) {
	return $.map( obj, function ( data, defn ) {
		return defn + ' ' + $.map( data, function ( word ) {
			return ( word.template
				? word.base + ' ' + word.template
				: word.base
			);
		} ).join( ', ' );
	} ).join( '; ' );
}

function refreshPreview( gui, active ) {
	var $ul = $( '<ul>' );
	
	$.each( gadget.drafts, function ( lang, draft ) {
		var $li, $change, $remove;
		
		$li = $( '<li>' )
			.append(
				$( '<strong>' ).text( mw.format( '$1:', lang ) ),
				$( '<code>' ).text( serializeDraft( draft ) ),
				$change = $( '<small>' ).text( mw.msg( 'transl-draft-change' ) ),
				' ',
				$remove = $( '<small>' ).text( mw.msg( 'transl-draft-remove' ) )
			)
			.appendTo( $ul );
		
		if ( lang === active ) {
			$li.addClass( 'transl-active' );
			$change.addClass( 'disabled' );
		} else {			
			$change.on( 'click', function () {
				gadget.activeLang = lang;
				onLoadLang( gui, lang );
				$ul.children().removeClass( 'transl-active' );
				$li.addClass( 'transl-active' );
			} );
		}
		
		$remove.on( 'click', function () {
			delete gadget.drafts[ lang ];
			$li.remove();
			
			if ( lang === active ) {
				resetForms( gui );
			}
		} );
	} );
	
	gui.$preview.replaceWith( $ul );
	gui.$preview = $ul;
}

function makeSummary() {
	var s, arr = [];
	
	var buildString = function ( a, delimiter ) {
		return mw.format( '+tłumaczenie na $1',
			a.join( delimiter )
		);
	};
	
	$.each( gadget.drafts, function ( lang, data ) {
		arr.push( mw.format( '$1: $2', lang, serializeDraft( data ) ) );
	} );
	
	arr.sort( function ( a, b ) {
		return a.localeCompare( b, 'pl' );
	} );
	
	s = buildString( arr, ' • ' );
	
	if ( $.byteLength( s ) > 255 ) {
		arr = [];
		
		$.each( gadget.drafts, function ( lang, data ) {
			arr.push( lang );
		} );
		
		arr.sort( function ( a, b ) {
			return a.localeCompare( b, 'pl' );
		} );
		
		s = buildString( arr, ', ' );
	}
	
	return s;
}

function onSubmit( gui, evt ) {
	evt.preventDefault();
	prepareDraft();
	
	gui.$submit
		.prop( 'disabled', true )
		.attr( 'value', mw.msg( 'transl-submitting' ) );
	
	api.postWithEditToken( {
		action:        'edit',
		title:          mw.config.get( 'wgPageName' ),
		text:           pageDraft,
		tags:           'script',
		summary:        makeSummary(),
		watchlist:      gui.$watch.prop( 'checked' ) ? 'watch' : 'nochange',
		notminor:       true,
		starttimestamp: starttimestamp,
		basetimestamp:  basetimestamp
	} )
	.done( function () {
		gui.$submit.attr( 'value', mw.msg( 'transl-reloading' ) );
		window.location.reload();
	} )
	.fail( function () {
		alert( mw.msg( 'transl-api-error' ) );
	} );
}

// [mediawiki/extensions/WikiEditor] / modules / jquery.wikiEditor.toolbar.js
function buildCharacter( character, actions ) {
	if ( typeof character === 'string' ) {
		character = {
			label: character,
			action: {
				type: 'replace',
				options: {
					peri: character,
					selectPeri: false
				}
			}
		};
	// In some cases the label for the character isn't the same as the
	// character that gets inserted (e.g. Hebrew vowels)
	} else if ( character && 0 in character && 1 in character ) {
		character = {
			label: character[ 0 ],
			action: {
				type: 'replace',
				options: {
					peri: character[ 1 ],
					selectPeri: false
				}
			}
		};
	}
	
	if ( character && 'action' in character && 'label' in character ) {
		actions[ character.label ] = character.action;
		
		if ( character.titleMsg !== undefined ) {
			return mw.html.element( 'span', {
				rel: character.label,
				title: mw.msg( character.titleMsg )
			}, character.label );
		} else {
			return mw.html.element( 'span', {
				rel: character.label
			}, character.label );
		}
	}
	
	mw.log( 'A character for the toolbar was undefined. This is not supposed to happen. Double check the config.' );
	// bug 31673; also an additional fix for bug 24208...
	return '';
}

function createMenu() {
	var gui = {}, $activeTextInput = $( [] ), charActions = {};
	
	gui.$langSelector = $( '<input>' )
		.attr( {
			type: 'text',
			size: 15
		} );
	
	gui.$textInputs = $( '<div>' )
		.attr( 'id', 'transl-textinputs' );
	
	$.each( gadget.defn, function ( i, defn ) {
		var $num;
		
		$( '<div>' )
			.data( 'defn', defn.num )
			.append(
				$num = $( '<span>' )
					.addClass( 'transl-def-label' )
					.attr( 'title', defn.text )
					.text( defn.num ),
				$( '<input>' )
					.attr( {
						'type': 'text',
						'size': 50
					} ),
				$( '<input>' )
					.attr( {
						'type': 'text',
						'size': 1
					} )
			)
			.appendTo( gui.$textInputs );
		
		$num.tipsy( {
			gravity: 'e'
		} );
	} );
	
	gui.$loadButton = $( '<input>' )
		.attr( {
			type:  'button',
			value: mw.msg( 'transl-load-button' )
		} )
		.on( 'click', $.proxy( onLoadLang, this, gui, null ) );
	
	gui.$apply = $( '<input>' )
		.attr( {
			'type':  'button',
			'value': mw.msg( 'transl-apply-button' )
		} )
		.on( 'click', $.proxy( onApplyChanges, this, gui ) );
			
	gui.$submit = $( '<input>' )
		.attr( {
			'type':  'button',
			'value': mw.msg( 'transl-submit-button' )
		} )
		.on( 'click', $.proxy( onSubmit, this, gui ) );
	
	gui.$watch = $( '<input>' )
		.attr( {
			type: 'checkbox',
			id:   'transl-watch'
		} )
		.prop( 'checked', !!mw.user.options.get( 'watchdefault' ) );
	
	gui.$specialCharsButton = $( '<small>' )
		.html( mw.msg( 'transl-keyboard' ) )
		.on( 'click', function () {
			gui.$keyboard.show();
		} );
	
	gui.$previewbox = $( '<div>' )
		.attr( 'id', 'transl-preview' )
		.append(
			$( '<div>' ).append(
				gui.$specialCharsButton,
				'&nbsp;',
				$( '<small>' ).html( mw.msg( 'transl-report-error' ) )
			),
			$( '<strong>' ).text( mw.msg( 'transl-preview-label' ) ),
			gui.$preview = $( '<ul>' )
		);
	
	gui.$keyboard = $( '<div>' )
		.attr( 'id', 'transl-keyboard' )
		.hide()
		.append(
			gui.$keyboardSelect = $( '<select>' ),
			$( '<input>' )
				.attr( {
					type: 'button',
					value: mw.msg( 'transl-keyboard-close' )
				} )
				.on( 'click', function ( evt ) {
					evt.preventDefault();
					gui.$keyboard.hide();
				} ),
			gui.$keyboardChars = $( '<div>' )
				.attr( 'id', 'transl-special-chars' )
		)
		.appendTo( mw.util.$content );
	
	$.each( mw.language.specialCharacters, function ( group, chars ) {
		var $group = $( '<div>' )
			.hide()
			.attr( 'data-group', group )
			.appendTo( gui.$keyboardChars );
		
		$( '<option>' )
			.attr( 'value', group )
			.text( mw.msg( 'special-characters-group-' + group ) )
			.appendTo( gui.$keyboardSelect );
		
		$.each( chars, function ( i, character ) {
			$( buildCharacter( character, charActions ) ).appendTo( $group );
		} );
	} );
	
	gui.$keyboardSelect.on( 'change', function () {
		var group = $( this ).find( ':selected' ).attr( 'value' );
		gui.$keyboardChars.children().hide();
		gui.$keyboardChars.find( '[data-group=' + group + ']' ).show();
		mw.cookie.set( 'TranslatorKeyboardGroup', group );
	} );
	
	gui.$keyboardSelect
		.find( mw.format(
			'[value="$1"]',
			mw.cookie.get( 'TranslatorKeyboardGroup', null, 'latin' )
		) )
		.prop( 'selected', true )
		.end()
		.trigger( 'change' );
	
	gui.$keyboardChars.on( 'click', 'span', function () {
		var label = $( this ).text();
		var action = charActions[ label ];
		var replace = ( action.type === 'replace' );
		
		// jquery.wikiEditor.toolbar.js, doAction()
		$activeTextInput.textSelection(
			'encapsulateSelection',
			$.extend( {}, action.options, {
				replace: replace
			} )
		).trigger( 'keypress' );
	} );
	
	gui.$editbox = $( '<div>' )
		.attr( 'id', 'transl-editbox' )
		.hide()
		.append(
			$( '<div>' ).append(
				gui.$langLabel = $( '<p>' )
					.attr( 'id', 'transl-lang-label' ),
				gui.$langSelector,
				gui.$loadButton
			),
			gui.$textInputs,
			$( '<div>' )
				.attr( 'id', 'transl-edit-buttons' )
				.append(
					gui.$apply, gui.$submit, gui.$watch,
					$( '<label>' )
						.attr( 'for', 'transl-watch' )
						.text( mw.msg( 'transl-watch-article' ) )
				),
			$( '<hr>' ),
			gui.$previewbox
		)
		.appendTo( '#transl-field' );
	
	gui.$editbox.on( 'focus', 'input[type="text"]', function () {
		$activeTextInput = $( this );
	} );
	
	gui.$langSelector.suggestions( {
		fetch: function ( input, response ) {
			var re = new RegExp( '^' + input );
			var arr = $.map( mw.config.get( 'lang2code' ), function ( code, lang ) {
				return lang;
			} );
			
			response( $.grep( arr, function ( s ) {
				return re.test( s );
			} ) );
		},
		highlightInput: true
	} )
	.on( 'keypress', function ( evt ) {
		if ( evt.keyCode === 13 ) { // Enter
			onLoadLang( gui, null );
		}
	} );
	
	resetForms( gui );
	
	if ( window.customLang ) {
		gadget.activeLang = customLang;
		gui.$langSelector.val( gadget.activeLang );
		onLoadLang( gui, customLang );
	}
}

if (
	mw.config.get( 'wgNamespaceNumber' ) === 0 &&
	mw.config.get( 'wgAction' ) === 'view'
) {
	if ( mw.util.getParamValue( 'printable' ) || (
		mw.util.getParamValue( 'oldid' ) &&
		mw.util.getParamValue( 'oldid' ) !== mw.config.get( 'wgCurRevisionId' ).toString()
	) ) {
		return;
	}
	
	mw.messages.set( messages );
	api = new mw.Api();
	
	mw.hook( mw.user.options.get( 'gadget-hide-empty-fields' )
		? 'hideEmptyFields.ready'
		: 'sectionLinks.ready'
	).add( initialize );
}