MediaWiki:Gadget-TranslationAdder.js: diferència entre les revisions

De Viccionari
Contingut suprimit Contingut afegit
Use of "wgAction"/"wgTitle" is deprecated. Use mw.config instead.
deprecated: wgUserName, wgPageName, wgScriptPath, wgEnableAPI
Línia 5: Línia 5:


if (!api_url) {
if (!api_url) {
if (typeof(wgEnableAPI) === 'undefined' || wgEnableAPI == false) throw "Local API is not usable.";
if (typeof(mw.config.get("wgEnableAPI")) === 'undefined' || mw.config.get("wgEnableAPI") == false) throw "Local API is not usable.";
api_url = wgScriptPath + "/api.php";
api_url = mw.config.get("wgScriptPath") + "/api.php";
}
}
if (!request_type) {
if (!request_type) {
Línia 240: Línia 240:


// @public - the JsMwApi object for the current page
// @public - the JsMwApi object for the current page
this.page = JsMwApi().page(wgPageName);
this.page = JsMwApi().page(mw.config.get("wgPageName"));


// get the current text of the article and call the callback with it
// get the current text of the article and call the callback with it
Línia 426: Línia 426:
try {saveLi.appendChild(newNode('span', newNode('b', " Desat "),
try {saveLi.appendChild(newNode('span', newNode('b', " Desat "),
newNode('a', {'href': wgScript +
newNode('a', {'href': wgScript +
'?title=' + encodeURIComponent(wgPageName) +
'?title=' + encodeURIComponent(mw.config.get("wgPageName")) +
'&diff=' + encodeURIComponent(res.edit.newrevid) +
'&diff=' + encodeURIComponent(res.edit.newrevid) +
'&oldid=' + encodeURIComponent(res.edit.oldrevid)}, "(Mostra canvis)")));
'&oldid=' + encodeURIComponent(res.edit.oldrevid)}, "(Mostra canvis)")));
Línia 484: Línia 484:
// pos is a position in the line containing the gloss
// pos is a position in the line containing the gloss
getWikitextGloss: function (txt, pos) {
getWikitextGloss: function (txt, pos) {
txt = txt.replace("{"+"{inici}}", "{"+"{inici|Traduccions de "+wgPageName+"}}");
txt = txt.replace("{"+"{inici}}", "{"+"{inici|Traduccions de "+mw.config.get("wgPageName")+"}}");
var g_start = txt.lastIndexOf('\n{'+'{inici', pos) + 1;
var g_start = txt.lastIndexOf('\n{'+'{inici', pos) + 1;
var g_end = txt.indexOf('\n', pos);
var g_end = txt.indexOf('\n', pos);
Línia 1.288: Línia 1.288:
if (prefs.get('enabled', 'true') == 'true') {
if (prefs.get('enabled', 'true') == 'true') {
if (! window.loadedEditor) {
if (! window.loadedEditor) {
prefs.setDefault('labeller', wgUserName ? 'true' : 'false' );
prefs.setDefault('labeller', mw.config.get("wgUserName") ? 'true' : 'false' );
window.loadedEditor = true;
window.loadedEditor = true;
var editor = new Editor();
var editor = new Editor();

Revisió del 00:59, 30 jul 2015

//Adaptat de :en:User:Conrad.Irwin/editor.js + common.js Més info a [[VC:T]]

//JsMwApi documentation is at http://en.wiktionary.org/wiki/User_talk:Conrad.Irwin/Api.js
function JsMwApi (api_url, request_type) {

 if (!api_url) {
  if (typeof(mw.config.get("wgEnableAPI")) === 'undefined' || mw.config.get("wgEnableAPI") == false) throw "Local API is not usable.";
  api_url = mw.config.get("wgScriptPath") + "/api.php";
 }
 if (!request_type) {
  if (api_url.indexOf('http://') == 0 || api_url.indexOf('https://') == 0)
   request_type = "remote";
  else request_type = "local";
 }
 function call_api (query, callback) {
  if(!query || !callback) throw "Insufficient parameters for API call";
  query = serialise_query(query);
  if(request_type == "remote")
 	request_remote(api_url, query, callback, call_api.on_error || default_on_error);
  else  request_local(api_url,  query, callback, call_api.on_error || default_on_error);
 }
 var default_on_error = JsMwApi.prototype.on_error || function (xhr, callback, res) {
  if (typeof(console) != 'undefined') console.log([xhr, res]);
  callback(null);
 }

 function get_xhr () {
  try{return new XMLHttpRequest();}
  catch(e){ try {return new ActiveXObject("Msxml2.XMLHTTP");}
	catch(e){ try {return new ActiveXObject("Microsoft.XMLHTTP");}
		catch(e){throw "Could not create an XmlHttpRequest";
	}}}
 }

 function request_local (url, query, callback, on_error) {
  var xhr = get_xhr();
  xhr.open('POST', url + '?format=json', true);
  xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");                  
  xhr.send(query);
  xhr.onreadystatechange = function () {
	if (xhr.readyState == 4) {
		var res;
		if (xhr.status != 200)
		 res = {error: {
		  code: '_badresponse', 
		  info: xhr.status + " " + xhr.statusText
		}};
		else {
		 try {res = JSON.parse("("+xhr.responseText+")");}
		 catch(e) {
		  res = {error: {
		   code: '_badresult',
		   info: "The server returned an incorrectly formatted response"
		  }};
		 }
		}
		if (!res || res.error ) on_error(xhr, callback, res);
		else callback(res);
	}
  }
 }

 function request_remote (url, query, callback, on_error) {
  if(! window.__JsMwApi__counter) window.__JsMwApi__counter = 0;
  var cbname = '__JsMwApi__callback' + window.__JsMwApi__counter++; 
  window[cbname] = function (res) {
	 if (res.error) on_error(null, callback, res);
	 else callback(res);
  }
  var script = document.createElement('script');
  script.setAttribute('type', 'text/javascript');
  script.setAttribute('src', url + '?format=json&callback=window.' + cbname + '&' + query);
  document.getElementsByTagName('head')[0].appendChild(script);
 }

 function serialise_query(obj) {
  var amp = "";
  var out = "";
  if (String(obj) === obj) {out = obj;}
  else if (obj instanceof Array) {
   for (var i=0; i < obj.length; i++) {
    out += amp + serialise_query(obj[i]);
    amp = (out == '' || out.charAt(out.length-1) == '&') ? '' : '&';
   }
  }
  else if (obj instanceof Object) {
   for (var k in obj) {
    if (obj[k] === true) out += amp + encodeURIComponent(k) + '=1';
    else if (obj[k] === false) continue;
    else if (obj[k] instanceof Array) out += amp + encodeURIComponent(k) + '=' + encodeURIComponent(obj[k].join('|'));
    else if (obj[k] instanceof Object) throw "API parameters may not be objects";
    else out += amp + encodeURIComponent(k) + '=' + encodeURIComponent(obj[k]);
    amp = '&';
   }
  }
  else if (typeof(obj) !== 'undefined' && obj !== null) {
   throw "An API query can only be a string or an object";
  }
  return out;
 }

 // Make JSON.parse work
 var JSON = (typeof JSON == 'undefined' ? new Object() : JSON);

 if (typeof JSON.parse != 'function') {JSON.parse = function (json) { return eval('(' + json + ')');};}

 // Allow .prototype. extensions
 if (JsMwApi.prototype) {
  for (var i in JsMwApi.prototype) {
   call_api[i] = JsMwApi.prototype[i];
  }
 }
 return call_api;
}

JsMwApi.prototype.page = function (title) {

	function call_with_page (params, callback) {
	 call_with_page.api([params, {title: title, titles: title}], callback);
	}

	call_with_page.api = this;

	call_with_page.edit = function (params, edit_function) {
		if (typeof(params) == 'function') {
			edit_function = params;
			params = null;
		}
		params = [params, {
			action: "query", 
			prop: ["info", "revisions"], 
			intoken: "edit", 
			rvprop: ["content", "timestamp"]
		}];

		call_with_page(params, function (res) {
			if (!res || !res.query || !res.query.pages) return edit_function(null);

			// Get the first (and only) page from res.query.pages
			for (var pageid in res.query.pages) break;
			var page = res.query.pages[pageid];

			var text = page.revisions ? page.revisions[0]['*'] : '';

			function save_function (ntext, params, post_save) {
			 if (typeof(params) == 'function') {
			  post_save = params;
			  params = null;
			 }
			 params = [params, {
			  action: "edit",
			  text: ntext,
			  token: page.edittoken,
			  starttimestamp: page.starttimestamp,
			  basetimestamp: (page.revisions ? page.revisions[0].timestamp : false)
			 }];
			 call_with_page(params, post_save);
			}
			edit_function(text, save_function, res);
		});
	}

	call_with_page.parse = function (to_parse, callback) {
	 if (typeof to_parse == "function") {
	  callback = to_parse;
	  to_parse = null;
	 }
	 var params = (to_parse == null ? {page: title} : {title: title, text: to_parse});
	 call_with_page.api([{action: "parse", pst: true}, params], function (res) {
	  if (!res || !res.parse || !res.parse.text) callback(null, res);
	  else callback(res.parse.text['*'], res);
	 })
	}

	call_with_page.parseFragment = function (to_parse, callback) {
	 call_with_page.parse("<div>\n" + to_parse + "</div>", function (parsed, res) {
	  callback(parsed ? parsed.replace(/^<div>\n?/,'').replace(/(\s*\n)?<\/div>\n*(<!--[^>]*-->\s*)?$/,'') : parsed, res);
	 })
	}

	return call_with_page;
}
/*Storage of "string" preferences.*/
function CookiePreferences (context) {
	//Repeated calls with the same context should get the same preferences object.
	if (arguments.callee[context])
		return arguments.callee[context];
	else arguments.callee[context] = this;

	/*Change the value of a preference and store into a cookie.*/
	this.set = function (name, value) {
	 if (value === null || storage[name] === value) return;
	 storage[name] = value;
	 updateCookie();
	}

	/*Get the value of a preference from the cookie or default.
	 * If the preference isn't set, return the second argument or undefined.*/
	this.get = function (name, def) {
	 if (storage[name]) return storage[name];
	 else if (defaults[name]) return defaults[name];
	 else return def;
	}

	/* Let the default for get(name) be value for this session */
	this.setDefault = function(name, value) {defaults[name] = value;}

	var storage = {};
	var defaults = {};

	// Save storage into the cookie.
	function updateCookie () {
	 var value = "";
	 for (var name in storage) {
	   value += '&' + encodeURIComponent(name) + "=" + encodeURIComponent(storage[name]);
	 }
	}
	
	// Load storage from the cookie.
	// NOTE: If you wish to update the cookie format, both loading and storing
	// must continue to work for 30 days.
	function updateStorage () {
	 var value = getCookie('preferences' + context, value) || '';
	 var pairs = value.split('&');
	 for (var i=1; i < pairs.length; i++) {
	  var val = pairs[i].split('=');
	  if (storage[val[0]] === val[1]) continue;
	  storage[val[0]] = val[1];
	 }
	}

	//__init__
	updateStorage();
}

function Editor () {
	//Singleton
	if (arguments.callee.instance) return arguments.callee.instance
	else arguments.callee.instance = this;

	// @public - the JsMwApi object for the current page
	this.page = JsMwApi().page(mw.config.get("wgPageName"));

	// get the current text of the article and call the callback with it
	// NOTE: This function also acts as a loose non-re-entrant lock to protect currentText.
	this.withCurrentText = function(callback) {
		if (callbacks.length == 0) {
			callbacks = [callback];
			for (var i=0; i<callbacks.length; i++) {callbacks[i](currentText);}
			return callbacks = [];
		} 

		if (callbacks.length > 0) {return callbacks.push(callback);}

		callbacks = [callback];
		thiz.page.edit(function (text, _save) {
		 if (text === null) return thiz.error("Could not connect to server");
		 currentText = originalText = text;
		 saveCallback = _save;
		 for (var i=0; i<callbacks.length; i++) {callbacks[i](currentText);}
		 callbacks = [];
		});
	}
	// A decorator for withCurrentText
	function performSequentially(f) {
		return (function () {
			var the_arguments = arguments;
			thiz.withCurrentText(function () {
				f.apply(thiz, the_arguments);
			});
		});
	}

	// add an edit to the editstack
	function addEdit(edit, node, fromRedo) {
	 withPresenceShowing(false, function () {
	  if (node) {
	   nodestack.push(node);
	   node.style.cssText = "border: 2px #00FF00 dashed;"
	  }
	  if (! fromRedo) redostack = [];
	   var ntext = false;
	   try {
		 ntext = edit.edit(currentText);
		 if (ntext && ntext != currentText) {
		   edit.redo();
		   currentText = ntext;
		 }
		 else return false;
	   }
	   catch (e) {
	   // TODO Uncaught TypeError: Object [object Window] has no method 'error'
	   // I may have just fixed this by changing "this" below to "thiz" ...
		  thiz.error("ERROR:" + e);
	   }
	   editstack.push(edit);
	  });
	 }
	this.addEdit = performSequentially(addEdit);

	// display an error to the user
	this.error = function (message) { 
	 if (!errorlog) {
	  errorlog = newNode('ul',{style: "background-color: #FFDDDD; margin: 0px -10px -10px -10px; padding: 10px;"});
	  withPresenceShowing(true, function (presence) {presence.appendChild(errorlog);});
	 }
	 errorlog.appendChild(newNode('li', message));
	}

	var thiz = this; // this is set incorrectly when private functions are used as callbacks.
	var editstack = []; // A list of the edits that have been applied to get currentText
	var redostack = []; // A list of the edits that have been recently undone.
	var nodestack = []; // A lst of nodes to which we have added highlighting
	var callbacks = {}; // A list of onload callbacks (initially .length == undefined)
	var originalText = ""; // What was the contents of the page before we fiddled?
	var currentText = ""; // What is the contents now?
	var saveCallback; // The callback returned by the api's edit function to save.
	var errorlog; // The ul for sticking errors in.
	var savelog; // The ul for save messages.

	//Move an edit from the editstack to the redostack 
	function undo () {
		if (editstack.length == 0)
			return false;
		var edit = editstack.pop();
		redostack.push(edit);
		edit.undo();

		var text = originalText;
		for (var i=0; i < editstack.length; i++) {
		 var ntext = false;
		 try {ntext = editstack[i].edit(text);}
		 catch (e) {thiz.error("ERROR:" + e);}
		 if (ntext && ntext != text) {text = ntext;}
		 else {
		  editstack[i].undo();
		  editstack = editstack.splice(0, i);
		  break;
		 }
		}
		currentText = text;
		return true;
	}
	this.undo = performSequentially(undo);

	//Move an edit from the redostack to the editstack
	function redo () {
	 if (redostack.length == 0) return;
	 var edit = redostack.pop();
	 addEdit(edit, null, true);
	}
	this.redo = performSequentially(redo);

	function withPresenceShowing(broken, callback) {
	 if (arguments.callee.presence) {
	  arguments.callee.presence.style.display = "block";
	  return callback(arguments.callee.presence);
	 }
	 var presence = newNode('div',{'style':"position: fixed; top:0px; left: 0px; background-color: #00FF00; z-index: 10;padding: 30px;"})
	 window.setTimeout(function () {
		presence.style.backgroundColor = "#CCCCFF";
		presence.style.padding = "10px";
	 }, 400);

	 presence.appendChild(newNode('div',{'style':"position: relative; top:0px; left:0px; margin: -10px; color: #0000FF;cursor:pointer;", click:performSequentially(close)},"X"))
	 document.body.insertBefore(presence, document.body.firstChild);

	 var contents=newNode('p',{style: 'text-align: center'},newNode('b',"Eina de traducció"),newNode('br'));

	 if (!broken) {
	  contents.appendChild(newNode('button',"Desa canvis", {'title':'Save your changes ['+mw.util.tooltipAccessKeyPrefix+'s]','accesskey':'s','click': save}));
	  contents.appendChild(newNode('br'));
	  contents.appendChild(newNode('button',"Desfés", {'title':'Undo last change ['+mw.util.tooltipAccessKeyPrefix+'z]','accesskey':'z','click': thiz.undo}));
	  contents.appendChild(newNode('button', "Refés", {'click':thiz.redo}));
	 }
	 presence.appendChild(contents);
	 arguments.callee.presence = presence;
	 callback(presence);
	}

	// Remove the button
	function close () {
	 while (undo())
	  ;
	 withPresenceShowing(true, function (presence) {
	  presence.style.display = "none";
	  if (errorlog) {
	   errorlog.parentNode.removeChild(errorlog);
	   errorlog = false;
	  }
	 });
	}

	//Send the currentText back to the server to save.
	function save () {
	 thiz.withCurrentText(function () {
		if (editstack.length == 0) return;
		var cleanup_callbacks = callbacks;
		callbacks = [];
		var sum = {};
		for (var i=0; i<editstack.length; i++) {
		 sum[editstack[i].summary] = true;
		 if (editstack[i].after_save) cleanup_callbacks.push(editstack[i].after_save);
		}
		var summary = "";
		for (var name in sum) {summary += name + " ";}
		editstack = [];
		redostack = [];
		var saveLi = newNode('li', 'Desant:' + summary + "...");
		withPresenceShowing(false, function (presence) {
		 if (! savelog) {
		  savelog = newNode('ul', {style: "background-color: #DDFFDD; margin: 0px -10px -10px -10px; padding: 10px;"});
		  presence.appendChild(savelog);
		 }
		 savelog.appendChild(saveLi);
		 if (originalText == currentText) return thiz.error("No hi ha cap canvi a la pàgina.");
		 else if (!currentText) return thiz.error("ERROR: page has become blank.");
		});

		originalText = currentText;
		var nst = []
		var node;
		while (node = nodestack.pop()) {nst.push(node);}
		saveCallback(currentText, {summary: summary + "([[VC:T]])"}, function (res) {
			if (res == null) return thiz.error("S'ha produït un error en desar.");
			try {saveLi.appendChild(newNode('span', newNode('b', " Desat "),
				newNode('a', {'href': wgScript + 
				'?title=' + encodeURIComponent(mw.config.get("wgPageName")) + 
				'&diff=' + encodeURIComponent(res.edit.newrevid) +
				'&oldid=' + encodeURIComponent(res.edit.oldrevid)}, "(Mostra canvis)")));
			}catch(e){
			 if (res.error) {thiz.error("Not saved: " + String(res.error.info));}
			 else {thiz.error(newNode('p',String(e)));}
			}

			for (var i=0; i < nst.length; i++) {nst[i].style.cssText = "background-color: #0F0;border: 2px #0F0 solid;";}

			window.setTimeout(function () {
			 var node;
			 while (node = nst.pop()) node.style.cssText = "";
			}, 400);

			// restore any callbacks that were waiting for currentText before we started
			for (var i=0; i < cleanup_callbacks.length; i++) {
			 thiz.withCurrentText(cleanup_callbacks[i]);
			}
		 });
		});
	}
}

var util = {
	getVanillaIndexOf: function (str, text, pos) {
		if (!pos) pos = 0;
		var cpos = 0, tpos = 0, wpos = 0, spos = 0;
		do {
		 cpos = text.indexOf('<!--', pos);
		 tpos = text.indexOf('{'+'{', pos);
		 wpos = text.indexOf('<nowiki>', pos);
		 spos = text.indexOf(str, pos);
		 pos = Math.min(
			Math.min(cpos == -1 ? Infinity : cpos, 
				 tpos == -1 ? Infinity : tpos), 
			Math.min(wpos == -1 ? Infinity : wpos,
				 spos == -1 ? Infinity : spos)
		 )
		 if (pos == spos) return pos == Infinity ? -1 : pos;
		 else if (pos == cpos) pos = text.indexOf('-->', pos) + 3;
		 else if (pos == wpos) pos = text.indexOf('</nowiki>', pos) + 9;
		 else if (pos == tpos) pos = text.indexOf('}}', pos) + 2;
		} while (pos < Infinity)
		return -1;
	},

	validateNoWikisyntax: function(field, nonempty) {
	 return function(txt, error) {
	  if(nonempty && !txt) {return error("No s'ha especificat " + field + ".");}
	  return txt;
	 }
	},

	escapeRe: function(txt) {return txt.replace(/([\\{}(\|)[\].?*+])/g, "\\$1");},

	// pos is a position in the line containing the gloss
	getWikitextGloss: function (txt, pos) {
	 txt = txt.replace("{"+"{inici}}", "{"+"{inici|Traduccions de "+mw.config.get("wgPageName")+"}}");
	 var g_start = txt.lastIndexOf('\n{'+'{inici', pos) + 1; 
	 var g_end = txt.indexOf('\n', pos);
	 var g_line = txt.substr(g_start, g_end - g_start);
	 return g_line.replace(/\{\{inici\|(.*)\}\}/, "$1");
	},

	// get [start_pos, end_pos] of position of wikitext for trans_table containing node in text
	getTransTable: function (text, node, recursive) {
	 var gloss = util.getTransGloss(node);
	 var pos = 0;
	 var transect = [];
	 while(pos > -1) {
	  pos = util.getVanillaIndexOf('{'+'{inici', text, pos+1);
	  if (pos > -1 && util.matchGloss(util.getWikitextGloss(text, pos), gloss)) {transect.push(pos);}
	 }
	 if (transect.length > 1) {
	  var poss = transect;
	  transect = [];
	  for (var i=0; i<poss.length; i++) {
	   pos = poss[i];
	   if (util.matchGloss(gloss, util.getWikitextGloss(text, pos))) {transect.push(pos);}
	  }
	  if (transect.length > 1 && !recursive) {transect = util.tieBreakTransTable(text, transect, node);}
	 }
	 if (transect.length == 1) {
	  pos = transect[0];
	  pos = util.getVanillaIndexOf("\n", text, pos) + 1;
	  var endpos = text.indexOf('{'+'{final}}', pos); 
	  if (endpos > -1 && pos > 0) {return [pos, endpos];}
	 }
	 return false;
	},

	// try to narrow down the correct poss if multiple matching trans tables
	tieBreakTransTable: function (text, poss, node) {
	 if (node.nodeName.toLowerCase() == 'div') {
		while (node && !(node.className && node.className.indexOf('NavFrame') > -1))
			node = node.parentNode;

		var nodes = node.getElementsByTagName('table');
		if (! nodes.length) return poss;
		node = nodes[0];
	 }
	 else {while (node && node.nodeName.toLowerCase() != 'table') node = node.parentNode;}

	 var tables = document.getElementsByTagName('table');
	 var before_count = 0;
	 var after_count = 0;
	 var is_found = false;
	 for (var i=0; i < tables.length; i++) {
		if (tables[i].className.indexOf('traduccions') >= 0) {
			var gloss = util.getTransGloss(tables[i]);
			if (tables[i] == node) {
			 is_found = true;
			 continue;
			}
			var pos = util.getTransTable(text, tables[i], true);

			if (pos) {
			 for (var j=0; j < poss.length; j++) {
			  if (poss[j] == pos) return util.tieBreakTransTable(poss.splice(j, 1), node);
			 }
			}
			else {
			 var matched = 0;
			 for (var j=0; j < poss.length; j++) {
			  if (util.matchGloss(util.getWikitextGloss(text, poss[j]), gloss) && util.matchGloss(gloss, util.getWikitextGloss(text, poss[j]))) {matched++;}
			 }
			 if (matched == poss.length) {
			  if (is_found) after_count++;
			  else before_count++;
			 }
			}
		}
	 }
	 if (before_count + 1 + after_count == poss.length) return [poss[before_count]];
	 else return poss;
	},

	matchGloss: function (line, gloss) { //line: text a {{inici}}, gloss: ídem però sense símbols
	 line = line.replace("{"+"{inici}}", "{"+"{inici|Traduccions}}");
	 if (gloss.match(/^ *$/)) {return !!(line.match(/\{\{inici\| *\}\}/) || line.match(/^ *$/));}
	 var words = gloss.split(/\W+/);
	 var pos = 0;
	 for (var i=0; i < words.length; i++) {
	   pos = line.indexOf(words[i], pos);
	   if (pos == -1) return false;
	 }
	 return pos > -1;
	},

	getTransGlossText: function (node) {
	 var ret = '';
	 var children = node.childNodes;
	 for (var i=0; i<children.length; i++) {
	  if (children[i].nodeType == 3) ret += children[i].nodeValue;
	  else if (children[i].nodeName.match(/^(i|b)$/i) || children[i].className.indexOf('wt-edit-recurse') > -1) ret += util.getTransGlossText(children[i]);
	  else if (ret.match(/\w$/)) ret += " "; //Prevent new words from being created across node boundaries
	 }
	 return ret.replace(/\W/g, ' '); // all characters except a-zA-Z0-9 are changed to spaces
	},

	getTransGloss: function (ul) { //Text de plantilla -trans-
	 var node = ul;
	 while (node && node.className.indexOf('NavFrame') == -1) {node = node.parentNode;}
	  if (!node) return ''; 
	  var children = node.childNodes;
	  for (var i=0; i< children.length; i++) {
	   if(children[i].className && children[i].className.indexOf('NavHead') > -1) return util.getTransGlossText(children[i]);
	  }
	  return '';
	},

	isTrreq: function (li) {
	 var spans = li.getElementsByTagName('span');
	 return (spans && spans.length > 0 && spans[0].className.indexOf("trreq") > -1)
	}
};

function AdderWrapper (editor, adder, insertNode, insertSibling) {
	var form = adder.createForm()
	var status = newNode('span');

	form.appendChild(status);
	if (insertSibling) insertNode.insertBefore(form, insertSibling);
	else insertNode.appendChild(form);

	adder.elements = {};

	//This is all because IE doesn't reliably allow form.elements['name']
	for (var i=0; i< form.elements.length; i++) {
	 adder.elements[form.elements[i].name] = form.elements[i];
	}

	form.onsubmit = function () {
	 try {
		var submit = true;
		var values = {}
		status.innerHTML = "";
		for (var name in adder.fields) {
		 if (adder.fields[name] == 'checkbox') {
		  values[name] = adder.elements[name].checked ? name : false;
		 }
		 else {
		  adder.elements[name].style.border = ''; // clear error styles
		  values[name] = adder.fields[name](adder.elements[name].value || '', function (msg) {
		   status.appendChild(newNode('span',{style:'color: red'}, newNode('img', {'src':'http://upload.wikimedia.org/wikipedia/commons/4/4e/MW-Icon-AlertMark.png'}), msg, newNode('br'))); 
		  adder.elements[name].style.border="solid #CC0000 2px";
		   return false
		  });
		  if (values[name] === false) {submit = false;}
		 }
		}
		if (!submit) return false;

		var loading = newNode('span', 'Loading...');
		status.appendChild(loading);
			
		adder.onsubmit(values, function (text, callback) {
		 editor.page.parseFragment(text, function (res) {
		  if (!res) return loading.appendChild(newNode('p', {style: 'color: red'}, "Could not connect to the server."));
		  callback(res);
		  status.removeChild(loading);
		 });
		});   
	}
	catch(e) {
	 status.innerHTML = "ERROR:" + e.description; 
	 return false;
	}
	return false;
       }
}

function TranslationAdders (editor) {
 function TranslationLabeller (insertDiv) {
  var original_span;
  var adder_form;
  var initial_value;
  var edit_button;
  var editing = false;
  var adderInterface = {
   'fields': {
	'gloss': function (txt, error) { return util.validateNoWikisyntax('accepció', true)(txt, error)}},
	'createForm': function () {
	  var thisId = "a" + String(Math.random()).replace(".","");
	  return adder_form = newNode('form', {style:'display: inline', width: 'auto', click: kill_event},
	newNode('label', {'for': thisId}, "Accepció: "),
	newNode('input', {type: 'text', name:'gloss', value: initial_value, style: 'width: 50%', title: 'Insert a summary of the relevant definition', id: thisId}),
	newNode('input', {type: 'submit', name: 'Previsualitza', value: 'Previsualitza'}),
	newNode('a', {href: '/wiki/VC:T'}, 'Ajuda'));
	},
	'onsubmit': function (values, render) {
	  render(values.gloss, function (new_html) {
		if (editing) toggle_editing(false);
		var old_html = original_span.innerHTML;
		editor.addEdit({
		 'undo': function () { original_span.innerHTML = old_html; if(!editing) toggle_editing();},
		 'redo': function () { original_span.innerHTML = new_html; if(editing) toggle_editing();},
		 'edit': function (text) { return perform_edit(text, values.gloss) },
		 'summary': 'acc.:"' + (values.gloss.length > 20 ? values.gloss.substr(0, 20) + '...' : values.gloss + '"')}, original_span);
		});
	 }
        };

	// The actual modification to the wikitext
	function perform_edit(wikitext, gloss) {
	 var pos = util.getTransTable(wikitext, insertDiv)[0] - 4;
	 var g_start = wikitext.lastIndexOf('\n{'+'{inici', pos) + 1;
	 var g_end = wikitext.indexOf('}}\n', pos) + 2;

	 if (g_start == 0 || wikitext.substr(g_start, g_end - g_start).indexOf("\n") > - 1) {
 	  editor.error("Taula de traducció no trobada.");
	  return wikitext;
	 }
	 else {
	  return wikitext.substr(0, g_start) + '{'+'{inici|' + gloss + '}}' + wikitext.substr(g_end);
	 }
	}

	// Don't open and close box when interacting with form.
	function kill_event(e) {
	 if (e && e.stopPropagation) e.stopPropagation();
	 else window.event.cancelBubble = true;
	}

	// What to do when the +/- button is clicked.
	function toggle_editing () {
	 if (editing) {
	  adder_form.style.display = "none";
	  original_span.style.display = "inline";
	  editing = false;
	  return;
	 }
	 editing = true;
	 edit_button.innerHTML = "Loading...";
	 editor.withCurrentText(function (currentText) {
	  var pos = util.getTransTable(currentText, insertDiv);
	  edit_button.innerHTML = '±';
	  if (!pos) return editor.error("No trobada taula de traducció");
	  var gloss_line = currentText.substr(currentText.lastIndexOf('\n', pos[0] - 2) + 1);
	  gloss_line = gloss_line.substr(0, gloss_line.indexOf('\n'));
	  initial_value = gloss_line.replace(/^\{\{inici(\|(.*)|)\}\}\s*$/,"$2");

	  if (initial_value.indexOf("\n") > 0) return editor.error("Internal error");

	  if (!original_span) {
	   original_span = newNode('span', {'class':'wt-edit-recurse'});
	   for (var i=0; i<insertDiv.childNodes.length; i++) {
		var child = insertDiv.childNodes[i];
		if (child != edit_button && (!child.className || child.className != 'NavToggle')) {
			original_span.appendChild(insertDiv.removeChild(child));
			i--;
		}
	   }
	   insertDiv.appendChild(original_span);

	   new AdderWrapper(editor, adderInterface, insertDiv, original_span);
	  }
	  original_span.style.display = "none";
	  adder_form.style.display = "inline";
	  adder_form.getElementsByTagName('input')[0].focus()
	 });
	}
	edit_button = newNode('a', '±', {href: '#', click: function (e) {
		if (e && e.preventDefault) e.preventDefault();
		kill_event(e); 
		toggle_editing(); 
		return false;
	}, title:"Edit table heading", style:"padding:2px; margin-left: -5px;"});
	insertDiv.insertBefore(edit_button, insertDiv.firstChild.nextSibling);
	}

	function TranslationAdder (insertUl) {
	 var langmetadata = new LangMetadata ();
	 this.fields =  {
		lang: function (txt, error) {
		 if (txt == 'ca') {return error("Seleccioneu un idioma diferent.");}
		 if (/^[a-z]{2,3}(-[a-z\-]{1,7})?$/.test(txt)) {return txt;}
		 return error("Seleccioni un idioma vàlid.");
		},
		word: function (txt, error) {
		 if (txt.indexOf(',') == -1 || forceComma) {
		  forceComma = false;
		  if (langmetadata.expectedCase(thiz.elements.lang.value, mw.config.get("wgTitle"), txt) || forceCase) {
		   forceCase = false;
		   return util.validateNoWikisyntax('traducció', true)(txt, error);
		  }
		  if (prefs.get('case-knowledge', 'none') == 'none') {
		   return error(newNode('span', "No pot portar majúscules ",
		    newNode('span', {style: "color: blue; text-decoration: underline; cursor: pointer;", click: function () {
			forceCase = true;
			inputForm.onsubmit();
			prefs.set('case-knowledge', 'guru');
		    }}, "Prem aquí per continuar")));
		  }
		  else {
		   var msg = newNode('span',newNode('span', {style: "color: blue; text-decoration: underline; cursor: pointer;", click: function () {
		     prefs.set('case-knowledge', 'none')
		     try{ msg.parentNode.removeChild(msg); } catch(e) {}
		     editor.undo()
		   }}, "Desfés"), " a no ser que estigueu segurs que aquesta traducció comença per majúscula.");
		   error(msg)
		   return txt;
		  }
		 }
		 if (prefs.get('comma-knowledge', 'none') == 'none') {
			return error(newNode('span',"You can only add one translation at a time.",
			 newNode('span', {style: "color: blue; text-decoration: underline; cursor: pointer;", click: function () {
				forceComma = true;
				inputForm.onsubmit();
				prefs.set('comma-knowledge', 'guru')
			 }}, "add it by clicking here.")))
		 }
		 else {
			var msg = newNode('span',
			newNode('span', {style: "color: blue; text-decoration: underline; cursor: pointer;", click: function () {
				prefs.set('comma-knowledge', 'none')
				try{ msg.parentNode.removeChild(msg); } catch(e) {}
				editor.undo()
			}}, "Please click undo"), " if you were trying to create a list of translations in one go, this is currently not supported.");
			error(msg)
			return txt;
		 }
		},
		qual: util.validateNoWikisyntax('qualifier'),
		alt: util.validateNoWikisyntax('page name'),
		m: 'checkbox', f: 'checkbox', n: 'checkbox', c: 'checkbox', p: 'checkbox'
		};

		this.createForm = function () {
		 var controls = {
		  lang: newNode('input', {size:4, type:'text', name:'lang', value:prefs.get('curlang',''), title:'The two or three letter ISO 639 language code'}),
		  qualifier: newNode('p', "Partícula posterior: ", newNode('input', {name: 'qual', title: " "}), " "),
		  display:  newNode('p',"Partícula prèvia: ", newNode('input', {name: 'alt', title: " "}), " "),
		  gender_m: newNode('label',newNode('input', {type: 'checkbox', name: 'm'}), 'masc. '),
		  gender_f: newNode('label',newNode('input', {type: 'checkbox', name: 'f'}), 'fem. '),
		  gender_n: newNode('label',newNode('input', {type: 'checkbox', name: 'n'}), 'neutre '),
		  gender_c: newNode('label',newNode('input', {type: 'checkbox', name: 'c'}), 'invariable '),
		  plural: newNode('label',newNode('input', {type: 'checkbox', name: 'p'}), 'plural ', newNode('br')) };
		  controls.gender = newNode('p', controls.gender_m, controls.gender_f, controls.gender_n, controls.gender_c, controls.plural);

		  langInput = controls.lang;
		  var showButton = newNode('span',{'click': function () {
			 if (!advancedMode) {
			  advancedMode = true;
			  showButton.innerHTML = " Menys";
			 }
			 else {
			  advancedMode = false;
			  showButton.innerHTML = " Més";
			 }
			 updateScriptGuess.call(langInput, true);
			}, 'style':"color: #0000FF;cursor: pointer;"}, advancedMode ? " Menys" : " Més");

		 function updateScriptGuess (preserve) {
			preserve = (preserve === true);

		 //show all arguments
		 function show () {
			for (var i=0; i<arguments.length; i++) {
				if (arguments[i].nodeName.toLowerCase() == 'p')
					arguments[i].style.display = "block";
				else	arguments[i].style.display = "inline";
			}
		 }
		 //hide all arguments
		 function hide () {
			for (var i=0; i < arguments.length; i++)
				arguments[i].style.display = "none";
		 }
		 //if 1st argument is false hide the remaining arguments, otherwise show them.
		 function toggle (condition) {
			 if (condition) show.apply(this,[].splice.call(arguments,1, arguments.length - 1));
			 else hide.apply(this, [].splice.call(arguments, 1, arguments.length - 1));
		 }

		 if (!preserve) langInput.value = langmetadata.cleanLangCode(langInput.value);

		 var guess = prefs.get('script-' + langInput.value, '');
		 var lang = langInput.value;

		 if (!advancedMode) {
			var g = langmetadata.getGenders(lang);
			if (!lang) {hide(controls.gender);}
			else if (g == undefined) {
			 show(controls.gender,controls.gender_m, controls.gender_f, controls.gender_n, controls.gender_c);
			}
			else {
			 toggle(g.indexOf('m') > -1, controls.gender);
			 toggle(g.indexOf('m') > -1, controls.gender_m);
			 toggle(g.indexOf('f') > -1, controls.gender_f);
			 toggle(g.indexOf('n') > -1, controls.gender_n);
			 toggle(g.indexOf('c') > -1, controls.gender_c);
			}
			var p = langmetadata.hasPlural(lang);
			toggle(p !== false, controls.plural);
			toggle(g || p, controls.gender);
			hide(controls.qualifier,controls.display); //only in more
		}
		else {
		 show(controls.gender,controls.gender_m, controls.gender_f,controls.gender_n,controls.gender_c,controls.plural,controls.qualifier,controls.display);
		}
		} 
			
		//autocomplete language names
		var langNameToCode={};
		function langAutoFill(e){
			e = (e || event).keyCode;
			if((e >= 33 && e <= 40) || e == 8 || e == 46 || e == 27 || e == 16){return;}
			var t = this, v = t.value;
			if(v.substr(0,1) != v.substr(0,1).toUpperCase()){return;}
			JsMwApi()({action:'query',generator:'allpages',gapnamespace:10,gapprefix:'langrev/'+v,gaplimit:3,prop:'revisions',rvprop:'content'},function(r){
				if(r.query && r.query.pages && t.value == v){
				 var l={}, ll={}
				 for(var i in r.query.pages){
				  var rqp = r.query.pages[i];ll = rqp.title < ll.title?ll:rqp;l = rqp.title > l.title?l:rqp;
				 }
				 if(!r['query-continue'] && ll.title.indexOf(l.title) == 0){
				  langNameToCode[l.title.substr(17)]=l.revisions[0]['*'];
				  if(l.title != "Template:langrev/"+v){
				   if (t.setSelectionRange){
				    t.setSelectionRange([t.value.length, t.value = l.title.substr(17)][0], t.value.length);
				   } else if (t.createTextRange) {
				    var z = t.createTextRange();
				    z.moveEnd('character', 0 - z.move('character', [t.value.length, t.value = l.title.substr(17)][0]) + t.value.length);
				    z.select()
				   }
				  }
				 }
				}
			})
		}
		langInput.onkeyup = langAutoFill;
		langInput.onblur = function(){
		 if(langNameToCode[this.value]){this.value=langNameToCode[this.value]}
			 updateScriptGuess.call(langInput)
		}

		window.setTimeout(function () {updateScriptGuess.call(langInput)}, 0);
		inputForm = newNode('form',newNode('p', newNode('a',{href:"/wiki/VC:T"},"Afegeix traducció"),' ',langInput, newNode('b',': '), newNode('input', {'name': 'word', size:20}), newNode('input',{'type': 'submit','value':'Previsualitza'}),showButton), controls.gender,controls.display, controls.qualifier)

			return inputForm;
		}

		this.onsubmit = function (values, render) {
		 var wikt_lang = values.lang;
		 var wikitext;
		 wikitext =
			'{'+'{' + values.lang + '}}: ' +  
			(values.alt ? values.alt : '') +
			'{'+'{trad' +
			'|' + (values.lang) +
			'|' + (values.word) +
			'}}' +
			(values.m ? ' {'+'{m}}' : '') +
			(values.f ? ' {'+'{f}}' : '') +
			(values.n ? ' {'+'{n}}' : '') +
			(values.c ? ' {'+'{i}}' : '') +
			(values.p ? ' {'+'{p}}' : '') +
			(values.qual ? values.qual : '');
		 render(wikitext, function (html) { registerEdits(values, wikitext, html)});
		 if (!this.balancer) this.balancer = new TranslationBalancer(editor, insertUl.parentNode.parentNode.parentNode);
		}
		var thiz = this;
		var prefs = new CookiePreferences('EditorJs');
		var langInput;
		var inputForm;
		var advancedMode = false;
		var forceComma = false;
		var forceCase = false;

		//Reset elements to default values.
		function resetElements () {
		 if (prefs.get('more-display', 'none') != advancedMode ? 'block' : 'none')
		  prefs.set('more-display', advancedMode ? 'block' : 'none'); //named for compatibility
		 thiz.elements.word.value = thiz.elements.alt.value = thiz.elements.qual.value = '';
		 thiz.elements.m.checked = thiz.elements.f.checked = thiz.elements.n.checked = thiz.elements.c.checked = thiz.elements.p.checked = false;
		 prefs.set('curlang', thiz.elements.lang.value);
		}

		// This is onsubmit after the wikitext has been rendered to give content
		function registerEdits (values, wikitext, content) {
			var li = newNode('li',{'class': 'trans-'+values.lang});
			li.innerHTML = content;
			var lang = getLangName(li);
			var summary = '+' + values.lang + ':' + values.alt +'[['+ values.word + ']]';
			var insertBefore = null;
			var nextLanguage = null;

			function addEdit (edit, span) {
			 editor.addEdit({
			  'undo': function ()  {
				edit.undo();
				if(thiz.elements.word.value == "" &&
				 thiz.elements.alt.value == "" &&
				 thiz.elements.qual.value == "") {
				  var fields = ["lang","word","alt","qual"];
				  var cb = "mnfcp".split("");
				  for (var i=0; i < fields.length; i++) {
				   thiz.elements[fields[i]].value = values[fields[i]];
				  }
				}
			  },
			 'redo': function () {
				 edit.redo();
				 var fields = ["lang","word","alt","qual"];
				 for (var i=0; i < fields.length; i++) {
				  if (thiz.elements[fields[i]].value != values[fields[i]]) return;
				 }
				 resetElements();
				},
				'edit': edit.edit,
				'summary': summary
			 }, span);
			}

			if (lang) {//Get all li's in this table row. 
			 var lis = [];
			 var ls = insertUl.parentNode.parentNode.getElementsByTagName('li');
			 for (var j=0; j < ls.length; j++) {lis.push(ls[j]);}

			 ls = insertUl.parentNode.parentNode.getElementsByTagName('dd');
			 for (var j=0; j < ls.length; j++) lis.push(ls[j]);

			 for (var j=0; j < lis.length; j++) {
				if (lis[j].getElementsByTagName('form').length > 0) continue;
				var ln = getLangName(lis[j]);
				if (ln == lang) {
				 var span = newNode('span');
				 var parent = lis[j];
				 if(!util.isTrreq(parent)) {//No és 1a traducció
				  if(parent.getElementsByTagName('ul').length + parent.getElementsByTagName('dl').length == 0){
					span.innerHTML = ", " + content.substr(content.indexOf(':') + 1);
					addEdit({
			'redo': function () { parent.appendChild(span) },
			'undo': function () { parent.removeChild(span) },
			'edit': getEditFunction(values, wikitext, ln, values.lang, false, function (text, ipos) {
				 	//We are adding the wikitext to a list of translations that already exists.
				 	var lineend = text.indexOf('\n', ipos);
				 	var wt = wikitext.replace('subst:','');
				 	wt = wt.substr(wt.indexOf(':') + 1);
				 	return text.substr(0, lineend) + "," + wt + text.substr(lineend);})
					}, span);
					return resetElements();
				  }
				 }//fi de !util.isTrreq(parent)
				}//fi de ln == lang
				else if (ln && ln > lang && (!nextLanguage || ln < nextLanguage) && lis[j].parentNode.parentNode.nodeName.toLowerCase() != 'li') { //Nou idioma
				 nextLanguage = ln;
				 var parent = lis[j];
				 insertBefore = [
					{
					'redo': function () {parent.parentNode.insertBefore(li, parent);},
					'undo': function () {parent.parentNode.removeChild(li)},
					'edit': getEditFunction(values, wikitext, ln, getLangCode(parent), util.isTrreq(parent), function (text, ipos) {//Adding a new language's translation before another language's translation
				var lineend = text.lastIndexOf('\n', ipos);
				return text.substr(0, lineend) + "\n*" + wikitext + text.substr(lineend);
						})
					},li];
				}
			 }//fi de j++
			}//fi de lang

			li.className = "trans-" + values.lang;
			if (insertBefore) {addEdit(insertBefore[0], insertBefore[1]);}
			else {
			 //Append the translations to the end (no better way found)
			 addEdit({
				'redo': function () {insertUl.appendChild(li);},
				'undo': function () {insertUl.removeChild(li)},
				'edit': getEditFunction(values, wikitext)
			 }, li);
			}
			return resetElements();
		}

		//Get the wikitext modification for the current form submission.
		function getEditFunction (values, wikitext, findLanguage, findLangCode, trreq, callback) {
		 return function(text) {
		  var p = util.getTransTable(text, insertUl);
		  if (!p) {return editor.error("No trobada taula '" + values.lang + ":" + values.word + "'");}

		  var stapos = p[0];
		  var endpos = p[1];

		  if (findLanguage) {//p.ex. findLanguage="Castellà" (idioma posterior)
		   var ipos = 0;

		   ipos = text.substr(stapos).search(RegExp("\\*[:*]? ?\\[\\[" + util.escapeRe(findLanguage) + "\\]\\]:"))+stapos;

		   if(ipos < stapos || ipos > endpos)
		    ipos=text.substr(stapos).search(RegExp('\\*[:*]? ?'+util.escapeRe(findLanguage)+ ':'))+stapos;
		   if(ipos < stapos || ipos > endpos)
		    ipos = text.indexOf('{'+findLangCode+'}}:', stapos);

		   if (ipos >= stapos && ipos < endpos) {return callback(text, ipos, trreq);}
		   else {return editor.error("Translation '" + values.lang + ":"+values.word+"' not found. Please reformat. ipos="+ipos+", stapos="+stapos+", endpos="+endpos+", findLanguage: "+findLanguage+", findLangCode: "+findLangCode);} //hi ha algun format estrany
		  }
		  return text.substr(0, endpos) + "*" + wikitext + "\n" + text.substr(endpos);
		 };
		}

		// For an <li> in well-formed translation sections, return the language name.
		function getLangName(li) {
		 var guess = li.textContent || li.innerText;
		 if (guess) {guess = guess.substr(0, guess.indexOf(':'));}
		 if (guess == 'Template') {return false;}
		 return guess.replace(/^[\s\n]*/,'');
		}

		// Try to get the language code from an <li> containing { {t t+ or t-	// }}
		//Quan no hi ha cap traducció en un idioma, necessitem l'ISO de l'idioma següent.
		function getLangCode(li) {
		 if (li.className.indexOf('trans-') == 0) return li.className.substr(6);
		 var spans = li.getElementsByTagName('span');
		 for (var i=0; i < spans.length; i++) {
		  if (spans[i].lang) {return spans[i].lang;}
		 }
		 return false;
		}
	}
	var tables = document.getElementsByTagName('table'); //taula creada per plantilla:inici
	for (var i=0; i<tables.length; i++) {
		if (tables[i].className.indexOf('traduccions') > -1) {
			var _lists = tables[i].getElementsByTagName('ul');
			var lists = [];
			for (var j=0; j<_lists.length; j++)
			 if (_lists[j].parentNode.nodeName.toLowerCase() == 'td') {lists.push(_lists[j]);}

			if (lists.length == 0) {
			 tables[i].getElementsByTagName('td')[0].appendChild(newNode('ul'));
			 lists = tables[i].getElementsByTagName('ul');
			}
			if (lists.length == 1) {
			 var table = tables[i].getElementsByTagName('td')[2]
			 if (table) {
			  table.appendChild(newNode('ul'));
			  lists = tables[i].getElementsByTagName('ul');
			 }
			}
			if (lists) {
			 var li = newNode('li');
			 var ul = lists[lists.length - 1];
			 var table = tables[i];
			 if (table.getElementsByTagName('tbody').length > 0) table = table.getElementsByTagName('tbody')[0];
			 table.appendChild(newNode('tr', newNode('td'), newNode('td'), newNode('td', {'style':'text-align: left'},newNode('ul', li))));
			 new AdderWrapper(editor, new TranslationAdder(ul), li);
			 if ((new CookiePreferences('EditorJs')).get('labeller') == 'true') {
			  var div = tables[i].parentNode.parentNode.getElementsByTagName('div')[0];
			  if (div.className.indexOf('NavHead') > -1) {new TranslationLabeller(div)}
			 }
			}
		}
	}
}

function TranslationBalancer (editor, insertTable) {
	var status;

	//create the form
	function init () {
		var cns = insertTable.getElementsByTagName('tr')[0].childNodes;
		var tds = []; 
		//Find all three table cells in the translation table.
		for (var i=0; i<cns.length; i++) {
		 if (cns[i].nodeName.toUpperCase() == 'TD') tds.push(cns[i])
		}

		//Ensure that there is a <ul> on the left side of the balancer.
		var left = tds[0].getElementsByTagName('ul');
		if (left.length > 0) left = left[0];
		else {
		 left = newNode('ul');
		 tds[0].appendChild(left);
		}

		//Ensure that there is a <ul> on the right side of the balancer.
		var right = tds[2].getElementsByTagName('ul');
		if (right.length > 0) right = right[0];
		else { right = newNode('ul');
		 tds[2].appendChild(right);
		}

		var moveLeft = newNode('input',{'type':'submit','name':'ml', 'value':'←', 'click': function(){return prepareEdits('←', left, right)}});
		var moveRight = newNode('input',{'type':'submit','name':'mr', 'value':'→', 'click': function(){return prepareEdits('→', left, right)}});
		status = newNode('span');

		var form = newNode('form', moveLeft, newNode('br'), moveRight, newNode('br'), status);
		tds[1].appendChild(form);
		form.onsubmit = function () { return false; } //Must be done after the appendChild for IE :(
	}

	function moveOneRight(left, right) {
	 var li = left.lastChild;
	 while (li && li.nodeName.toLowerCase() != 'li') li = li.previousSibling;
	 if (li) right.insertBefore(left.removeChild(li), right.firstChild);
	}

	function moveOneLeft(left, right) {
	 var li = right.firstChild;
	 while (li && li.nodeName.toLowerCase() != 'li') li = li.nextSibling;
	 if (li) left.appendChild(right.removeChild(li));
	}

	//store the edit object with the editor
	function prepareEdits(direction, left, right) {
	 status.innerHTML = "Loading...";
	 editor.addEdit({
	 	'redo': function () { (direction == '→' ? moveOneRight : moveOneLeft)(left, right) },
		'undo': function () { (direction == '→' ? moveOneLeft : moveOneRight)(left, right) },
		'edit': function (text) {return editWikitext(right, direction, text);},
		'summary': '←'
	 });
	}

	//get the wikitext modification
	function editWikitext(insertUl, direction, text) {
		status.innerHTML = "";
		//Find the position of the translation table
		var p = util.getTransTable(text, insertUl);
		if (!p) {return editor.error("Taula de traducció no trobada. Corregiu-la manualment.");}

		var stapos = p[0];
		var endpos = p[1];

		//Find the start and end of the { {mig}} in the table
		var midpos = text.indexOf('{'+'{mig}}', stapos);
		var midstart = text.lastIndexOf("\n", midpos);
		var midend = text.indexOf("\n", midpos);
		if (midstart < stapos - 1 || midend > endpos)
			return editor.error("Could not find {'+'{mig}}, please correct page.");

		if (direction == '→') {
			// Select the last list item of the left list (may be more than one line if nested translations are present)
			var linestart = text.lastIndexOf("\n", midstart - 3);
			while (/^[:*#;]$/.test(text.substr(linestart+2,1)))
				linestart = text.lastIndexOf("\n", linestart - 1);

			if (linestart < stapos - 1 || linestart >= endpos) 
				return editor.error("No translations to move");

			return text.substr(0, linestart)  //Everything before the item we are moving
				+ text.substr(midstart, midend - midstart) //Then { {mig}}
				+ text.substr(linestart, midstart - linestart) //Then the item we are moving
				+ text.substr(midend); //Then everything after { {mig}}
		}
		else if (direction == '←') {
			// Select the first list item of the right list (may be more than one line if nested translations are present)
			var lineend = text.indexOf("\n", midend + 3);
			while (/^[:*#;]$/.test(text.substr(lineend+2,1))) lineend = text.indexOf("\n", lineend + 1);

			if (lineend < stapos - 1 || lineend >= endpos) return editor.error("No translations to move");

			return text.substr(0, midstart) //Everything before { {mig}}
				+ text.substr(midend, lineend - midend) //Then the item we are moving
				+ text.substr(midstart, midend - midstart) //Then { {mig}}
				+ text.substr(lineend); //Then everything after the item we are moving
		}
		return text;
	}
	init();
}

function LangMetadata () {

 if (arguments.callee.instance) return arguments.callee.instance
 else arguments.callee.instance = this;

 var metadata = {af:{g:"",p:1},akk:{g:"mf",p:1},am:{g:"mf",p:1},an:{g:"mf",p:1},ang:{g:"mfn",p:1},ar:{g:"mf",p:1},arc:{g:"mf",p:1},arz:{g:"mf",p:1},ast:{g:"mf",p:1},axm:{g:""},az:{g:"",p:1},"bat-smg":{g:"mf",p:1},be:{g:"mfn",p:1},bg:{g:"mfn",p:1},bn:{g:""},br:{g:"mf"},ca:{g:"mf",p:1},cdo:{g:"",p:0},cjy:{g:"",p:0},cmn:{g:"",p:0},cpx:{g:"",p:0},crh:{g:""},cs:{g:"mfn",p:1},cu:{g:"mfn",p:1},cv:{g:""},cy:{g:"mf",p:1},czh:{g:"",p:0},czo:{g:"",p:0},da:{g:"cn",p:1},de:{g:"mfn",p:1},dng:{g:"",p:0},dv:{p:1},el:{g:"mfn",p:1},en:{g:"",p:1},eo:{g:"",p:1},es:{g:"mf",p:1},et:{g:"",p:1},ett:{p:1},eu:{g:"",p:1},fa:{g:""},fi:{g:"",p:1},fil:{g:"",p:0},fo:{g:"mfn"},fr:{g:"mf",p:1},frm:{g:"mf",p:1},fro:{g:"mf",p:1},ga:{g:"mf",p:1},gan:{g:"",p:0},gd:{g:"mf",p:1},gl:{g:"mf",p:1},got:{g:"mfn",p:1},grc:{g:"mfn",p:1},gu:{g:"mfn",p:1},hak:{g:"",p:0},he:{g:"mf",p:1},hi:{g:"mf",p:1},hr:{g:"mfn",p:1},hsn:{g:"",p:0},hu:{g:"",p:1},hy:{g:""},ia:{g:""},ie:{g:""},is:{g:"mfn",p:1},it:{g:"mf",p:1},ja:{g:"",p:0},ka:{g:""},kk:{g:""},ko:{g:"",p:0},krc:{g:"",p:1},ky:{g:""},la:{g:"mfn",p:1},lo:{g:"",p:0},lt:{g:"mf",p:1},lv:{g:"mf",p:1},mi:{g:0},mk:{g:"mfn",p:1},ml:{g:""},mn:{g:""},mnp:{g:"",p:0},mr:{g:"mfn"},mt:{g:"mf"},nan:{g:"",p:0},nb:{g:"mfn",p:1},nl:{g:"mfn",p:1},nn:{g:"mfn",p:1},no:{g:"mfn",p:1},non:{g:"mfn",p:1},oc:{g:"mf",p:1},os:{g:""},pa:{g:"mf",p:1},pl:{g:"mfn",p:1},pt:{g:"mf",p:1},rm:{g:"mf"},ro:{g:"mfn",p:1},ru:{g:"mfn",p:1},ruo:{g:"mfn",p:1},rup:{g:"mfn",p:1},ruq:{g:"mfn",p:1},sa:{g:"mfn",p:1},scn:{g:"mf",p:1},sk:{g:"mfn",p:1},sl:{g:"mfn",p:1},sq:{g:"mf"},sr:{g:"mfn",p:1},sv:{g:"cn",p:1},sw:{g:""},ta:{g:""},te:{g:""},tg:{g:""},th:{g:"",p:0},tk:{g:""},tl:{g:"",p:0},tr:{g:"",p:1},tt:{g:""},uk:{g:"mfn",p:1},ur:{g:"mf",p:1},uz:{g:""},vi:{g:"",p:0},wuu:{g:"",p:0},xcl:{g:""},xno:{g:"mf",p:1},yi:{g:"mfn",p:1},yua:{g:"",p:1},yue:{g:"",p:0},zh:{g:"",p:0},zu:{nc:1}}
 var clean = {aar:"aa",afar:"aa",abk:"ab",abkhazian:"ab",afr:"af",afrikaans:"af",aka:"ak",akan:"ak",amh:"am",amharic:"am",ara:"ar",arabic:"ar",arg:"an",aragonese:"an",asm:"as",assamese:"as",ave:"ae",avestan:"ae",aym:"ay",aymara:"ay",aze:"az",azerbaijani:"az",bak:"ba",bashkir:"ba",bam:"bm",bambara:"bm",bel:"be",belarusian:"be",ben:"bn",bengali:"bn",bis:"bi",bislama:"bi",bod:"bo",tibetan:"bo",bs:"sh",bos:"sh",bosnian:"sh",bre:"br",breton:"br",bul:"bg",bulgarian:"bg",cat:"ca",catalan:"ca",ces:"cs",czech:"cs",cha:"ch",chamorro:"ch",che:"ce",chechen:"ce",cor:"kw",cornish:"kw",cos:"co",corsican:"co",cre:"cr",cree:"cr",cym:"cy",welsh:"cy",dan:"da",danish:"da",deu:"de",german:"de",div:"dv",dhivehi:"dv",dzo:"dz",dzongkha:"dz",ell:"el",greek:"el",eng:"en",english:"en",epo:"eo",esperanto:"eo",est:"et",estonian:"et",eus:"eu",basque:"eu",ewe:"ee",fao:"fo",faroese:"fo",fas:"fa",persian:"fa",fij:"fj",fijian:"fj",fil:"tl",fin:"fi",finnish:"fi",fra:"fr",french:"fr",fry:"fy",westernfrisian:"fy",ful:"ff",fulah:"ff",gla:"gd",scottishgaelic:"gd",gle:"ga",irish:"ga",glg:"gl",galego:"gl",galician:"gl",glv:"gv",manx:"gv",grn:"gn",guarani:"gn",guj:"gu",gujarati:"gu",hat:"ht",haitian:"ht",hau:"ha",hausa:"ha",heb:"he",hebrew:"he",her:"hz",herero:"hz",hin:"hi",hindi:"hi",hmo:"ho",hirimotu:"ho",hr:"sh",hrv:"sh",croatian:"sh",hun:"hu",hungarian:"hu",hye:"hy",armenian:"hy",ibo:"ig",igbo:"ig",ido:"io",iii:"ii",iku:"iu",inuktitut:"iu",ile:"ie",interlingue:"ie",ina:"ia",interlingua:"ia",ind:"id",indonesian:"id",ipk:"ik",inupiaq:"ik",isl:"is",icelandic:"is",ita:"it",italian:"it",jav:"jv",javanese:"jv",jpn:"ja",japanese:"ja",kal:"kl",kan:"kn",kannada:"kn",kas:"ks",kashmiri:"ks",kat:"ka",georgian:"ka",kaz:"kk",kazakh:"kk",khm:"km",kik:"ki",kikuyu:"ki",kin:"rw",kir:"ky",kirghiz:"ky",kor:"ko",korean:"ko",kua:"kj",kur:"ku",kurd:"ku",lao:"lo",lat:"la",latin:"la",lav:"lv",latvian:"lv",lim:"li",limburgan:"li",lin:"ln",lingala:"ln",lit:"lt",lithuanian:"lt",ltz:"lb",luxembourgish:"lb",lub:"lu",lubakatanga:"lu",lug:"lg",ganda:"lg",mah:"mh",mal:"ml",malayalam:"ml",mar:"mr",marathi:"mr",mkd:"mk",macedonian:"mk",mlg:"mg",malagasy:"mg",mlt:"mt",maltese:"mt",mon:"mn",mongolian:"mn",mri:"mi",maori:"mi",msa:"ms",malay:"ms",mya:"my",burmese:"my",nau:"na",nauru:"na",nav:"nv",navajo:"nv",nbl:"nr",nep:"ne",nepali:"ne",nld:"nl",holandés:"nl",dutch:"nl",nno:"nn",norwegiannynorsk:"nn",nob:"nb",norwegianbokmal:"nb",nor:"no",noruec:"no",oci:"oc",occitan:"oc",oji:"oj",ojibwa:"oj",ori:"or",oriya:"or",orm:"om",oromo:"om",oss:"os",ossetian:"os",pan:"pa",panjabi:"pa",pli:"pi",pali:"pi",pol:"pl",polish:"pl",por:"pt",portuguese:"pt",pus:"ps",pushto:"ps",que:"qu",quechua:"qu",roh:"rm",romansh:"rm",ron:"ro",romanian:"ro",run:"rn",rundi:"rn",rus:"ru",russian:"ru",sag:"sg",sango:"sg",san:"sa",sanskrit:"sa",sin:"si",sinhala:"si",slk:"sk",slovak:"sk",slv:"sl",slovenian:"sl",sme:"se",northernsami:"se",smo:"sm",samoan:"sm",sna:"sn",shona:"sn",snd:"sd",sindhi:"sd",som:"so",somali:"so",sot:"st",southernsotho:"st",spa:"es",spanish:"es",castellà:"es",español:"es",castella:"es",sqi:"sq",albanian:"sq",srd:"sc",sardinian:"sc",sr:"sh",srp:"sh",serbian:"sh",ssw:"ss",swati:"ss",sun:"su",swa:"sw",swahili:"sw",swe:"sv",swedish:"sv",tah:"ty",tahitian:"ty",tam:"ta",tamil:"ta",tat:"tt",tatar:"tt",tel:"te",telugu:"te",tgk:"tg",tajik:"tg",tgl:"tl",tagalog:"tl",tha:"th",thai:"th",tuk:"tk",turkmen:"tk",tur:"tr",turkish:"tr",twi:"tw",uig:"ug",uighur:"ug",ukr:"uk",ukrainian:"uk",urd:"ur",urdu:"ur",uzb:"uz",uzbek:"uz",ven:"ve",venda:"ve",vie:"vi",vietnamese:"vi",vol:"vo",volapuk:"vo",wln:"wa",walloon:"wa",wol:"wo",wolof:"wo",xho:"xh",xhosa:"xh",yid:"yi",yiddish:"yi",yor:"yo",yoruba:"yo",zha:"za",zhuang:"za",xinès:"zh",zho:"cmn",chinese:"zh",xinès:"zh",zul:"zu",zulu:"zu"};

 this.expectedCase = function (lang, title, word) { //Majúscula inicial
  if (lang == 'de' || lang == 'lb') return true;
  if (title.substr(0, 1).toLowerCase() != title.substr(0, 1)) return true;
  return word.substr(0, 1).toLowerCase() == word.substr(0, 1)
 }

 //Returns a string of standard gender letters (mfnc) or an empty string
 this.getGenders = function(lang) {
  if (metadata[lang]) return metadata[lang].g;
 }

 //Returns true if the specified lang has the concept of plural nouns
 this.hasPlural = function(lang) {
  if (metadata[lang]) return metadata[lang].p;
 }

 //Given user input, return a language code. Normalises ISO 639-1 codes and names to 639-3.
 this.cleanLangCode = function(lang) {
  var key = lang.toLowerCase().replace(' ','');
  if (clean[key]) return clean[key];
  else return lang;
 }
}

addOnloadHook(function () {
 var actions = window.location.toString().replace(/.*\?/, '&');
 if (mw.config.get("wgAction")!= 'view' || actions.indexOf('&printable=yes') > -1 || actions.indexOf('&diff=') > -1 || actions.indexOf('&oldid=') > -1 || mw.config.get("wgNamespaceNumber")!=0) {return;} //Només espai ppal.
 // Check that we have not been disabled
 var prefs = new CookiePreferences('EditorJs');
 if (prefs.get('enabled', 'true') == 'true') {
  if (! window.loadedEditor) {
   prefs.setDefault('labeller', mw.config.get("wgUserName") ? 'true' : 'false' );
   window.loadedEditor = true;
   var editor = new Editor();
   TranslationAdders(editor);
  }
 }
})