MediaWiki:Gadget-translation editor.js

OBS: Efter du har publicerat sidan kan du behöva tömma din webbläsares cache för att se ändringarna.

  • Firefox / Safari: Håll ned Skift och klicka på Uppdatera sidan eller tryck Ctrl-F5 eller Ctrl-R (⌘-R på Mac)
  • Google Chrome: Tryck Ctrl-Skift-R (⌘-Skift-R på Mac)
  • Internet Explorer / Edge: Håll ned Ctrl och klicka på Uppdatera eller tryck Ctrl-F5
  • Opera: Tryck Ctrl-F5.
// This script enables adding translations without handling wikitext.
//
// It is very much inspired by User:Conrad.Irwin/editor.js. The reason
// I created this script was because editor.js was difficult to modify
// for the needs of sv-wikt. This script is, currently, no better, with
// sv-wikt's idiosyncrasies and Swedish littered all-over. The goal is,
// however, to move all of that to a configuration section.
//
// Main differences:
// * This uses jQuery, which greatly simplifies a lot of DOM stuff.
// * This handles fewer special cases and is therefore smaller.
// * This uses mw.Api (or $.ajax where mw.Api isn't sufficient).
// * This uses promises, editor.js uses callbacks.
//
// TODO:
// * Move out sv-wikt-specific stuff
// * Structure the code better
// * Handle special cases:
//   - Reference to another page's translations
//   - Parameter halv=
//   - Translations that need to be checked
//
//
// - Skalman
//
// P.S.
// Please contact me if you have questions!
// https://sv.wiktionary.org/wiki/Anv%C3%A4ndardiskussion:Skalman
'use strict';

/* global $, mw, editor, silentFailStorage */

if (editor.enabled) {
	$('.översättningar')
		.each(function (i) {
			add_translation_form(this, i);
			add_heading_updater(this);
		});
}

function get_translation_table_index(table) {
	return $.inArray(table, $('.översättningar'));
}

function get_error_html(message) {
	return (
		'<img src="//upload.wikimedia.org/wikipedia/commons/4/4e/MW-Icon-AlertMark.png"> ' +
		(message + '')
			.replace(/&/g, '&amp;')
			.replace(/</g, '&lt;')
	);
}

var heading_id_counter = 0;
function add_heading_updater(table) {
	var id = heading_id_counter++;

	var self = $(table).find('.NavHead');

	var edit_head = $('<a>', {
		href: '#',
		text: '±',
		'class': 'ed-edit-head',
		title: 'Ändra översättningsrubrik'
	}).prependTo(self);

	function remove_gloss_nodes() {
		var nodes = [];
		$.each(self[0].childNodes, function (i, node) {
			if (node.className !== 'ed-edit-head' && node.className !== 'NavToggle') {
				nodes.push(node);
			}
		});
		$(nodes).detach();
		return nodes;
	}

	var gloss_nodes;
	edit_head.click(function (e) {
		e.preventDefault();
		if (self.find('form').length) {
			self.find('form').remove();
			self.append(gloss_nodes);
			return;
		}

		edit_head.text('Laddar...');

		editor.wikitext()
		.then(function (wikitext) {
			edit_head.text('±');

			var gloss;
			try {
				gloss = translation.get_gloss(wikitext, get_translation_table_index(table));
			} catch (e) {
				edit_head.remove();
				$('<span>', { class: 'ed-errors', html: get_error_html(e) }).appendTo(self);
				return;
			}

			gloss_nodes = remove_gloss_nodes();
			var prev_gloss_nodes = gloss_nodes;

			var form = $('<form>', { html:
				'<label>Definition: <input name="gloss"></label>' +
				'<button type="submit">Förhandsgranska</button> ' +
				'<span class="ed-loading">Laddar...</span>' +
				'<span class="ed-errors"></span>'
			});
			function error() {
				form.find('.ed-errors')
					.html(get_error_html('Ange antingen hela mallen {{ö-topp}} eller undvik helt wikitext ([]{}#|).'));
			}

			self.append(form);

			form.find('input')
				.val(gloss.standard ? gloss.text : gloss.trans_top)
				.focus();
			form.click(function (e) {
				e.stopPropagation();
			}).submit(function (e) {
				e.preventDefault();
				var gloss_wikitext = $.trim(this.gloss.value);
				if (!translation.is_trans_top(gloss_wikitext) && translation.contains_wikitext(gloss_wikitext)) {
					error();
					return;
				}
				form.find('.ed-loading').show();

				$.when(
					parse_wikitext(translation.make_trans_top(gloss_wikitext)),

					// get wikitext again in case it has changed since last time
					editor.wikitext()
				).done(function (gloss_html, wikitext) {
					gloss_html = $(gloss_html);
					var prev_class = self.parent('.NavFrame').attr('class');
					var new_class = gloss_html.filter('.NavFrame').attr('class');

					gloss_html = gloss_html.find('.NavHead').contents();
					if (!gloss_html.length) {
						error();
						form.find('.ed-loading').hide();
						return;
					}

					form.remove();
					wikitext = translation.set_gloss(
						wikitext,
						get_translation_table_index(table),
						gloss_wikitext
					);
					editor.edit({
						wikitext: wikitext,
						summary: 'översättningsrubrik: "' + gloss_wikitext + '"',
						summary_type: 'gloss' + id,
						redo: function () {
							remove_gloss_nodes();
							$('<span>', {
								'class': 'ed-added',
								html: gloss_html
							}).appendTo(self);
							if (prev_class !== new_class) {
								self.parent('.NavFrame').attr('class', new_class);
							}
						},
						undo: function () {
							remove_gloss_nodes();
							self.append(prev_gloss_nodes);
							if (prev_class !== new_class) {
								self.parent('.NavFrame').attr('class', prev_class);
							}
						}
					});
				});
			});
		});
	});
}

