MediaWiki:Gadget-translation-editor.js: Różnice pomiędzy wersjami

Z Wikisłownika – wolnego słownika wielojęzycznego
Usunięta treść Dodana treść
nie wyswietlaj pola 'Obserwuj' uzytkownikom anonimowym
umozliwij naprawianie tlumaczen zawierajacych bledy skladniowe
Linia 26: Linia 26:
'transl-validate-error': 'Lista tłumaczeń zawiera niewspierane języki albo błędy składniowe.',
'transl-validate-error': 'Lista tłumaczeń zawiera niewspierane języki albo błędy składniowe.',
'transl-load-error': 'Istnieje nowsza wersja strony. Przeładuj ją i spróbuj ponownie.',
'transl-load-error': 'Istnieje nowsza wersja strony. Przeładuj ją i spróbuj ponownie.',
'transl-invalid-lang': 'Niewspierany język lub wykryto błedy składniowe w wikikodzie.',
'transl-issues-found': 'Wykryto błedy składniowe w wikikodzie.',
'transl-invalid-code': 'Nie rozpoznano kodu języka.',
'transl-invalid-code': 'Nie rozpoznano kodu języka: „$1”.',
'transl-select-lang': 'Wybierz język:',
'transl-select-lang': 'Wybierz język:',
'transl-lang-label': 'Tłumaczysz na<br><strong>$1</strong> <small>(<strong>nieznany kod</strong>)</small>',
'transl-lang-label': 'Tłumaczysz na<br><strong>$1</strong> <small>(<strong>nieznany kod</strong>)</small>',
Linia 56: Linia 56:
'transl-help-syntax': '(<a href="//upload.wikimedia.org/wiktionary/pl/1/1d/Edytor_t%C5%82umacze%C5%84_%28sk%C5%82adnia%29.png" target="_blank">składnia</a>)',
'transl-help-syntax': '(<a href="//upload.wikimedia.org/wiktionary/pl/1/1d/Edytor_t%C5%82umacze%C5%84_%28sk%C5%82adnia%29.png" target="_blank">składnia</a>)',
'transl-lang-suggest-code': 'Język z kodem <strong>$1</strong>',
'transl-lang-suggest-code': 'Język z kodem <strong>$1</strong>',
'transl-forbidden-lang': 'Brak wsparcia dla języka: $1.',
'transl-forbidden-lang': 'Narzędzie nie wspiera tłumaczeń na język: $1”.',
'transl-confirm-close': 'Masz niezapisane tłumaczenia. Czy na pewno chcesz opuścić stronę?',
'transl-confirm-close': 'Masz niezapisane tłumaczenia. Czy na pewno chcesz opuścić stronę?',
'transl-preview-parsing': 'poczekaj, trwa parsowanie wikikodu'
'transl-preview-parsing': 'poczekaj, trwa parsowanie wikikodu'
Linia 274: Linia 274:
if ( forbiddenTranslations.indexOf( lang ) !== -1 ) {
if ( forbiddenTranslations.indexOf( lang ) !== -1 ) {
drafts[ lang ] = terms;
drafts[ lang ] = terms;
metadata[ lang ].invalid = true;
metadata[ lang ].unsupported = true;
} else {
} else {
drafts[ lang ] = parseTerms( terms );
drafts[ lang ] = parseTerms( terms ) || terms;
}
}
}
}
Linia 331: Linia 331:
} );
} );
return obj || terms;
return obj;
}
}


function validateDrafts( drafts, metadata ) {
function validateDrafts( drafts, metadata ) {
Object.keys( drafts ).filter( function ( lang ) {
var lang, term;
return !(
'invalid' in metadata[ lang ] ||
langs:
'unsupported' in metadata[ lang ]
for ( lang in drafts ) {
);
if ( 'invalid' in metadata[ lang ] ) {
} ).forEach( function ( lang ) {
continue;
}
if (
if (
!$.isPlainObject( drafts[ lang ] ) ||
!$.isPlainObject( drafts[ lang ] ) ||
$.isEmptyObject( drafts[ lang ] )
$.isEmptyObject( drafts[ lang ] ) ||
!validateDraft( drafts[ lang ] )
) {
) {
metadata[ lang ].invalid = true;
metadata[ lang ].invalid = true;
continue;
}
}
} );
terms:
for ( term in drafts[ lang ] ) {
if ( !gadget.$defn.filter( function () {
return $( this ).text() === term;
} ).length ) {
metadata[ lang ].invalid = true;
continue langs;
}
if ( drafts[ lang ][ term ].some( function ( subterm ) {
return /\(\d+\.\d+\)/.test( subterm.base );
} ) ) {
metadata[ lang ].invalid = true;
continue langs;
}
}
}
return !Object.keys( metadata ).some( function ( lang ) {
return !Object.keys( metadata ).some( function ( lang ) {
return 'invalid' in metadata[ lang ];
return 'invalid' in metadata[ lang ];
} );
}

function validateDraft( draft ) {
return Object.keys( draft ).every( function ( term ) {
return gadget.$defn.filter( function () {
return $( this ).text() === term;
} ).length && !draft[ term ].some( function ( subterm ) {
return /\(\d+\.\d+\)/.test( subterm.base );
} );
} );
} );
}
}
Linia 389: Linia 380:
gui.$langSelector.val( '' ).trigger( 'focus' );
gui.$langSelector.val( '' ).trigger( 'focus' );
gui.$saveButton.prop( 'disabled', true );
gui.$saveButton.prop( 'disabled', true );
gui.$textInputs.find( 'input' ).val( '' );
gui.$textInputs.find( 'input' ).val( '' ).prop( 'disabled', true );
gui.$textArea.find( 'textarea' ).val( '' );
gui.$apply.prop( 'disabled', true );
gui.$apply.prop( 'disabled', true );
gui.$textInputs.find( 'input' ).prop( 'disabled', true );
}
}


Linia 405: Linia 396:


