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, '&')
.replace(/</g, '<')
);
}
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;