function add_translation_form(table) {
	var self = $(table);
	var lang_meta = {
		// en, eo, fi, hu
		'': 'p',

		ab: 'trans p',
		am: 'trans',
		an: 'm f p',
		ar: 'trans m f p',
		arc: 'trans m f',
		ast: 'm f p',
		ba: 'trans p',
		be: 'trans m f n p impf pf impfpf',
		bg: 'trans m f n p impf pf',
		bn: 'trans',
		bo: 'trans',
		bpy: 'trans',
		bs: 'm f n p impf pf',
		ca: 'm f mf p',
		ce: 'trans',
		chr: 'trans p',
		co: 'm f',
		cop: 'm f',
		cs: 'm f n p impf pf',
		csb: 'm f n impf pf',
		cu: 'trans m f n p impf pf',
		cv: 'trans',
		cy: 'm f',
		da: 'u n p',
		de: 'm f n p',
		dlm: 'm f',
		dsb: 'impf pf',
		dv: 'trans',
		dz: 'trans',
		el: 'trans m f n p',
		es: 'm f mf p',
		ext: 'm f mf p',
		fa: 'trans',
		fo: 'm f n',
		fr: 'm f mf p',
		ga: 'm f mf d p',
		gd: 'm f',
		gl: 'm f mf p',
		gmh: 'm f n',
		'gmq-bot': 'm f n p',
		'gmq-fda': 'm f n',
		'gmq-fsv': 'm f n',
		goh: 'm f n',
		got: 'm f n p trans',
		grc: 'trans m f n p d',
		gu: 'trans',
		ha: 'm f',
		he: 'trans m f p',
		hi: 'trans m f p',
		hr: 'm f n p impf pf',
		hsb: 'impf pf',
		hy: 'trans',
		is: 'm f n p',
		it: 'm f mf p',
		ja: 'trans',
		ka: 'trans p',
		kbd: 'trans',
		kk: 'trans p',
		km: 'trans p',
		kn: 'trans',
		ko: 'trans',
		kpv: 'trans p',
		krc: 'trans p',
		ks: 'trans',
		ku: 'm f',
		ky: 'trans p',
		la: 'm f n p',
		lad: 'm f',
		lez: 'trans p',
		lo: 'trans',
		lt: 'm f p',
		lv: 'm f p',
		mk: 'trans m f n p impf pf',
		ml: 'trans',
		mn: 'trans',
		mr: 'trans m f n',
		mt: 'm f p',
		my: 'trans',
		mzn: 'trans',
		nds: 'm f n',
		nl: 'm f n p',
		nn: 'm f n p',
		no: 'm f n p',
		non: 'm f n',
		oc: 'm f mf p',
		or: 'trans',
		orv: 'impf pf',
		os: 'trans',
		osp: 'm f',
		ovd: 'm f n p',
		pl: 'm f n p impf pf',
		pox: 'm f impf pf',
		ps: 'trans m f',
		pt: 'm f mf p',
		ro: 'm f n p',
		ru: 'trans m f n p impf pf impfpf',
		rue: 'impf pf',
		sa: 'trans m f n p d',
		sah: 'trans',
		sc: 'm f',
		scn: 'm f p',
		si: 'trans',
		sjd: 'trans',
		sjt: 'trans',
		sk: 'm f n p impf pf',
		sl: 'm f n p impf pf',
		sq: 'm f n p',
		sr: 'm f n p impf pf',
		szl: 'impf pf',
		ta: 'trans',
		te: 'trans',
		tg: 'trans',
		th: 'trans',
		tt: 'trans',
		tyv: 'trans',
		udm: 'trans',
		uk: 'trans m f n p impf pf',
		ur: 'trans',
		vec: 'm f',
		wa: 'm f',
		yi: 'trans m f n p',
		zh: 'trans'
	};
	var options = $.map({
		gender: {
			m: 'mask.',
			f: 'fem.',
			mf: 'mask. & fem.',
			n: 'neut.',
			u: 'utr.'
		},
		number: {
			s: 'singular',
			d: 'dualis',
			p: 'plural'
		},
		aspect: {
			impf: 'imperfektiv aspekt',
			pf: 'perfektiv aspekt',
			impfpf: 'imperfektiv & perfektiv aspekt'
		}
	}, function (items, name) {
		items = $.map(items, function (text, value) {
			return '<label class="ed-' + value + '">' +
				'<input type="radio" name="' + name + '" value="' + value + '">' +
				text +
				'</label>';
		});
		return '<p class="ed-options ed-' + name + '">' + items.join(' ') + '</p>';
	}).join('') +
	'<p class="ed-options"><label class="ed-trans">Translitteration: <input name="trans"></label></p>' +
	'<p class="ed-options"><label class="ed-note">Not: <input name="note"></label></p>' +
	'<p class="ed-options"><label class="ed-display_text">Visad text: <input name="display_text"></label></p>';

	var form = $($.parseHTML('<form><ul><li>' +
		'<p><label>Lägg till översättning ' +
		'<input class="ed-lang-code" name="lang_code" size="3" placeholder="Språkkod" title="Språkkod (två eller tre bokstäver)"></label>: ' +
		'<input class="ed-word" name="word" size="20" placeholder="Ord på det andra språket" title="Ord på det andra språket"> ' +
		'<button type="submit">Förhandsgranska</button> ' +
		'<a href="#" class="ed-more">Mer</a></p>' +
		options +
		'<div class="ed-errors"></div>' +
		'</li></ul></form>'));

	// Make radio buttons deselectable
	form.find(':radio').click(function last_click(e) {
		if (last_click[this.name] === this) {
			last_click[this.name] = null;
			this.checked = false;
		} else {
			last_click[this.name] = this;
		}
	});
	var show_all_opts = false;
	form.find('.ed-lang-code')
		.blur(update_options_visibility)

		// If the item exists, the value will be used as the value,
		// otherwise it's 'null', which empties (the already empty)
		// text field.
		.val(silentFailStorage.getItem('trans-lang'));
	form.find('.ed-more').click(function (e) {
		e.preventDefault();
		show_all_opts = !show_all_opts;
		$(this).text(show_all_opts ? 'Mindre' : 'Mer');
		update_options_visibility();
	});
	update_options_visibility();
	function update_options_visibility() {
		var elems = form.find('.ed-options label');
		if (show_all_opts) {
			elems.show();
		} else {
			var opts = lang_meta[form[0].lang_code.value] || lang_meta[''];
			elems
				.hide()
				.filter('.ed-' + opts.replace(/ /g, ', .ed-')).show();
		}
	}

	self.find('.NavContent').append(form);

	self.find('input').focus(function () {
		editor.init();
	});
	var hasSeenCommaWarning = false;
	var hasSeenCapitalWarning = false;
	form.submit(function (e) {
		e.preventDefault();
		var lang_code = $.trim(this.lang_code.value);
		var word = $.trim(this.word.value);
		var gender = form.find('.ed-gender input:checked').prop('checked', false).val();
		var number = form.find('.ed-number input:checked').prop('checked', false).val();
		var aspect = form.find('.ed-aspect input:checked').prop('checked', false).val();
		var trans = this.trans.value;
		var note = this.note.value;
		var display_text = this.display_text.value;

		if (!lang_code) {
			show_error(new BadInputError('lang-code', 'no-lang-code'));
			return;
		} else if (lang_code === translation.origin_lang) {
			show_error(new BadInputError('lang-code', 'origin-lang'));
			return;
		} else if (!word) {
			show_error(new BadInputError('word', 'no-word'));
			return;
		} else if (!hasSeenCommaWarning && mw.config.get('wgPageName').indexOf(',') === -1 && word.indexOf(',') !== -1) {
			show_error(new BadInputError('word', 'comma-in-word'));
			hasSeenCommaWarning = true;
			return;
		} else if (!hasSeenCapitalWarning && lang_code !== 'de' && !hasCapitalFirst(mw.config.get('wgPageName')) && hasCapitalFirst(word)) {
			show_error(new BadInputError('word', 'capital-first'));
			hasSeenCapitalWarning = true;
			return;
		}

		var word_options = {
			lang_code: lang_code,
			word: word,
			lang_name: null,
			exists: null,
			gender: gender,
			number: number,
			aspect: aspect,
			trans: trans,
			note: note,
			display_text: display_text
		};

		function show_error(e) {
			form.find('.ed-error').removeClass('ed-error');
			if (!e) {
				form.find('.ed-errors').empty();
				return;
			}
			if (e instanceof UnknownLangCodeError) {
				form.find('.ed-lang-code').addClass('ed-error').focus();
				e = 'Språkkoden "' + e.lang_code + '" finns inte eller används inte';
			} else if (e instanceof BadInputError) {
				form.find('.ed-' + e.input).addClass('ed-error').focus();
				if (e.type === 'no-lang-code') {
					e = 'Ange språkkod (en, fr, abq)';
				} else if (e.type === 'origin-lang') {
					e = 'Ange språkkod för språket som det svenska ordet ska översättas till';
				} else if (e.type === 'no-word') {
					e = 'Ange översättning';
				} else if (e.type === 'comma-in-word') {
					e = 'Ange en översättning i taget. Om du är helt säker på att det blivit rätt så kan du trycka på "Förhandsgranska" igen.';
				} else if (e.type === 'capital-first') {
					e = 'Översättningar anges normalt med liten första bokstav. Om du är helt säker på att det blivit rätt så kan du trycka på "Förhandsgranska" igen.';
				}
			} else if (e instanceof HttpError) {
				e = 'Kan inte ladda översättning. Är du online?';
			}
			form.find('.ed-errors').html(get_error_html(e));
		}


		$.when(
			// word_html
			get_pagename(lang_code, word)
			.then(function (pagename) {
				return page_exists(lang_code, pagename);
			})
			.then(function (page_exists) {
				word_options.exists = page_exists;
				return parse_wikitext(translation.get_formatted_word(word_options));
			}),
			// wikitext
			editor.wikitext(),
			// lang_name
			translation.get_language(lang_code)
		).fail(function (error) {
			if (error === 'http') {
				// jQuery HTTP error
				show_error(new HttpError());
			} else {
				show_error(error);
			}
		}).done(function (word_html, wikitext, lang_name) {
			show_error(false);

			silentFailStorage.setItem('trans-lang', lang_code);

			word_options.lang_name = lang_name;
			var added_elem;
			var index = get_translation_table_index(table);

			try {
				wikitext = translation.add(wikitext, index, word_options);
			} catch (e) {
				show_error(e);
				return;
			}

			form[0].word.value = '';
			form[0].trans.value = '';
			form[0].note.value = '';
			form[0].display_text.value = '';

			editor.edit({
				summary: '+' + lang_code + ': [[' + word + ']]',
				wikitext: wikitext,
				redo: function () {
					var translations = self.find('.ö-content > ul > li');
					translation.add_to_list({
						items: translations,
						add_only_item: function () {
							added_elem = $('<ul>')
								.append($('<li>', { html: lang_name + ': ' + word_html }))
								.appendTo(self.find('.ö-content'));
						},
						sort_order: function (item) {
							var match = /^\s*([^:]+):/.exec($(item).text());
							if (match) {
								return match[1].localeCompare(lang_name, translation.origin_lang);
							}
							return false;
						},
						add_to_item: function (item) {
							var sub_list = $(item).find('ul');
							added_elem = $('<span>', { html: ', ' + word_html});
							if (sub_list.length)
								added_elem.insertBefore(sub_list);
							else
								added_elem.appendTo(item);
						},
						add_after: function (item) {
							added_elem = $('<li>', { html: lang_name + ': ' + word_html })
								.insertAfter(item);
						},
						add_before: function (item) {
							added_elem = $('<li>', { html: lang_name + ': ' + word_html })
								.insertBefore(item);
						},
					});
					added_elem.addClass('ed-added');
					mw.hook('wikipage.content').fire(added_elem);
				},
				undo: function () {
					added_elem.remove();
				}
			});
		});
	});
	
	function hasCapitalFirst(str) {
		return str[0].toLowerCase() !== str[0];
	}
}