function onLoadLang( gui, targetLang, evt ) {
function onLoadLang( gui, targetLang, evt ) {
var issuesFound,
var lang = targetLang || gui.$langSelector.val();
lang = targetLang || gui.$langSelector.val();
evt && evt.preventDefault();
evt && evt.preventDefault();
Linia 418: Linia 410:
resetForms( gui );
resetForms( gui );
if ( lang in gadget.draftMetadata && gadget.draftMetadata[ lang ].invalid ) {
mw.notify( mw.msg( 'transl-invalid-lang' ), { type: 'error' } );
return;
}
if ( forbiddenLanguageNames.indexOf( lang ) !== -1 ) {
if ( forbiddenLanguageNames.indexOf( lang ) !== -1 ) {
Linia 428: Linia 415:
return;
return;
}
}
issuesFound = lang in gadget.draftMetadata && gadget.draftMetadata[ lang ].invalid;
gadget.activeLang = lang;
gadget.activeLang = lang;
Linia 437: Linia 426:
if ( !( lang in config.lang2code ) ) {
if ( !( lang in config.lang2code ) ) {
mw.notify( mw.msg( 'transl-invalid-code' ), { type: 'warn' } );
mw.notify( mw.msg( 'transl-invalid-code', lang ), { type: 'warn' } );
}
}
Linia 447: Linia 436:
);
);
if ( issuesFound ) {
gui.$textInputs.find( 'input' ).prop( 'disabled', false );
mw.notify( mw.msg( 'transl-issues-found' ), { type: 'warn' } );
gui.$textInputs.hide();
if ( lang in gadget.drafts ) {
gui.$textInputs.children().each( $.proxy( loadDrafts, null, gadget.drafts[ lang ] ) );
gui.$textArea.show().find( 'textarea' ).val( gadget.drafts[ lang ] );
} else {
gui.$textInputs.show().find( 'input' ).prop( 'disabled', false );
gui.$textArea.hide();
if ( lang in gadget.drafts ) {
gui.$textInputs.children().each( $.proxy( loadDrafts, null, gadget.drafts[ lang ] ) );
}
}
}
Linia 457: Linia 453:
setTimeout( function () {
setTimeout( function () {
gui.$textInputs.find( 'input' ).first().trigger( 'focus' );
var $el = issuesFound ? gui.$textArea.find( 'textarea' ) : gui.$textInputs.find( 'input' ).first();
$el.trigger( 'focus' );
}, 1 );
}, 1 );
}
}
Linia 542: Linia 539:
function onApplyChanges( gui, evt ) {
function onApplyChanges( gui, evt ) {
var apiPromise, $line,
var apiPromise, $line,
rawDraft = '',
draft = {},
draft = {},
metadata = gadget.draftMetadata[ gadget.activeLang ] || {},
metadata = gadget.draftMetadata[ gadget.activeLang ] || {},
Linia 547: Linia 545:
evt && evt.preventDefault();
evt && evt.preventDefault();
gui.$textInputs.children().each( $.proxy( constructDrafts, null, draft ) );
if ( $.isEmptyObject( draft ) ) {
if ( 'invalid' in metadata ) {
rawDraft = gui.$textArea.find( 'textarea' ).val();
return;
draft = parseTerms( rawDraft ) || {};
if ( !$.isEmptyObject( draft ) && validateDraft( draft ) ) {
gui.$textInputs.show().find( 'input' ).prop( 'disabled', false );
gui.$textArea.hide();
gui.$textInputs.children().each( $.proxy( loadDrafts, null, draft ) );
delete metadata.invalid;
}
} else {
gui.$textInputs.children().each( $.proxy( constructDrafts, null, draft ) );
if ( $.isEmptyObject( draft ) ) {
return;
}
}
}
Linia 556: Linia 567:
gui.$submit.prop( 'disabled', true );
gui.$submit.prop( 'disabled', true );
gadget.drafts[ gadget.activeLang ] = draft;
gadget.drafts[ gadget.activeLang ] = !$.isEmptyObject( draft ) ? draft : rawDraft;
if ( isFreshDraft ) {
if ( isFreshDraft ) {
Linia 566: Linia 577:
formatversion: 2,
formatversion: 2,
action: 'parse',
action: 'parse',
text: serializeDraft( draft ),
text: !$.isEmptyObject( draft ) ? serializeDraft( draft ) : rawDraft,
contentmodel: 'wikitext',
contentmodel: 'wikitext',
disablelimitreport: true
disablelimitreport: true
Linia 769: Linia 780:
$li.append( $change, $remove, $abort ).appendTo( $ul );
$li.append( $change, $remove, $abort ).appendTo( $ul );
if ( 'invalid' in metadata ) {
$li.addClass( 'transl-entry-invalid' );
} else if ( 'unsupported' in metadata ) {
$change.addClass( 'disabled' );
}
if ( lang === active ) {
if ( lang === active ) {
$li.addClass( 'transl-active' );
$li.addClass( 'transl-active' );
$change.addClass( 'disabled' );
$change.addClass( 'disabled' );
} else if ( metadata.invalid ) {
} else {
$change.addClass( 'disabled' );
$li.addClass( 'transl-entry-invalid' );
} else {
$change.on( 'click', function () {
$change.on( 'click', function () {
gadget.activeLang = lang;
gadget.activeLang = lang;
Linia 992: Linia 1006:
gui.$textInputs = $( '<div>' )
gui.$textInputs = $( '<div>' )
.attr( 'id', 'transl-textinputs' );
.attr( 'id', 'transl-textinputs' );
gui.$textArea = $( '<div>' )
.attr( 'id', 'transl-textarea' )
.append( $( '<textarea>' )
.attr( {
rows: 6,
cols: 71
} )
)
.hide();
gadget.$defn.each( function ( i, el ) {
gadget.$defn.each( function ( i, el ) {
Linia 1149: Linia 1173:
),
),
gui.$textInputs,
gui.$textInputs,
gui.$textArea,
$( '<div>' )
$( '<div>' )
.attr( 'id', 'transl-edit-buttons' )
.attr( 'id', 'transl-edit-buttons' )
Linia 1176: Linia 1201:
}
}
gui.$editbox.on( 'focus', 'input[type="text"]', function () {
gui.$editbox.on( 'focus', 'input[type="text"], textarea', function () {
$activeTextInput = $( this );
$activeTextInput = $( this );
} );
} );

Wersja z 20:32, 15 lut 2020

var gadget = mw.libs.translationEditor = {};

var PREFERRED_LANG_OPTION_KEY = 'userjs-translation-editor-preferred-lang';
var PREFERRED_LANG_COOKIE = 'TranslationEditorPreferredLang';
var POST_EDIT_COOKIE_KEY = 'PostEditTranslationEditor';

var EDITBOX_TOP_OFFSET = 100;
var EDITBOX_SCROLL_DURATION = 500;
var PREVIEW_SCROLL_DURATION = 500;

var forbiddenLanguageNames = [ 'polski', 'termin obcy w języku polskim', 'użycie międzynarodowe' ];
var forbiddenLanguageCodes = [];
var forbiddenTranslations = [ 'polski język migowy' ];

var postEditCookieOpts = {
	expires: 1200 // za EditPage::POST_EDIT_COOKIE_DURATION w EditPage.php
};

var api = null;

var messages = {
	'transl-edit-launcher':   '(edytuj)',
	'transl-api-error':       'Błąd API. Kod: $1. Komunikat: $2.',
	'transl-save-error':      'Prawdopodobnie nastąpił konflikt edycji. Przeładuj stronę i spróbuj ponownie.',
	'transl-parse-error':     'Nie udało się odczytać kodu strony. Przejdź na tryb edycji wikikodu i sprawdź, czy nie ma błędów składniowych.',
	'transl-validate-error':  'Lista tłumaczeń zawiera niewspierane języki albo błędy składniowe.',
	'transl-load-error':      'Istnieje nowsza wersja strony. Przeładuj ją i spróbuj ponownie.',
	'transl-issues-found':    'Wykryto błedy składniowe w wikikodzie.',
	'transl-invalid-code':    'Nie rozpoznano kodu języka: „$1”.',
	'transl-select-lang':     'Wybierz język:',
	'transl-lang-label':      'Tłumaczysz na<br><strong>$1</strong> <small>(<strong>nieznany kod</strong>)</small>',
	'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-save-button':     'Zapamiętaj',
	'transl-forget-button':   'Zapomnij',
	'transl-save-save':       'Zapisz',
	'transl-save-cancel':     'Anuluj',
	'transl-prompt-title':    'Ustawienia domyślnego języka',
	'transl-save-prompt':     'Język o nazwie „$1” zostanie załadowany domyślnie podczas następnego uruchomienia edytora tłumaczeń.',
	'transl-save-current':    'Twoje obecne ustawienie to „$1”.',
	'transl-forget-prompt':   'Język o nazwie „$1” przestanie być ładowany domyślnie.',
	'transl-save-success':    'Zapisano preferencje.',
	'transl-save-fail':       'Wystąpił błąd podczas zapisywania preferencji.',
	'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-draft-abort':     '(wstrzymaj)',
	'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=Edytor%20t%C5%82umacze%C5%84%20%28b%C5%82%C4%85d%29&nosummary=" target="_blank">zgłoś&nbsp;błąd</a>)',
	'transl-help-syntax':     '(<a href="//upload.wikimedia.org/wiktionary/pl/1/1d/Edytor_t%C5%82umacze%C5%84_%28sk%C5%82adnia%29.png" target="_blank">składnia</a>)',
	'transl-lang-suggest-code': 'Język z kodem <strong>$1</strong>',
	'transl-forbidden-lang':  'Narzędzie nie wspiera tłumaczeń na język: „$1”.',
	'transl-confirm-close':   'Masz niezapisane tłumaczenia. Czy na pewno chcesz opuścić stronę?',
	'transl-preview-parsing': 'poczekaj, trwa parsowanie wikikodu'
};

var config = mw.config.get( [
	'wgPageName',
	'wgNamespaceNumber',
	'wgAction',
	'wgRevisionId',
	'wgCurRevisionId',
	'wgArticleId',
	'wgDiffNewId',
	'wgDiffOldId'
] );

function initialize( $content ) {
	var $transl, $defs, $buttonHolder;
	
	gadget.activeLang = '';
	gadget.drafts = {};
	gadget.draftMetadata = {};
	gadget.currentRequest = null;
	
	gadget.preferredLang = !mw.user.isAnon()
		? mw.user.options.get( PREFERRED_LANG_OPTION_KEY )
		: mw.cookie.get( PREFERRED_LANG_COOKIE );
	
	$transl = $content.find( 'dt.lang-pl.fldt-tlumaczenia' ).first();
	$defs = $content.find( 'dd.lang-pl.fldt-znaczenia' ).not( ':empty' ).has( '.term-num' );
	
	if ( !$transl.length || !$defs.length ) {
		return;
	}
	
	gadget.$translList = $transl.parent().next( 'ul' );
	
	$transl
		.attr( 'id', 'transl-field' )
		.children( '#transl' )
		.css( 'display', 'inline' );
	
	$buttonHolder = $( '<div>' )
		.attr( 'id', 'transl-buttons' )
		.appendTo( $transl );
	
	if ( !!gadget.preferredLang && typeof hideTranslations === 'function' ) {
		hideTranslations( gadget.preferredLang );
	}
	
	gadget.$defn = $defs.find( '.term-num:first' );
	
	if ( gadget.$defn.length !== $defs.length ) {
		return;
	}
	
	$( '<small>' )
		.text( mw.msg( 'transl-edit-launcher' ) )
		.attr( 'id', 'transl-addbutton' )
		.attr( 'accesskey', '+' )
		.appendTo( $buttonHolder )
		.one( 'click', $.proxy( loadEditor, null, $content, $transl ) )
		.on( 'click', function () {
			var $editbox = $content.find( '#transl-editbox' );
			
			if ( $editbox.length ) {
				gadget.$translList.toggle();
				$editbox.toggle();
				$content.find( '#transl-langselector' ).trigger( 'focus' );
			}
		} );
	
	$transl.parent().show();
}

function loadEditor( $content, $transl, evt ) {
	var $spinner = $.createSpinner( {
		size: 'large',
		type: 'block'
	} ).appendTo( $transl );
	
	makeRequests( function () {
		var $editbox = $content.find( '#transl-editbox' );
		
		$spinner.remove();
		$editbox.fadeIn();
		
		if ( !$editbox.length ) {
			$( '#transl-addbutton' ).hide();
			return;
		}
		
		gadget.$translList.hide();
		
		if ( $editbox.offset().top - $( window ).scrollTop() > EDITBOX_TOP_OFFSET ) {
			$( 'html, body' ).animate( {
				scrollTop: $editbox.offset().top - EDITBOX_TOP_OFFSET
			}, EDITBOX_SCROLL_DURATION );
		}
	} );
}

function makeRequests( callback ) {
	mw.loader.using( [
		'mediawiki.api',
		'mediawiki.language.specialCharacters',
		'mediawiki.String',
		'mediawiki.notify',
		'mediawiki.confirmCloseWindow',
		'jquery.suggestions',
		'jquery.textSelection',
		'jquery.ui',
		'ext.gadget.langdata',
		'ext.gadget.term-preview'
	] )
	.then( function ( require ) {
		mw.libs.String = require( 'mediawiki.String' );
		mw.libs.termPreview = require( 'ext.gadget.term-preview' );
		mw.libs.specialCharacters = require( 'mediawiki.language.specialCharacters' );
		
		api = new mw.Api();
		
		config.lang2code = mw.config.get( 'lang2code' );
		config.code2lang = mw.config.get( 'code2lang' );
		
		$.each( forbiddenLanguageNames, function ( i, lang ) {
			if ( lang in config.lang2code ) {
				forbiddenLanguageCodes.push( config.lang2code[ lang ] );
			}
		} );
		
		return api.get( {
			formatversion: 2,
			prop:         'revisions',
			rvprop:       [ 'timestamp', 'content', 'ids' ],
			rvslots:      'main',
			titles:        config.wgPageName,
			curtimestamp:  true
		} );
	} ) 
	.done( function ( res ) {
		var revision = res.query.pages[ 0 ].revisions[ 0 ];
		
		if ( revision.revid !== config.wgCurRevisionId ) {
			mw.notify( mw.msg( 'transl-load-error' ), { type: 'error' } );
		} else {
			gadget.content = revision.slots.main.content;
			gadget.starttimestamp = res.curtimestamp;
			gadget.basetimestamp = revision.timestamp;
			
			if ( analyzePage() ) {
				createMenu();
			} else {
				mw.notify( mw.msg( 'transl-parse-error' ), { type: 'error' } );
			}
		}
	} )
	.done( callback )
	.fail( function ( code, details ) {
		if ( typeof details === 'object' ) {
			details = details.error && details.error.info || details.textStatus || '---';
		}
		
		mw.notify( mw.msg( 'transl-api-error', code, details ), { type: 'error' } );
	} );
}

function analyzePage() {
	var a2, a3, a4, b, translations, langSection, langs, targetLang, targetRow, drafts,
		pageContent = gadget.content,
		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 = [];
		drafts = {};
		metadata = {};
		
		$.each( translations.slice( a3 + 1, b ).split( '\n' ), function ( i, line ) {
			var lang, terms,
				res = line.match( /^\* *([^:]+): *(.+)/ );
			
			if ( res && res[ 1 ] && res[ 2 ] ) {
				lang = res[ 1 ].trim();
				terms = res[ 2 ].trim();
				langs.push( lang );
				metadata[ lang ] = { $original: gadget.$translList.children().eq( i ) };
				
				if ( forbiddenTranslations.indexOf( lang ) !== -1 ) {
					drafts[ lang ] = terms;
					metadata[ lang ].unsupported = true;
				} else {
					drafts[ lang ] = parseTerms( terms ) || terms;
				}
			}
		} );
		
		if ( !langs.length || langs.length !== gadget.$translList.children().length ) {
			return false;
		}
		
		if ( !validateDrafts( drafts, metadata ) ) {
			mw.notify( mw.msg( 'transl-validate-error' ), { type: 'warn' } );
		}
		
		gadget.drafts = drafts;
		gadget.draftMetadata = metadata;
		a3++;
	} else {
		a3 = b;
	}
	
	gadget.initialDrafts = $.extend( {}, gadget.drafts );
	gadget.startIndex = a3;
	gadget.endIndex = b;
	
	return true;
}

function parseTerms( terms ) {
	var obj = {},
		reTerm = /^(\(\d+\.\d+\)) *(.+)/,
		reSubterm = /^\[\[([^\]]+?)\]\](?: +\{\{([^\}]+?)\}\})?$/;
	
	terms.split( / *; */ ).forEach( function ( term ) {
		var m = term.match( reTerm );
		
		if ( obj === null || m === null || m[ 1 ] in obj ) {
			obj = null;
			return;
		}
		
		obj[ m[ 1 ] ] = m[ 2 ].trim().split( / *, */ ).map( function ( subterm ) {
			var base, template,
				mm = subterm.match( reSubterm );
			
			if ( mm !== null ) {
				base = mw.format( '[[$1]]', mm[ 1 ].trim() );
				template = mm[ 2 ] && mw.format( '{{$1}}', mm[ 2 ].trim() );
			} else {
				base = subterm;
			}
			
			return { base: base, template: template };
		} );
	} );
	
	return obj;
}

function validateDrafts( drafts, metadata ) {
	Object.keys( drafts ).filter( function ( lang ) {
		return !(
			'invalid' in metadata[ lang ] ||
			'unsupported' in metadata[ lang ]
		);
	} ).forEach( function ( lang ) {
		if (
			!$.isPlainObject( drafts[ lang ] ) ||
			$.isEmptyObject( drafts[ lang ] ) ||
			!validateDraft( drafts[ lang ] )
		) {
			metadata[ lang ].invalid = true;
		}
	} );
	
	return !Object.keys( metadata ).some( function ( lang ) {
		return 'invalid' in metadata[ lang ];
	} );
}

function validateDraft( draft ) {
	return Object.keys( draft ).every( function ( term ) {
		return gadget.$defn.filter( function () {
				return $( this ).text() === term;
			} ).length && !draft[ term ].some( function ( subterm ) {
				return /\(\d+\.\d+\)/.test( subterm.base );
			} );
	} );
}

function comparatorPl( a, b ) {
	return a.localeCompare( b, 'pl' );
}

function prepareDraft() {
	return Object.keys( gadget.drafts ).sort( comparatorPl ).map( function ( lang ) {
		return mw.format( '* $1: $2\n', lang, serializeDraft( gadget.drafts[ lang ] ) );
	} ).join( '' );
}

function resetForms( gui ) {
	gadget.activeLang = null;
	gui.$langLabel.html( mw.msg( 'transl-select-lang' ) ),
	gui.$langSelector.val( '' ).trigger( 'focus' );
	gui.$saveButton.prop( 'disabled', true );
	gui.$textInputs.find( 'input' ).val( '' ).prop( 'disabled', true );
	gui.$textArea.find( 'textarea' ).val( '' );
	gui.$apply.prop( 'disabled', true );
}

function resetSubmitButtons( gui ) {
	var hasChanged = testDraftDivergence();
	gui.$watch.prop( 'disabled', !hasChanged );
	gui.$submit.prop( 'disabled', !hasChanged );
}

function testDraftDivergence() {
	return JSON.stringify( gadget.initialDrafts ) !== JSON.stringify( gadget.drafts );
}

function onLoadLang( gui, targetLang, evt ) {
	var issuesFound,
		lang = targetLang || gui.$langSelector.val();
	
	evt && evt.preventDefault();
	
	if ( !lang ) {
		return;
	}
	
	if ( gadget.currentRequest ) {
		gadget.currentRequest.abort();
	}
	
	resetForms( gui );
	
	if ( forbiddenLanguageNames.indexOf( lang ) !== -1 ) {
		mw.notify( mw.msg( 'transl-forbidden-lang', lang ), { type: 'error' } );
		return;
	}
	
	issuesFound = lang in gadget.draftMetadata && gadget.draftMetadata[ lang ].invalid;
	
	gadget.activeLang = lang;
	
	gui.$langLabel.html( lang in config.lang2code
		? mw.msg( 'transl-lang-label-iso', lang, config.lang2code[ lang ] )
		: mw.msg( 'transl-lang-label', lang )
	);
	
	if ( !( lang in config.lang2code ) ) {
		mw.notify( mw.msg( 'transl-invalid-code', lang ), { type: 'warn' } );
	}
	
	gui.$saveButton
		.prop( 'disabled', false )
		.attr( 'value', gadget.preferredLang === lang
			? mw.msg( 'transl-forget-button' )
			: mw.msg( 'transl-save-button' )
		);
	
	if ( issuesFound ) {
		mw.notify( mw.msg( 'transl-issues-found' ), { type: 'warn' } );
		gui.$textInputs.hide();
		gui.$textArea.show().find( 'textarea' ).val( gadget.drafts[ lang ] );
	} else {
		gui.$textInputs.show().find( 'input' ).prop( 'disabled', false );
		gui.$textArea.hide();
		
		if ( lang in gadget.drafts ) {
			gui.$textInputs.children().each( $.proxy( loadDrafts, null, gadget.drafts[ lang ] ) );
		}
	}
	
	refreshPreview( gui, lang );
	gui.$apply.prop( 'disabled', false );
	
	setTimeout( function () {
		var $el = issuesFound ? gui.$textArea.find( 'textarea' ) : gui.$textInputs.find( 'input' ).first();
		$el.trigger( 'focus' );
	}, 1 );
}

function loadDrafts( draft, i, el ) {
	var $el = $( el ),
		defn = $el.data( 'defn' ),
		$inputs = $el.children( 'input' ),
		$text = $inputs.first(),
		$tmpl = $inputs.last(),
		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( ',' ) );
	}
}

function onSaveLang( gui, evt ) {
	var message, updateLang, newValue;
	
	if ( gadget.activeLang !== gadget.preferredLang ) {
		message = mw.msg( 'transl-save-prompt', gadget.activeLang );
		
		if ( !!gadget.preferredLang ) {
			message += ' ' + mw.msg( 'transl-save-current', gadget.preferredLang );
		}
		
		updateLang = true;
	} else {
		message = mw.msg( 'transl-forget-prompt', gadget.preferredLang );
		updateLang = false;
	}
	
	newValue = updateLang ? gadget.activeLang : null;
	
	gui.$confirmDialog.append( message ).data( {
		newValue: newValue,
		updateLang: updateLang
	} ).dialog( 'open' );
}

function savePreferredLangRequest( gui ) {
	var newValue = gui.$confirmDialog.data( 'newValue' ),
		updateLang = gui.$confirmDialog.data( 'updateLang' );
	
	if ( !mw.user.isAnon() ) {
		return api.saveOption( PREFERRED_LANG_OPTION_KEY, newValue )
			.done( function () {
				gadget.preferredLang = newValue;
				gui.$saveButton.attr( 'value', mw.msg( updateLang
					? 'transl-forget-button'
					: 'transl-save-button'
				) );
				mw.notify( mw.msg( 'transl-save-success' ) );
			} );
	} else {
		mw.cookie.set( PREFERRED_LANG_COOKIE, newValue );
		gadget.preferredLang = newValue;
		gui.$saveButton.attr( 'value', mw.msg( updateLang
			? 'transl-forget-button'
			: 'transl-save-button'
		) );
		mw.notify( mw.msg( 'transl-save-success' ) );
		return $().promise();
	}
}

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

function onApplyChanges( gui, evt ) {
	var apiPromise, $line,
		rawDraft = '',
		draft = {},
		metadata = gadget.draftMetadata[ gadget.activeLang ] || {},
		isFreshDraft = !( gadget.activeLang in gadget.drafts );
	
	evt && evt.preventDefault();
	
	if ( 'invalid' in metadata ) {
		rawDraft = gui.$textArea.find( 'textarea' ).val();
		draft = parseTerms( rawDraft ) || {};
		
		if ( !$.isEmptyObject( draft ) && validateDraft( draft ) ) {
			gui.$textInputs.show().find( 'input' ).prop( 'disabled', false );
			gui.$textArea.hide();
			gui.$textInputs.children().each( $.proxy( loadDrafts, null, draft ) );
			delete metadata.invalid;
		}
	} else {
		gui.$textInputs.children().each( $.proxy( constructDrafts, null, draft ) );
		
		if ( $.isEmptyObject( draft ) ) {
			return;
		}
	}
	
	gui.$watch.prop( 'disabled', true );
	gui.$submit.prop( 'disabled', true );
	
	gadget.drafts[ gadget.activeLang ] = !$.isEmptyObject( draft ) ? draft : rawDraft;
	
	if ( isFreshDraft ) {
		sortDrafts();
		gadget.draftMetadata[ gadget.activeLang ] = metadata;
	}
	
	apiPromise = api.get( {
		formatversion:      2,
		action:            'parse',
		text:               !$.isEmptyObject( draft ) ? serializeDraft( draft ) : rawDraft,
		contentmodel:      'wikitext',
		disablelimitreport: true
	} );
	
	gadget.currentRequest = metadata.request = apiPromise
		.then( function ( data ) {
			return parsedWikitextCallback( data, gadget.activeLang );
		} )
		.always( function () {
			gadget.currentRequest = null;
			delete metadata.request;
			resetSubmitButtons( gui );
		} )
		.promise( {
			abort: apiPromise.abort
		} );
	
	refreshPreview( gui, gadget.activeLang );
	
	$line = gui.$previewbox.find( mw.format( '[data-lang="$1"]', gadget.activeLang ) );
	
	if ( $line.length ) {
		// https://stackoverflow.com/a/18927969
		gui.$previewbox.animate( {
			scrollTop: gui.$previewbox.scrollTop() - gui.$previewbox.offset().top + $line.offset().top
		}, PREVIEW_SCROLL_DURATION );
	}
}

function sortDrafts() {
	// https://stackoverflow.com/a/31102605
	var temp = {};
	
	Object.keys( gadget.drafts ).sort( comparatorPl ).forEach( function ( lang ) {
		temp[ lang ] = gadget.drafts[ lang ];
	} );
	
	gadget.drafts = temp;
}

function constructDrafts( draft, i, el ) {
	var terms, tmpls, temp, arr,
		$this = $( el ),
		$inputs = $this.children( 'input' ),
		text = $.trim( $inputs.first().val() ),
		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 ( /[\[\]\(\)\{\}<>\/]/.test( term ) ) {
			base = term;
		} else {
			base = '[' + '[' + term.trim() + ']]';
		}
		
		if ( tmpls && tmpls[ j ] ) {
			template = '{' + '{' + tmpls[ j ].trim() + '}}';
		}
		
		if ( template === "{{f}}" ) {
			// [[Special:PermaLink/7109640#Hiperonimy i hiponimy oraz femininum.]]
			template = "{{ż}}";
		}
		
		arr.push( {
			base:     base,
			template: template
		} );
	} );
	
	draft[ $this.data( 'defn' ) ] = arr;
}

function parsedWikitextCallback( data, lang ) {
	var $parsed, $els, links;
	
	$parsed = $( data.parse.text ).filter( '.mw-parser-output' ).children( 'p' ).first();
	$els = $parsed.find( 'a' );
	
	if ( !$els.length || !( lang in config.lang2code ) ) {
		return $parsed;
	}
	
	// TODO: użycie modułu sectionLinks
	$els.each( function () {
		var href = $( this ).attr( 'href' );
		
		if ( href.indexOf( '#' ) === -1 ) {
			href += '#' + config.lang2code[ lang ];
			$( this ).attr( 'href', href );
		}
	} );
	
	if ( Number( mw.user.options.get( 'gadget-false-blue-links' ) ) ) {
		links = data.parse.links.filter( function ( obj ) {
			return obj.ns === 0 && obj.exists;
		} ).map( function ( obj ) {
			return obj.title;
		} );
		
		// TODO: sprawdzić, czy da się wstrzymać ('abort') z poziomu metadata.request
		gadget.currentRequest = mw.libs.falseBlueLinks.inspectTitles( links );
		
		return gadget.currentRequest.then( function () {
			mw.libs.falseBlueLinks.processElements( $parsed );
			return $parsed;
		} );
	} else {
		return $parsed;
	}
}

function serializeDraft( draft ) {
	var copy;
	
	if ( typeof( draft ) === 'string' ) {
		return draft;
	}
	
	copy = $.extend( true, {}, draft );
	
	return $.map( copy, 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>' );
	
	Object.keys( gadget.drafts ).sort( comparatorPl ).forEach( function ( lang ) {
		var $li, $change, $remove, $abort, $preview, $serializedPreview, metadata,
			draft = gadget.drafts[ lang ];
		
		if ( lang in gadget.draftMetadata ) {
			metadata = gadget.draftMetadata[ lang ];
		} else {
			gadget.draftMetadata[ lang ] = metadata = {};
		}
		
		$serializedPreview = $( '<code>' )
			.addClass( 'transl-preview-entry' )
			.text( serializeDraft( draft ) );
		
		$change = $( '<small>' ).text( mw.msg( 'transl-draft-change' ) );
		$remove = $( '<small>' ).text( mw.msg( 'transl-draft-remove' ) );
		$abort  = $( '<small>' ).text( mw.msg( 'transl-draft-abort' ) );
		
		if ( metadata.request ) {
			metadata.$parsed = metadata.$parsed || null; // dummy temporary value
			
			$change.hide();
			$remove.hide();
			
			$preview = $( '<span>' )
				.addClass( 'transl-parsing transl-preview-entry' )
				.text( mw.msg( 'transl-preview-parsing' ) );
			
			metadata.request
				.done( function ( $parsed ) {
					$preview.removeClass( 'transl-parsing' ).html( $parsed.html() );
					metadata.$parsed = $preview;
				} )
				.fail( function () {
					$preview.replaceWith( $serializedPreview );
					delete metadata.$parsed;
				} )
				.always( function () {
					$change.show();
					$remove.show();
					$abort.hide();
				} );
		} else {
			$abort.hide();
			$preview = metadata.$parsed || $serializedPreview;
		}
		
		$li = $( '<li>' ).attr( 'data-lang', lang );
		
		if ( '$parsed' in metadata ) {
			$li.append( $( '<span>' ).text( mw.format( '$1: ', lang ) ), $preview );
		} else {
			$li.append( $( '<span>' )
				.addClass( 'transl-preview-entry' )
				.html( metadata.$original.html() )
			);
		}
		
		$li.append( $change, $remove, $abort ).appendTo( $ul );
		
		if ( 'invalid' in metadata ) {
			$li.addClass( 'transl-entry-invalid' );
		} else if ( 'unsupported' in metadata ) {
			$change.addClass( 'disabled' );
		}
		
		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 ) {
				if ( gadget.currentRequest ) {
					gadget.currentRequest.abort();
				}
				
				resetForms( gui );
			}
			
			resetSubmitButtons( gui );
		} );
		
		$abort.on( 'click', function () {
			if ( gadget.currentRequest ) {
				gadget.currentRequest.abort();
			}
		} );
	} );
	
	gui.$preview.replaceWith( $ul );
	gui.$preview = $ul;
}

function makeSummary() {
	var s, addedLangs, deletedLangs, modifiedLangs, buildString, addedDrafts,
		initialLangs = Object.keys( gadget.initialDrafts ),
		draftLangs = Object.keys( gadget.drafts );
	
	buildString = function ( added, modified, deleted, delimiter ) {
		var out = [];
		
		if ( addedLangs.length ) {
			out.push( 'dodano ' + added.join( delimiter || ', ' ) );
		}
		
		if ( modifiedLangs.length ) {
			out.push( 'zmodyfikowano ' + modified.join( ', ' ) );
		}
		
		if ( deletedLangs.length ) {
			out.push( 'usunięto ' + deleted.join( ', ' ) );
		}
		
		return out.join( ' •• ' );
	};
	
	addedLangs = draftLangs.filter( function ( lang ) {
		return initialLangs.indexOf( lang ) === -1;
	} );
	
	deletedLangs = initialLangs.filter( function ( lang ) {
		return draftLangs.indexOf( lang ) === -1;
	} );
	
	modifiedLangs = draftLangs.filter( function ( lang ) {
		var helper = function ( draft ) {
			return JSON.stringify( Object.keys( draft ).map( function ( key ) {
				return { num: key, val: draft[ key ] };
			} ) );
		};
		
		return (
			initialLangs.indexOf( lang ) !== -1 &&
			helper( gadget.initialDrafts[ lang ] ) !== helper( gadget.drafts[ lang ] )
		);
	} );
	
	addedDrafts = addedLangs.map( function ( lang ) {
		return mw.format( '$1: $2', lang, serializeDraft( gadget.drafts[ lang ] ) );
	} );
	
	s = buildString( addedDrafts, modifiedLangs, deletedLangs, ' • ' );
	
	if ( mw.libs.String.byteLength( s ) > 1000 ) {
		s = buildString( addedLangs, modifiedLangs, deletedLangs );
	}
	
	return s;
}

function onSubmit( gui, evt ) {
	var translationsDraft = prepareDraft(),
		pageDraft =
			gadget.content.slice( 0, gadget.startIndex ) +
			translationsDraft +
			gadget.content.slice( gadget.endIndex, gadget.content.length );
	
	evt.preventDefault();
	
	gui.$submit
		.prop( 'disabled', true )
		.attr( 'value', mw.msg( 'transl-submitting' ) );
	
	api.postWithEditToken( {
		action:        'edit',
		title:          config.wgPageName,
		text:           pageDraft,
		tags:          'translation-editor',
		summary:        makeSummary(),
		watchlist:      !!gui.$watch.prop( 'checked' ) ? 'watch' : 'nochange',
		notminor:       true,
		starttimestamp: gadget.starttimestamp,
		basetimestamp:  gadget.basetimestamp
	} )
	.done( function () {
		var cookieKey = POST_EDIT_COOKIE_KEY + config.wgArticleId;
		gui.$submit.attr( 'value', mw.msg( 'transl-reloading' ) );
		mw.cookie.set( cookieKey, 'saved', postEditCookieOpts );
		location.reload();
	} )
	.fail( function () {
		mw.notify( mw.msg( 'transl-save-error' ), { type: '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 unicodeToAscii( s ) {
	var n, ch, index,
		out = '',
		utf   = 'ąåãáćçęèłńóõüśźż',
		ascii = 'aaaacceelnoouszz';
	
	for ( n = 0; n < s.length; n++ ) {
		ch = s.charAt( n );
		index = utf.indexOf( ch );
		
		if ( index !== -1 ) {
			out += ascii.charAt( index );
		} else {
			out += ch;
		}
	}
	
	return out;
}

function createMenu() {
	var allowCloseWindow,
		gui = {},
		$activeTextInput = $( [] ),
		charActions = {};
	
	allowCloseWindow = mw.confirmCloseWindow( {
		test: testDraftDivergence,
		message: mw.msg( 'transl-confirm-close' ),
		namespace: 'translationeditor-editwarning'
	} );
	
	gui.$langSelector = $( '<input>' )
		.attr( {
			id:   'transl-langselector',
			type: 'text',
			size: 22
		} );
	
	gui.$textInputs = $( '<div>' )
		.attr( 'id', 'transl-textinputs' );
	
	gui.$textArea = $( '<div>' )
		.attr( 'id', 'transl-textarea' )
		.append( $( '<textarea>' )
			.attr( {
				rows: 6,
				cols: 71
			} )
		)
		.hide();
	
	gadget.$defn.each( function ( i, el ) {
		var $num,
			$defn = $( el );
		
		$( '<div>' )
			.data( 'defn', $defn.text() )
			.append(
				$num = $( '<span>' )
					.addClass( 'transl-def-label' )
					.text( $defn.text() ),
				$( '<input>' )
					.attr( {
						type: 'text',
						size: 50
					} ),
				$( '<input>' )
					.attr( {
						type: 'text',
						size: 1
					} )
			)
			.appendTo( gui.$textInputs );
		
		mw.libs.termPreview.enablePreview( $num, 'pl' );
	} );
	
	gui.$loadButton = $( '<input>' )
		.attr( {
			type:  'button',
			value: mw.msg( 'transl-load-button' )
		} )
		.on( 'click', $.proxy( onLoadLang, this, gui, null ) );
	
	gui.$saveButton = $( '<input>' )
		.attr( {
			type:  'button',
			value: mw.msg( 'transl-save-button' )
		} )
		.on( 'click', $.proxy( onSaveLang, this, gui ) );
	
	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' )
		} )
		.prop( 'disabled', true )
		.on( 'click', allowCloseWindow.release )
		.on( 'click', $.proxy( onSubmit, this, gui ) );
	
	gui.$watch = mw.user.isAnon() ? $( [] ) : $( '<input>' )
		.attr( {
			type: 'checkbox',
			id:   'transl-watch'
		} )
		.prop( {
			checked: !!Number( mw.user.options.get( 'watchdefault' ) ),
			disabled: true
		} );
	
	gui.$specialCharsButton = $( '<small>' )
		.html( mw.msg( 'transl-keyboard' ) )
		.on( 'click', function () {
			gui.$keyboard.show();
		} );
	
	gui.$previewbox = $( '<div>' )
		.attr( 'id', 'transl-preview' )
		.append( 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.libs.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(),
			action = charActions[ label ],
			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,
				$( '<div>' )
					.attr( 'id', 'transl-selector-buttons' )
					.append(
						gui.$loadButton, gui.$saveButton
					)
			),
			gui.$textInputs,
			gui.$textArea,
			$( '<div>' )
				.attr( 'id', 'transl-edit-buttons' )
				.append( gui.$apply, gui.$submit ),
			$( '<div>' ).append(
				gui.$specialCharsButton,
				'&nbsp;',
				$( '<small>' ).html( mw.msg( 'transl-report-error' ) ),
				'&nbsp;',
				$( '<small>' ).html( mw.msg( 'transl-help-syntax' ) )
			),
			$( '<hr>' ),
			$( '<center>' ).append(
				$( '<strong>' ).text( mw.msg( 'transl-preview-label' ) )
			),
			gui.$previewbox
		)
		.insertAfter( $( '#transl-field' ).parent() );
	
	if ( !mw.user.isAnon() ) {
		gui.$submit.after(
			gui.$watch,
			$( '<label>' )
				.attr( 'for', 'transl-watch' )
				.text( mw.msg( 'transl-watch-article' ) )
		);
	}
	
	gui.$editbox.on( 'focus', 'input[type="text"], textarea', function () {
		$activeTextInput = $( this );
	} );
	
	gui.$langSelector.suggestions( {
		fetch: function ( input, response, maxRows ) {
			var langs = $.map( config.lang2code, function ( code, lang ) {
				return lang;
			} );
			
			input = unicodeToAscii( input.toLowerCase() );
			
			response( $.grep( langs, function ( lang ) {
				var normalized = unicodeToAscii( lang.toLowerCase() );
				return (
					normalized.indexOf( input ) === 0 &&
					forbiddenLanguageNames.indexOf( lang ) === -1
				);
			} ) );
		},
		result: {
			render: function ( suggestion, context ) {
				context.data.$container.find( '.suggestions-special' ).hide();
				this.text( suggestion );
			},
			select: function ( $input ) {
				onLoadLang( gui, null );
			}
		},
		special: {
			render: function ( query, context ) {
				var $label, $result;
				
				if ( !this.children().length ) {
					this.append(
						$label = $( '<div>' ).addClass( 'special-label' ),
						$result = $( '<div>' ).addClass( 'special-query' )
					);
				} else {
					$label = this.find( '.special-label' );
					$result = this.find( '.special-query' );
				}
				
				query = query.toLowerCase();
				
				if (
					query in config.code2lang &&
					forbiddenLanguageCodes.indexOf( query ) === -1
				) {
					$label.html( mw.msg( 'transl-lang-suggest-code', query ) );
					$result.text( config.code2lang[ query ] );
					this.show();
				}
			},
			select: function ( $input ) {
				var lang = this.find( '.special-query' ).text();
				$input.val( lang );
				onLoadLang( gui, null );
			}
		},
		highlightInput: true
	} )
	.on( 'keypress', function ( evt ) {
		if ( evt.keyCode === 13 ) { // Enter
			onLoadLang( gui, null );
		}
	} )
	.on( 'paste cut drop', function ( evt ) {
		$( this ).trigger( 'keypress' );
	} );
	
	gui.$confirmSpinner = $.createSpinner().css( 'margin-right', '1em' ).hide();
	
	gui.$confirmDialog = $( '<p>' ).dialog( {
		resizable: false,
		minHeight: 150,
		modal: true,
		autoOpen: false,
		draggable: false,
		title: mw.msg( 'transl-prompt-title' ),
		dialogClass: 'transl-dialog-no-close',
		buttons: [
			{
				text: mw.msg( 'transl-save-save' ),
				click: function () {
					if ( gui.$confirmDialog.data( 'hasPendingRequest' ) ) {
						return;
					} else {
						gui.$confirmSpinner.show();
						gui.$confirmDialog.data( 'hasPendingRequest', true );
					}
					
					savePreferredLangRequest( gui )
						.fail( function () {
							mw.notify( mw.msg( 'transl-save-fail' ), { type: 'error' } );
						} )
						.always( function () {
							gui.$confirmDialog.dialog( 'close' );
						} );
				}
			},
			{
				text: mw.msg( 'transl-save-cancel' ),
				click: function () {
					if ( !gui.$confirmDialog.data( 'hasPendingRequest' ) ) {
						gui.$confirmDialog.dialog( 'close' );
					}
				}
			}
		],
		close: function ( event, ui ) {
			gui.$confirmDialog.empty().removeData( 'hasPendingRequest' );
			gui.$confirmSpinner.hide();
		}
	} );
	
	gui.$textInputs.find( 'input' ).on( 'keypress', function ( evt ) {
		if ( evt.keyCode === 13 ) { // Enter
			onApplyChanges( gui );
		}
	} );
	
	gui.$confirmSpinner.prependTo( gui.$confirmDialog.parent().find( '.ui-dialog-buttonset' ) );
	
	resetForms( gui );
	refreshPreview( gui );
	
	if ( !!gadget.preferredLang ) {
		gadget.activeLang = gadget.preferredLang;
		gui.$langSelector.val( gadget.activeLang );
		onLoadLang( gui, gadget.activeLang );
	}
}

if (
	config.wgNamespaceNumber === 0 && config.wgAction === 'view' &&
	config.wgRevisionId === config.wgCurRevisionId &&
	config.wgDiffNewId === null && config.wgDiffNewId === null &&
	!mw.util.getParamValue( 'printable' )
) {
	if ( mw.cookie.get( POST_EDIT_COOKIE_KEY + config.wgArticleId ) === 'saved' ) {
		mw.loader.using( 'mediawiki.action.view.postEdit' ).done( function () {
			mw.hook( 'postEdit' ).fire();
			mw.cookie.set( POST_EDIT_COOKIE_KEY + config.wgArticleId, null );
		} );
	}
	
	mw.messages.set( messages );
	mw.hook( 'sectionLinks.ready' ).add( initialize );
}