function parse_wikitext(wikitext) {
	return new mw.Api().get({
		action: 'parse',
		prop: 'text',
		disablelimitreport: '',
		wrapoutputclass: '',
		text: '<div>' + wikitext + '</div>',
		title: mw.config.get('wgPageName')
	}).then(function (data) {
		var html = data.parse.text['*'];
		// Get only the parts between <div> and </div>
		html = html.substring(
			html.indexOf('<div>') + '<div>'.length,
			html.lastIndexOf('</div>')
		);
		return $.trim(html);
	});
}

function get_pagename(lang_code, title) {
	return parse_wikitext('{' + '{sidnamn|' + lang_code + '|' + title + '}}');
}

function page_exists(lang_code, page) {
	return $.ajax({
		url: '//' + lang_code + '.wiktionary.org/w/api.php?origin=' + location.protocol + '//' + location.host,
		data: {
			action: 'query',
			titles: page,
			format: 'json'
		},
		dataType: 'json'
	}).then(function (data) {
		return !data.query.pages[-1];
	})['catch'](function () {
		return false;
	});
}

var translation = {
	origin_lang: 'sv',
	
	re_wikitext: /[[\]{}#|]/,

	contains_wikitext: function (str) {
		return translation.re_wikitext.test(str);
	},

	re_gloss: /\{\{ö\-topp([^\}]*)\}\}/g,

	re_section: /(\{\{ö\-topp[^\}]*\}\})([\s\S]*?)(\{\{ö\-botten\}\})/g,

	is_trans_top: function (gloss) {
		return gloss.replace(translation.re_gloss, '-') === '-';
	},

	make_trans_top: function (gloss) {
		if (translation.is_trans_top(gloss)) {
			return gloss;
		} else {
			return '{{ö-topp|' + gloss + '}}';
		}
	},

	get_gloss: function (wikitext, index) {
		if (wikitext.indexOf('<!--') !== -1) {
			throw new Error('Wikitext innehåller "<!--". Ändra manuellt.');
		}
		translation.re_gloss.lastIndex = 0;

		for (var i = 0; i <= index; i++) {
			var match = translation.re_gloss.exec(wikitext);
			if (i === index && match) {
				var standard = /^(|\|[^\|=]*)$/.test(match[1]);
				return {
					trans_top: match[0],
					text: standard ? match[1].substr(1) : void 0,
					standard: standard
				};
			}
		}
		throw new Error('Hittar inte {{ö-topp}} nr. ' + (index+1) + ' i wikitexten.');
	},
	set_gloss: function (wikitext, index, gloss) {
		index++;
		var count = 0;
		return wikitext.replace(translation.re_gloss, function (match, p1, p2) {
			count++;
			if (count !== index) {
				return match;
			}
			return translation.make_trans_top(gloss);
		});
	},
	get_formatted_word: function (opts) {
		var tpl = [
			opts.exists ? 'ö+' : 'ö',
			opts.lang_code,
			opts.word
		];
		opts.gender && tpl.push(opts.gender);
		opts.number && tpl.push(opts.number);
		opts.aspect && tpl.push(opts.aspect);
		opts.display_text && tpl.push('text=' + opts.display_text);
		opts.trans && tpl.push('tr=' + opts.trans);
		opts.note && tpl.push('not=' + opts.note);
		return '{{' + tpl.join('|') + '}}';
	},
	// Options:
	// - items: Array of items
	// - sort_order: Function that returns either 'equal', 'before', 'after' or false
	// - add_to_item: Adds a word to an item
	// - add_after: Adds the item after an item
	// - add_before: Adds the item before an item
	add_to_list: function (opts) {
		var items = opts.items;
		var len = items.length;
		if (!len) {
			items[0] = opts.add_only_item();
			return items;
		}
		for (var i = 0; i < len; i++) {
			var sort_order = opts.sort_order(items[i]);
			if (sort_order === 0) { // Equal
				items[i] = opts.add_to_item(items[i]);
				return items;
			} else if (sort_order === 1) { // After
				items[i] = opts.add_before(items[i]);
				return items;
			}
		}
		items[len-1] = opts.add_after(items[len-1]);
		return items;
	},
	add: function (wikitext, index, opts) {
		if (wikitext.indexOf('<!--') !== -1) {
			throw new Error('Wikitext innehåller "<!--". Ändra manuellt.');
		}
		index++;
		var count = 0;
		return wikitext.replace(translation.re_section, function (match, p1, p2, p3) {
			count++;
			if (count !== index) {
				return match;
			}

			p2 = $.trim(p2);

			var formatted_word = translation.get_formatted_word(opts);

			var lines = translation.add_to_list({
				// split into lines
				items: p2 ? p2.split('\n') : [],
				add_only_item: function () {
					return '*' + opts.lang_name + ': ' + formatted_word;
				},
				sort_order: function (line) {
					var match = /^\*\s*([^:]+):/.exec(line);
					if (match) {
						return match[1].localeCompare(opts.lang_name, translation.origin_lang);
					}
					return false;
				},
				add_to_item: function (line) {
					return line + ', ' + formatted_word;
				},
				add_before: function (line) {
					return this.add_only_item() + '\n' + line;
				},
				add_after: function (line) {
					return line + '\n' + this.add_only_item();
				}
			});

			return p1 + '\n' + lines.join('\n') + '\n' + p3;
		});
	},
	get_language: function (lang_code) {
		// Avoid extra HTTP request for the most common languages.
		var known = {
			no: 'bokmål',
			da: 'danska',
			en: 'engelska',
			fi: 'finska',
			fr: 'franska',
			is: 'isländska',
			pl: 'polska',
			ru: 'ryska',
			es: 'spanska',
			de: 'tyska',
		};
		if (lang_code in known) {
			return $.Deferred().resolve(known[lang_code]).promise();
		} else {
			return new mw.Api().get({
				action: 'expandtemplates',
				prop: 'wikitext',
				text: '{{#invoke:langfortemplate|go|' + lang_code + '}}'
			}).then(function (data) {
				data = data.expandtemplates.wikitext;
				if (data === 'okänt språk') {
					return $.Deferred().reject(new UnknownLangCodeError(lang_code)).promise();
				}
				return data;
			});
		}
	}
};


function extend_error(name, p1_name, p2_name) {
	function E(p1, p2) {
		this.message = p1;
		if (p1_name) this[p1_name] = p1;
		if (p2_name) this[p2_name] = p2;
	}
	E.prototype = new Error();
	E.prototype.constructor = E;
	E.prototype.name = name;
	return E;
}

var UnknownLangCodeError = extend_error('UnknownLangCodeError', 'lang_code');
var BadInputError = extend_error('BadInputError', 'input', 'type');
var HttpError = extend_error('HttpError');

// Export some useful components
window.translation = translation;
window.parse_wikitext = parse_wikitext;
window.add_heading_updater = add_heading_updater;
window.add_translation_form = add_translation_form;