User:YMS/labelcollect2.js

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
  • Opera: Press Ctrl-F5.
/**
  Label Collector
  By Yannick M. Schmitt, [[User:YMS]]
  Using Steven Levithan's XRegExp 2.0 library, MIT License, http://xregexp.com/

  See [[User:YMS/LC]] for documentation
  <nowiki>
 */
(function() {
  "use strict";

  // Supported languages
  // Regular expressions in XRegExp syntax (http://xregexp.com/syntax/), main magic happening in "suggest":
  // - descAnd: term used to concatenate two values a and b with "and", as in "actor and singer" (must contain two named groups a and b)
  // - descBy: term used to concatenate two values a and b with "by", as in "book by Stephen King" (must contain two named groups a and b)
  // - descFromPlace: term used to concatenate two values a and b with "from", as in "band from Germany" (must contain two named groups a and b) [currently not used]
  // - descFromTime: term used to concatenate two values a and b with "from", as in "film from 1980" (must contain two named groups a and b; note that in English this is empty: "{b} {a}" -> "1980 film")
  // - descIn: term used to concatenate two values a and b with "in", as in "town in China" (must contain two named groups a and b)
  // - descOf: term used to concatenate two values a and b with "of", as in "episode of Breaking Bad" (must contain two named groups a and b)
  // - separator: sentence separator (must contain a named group "sentence" to return the sentence), used to give only the sentence of the article if no better suggestion can be made
  // - suggest: main expression to find the description suggestion in the article text (must contain a named group "desc" to return the suggestion)
  // Other data:
  // - script: writing system used in that language; used to suggest labels for all languages that use the same script if all existing labels of that script are the same
  // - sisters: languages that should use the same rules and projects than this language (e.g. en-gb should use the same rules as en, and it should take contents from enwiki & Co)
  // If something isn't defined for one language, it will fall back to "defaultLang"
  var languageData = {
    "defaultLang": {
      "separator": "^(?<sentence>.*?([ \\(](?!(ca|Dr|etc|I|II|IV|IX|Inc|Jr|Ltd|Mr|Mrs|Ms|St|V|VI|X|XI|XV|XX)\\.)\\p{Alphabetic}{2,}|\\p{Alphabetic}{4,}|\\d{4}|\\d\\p{Alphabetic}|\\)))\\.(\\s.*)?$",
      "descAnd": "{a} & {b}",
      "descBy": "{a}, {b}",
      "descFromPlace": "{a}, {b}",
      "descFromTime": "{b} {a}",
      "descIn": "{a}, {b}",
      "descOf": "{b} {a}"
    },
    "af": {
      "suggest": "( (is) (die|('|’)n)) (?<desc>.*)$",
      "descAnd": "{a} en {b}",
      "descBy": "{a} deur {b}",
      "descFromPlace": "{a} van {b}",
      "descFromTime": "{b}-{a}",
      "descIn": "{a} in {b}",
      "script": "Latin"
    },
    "az": {
      "suggest": "( (-|–|—)) (?<desc>.*)$",
      "descAnd": "{a} və {b}",
      "script": "Latin"
    },
    "bg": {
      "suggest": "( (са|е)( професионална)?) (?<desc>.*)$",
      "descAnd": "{a} и {b}",
      "descBy": "{a} на {b}",
      "descFromPlace": "{a} от {b}",
      "descFromTime": "{a} от {b}",
      "descIn": "{a} в {b}",
      "script": "Cyrillic"
    },
    "ca": {
      "suggest": "( (és|fou|són) (el |l('|’)|la |una? ))(?<desc>.*)$",
      "descAnd": "{a} i {b}",
      "descBy": "{a} de {b}",
      "descFromPlace": "{a} de {b}",
      "descFromTime": "{a} el {b}",
      "descIn": "{a} a {b}",
      "script": "Latin"
    },
    "cs": {
      "suggest": "( (byl(a|i|o|y)?|je|jsou))( znám(á|é|í|ý)? jako)?( ((proslul|slavn|takzvan|významn|znám)(á|é|í|ý)|přední)| tzv\\.)? (?<desc>.*?)(, (pokračovatel|proslul|proslaven|slavn|významn|znám).*)?(, v (letech|roce).*|,( do)? roku|, po roce)?(, (ve |se |do )?kte(r|ř).*|, jen?ž.*)?$",
      "descAnd": "{a} a {b}",
      "descFromPlace": "{a} ze {b}",
      "descFromTime": "{a} z {b}",
      "descIn": "{a} v {b}",
      "script": "Latin"
    },
    "da": {
      "suggest": "( (er|var) (en|et)?) (?<desc>.*)$",
      "descAnd": "{a} og {b}",
      "descBy": "{a} af {b}",
      "descFromPlace": "{a} fra {b}",
      "descFromTime": "{a} fra {b}",
      "descIn": "{a} i {b}",
      "script": "Latin"
    },
    "de": {
      "sisters": [ "de-at", "de-ch" ],
      "suggest": "( ((ist|sind|war(en)?)( (das|der|die|ein(e|er)?))( ehemaliger?)?( bekannter?| professioneller?)?)|((bezeichnet|nennt)( man)? (das|den|die|ein(en?)?))) (?<desc>.*?)(, (das|der|die|welcher?).*)?$",
      "descAnd": "{a} und {b}",
      "descBy": "{a} von {b}",
      "descFromPlace": "{a} aus {b}",
      "descFromTime": "{a} von {b}",
      "descIn": "{a} in {b}",
      "descOf": "{a} von {b}",
      "script": "Latin"
    },
    "el": {
      "suggest": "( (είναι|ήταν)( (ο|η))?) (?<desc>.*)$",
      "descAnd": "{a} και {b}",
      "descBy": "{a} των {b}",
      "descFromPlace": "{a} από {b}",
      "script": "Greek"
    },
    "en": {
      "sisters": [ "en-ca", "en-gb" ],
      "suggest": "( ((are|has been|have been|is|was|were) (an?|the)( defunct| extinct| former| retired)?( award-winning| discovered (on|by)| distinguished| famous| influential| noted| professional| renowned| well-known)?)|(refers to( an?| the)?)) (?<desc>.*?)((,? )(and )?(best known|currently|famous|focuss?ing|known|most notably|playing|probably|starring|that|which|who).*)?$",
      "descAnd": "{a} and {b}",
      "descBy": "{a} by {b}",
      "descFromPlace": "{a} from {b}",
      "descFromTime": "{b} {a}",
      "descIn": "{a} in {b}",
      "descOf": "{a} of {b}",
      "script": "Latin"
    },
    "eo": {
      "suggest": "( (est(a|i)s)( la)?) (?<desc>.*)$",
      "descAnd": "{a} kaj {b}",
      "descBy": "{a} de {b}",
      "descFromPlace": "{a} de {b}",
      "descFromTime": "{a} de {b}",
      "descIn": "{a} en {b}",
      "script": "Latin"
    },
    "es": {
      "suggest": "( (es|fue|son)( (el|la|le|un(a|o)?))( ex)?) (?<desc>.*?)(,? (que).*)?$",
      "descAnd": "{a} y {b}",
      "descBy": "{a} de {b}",
      "descFromPlace": "{a} de {b}",
      "descFromTime": "{a} de {b}",
      "descIn": "{a} de {b}",
      "descOf": "{a} de {b}",
      "script": "Latin"
    },
    "et": {
      "suggest": "( (olid?|on)) (?<desc>.*)$",
      "descAnd": "{a} ja {b}",
      "descFromTime": "{a} on {b}",
      "script": "Latin"
    },
    "fi": {
      "suggest": "( (oli|on)) (?<desc>.*)$",
      "descAnd": "{a} ja {b}",
      "descFromTime": "{a} vuodelta {b}",
      "script": "Latin"
    },
    "fr": {
      "suggest": "( (est|était|sera|seront|sont) (l('|’)|la |les? |une? )(ancien )?)(?<desc>.*?)((,? connu|,? née?|,? qui|, spécialiste).*)?$",
      "descAnd": "{a} et {b}",
      "descBy": "{a} de {b}",
      "descFromPlace": "{a} de {b}",
      "descFromTime": "{a} sorti en {b}",
      "descIn": "{a} en {b}",
      "descOf": "{a} de {b}",
      "script": "Latin"
    },
    "gan": {
      "separator": "^(?<sentence>[^。]*)。",
      "suggest": "(係|系|是)(?<desc>.*)$",
      "descAnd": "{a}和{b}",
      "descIn": "{a}在{b}",
      "script": "Chinese"
    },
    "gl": {
      "suggest": "( (é|e|es|foi|son) (o|unh?a?)) (?<desc>.*)$",
      "descAnd": "{a} e {b}",
      "descBy": "{a} de {b}",
      "descFromPlace": "{a} de {b}",
      "descFromTime": "{a} de {b}",
      "descIn": "{a} en {b}",
      "script": "Latin"
    },
    "gsw": {
      "suggest": "( (isch) (de|dr|e|s)) (?<desc>.*)$",
      "descAnd": "{a} un {b}",
      "descBy": "{a} vo {b}",
      "descFromPlace": "{a} us {b}",
      "descFromTime": "{a} vo {b}",
      "descIn": "{a} in {b}",
      "script": "Latin"
    },
    "hr": {
      "suggest": "(( bio)? (je)( bio)?) (?<desc>.*)$",
      "descAnd": "{a} i {b}",
      "descFromPlace": "{a} iz {b}",
      "descFromTime": "{a} iz {b}",
      "descIn": "{a} u {b}",
      "script": "Latin"
    },
    "hu": {
      "suggest": "( (egy)) (?<desc>.*)$",
      "descAnd": "{a} és {b}",
      "script": "Latin"
    },
    "id": {
      "suggest": "( (adalah|merupakan)( sebuah| seorang)?) (?<desc>.*)$",
      "descAnd": "{a} dan {b}",
      "descFromPlace": "{a} dari {b}",
      "descFromTime": "{a} dari tahun {b}",
      "descIn": "{a} di {b}",
      "script": "Latin"
    },
    "it": {
      "suggest": "( (è|sono) (il |l('|’)|una? ))(?<desc>.*)$",
      "descAnd": "{a} e {b}",
      "descBy": "{a} da {b}",
      "descFromPlace": "{a} di {b}",
      "descFromTime": "{a} del {b}",
      "descIn": "{a} in {b}",
      "script": "Latin"
    },
    "ja": {
      "separator": "^(?<sentence>[^。]*)。",
      "suggest": "(は)(?<desc>.*)$",
      "descAnd": "{a}と{b}",
      "descBy": "{a}の{b}",
      "descIn": "{a}の{b}",
      "script": "Japanese"
    },
    "kk": {
      "suggest": "( (-|–|—)) (?<desc>.*)$",
      "script": "Cyrillic"
    },
    "ko": {
      "suggest": "(이다? )(?<desc>.*)$",
      "script": "Korean"
    },
    "la": {
      "suggest": "( est) (?<desc>.*)$",
      "descAnd": "{a} et {b}",
      "descFromPlace": "{a} e {b}",
      "descFromTime": "{a} a {b}",
      "script": "Latin"
    },
    "lb": {
      "suggest": "( (ass|sinn) (den|e|eng)) (?<desc>.*)$",
      "script": "Latin"
    },
    "lt": {
      "suggest": "( (-|–|—|yra)) (?<desc>.*)$",
      "descAnd": "{a} ir {b}",
      "script": "Latin"
    },
    "lzh": {
      "sisters": [ "zh-classical" ],
      "separator": "^(?<sentence>[^。]*)。",
      "suggest": "(者|,){1,2}為?(?<desc>.*)也?$",
      "descAnd": "{a}及{b}",
      "descIn": "{a}在{b}",
      "script": "Chinese"
    },
    "ms": {
      "suggest": "( (adalah|ialah|merupakan)( sebuah)?) (?<desc>.*)$",
      "descAnd": "{a} dan {b}",
      "descFromPlace": "{a} dari {b}",
      "descFromTime": "{a} dari {b}",
      "descIn": "{a} di {b}",
      "script": "Latin"
    },
    "nb": {
      "suggest": "( (er|var) (en|et)?) (?<desc>.*)$",
      "descAnd": "{a} og {b}",
      "descBy": "{a} av {b}",
      "descFromPlace": "{a} fra {b}",
      "descFromTime": "{a} fra {b}",
      "descIn": "{a} i {b}",
      "script": "Latin"
    },
    "nl": {
      "suggest": "( (is|was|zijn) (de|een|één)) (?<desc>.*)$",
      "descAnd": "{a} en {b}",
      "descBy": "{a} van {b}",
      "descFromPlace": "{a} van {b}",
      "descFromTime": "{a} uit {b}",
      "descIn": "{a} in {b}",
      "script": "Latin"
    },
    "nn": {
      "suggest": "( (er|var) (den|ei(n|t)?)) (?<desc>.*)$",
      "descAnd": "{a} og {b}",
      "descBy": "{a} av {b}",
      "descFromPlace": "{a} fra {b}",
      "descFromTime": "{a} frå {b}",
      "descIn": "{a} i {b}",
      "script": "Latin"
    },
    "oc": {
      "suggest": "( (es|foguèt|son)( una?)?) (?<desc>.*)$",
      "descAnd": "{a} e {b}",
      "script": "Latin"
    },
    "pl": {
      "suggest": "( (-|–|—|byly?|jest)) (?<desc>.*)$",
      "descAnd": "{a} a {b}",
      "descBy": "{a} z {b}",
      "descFromPlace": "{a} z {b}",
      "descFromTime": "{a} z {b}",
      "descIn": "{a} w {b}",
      "script": "Latin"
    },
    "pt": {
      "sisters": [ "pt-br" ],
      "suggest": "( (é|foi) (a|o|uma?)) (?<desc>.*)$",
      "descAnd": "{a} e {b}",
      "descBy": "{a} do {b}",
      "descFromPlace": "{a} de {b}",
      "descFromTime": "{a} de {b}",
      "descIn": "{a} na {b}",
      "script": "Latin"
    },
    "ro": {
      "suggest": "( (a fost|este)( cel| o| una?)?) (?<desc>.*)$",
      "descAnd": "{a} și {b}",
      "descBy": "{a} de {b}",
      "descFromPlace": "{a} din {b}",
      "descFromTime": "{a} din {b}",
      "descIn": "{a} în {b}",
      "script": "Latin"
    },
    "ru": {
      "suggest": "( (-|–|—)) (?<desc>.*)$",
      "descAnd": "{a} и {b}",
      "descFromPlace": "{a} из {b}",
      "descIn": "{a} в {b}",
      "script": "Cyrillic"
    },
    "sh": {
      "suggest": "( (je)( (bio|na))?) (?<desc>.*)$",
      "descFromTime": "{a} iz {b}",
      "script": "Latin"
    },
    "sk": {
      "suggest": "( (bol(a|i|o)?|je|sú))( významn(á|é|ý))? (?<desc>.*?)(, ktor.*)?$",
      "descAnd": "{a} a {b}",
      "descFromPlace": "{a} z {b}",
      "descFromTime": "{a} z {b}",
      "descIn": "{a} v {b}",
      "script": "Latin"
    },
    "sl": {
      "suggest": "( je) (?<desc>.*)$",
      "descAnd": "{a} in {b}",
      "descFromPlace": "{a} iz {b}",
      "descFromTime": "{a} iz {b}",
      "descIn": "{a} v {b}",
      "script": "Latin"
    },
    "sr": {
      "suggest": "( (је)) (?<desc>.*)$",
      "descAnd": "{a} и {b}",
      "descFromPlace": "{a} из {b}",
      "descFromTime": "{a} из {b}",
      "descIn": "{a} у {b}",
      "script": "Cyrillic"
    },
    "sv": {
      "suggest": "( (är|var) (en|ett)) (?<desc>.*)$",
      "descAnd": "{a} och {b}",
      "descBy": "{a} av {b}",
      "descFromPlace": "{a} från {b}",
      "descFromTime": "{a} från {b}",
      "descIn": "{a} i {b}",
      "script": "Latin"
    },
    "uk": {
      "suggest": "((-|–|—)) (?<desc>.*)$",
      "descAnd": "{a} та {b}",
      "descFromPlace": "{a} із {b}",
      "descIn": "{a} в {b}",
      "script": "Cyrillic"
    },
    "vi": {
      "suggest": "( (là)( tên)? (m?t)) (?<desc>.*)$",
      "descAnd": "{a} và {b}",
      "descBy": "{a} của {b}",
      "descIn": "{a} ở {b}",
      "script": "Latin-QuocNgu"
    },
    "wuu": {
      "separator": "^(?<sentence>[^。]*)。",
      "suggest": "(是)(?<desc>.*)$",
      "descAnd": "{a}搭{b}",
      "descIn": "{a}勒勒{b}",
      "script": "Chinese"
    },
    "yue": {
      "sisters": [ "zh-yue" ],
      "separator": "^(?<sentence>[^。]*)。",
      "suggest": "(喺|係|系|是)(?<desc>.*)$",
      "descAnd": "{a}同{b}",
      "descIn": "{a}在{b}",
      "script": "Chinese"
    },
    "zh": {
      "separator": "^(?<sentence>[^。]*)。",
      "suggest": "(是|为|為)(?<desc>.*)$",
      "descAnd": "{a}和{b}",
      "descIn": "{a}在{b}",
      "script": "Chinese"
    }
  };

  // Language remappings (per https://noc.wikimedia.org/conf/InitialiseSettings.php.txt)
  var realLanguage = {
    "als": "gsw",
    "bat_smg": "sgs",
    "be_x_old": "be-tarask",
    "bh": "bho",
    "crh": "crh-latn",
    "fiu_vro": "vro",
    "no": "nb",
    "roa_rup": "rup",
    "simple": "en",
    "zh_classical": "lzh",
    "zh_min_nan": "nan",
    "zh_yue": "yue"
  };


  // i18n Labels
  // Skip a translation to fall back to English version
  var i18n = {
    "intError": {
      "de": "Fehler: {txt}",
      "en": "Error: {txt}"
    },
    "intFailedToLoad": {
      "de": "Fehler beim Laden: ",
      "en": "Failed to load: "
    },
    "intLabelMissing": {
      "de": "Fehler: Text fehlt",
      "en": "Error: Text missing"
    },
    "intTitle": {
      "en": "Label Collector"
    },
    "intTitleShort": {
      "en": "LC"
    },
    "btnActions": {
      "de": "Aktionen",
      "en": "Actions"
    },
    "btnAdd": {
      "de": "Sprache hinzufügen",
      "en": "Add language"
    },
    "btnClearAll": {
      "de": "Alle Bezeichner, Beschreibungen und Aliasse leeren/löschen",
      "en": "Clear (empty) all labels, descriptions, aliases"
    },
    "btnClose": {
      "de": "Schließen",
      "en": "Close"
    },
    "btnExpandCollapseAll": {
      "de": "Alle auf-/zuklappen",
      "en": "Expand/collapse all"
    },
    "btnJumpTo": {
      "de": "Los",
      "en": "Go"
    },
    "btnOkay": {
      "en": "Okay"
    },
    "btnOptions": {
      "de": "Optionen",
      "en": "Options"
    },
    "btnResetAll": {
      "de": "Alle Bezeichner, Beschreibungen und Aliasse zurücksetzen",
      "en": "Reset all labels, descriptions, aliases to existing values"
    },
    "btnSaveClose": {
      "de": "Speichern & Schließen",
      "en": "Save & Close"
    },
    "btnSaveNext": {
      "de": "Speichern & Nächstes",
      "en": "Save & Next"
    },
    "btnSkip": {
      "de": "Überspringen",
      "en": "Skip"
    },
    "btnSuggest": {
      "en": "?!"
    },
    "btnReset": {
      "en": "x"
    },
    "fldJumpTo": {
      "de": "Q..., User:..., Special:Random oder Special:Search/...",
      "en": "Q..., User:..., Special:Random or Special:Search/..."
    },
    "lnkContributions": {
      "de": "Beiträge",
      "en": "Contribs"
    },
    "lnkFeedback": {
      "en": "Feedback"
    },
    "lnkHelp": {
      "de": "Hilfe",
      "en": "Help"
    },
    "lnkHistory": {
      "de": "Versionen",
      "en": "history"
    },
    "lnkTalkPage": {
      "de": "Diskussion",
      "en": "Talk"
    },
    "lnkWatchlist": {
      "de": "Beobachtung",
      "en": "Watchlist"
    },
    "ttExistingAliases": {
      "de": "Bestehende Aliasse: {txt}",
      "en": "Existing aliases: {txt}"
    },
    "ttExistingDescription": {
      "de": "Bestehende Beschreibung: {txt}",
      "en": "Existing description: {txt}"
    },
    "ttExistingLabel": {
      "de": "Bestehender Bezeichner: {txt}",
      "en": "Existing label: {txt}"
    },
    "ttReset": {
      "de": "Zurücksetzen zum bestehenden Wert",
      "en": "Reset to existing value"
    },
    "ttSuggest": {
      "de": "Setze den Vorschlag ein: {txt}",
      "en": "Insert suggestion: {txt}"
    },
    "ttSuggestNone": {
      "de": "Kein Vorschlag",
      "en": "No suggestion"
    },
    "ttWontSave": {
      "de": "Achtung: Evt. vorhandenene Änderungen werden nicht gespeichert",
      "en": "Attention: Any changes made here won't be saved"
    },
    "txtAlias": {
      "de": "Alias",
      "en": "alias"
    },
    "txtAliases": {
      "de": "Aliasse",
      "en": "aliases"
    },
    "txtAlwaysExpandEditorLangs": {
      "de": "Editor-Sprachen immer ausklappen",
      "en": "Always expand editor languages"
    },
    "txtAlwaysExpandOtherLangs": {
      "de": "Nicht-Editor-Sprachen immer ausklappen",
      "en": "Always expand non-editor languages"
    },
    "txtApiError": {
      "de": "API-Fehler",
      "en": "API error"
    },
    "txtAutoFill": {
      "de": "Vorschläge für Bezeichner, Beschreibungen und Aliasse automatisch eintragen",
      "en": "Automatically fill proposals for labels, descriptions and aliasses"
    },
    "txtCleanDescription": {
      "de": "Beschreibung automatisch bereinigen (Wiki-Markup, doppelte Leerzeichen, etc.)",
      "en": "Automatically clean description (wiki markup, double spaces, etc.)"
    },
    "txtDebugMode": {
      "de": "Debug-Modus",
      "en": "Debug mode"
    },
    "txtFailedToLoadNext": {
      "de": "Nächstes Item konnte nicht geladen werden.",
      "en": "Failed to load next item."
    },
    "txtFailedToLoadSearchResults": {
      "de": "Suche nach '{term}' konnte nicht ausgeführt werden oder ergab keine Ergebnisse.",
      "en": "Search for '{term}' could not be performed or did not deliver any results."
    },
    "txtFailedToLoadUsersItems": {
      "de": "Bearbeitete Items des Benutzers {term} konnten nicht geladen werden.",
      "en": "Failed to load items edited by user {term}."
    },
    "txtFailedToItem": {
      "de": "Item {txt} konnte nicht geladen werden.",
      "en": "Failed to load item {txt}."
    },
    "txtFailedToParseIntro": {
      "de": "Fehler beim Verarbeiten des Artikeltexts von {db}: {txt}",
      "en": "Failed to parse {db} article intro: {txt}"
    },
    "txtFailedToReadInput": {
      "de": "Eingabe ist keine Item-ID und kein Benutzername: {txt}",
      "en": "Input is no item ID or user name: {txt}"
    },
    "txtInvalidProject": {
      "de": "Ungültige Projektdefinition: {txt}",
      "en": "Invalid project definition: {txt}"
    },
    "txtInvalidLanguage": {
      "de": "{code} ist keine gültige Sprache. Die Änderung wird ignoriert.",
      "en": "{code} is no valid language. The change will be ignored."
    },
    "txtInvalidLanguageToAdd": {
      "de": "{code} ist keine gültige Sprache, oder bereits vorhanden.",
      "en": "{code} is no valid language, or already present."
    },
    "txtInvalidRegex": {
      "de": "Ungültiger regulärer Ausdruck für {db}: {txt}",
      "en": "Invalid regular expression for {db}: {txt}"
    },
    "txtLanguage": {
      "de": "Sprachcode:",
      "en": "Language code:"
    },
    "txtLoading": {
      "de": "lädt...",
      "en": "loading..."
    },
    "txtNoSitelink": {
      "de": "(Keine Sitelinks)",
      "en": "(No sitelinks)"
    },
    "txtNothing": {
      "en": "—"
    },
    "txtNothingToSave": {
      "de": "Keine Änderung zu speichern",
      "en": "Nothing to save"
    },
    "txtRedirect": {
      "de": "(Weiterleitung)",
      "en": "(Redirect)"
    },
    "txtSavingFailed": {
      "de": "Speichern fehlgeschlagen: {txt}",
      "en": "Saving failed: {txt}"
    },
    "txtSkipIfEditorLangsSetOrNoLinks": {
      "de": "Items überspringen, wenn für alle Editor-Sprachen bereits Bezeichner und Beschreibungen gesetzt sind oder keine Links vorhanden sind",
      "en": "Skip items if all editor languages labels and descriptions are set already or there are no sitelinks for them"
    },
    "txtSkipIfEditorLangsSet": {
      "de": "Items überspringen, wenn für alle Editor-Sprachen bereits Bezeichner und Beschreibungen gesetzt sind",
      "en": "Skip items if all editor languages labels and descriptions are set already"
    },
    "txtEditorLanguages": {
      "de": "Editor-Sprachen: ",
      "en": "Editor languages: "
    },
    "statusInitItem": {
      "de": "Initialisiere Item...",
      "en": "Initializing item..."
    },
    "statusInitProjects": {
      "de": "Initialisiere Sprachen und Projekte...",
      "en": "Initializing languages and projects..."
    },
    "statusLoadBabel": {
      "de": "Lade Babel-Sprachen von der Benutzerseite [[{userpage}]]...",
      "en": "Loading Babel languages from userpage [[{userpage}]]..."
    },
    "statusLoadEntity": {
      "de": "Lade Item {qid}...",
      "en": "Loading item {qid}..."
    },
    "statusLoadItemLabels": {
      "de": "Lade Labels von verwendeten Items: {qids}",
      "en": "Loading external item labels: {qids}"
    },
    "statusLoadNextItem": {
      "de": "Suche nächstes Item zum Bearbeiten, ab Q{qid}...",
      "en": "Searching for next item to edit, starting from Q{qid}..."
    },
    "statusLoadSearchResults": {
      "de": "Lade Items mit dem Suchstring '{term}'...",
      "en": "Load items containing search string '{term}'..."
    },
    "statusLoadUserEdits": {
      "de": "Lade bearbeitete Items des Benutzers {term}...",
      "en": "Load items edited by user {term}..."
    },
    "statusLoadXRegExp": {
      "de": "Lade XRegExp-Bibliothek: ",
      "en": "Loading XRegExp library: "
    }
  };

  // Used properties
  var properties = {
    "administrativeType": 132,
    "administrativeUnit": 131,
    "author": 50,
    "astronomicalObject": 60,
    "citizenship": 27,
    "composer": 86,
    "continent": 30,
    "country": 17,
    "creator": 170,
    "dateOfPublication": 577,
    "designer": 287,
    "developer": 178,
    "director": 57,
    "editor": 98,
    "electionType": 173,
    "instance": 31,
    "lakeType": 202,
    "locatedTerrain": 706,
    "locatedWater": 206,
    "manufacturer": 176,
    "occupation": 106,
    "performer": 175,
    "producer": 162,
    "publisher": 123,
    "series": 179,
    "structureType": 168,
    "subclass": 279,
    "taxonRank": 105,
    "texter": 676
  };

  // Used items
  var items = {
    "disambiguation": 4167410,
    "episode": 1983062,
    "human": 5
  };

  // Supported projects
  var projectCodes = {
    "wikibooks": "b",
    "wikinews": "n",
    "wikipedia": "w",
    "wikiquote": "q",
    "wikisource": "s",
    "wikivoyage": "voy"
  };

  // Colors
  var colors = {
    "background": "#B7C5E2",
    "body": "#FFFFFF",
    "border": "#8A9AB9",
    "enabled": "#EEFFEE",
    "existing": "#EEEEEE",
    "headerCollapsed": "#C8C8D3",
    "headerCollapsedBabel": "#C8C8D8",
    "headerExpanded": "#8A9AB9",
    "link": "#0645AD",
    "linkVisited": "#0B0080",
    "modified": "#EEFFEE",
    "same": "#EEEEEE",
    "talkAlert": "#FF0000",
    "triangle": "#888888"
  };

  // CSS Classes
  var classes = {
    "alias": "lcAlias",
    "babel": "lcBabel",
    "buttons": "lcButtons",
    "center": "lcCentered",
    "collapsed": "lcCollapsed",
    "desc": "lcDesc",
    "ellipsis": "lcEllipsis",
    "entry": "lcEntry",
    "existing": "lcExisting",
    "header": "lcHeader",
    "headerLabel": "lcHeaderLabel",
    "label": "lcLabel",
    "main": "lcMain",
    "mainButton": "lcMainButton",
    "modified": "lcModified",
    "mwSpinner": "mw-spinner",
    "newValue": "lcNew",
    "nonBabel": "lcNonBabel",
    "preview": "lcPreview",
    "secondButton": "lcSecondButton",
    "talkAlert": "lcTalkpageAlert",
    "top": "lcTop",
    "triangle": "lcTriangle",
    "triangleRight": "lcTriangleRight"
  };

  // CSS IDs
  var ids = {
    "aliasesExisting": "lcAliasesExisting_",
    "aliasesNew": "lcAliasesNew_",
    "descExisting": "lcDescExisting_",
    "descNew": "lcDescNew_",
    "header": "lcHeader",
    "labelExisting": "lcLabelExisting_",
    "labelNew": "lcLabelNew_",
    "mwBodyContent": "bodyContent",
    "pageTitle": "lcPageTitle",
    "preview": "lcPreview_",
    "previewArea": "lcPreviewArea_",
    "suggestAlias": "lcSuggestAlias_",
    "suggestDesc": "lcSuggestDesc_",
    "suggestLabel": "lcSuggestLabel_",
    "talkPage": "lcTalkPage",
    "userNav": "lcUserNav"
  };

  // Interal configuration
  var lcConfig = {
    "aliasesSummaryTmp": "alias{plural} ({txt})",
    "alwaysExpandEditorLangs": false,
    "alwaysExpandOtherLangs": false,
    "autoFill": true,
    "checkNextItems": 50,
    "checkUserContribs": 500,
    "checkUserTalk": 10,
    "cleanDescription": true,
    "debugMode": false,
    "descSummaryTmp": "description{plural} ({txt})",
    "labelSummaryTmp": "label{plural} ({txt})",
    "pageHistory": "//www.wikidata.org/w/index.php?action=history&title=",
    "pageTool": "User:YMS/LC",
    "pageToolTalk": "User talk:YMS/LC",
    "pageUserContrib": "Special:Contributions/" + mw.config.get("wgUserName"),
    "pageUserTalk": "User talk:" + mw.config.get("wgUserName"),
    "pageUserWatch": "Special:Watchlist",
    "skipIfEditorLangsSetOrNoLinks": true,
    "skipIfEditorLangsSet": false,
    "summaryTmp": "; [[User:YMS/LC|LC.js]]",
    "timeout": 15000
  };

  // Internal global variables
  var lcGlobal = {
    "cachedItemLabels": {},
    "claimValues": undefined,
    "entity": "",
    "globalLabel": {},
    "hasMessages": false,
    "instances": [],
    "itemList": [],
    "languages": {},
    "saveCounter": 0,
    "searchTerm": undefined,
    "srOffset": undefined,
    "usersContribs": undefined,
    "itemPointer": 0,
    "userLanguages": [],
    "userUcContinue": undefined
  };

  // Startup
  $(document).ready(function() {
    debug("Start");
    initTool();
  });



  // Shortcut method to avoid concatenation with "." all the time
  function $getByClass(className) {
    return $("." + className);
  }

  // Shortcut method to avoid concatenation with "#" all the time
  function $getById(idName) {
    return $("#" + idName);
  }

  // Adds an entry for a language not yet displayed
  function addEntry(language, forceShow) {
    // Get language data
    var lang = language.lang;
    var ignoreSitelinks = language.ignoreSitelinks;
    var projects = language.projects;
    var script = language.script;

    // Detect sitelinks, label, description and aliases
    var $sitelinksList = $("<span />");
    var aliases = "";
    var label = (lcGlobal.entity.labels === undefined || lcGlobal.entity.labels[lang] === undefined) ? "" : lcGlobal.entity.labels[lang].value;
    var newLabel = "";
    var sitelinks = [];

    var $sitelinkEntry;
    var code, db, value;

    debug("addEntry", lang);

    // Get sitelinks and labels
    if (projects !== undefined && projects.length > 0) {
      $.each(projects, function() {
        db = this.db;
        code = this.code;

        if (lcGlobal.entity.sitelinks !== undefined && lcGlobal.entity.sitelinks[db] !== undefined) {
          $sitelinkEntry = $("<b />").append($("<a />", {
            "html": lcGlobal.entity.sitelinks[db].title,
            "href": wikibase.sites.getSite(lcGlobal.entity.sitelinks[db].site).getUrlTo(lcGlobal.entity.sitelinks[db].title)
          }));

          if (sitelinks.length > 0) {
            $sitelinksList.append(" / " + code + ":").append($sitelinkEntry);
          } else {
            $sitelinksList.append(code + ":").append($sitelinkEntry);
          }

          sitelinks.push(lcGlobal.entity.sitelinks[db]);

          // Get label suggestion from title, if not set in Wikidata yet
          if (newLabel === "") {
            newLabel = parseLabel(lcGlobal.entity.sitelinks[db].title);
          }
        }
      });
    }

    // Get aliases
    if (lcGlobal.entity.aliases !== undefined && lcGlobal.entity.aliases[lang] !== undefined) {
      $.each(lcGlobal.entity.aliases[lang], function() {
        value = this.value;
        aliases += ((aliases === "") ? "" : "|") + value;
      });
    }

    // If all existing labels for one script system are the same, possibly set all other labels also
    if (label !== "" && (newLabel === "" || newLabel === label)) {
      if (lcGlobal.globalLabel[script] !== undefined && label !== lcGlobal.globalLabel[script]) {
        lcGlobal.globalLabel[script] = "";
      } else {
        lcGlobal.globalLabel[script] = label;
      }
    }

    // Suggest globalLabel for newly added entries
    if (label === "" && newLabel === "" && checkGlobalLabelValidity(script)) {
      newLabel = lcGlobal.globalLabel[lcGlobal.languages[lang].script];
    }

    showEntry(language, aliases, forceShow, newLabel, sitelinks, $sitelinksList);
  }

  // Adds an entry for a language not yet displayed
  function addLanguage() {
    var lang = window.prompt(getText(i18n.txtLanguage)).toLowerCase();

    debug("addLanguage", lang);

    if (lcGlobal.languages[lang] === undefined || $getById(ids.labelExisting + lang).length > 0) {
      showMessage(getText(i18n.txtInvalidLanguageToAdd, { "code": lang }));
    } else {
      addEntry(lcGlobal.languages[lang], true);
      $("body").animate({ "scrollTop": $getByClass(classes.main)[0].scrollHeight }, 300);
    }
  }

  // Apply CSS definitions
  function applyCSS() {
    mw.util.addCSS(getText("body { padding: 2px 20px; background: {bg}; font-size: 100%; overflow-y: scroll; }",
                    { "bg": colors.body }));
    mw.util.addCSS(getText("a { color: {col}; }",
                    { "col" : colors.link }));
    mw.util.addCSS(getText("a:visited { color: {col}; }",
                    { "col": colors.linkVisited }));
    mw.util.addCSS(getText(".{class}#{id} { width: 100%; max-width: 100%; }",
                    { "class": classes.main, "id": ids.mwBodyContent }));
    mw.util.addCSS(getText(".{class} table { width: 100%; }",
                    { "class": classes.main }));
    mw.util.addCSS(getText(".{class} { background: {bg}; border: 1px solid {border}; margin: 0px; padding: 0px; }",
                    { "class": classes.entry, "bg": colors.background, "border": colors.border }));
    mw.util.addCSS(getText(".{class} > div { padding: 5px; }",
                    { "class": classes.entry }));
    mw.util.addCSS(getText(".{class} + .{childClass} { margin-top: 8px; }",
                    { "class": classes.babel, "childClass": classes.nonBabel }));
    mw.util.addCSS(getText(".{class} + .{childClass} { margin-top: 8px; }",
                    { "class": classes.nonBabel, "childClass": classes.babel }));
    mw.util.addCSS(getText(".{class} .{subClass} { background: {bg}; }",
                    { "class": classes.babel, "subClass": classes.header, "bg": colors.headerCollapsedBabel }));
    mw.util.addCSS(getText(".{class} .{subClass} { background: {bg}; }",
                    { "class": classes.nonBabel, "subClass": classes.header, "bg": colors.headerCollapsed }));
    mw.util.addCSS(getText(".{class}:not(.{subClass}) { background: {bg}; }",
                    { "class": classes.header, "subClass": classes.collapsed, "bg": colors.headerExpanded }));
    mw.util.addCSS(getText(".{class} table tr td:first-child { width: 15px; }",
                    { "class": classes.header }));
    mw.util.addCSS(getText(".{class} table tr td:nth-child(2) { white-space: nowrap; padding-right: 10px; }",
                    { "class": classes.header }));
    mw.util.addCSS(getText(".{class} { width: 100%; }",
                    { "class": classes.headerLabel }));
    mw.util.addCSS(getText(".{class} { width: 0px; height: 0px; border-top: none; border-right: none; border-bottom: 10px solid {border}; border-left: 10px solid transparent; }",
                    { "class": classes.triangle, "border": colors.triangle }));
    mw.util.addCSS(getText(".{class} { border-top: 6px solid transparent; border-right:none; border-bottom: 6px solid transparent; border-left: 10px solid {border}; }",
                    { "class": classes.triangleRight, "border": colors.triangle }));
    mw.util.addCSS(getText(".{class} table { border-spacing: 0px; }",
                    { "class": classes.main }));
    mw.util.addCSS(getText(".{class} { width: 40%; }",
                    { "class": classes.existing }));
    mw.util.addCSS(getText(".{class} { width: 60%; }",
                    { "class": classes.newValue }));
    mw.util.addCSS(getText(".{class} input[type=text] { background: {bg}; }",
                    { "class": classes.existing, "bg": colors.existing }));
    mw.util.addCSS(getText(".{class} input[type=text] { background: {bg}; }",
                    { "class": classes.newValue, "bg": colors.same }));
    mw.util.addCSS(getText(".{class} input[type=text].{subClass} { background: {bg}; }",
                    { "class": classes.newValue, "subClass": classes.modified, "bg": colors.modified }));
    mw.util.addCSS(getText(".{class} input[type=text] { width: 100%; font-weight: bold; }",
                    { "class": classes.label }));
    mw.util.addCSS(getText(".{class} input[type=text] { width: 100%; font-style: italic; }",
                    { "class": classes.alias }));
    mw.util.addCSS(getText(".{class} input[type=text] { width: 100%; }",
                    { "class": classes.desc }));
    mw.util.addCSS(getText(".{class} input[type=button] { margin: 0px; }",
                    { "class": classes.main }));
    mw.util.addCSS(getText(".{class} input[type=button]:enabled { background: {bg}; }",
                    { "class": classes.main, "bg": colors.enabled }));
    mw.util.addCSS(getText(".{class} table { table-layout: fixed }",
                    { "class": classes.preview }));
    mw.util.addCSS(getText(".{class} table tr td:first-child { width: 15px; }",
                    { "class": classes.preview }));
    mw.util.addCSS(getText(".{class} table tr td:nth-child(2) { width: 30px; }",
                    { "class": classes.preview }));
    mw.util.addCSS(getText(".{class} table tr td:last-child { width: 100%; }",
                    { "class": classes.preview }));
    mw.util.addCSS(getText(".{class} { white-space: nowrap; text-overflow: ellipsis; overflow: hidden; width: 100%; }",
                    { "class": classes.ellipsis }));
    mw.util.addCSS(getText(".{class} p { line-height: 1em; }",
                    { "class": classes.ellipsis }));
    mw.util.addCSS(getText(".{class}:not(.{notClass}) .{subClass} { display: none; }",
                    { "class": classes.header, "notClass": classes.collapsed, "subClass": classes.headerLabel }));
    mw.util.addCSS(getText(".{class} table { width: 100%; margin-top: 10px; margin-bottom: 10px; }",
                    { "class": classes.buttons }));
    mw.util.addCSS(getText(".{class} input { padding: 10px; }",
                    { "class": classes.buttons }));
    mw.util.addCSS(getText(".{class} tr td:first-child { text-align: left; }",
                    { "class": classes.buttons }));
    mw.util.addCSS(getText(".{class} tr td:last-child { text-align: right; }",
                    { "class": classes.buttons }));
    mw.util.addCSS(getText(".{class}, .{secondclass} { min-width: 150px; }",
                    { "class": classes.mainButton, "secondclass": classes.secondButton }));
    mw.util.addCSS(getText(".{class} { font-weight: bold; }",
                    { "class": classes.mainButton }));
    mw.util.addCSS(getText("#{id} { text-align: right; list-style-type: none; list-style-image: none; margin: 0px; padding: 0px; font-size: 80%; }",
                    { "id": ids.userNav }));
    mw.util.addCSS(getText("#{id} li { display: inline; padding: 0px 5px; }",
                    { "id": ids.userNav }));
    mw.util.addCSS(getText(".{class}#{id} { color: {col}; font-weight: bold; }",
                    { "class": classes.talkAlert, "col": colors.talkAlert, "id": ids.talkPage }));
    mw.util.addCSS(getText("#{id} { text-align: left; font-size: 180%; width: 90%; }",
                    { "id": ids.pageTitle }));
    mw.util.addCSS(getText("#{id} sup { font-size: 50%; padding-left: 10px; }",
                    { "id": ids.pageTitle }));
    mw.util.addCSS(getText(".{class} { display: table; width: 100%; }",
                    { "class": classes.top }));
    mw.util.addCSS(getText(".{class} span, .{class} form, .{class} ul { display: table-cell; vertical-align: top; text-align: center; white-space: nowrap; }",
                    { "class": classes.top }));
  }

  // Action for the "Save & Close" button
  function buttonActionSaveClose() {
    enableButtons(false);
    saveItemReload();
  }

  // Action for the "Save & Next" button
  function buttonActionSaveNext() {
    enableButtons(false);
    saveItemNext();
  }

  // Action for the "Skip" button
  function buttonActionSkip() {
    enableButtons(false);
    loadNextItem();
  }

  // Action for the "Talk Page" link
  function buttonActionTalkPage() {
    lcGlobal.hasMessages = false;
    $getById(ids.talkPage).toggleClass(classes.talkAlert, lcGlobal.hasMessages);
  }

  // Returns whether this item and this script system allows the application of the globalLabel suggestion
  function checkGlobalLabelValidity(script) {
    var hasGlobalLabel = (script !== undefined && lcGlobal.globalLabel[script] !== undefined && lcGlobal.globalLabel[script] !== "");
    var isDisambig, isPerson;

    if (lcGlobal.instances === undefined) {
      debug("checkGlobalLabelValidity false", script);
      return false;
    }

    isDisambig = inArray(items.disambiguation, lcGlobal.instances);
    isPerson = inArray(items.human, lcGlobal.instances);

    debug("checkGlobalLabelValidity", (hasGlobalLabel && (isDisambig || isPerson)), hasGlobalLabel, isDisambig, isPerson);
    return (hasGlobalLabel && (isDisambig || isPerson));
  }

  // Polls the user's talk page for new messages. If there are, the according link is highlighted.
  function checkUserTalkPage() {
    debug("checkUserTalkPage");

    $.ajax({
      "type": "POST",
      "url": mw.util.wikiScript("api"),
      "data": {
        "action": "query",
        "format": "json",
        "meta": "userinfo",
        "uiprop": "hasmsg"
      },
      "success": function(data) {
        if (! data.hasOwnProperty("error") && data.query !== undefined && data.query.userinfo !== undefined) {
          debug("checkUserTalkPage success");
          lcGlobal.hasMessages = (data.query.userinfo.messages !== undefined);
          $getById(ids.talkPage).toggleClass(classes.talkAlert, lcGlobal.hasMessages);
        } else {
          debug("checkUserTalkPage error", getAjaxErrorMessage(data));
        }
      }
    });
  }

  // Clean a value (remove duplicated or leading/trailing spaces)
  function cleanValue($fld) {
    return XRegExp.replace($fld.val().trim(), XRegExp.cache("\\s{2,}", "g"), " ");
  }

  // Remove aliases from one language
  // Can't be done in the same API call as the other changes, and has to be done separately for any language
  function clearAliases(clearLanguages, loadNext) {
    var existingValues, lang, summary;

    if (clearLanguages !== undefined && clearLanguages.length > 0) {
      lang = clearLanguages.shift();
      existingValues = $getById(ids.aliasesExisting + lang).val();
      summary = existingValues.replace("|", ", ") + lcConfig.summaryTmp;

      debug("clearAliases", lang);

      // Send secondary request to server: Clear aliases
      $.ajax({
        "type": "POST",
        "url": mw.util.wikiScript("api"),
        "data": {
          "action": "wbsetaliases",
          "assert": "user",
          "format": "json",
          "id": lcGlobal.entity.id,
          "language": lang,
          "remove": existingValues,
          "summary": summary,
          "type": "item",
          "token": mw.user.tokens.get("csrfToken")
        },
        "success": function(data) {
          if (data.hasOwnProperty("error")) {
            showMessage(getText(i18n.txtSavingFailed, { "txt": getAjaxErrorMessage(data) }));
            debug("clearAliases error", getAjaxErrorMessage(data));
            enableButtons(true);
          } else {
            debug("clearAliases success");
          }

          // Clear aliases in next language
          clearAliases(clearLanguages, loadNext);
        },
        "error": function() {
          showMessage(getText(i18n.txtSavingFailed, { "txt": i18n.txtApiError }));
          debug("clearAliases error");
          enableButtons(true);
        }
      });
    } else {
      // No more aliases to clear, load next item
      if (loadNext) {
        loadNextItem();
      } else {
        loadItemPage(lcGlobal.entity.id);
      }
    }
  }

  // Action: Clear all
  function clearAll() {
    $.each(lcGlobal.languages, function() {
      var lang = this.lang;

      // Skip if collapsed
      if (! $getById(ids.labelNew + lang).is(":visible")) {
        return true; // continue each
      }

      debug("clear", lang);

      setFieldValue(ids.labelNew + lang, "", false);
      setFieldValue(ids.aliasesNew + lang, "", false);
      setFieldValue(ids.descNew + lang, "", false);
    });
  }

  // Create a button
  function $createButton(clickFunction, accessKey, label, tooltip, id, cls, disabled) {
    var $btn = $("<input />", {
      "accesskey": accessKey,
      "class": cls,
      "disabled": disabled,
      "id": id,
      "title": (tooltip === null) ? label : tooltip,
      "type": "button",
      "value": label
    });

    if (clickFunction !== null && clickFunction !== undefined) {
      $btn.click(function() {
        clickFunction();
      });
    }

    return $btn;
  }

  // Create a div
  function $createDiv(cls, id) {
    var $div = $("<div />", {
      "class": cls,
      "id": id
    });

    return $div;
  }

  // Create an input field with a pattern, placeholder, etc.
  function $createInputFieldPattern(maxlength, pattern, placeholder, tooltip, required) {
    var $fld = $("<input />", {
      "maxlength": maxlength,
      "pattern": pattern,
      "placeholder": placeholder,
      "title": tooltip,
      "type": "text",
      "required": required
    });

    return $fld;
  }

  // Create an input field with a value
  function $createInputFieldValue(disabled, id, lang, tooltip, value) {
    var $fld = $("<input />", {
      "disabled": disabled,
      "id": id,
      "lang": lang,
      "title": tooltip,
      "type": "text",
      "value": value
    });

    return $fld;
  }

  // Create an HTML link
  function $createLink(url, text, clickFunction, id) {
    var $lnk = $("<a />", {
      "href": mw.util.getUrl(url),
      "html": text,
      "id": id
    });

    if (clickFunction !== null && clickFunction !== undefined) {
      $lnk.click(function() {
        clickFunction();
      });
    }

    return $lnk;
  }

  // Create the basic HTML structure
  function createPage() {
    var $header = $("<span />", {
      "id": ids.pageTitle
    });

    var $btnSaveNext = $createButton(buttonActionSaveNext, "s", getText(i18n.btnSaveNext), null, null, classes.mainButton);
    var $btnSaveClose = $createButton(buttonActionSaveClose, "c", getText(i18n.btnSaveClose), null, null, classes.secondButton);
    var $btnSkip = $createButton(buttonActionSkip, "k", getText(i18n.btnSkip), getText(i18n.ttWontSave), null, classes.mainButton);
    var $btnActions = $createButton(showActions, "a", getText(i18n.btnActions));
    var $btnOptions = $createButton(showOptions, "o", getText(i18n.btnOptions));
    var $fldJumpTo = $createInputFieldPattern(255, "([qQ][1-9][0-9]*|User:.+|Special:Random|Special:Search/.+)", getText(i18n.fldJumpTo), getText(i18n.fldJumpTo), "required");
    var $btnJumpTo = $createSubmitButton("g", getText(i18n.btnJumpTo), getText(i18n.ttWontSave));

    var $frmJumpTo = $("<form />");
    var $headerDiv = $createDiv(classes.top);
    var $buttonsDiv = $createDiv(classes.buttons);
    var $buttonLine = $("<tr />");
    var $buttonLeft = $("<td />");
    var $buttonRight = $("<td />");
    var $userNav = $("<ul />", { "id": ids.userNav });

    $frmJumpTo.submit(function() {
      jumpTo($fldJumpTo.val());
      return false;
    });

    $frmJumpTo.append($fldJumpTo);
    $frmJumpTo.append($btnJumpTo);

    $userNav.append($("<li />").append($createLink(lcConfig.pageUserWatch, getText(i18n.lnkWatchlist))));
    $userNav.append($("<li />").append($createLink(lcConfig.pageUserTalk, getText(i18n.lnkTalkPage), buttonActionTalkPage, ids.talkPage)));
    $userNav.append($("<li />").append($createLink(lcConfig.pageUserContrib, getText(i18n.lnkContributions))));
    $userNav.append($("<li />").append($createLink(lcConfig.pageToolTalk, getText(i18n.lnkFeedback))));
    $userNav.append($("<li />").append($createLink(lcConfig.pageTool, getText(i18n.lnkHelp))));

    $headerDiv.append($header);
    $headerDiv.append($frmJumpTo);
    $headerDiv.append($userNav);

    $buttonRight.append([ $btnSaveNext, $btnSaveClose, $btnSkip ]);
    $buttonLeft.append([ $btnActions, $btnOptions ]);
    $buttonLine.append([ $buttonLeft, $buttonRight ]);
    $buttonsDiv.append($("<table />").append($buttonLine));

    $buttonsDiv.find("input").updateTooltipAccessKeys();
    $btnJumpTo.updateTooltipAccessKeys();

    $("body").empty();
    $("body").append($headerDiv);
    $("body").append($buttonsDiv);
    $("body").append($createDiv(null, ids.mwBodyContent));
    $("body").append($buttonsDiv.clone(true));

    // All main buttons should have the same width
    $("." + classes.mainButton + ", ." + classes.secondButton).width(Math.max($btnSaveNext.width(), $btnSaveClose.width(), $btnSkip.width()));
  }

  // Create a submit button
  function $createSubmitButton(accessKey, label, tooltip) {
    var $btn = $("<input />", {
      "accesskey": accessKey,
      "title": tooltip,
      "type": "submit",
      "value": label
    });

    return $btn;
  }

  // Print out a debug log message (if debug mode is enabled)
  function debug() {
    if (lcConfig.debugMode) {
      console.group();
      console.log(arguments);
      console.trace();
      console.groupEnd();
    }
  }

  // Enable/disable buttons
  // e.g. to prevent that the "Save" button is pressed multiple times before the next page is loaded, or to allow another save after the first one has failed
  function enableButtons(enable) {
    $getByClass(classes.buttons).find("input").attr("disabled", ! enable);
  }

  // From the items delivered by the API, select the next one which exists and should not be skipped
  function findNextValidItem(entities) {
    var loadThis = false;
    var entity, gotDescription, gotLabel, gotSitelink, language, project;

    // Load first existing item (remind that the API query returns results for missing items, too)
    $.each(entities, function () {
      entity = this;

      if (entity.missing === undefined) {
        // Item exists, so remember it
        lcGlobal.entity = entity;

        // Check whether we should skip the item
        $.each(lcGlobal.userLanguages, function() {
          language = this;
          gotLabel = (lcGlobal.entity.labels !== undefined && lcGlobal.entity.labels[language] !== undefined);
          gotDescription = (lcGlobal.entity.descriptions !== undefined && lcGlobal.entity.descriptions[language] !== undefined);
          gotSitelink = false;

          if (lcGlobal.languages[language].projects !== undefined) {
            $.each(lcGlobal.languages[language].projects, function() {
              project = this;
              gotSitelink = (lcGlobal.entity.sitelinks !== undefined && lcGlobal.entity.sitelinks[project.db] !== undefined);

              if (gotSitelink) {
                return false; // break each
              }
            });
          }

          if (lcConfig.skipIfEditorLangsSet && gotLabel && gotDescription) {
            // Skip
            debug("skipped item, editor langs set", entity);
          } else if (lcConfig.skipIfEditorLangsSetOrNoLinks && (! gotSitelink || (gotLabel && gotDescription))) {
            // Skip
            debug("skipped item, editor langs set or no sitelinks", entity);
          } else {
            loadThis = true;
            debug("load item", entity);
            return false; // break each
          }
        });

        if (loadThis) {
          return false; // break each
        }
      } else {
        debug("skipped missing item", entity);
      }
    });

    return loadThis;
  }

  // Generates a description from the Wikidata statements, if possible in the selected language (otherwise undefined)
  // In prepareMode, no description is generated, but a list of the needed items is returned
  function generateDescription(prepareMode, lang) {
    var itemIds = [];
    var language = (lcGlobal.languages[lang] === undefined) ? {} : lcGlobal.languages[lang];
    var composers, foundYear, date, dates, description, texters, year;

    var creators = getFirstPropertysValues([
      properties.performer,
      properties.developer,
      properties.director,
      properties.composer,
      properties.author,
      properties.manufacturer,
      properties.designer,
      properties.creator,
      properties.producer,
      properties.editor,
      properties.publisher
    ]);

    var locationsBig = getFirstPropertysValues([
      properties.country,
      properties.continent
    ]);

    var locationsSmall = getFirstPropertysValues([
      properties.administrativeUnit,
      properties.locatedTerrain,
      properties.locatedWater
    ]);

    if (getClaimValues(properties.occupation) !== undefined) {
      // Persons (occupation)
      debug("generateDescription person", lang);
      description = generateDescriptionInternal([[ getClaimValues(properties.occupation) ]], lang, itemIds);
    } else if (getClaimValues(properties.series) !== undefined && isInstance(items.episode)) {
      // Episode of a series
      debug("generateDescription episode", lang);
      description = generateDescriptionInternal([
        [ getClaimValues(properties.instance) ],
        [ getClaimValues(properties.series), language.descOf ]
      ], lang, itemIds);
    } else if (creators !== undefined) {
      // Creative works (creator of ONE kind, possibly year of publication), products
      // (Exception: musical works may have composer and texter if they don't have a performer)
      dates = getClaimValues(properties.dateOfPublication);
      texters = getClaimValues(properties.texter);
      composers = getClaimValues(properties.composer);

      // Special rule for musical works without a performer known:
      // Add lyrics texter only if available and different from composer
      if (creators !== composers || creators === texters) {
        texters = undefined;
      }

      if (dates !== undefined) {
        $.each(dates, function() {
          // Extract the year from the ISO 8601 date manually, since Date.parse() fails to do so on some browsers
          // (this implementation fails on B.C. dates - considered irrelevant)
          date = XRegExp.exec(this, XRegExp.cache("\\+(?<year>[1-9][0-9]*)-"));
          foundYear = (date !== null && date !== undefined && date.year !== undefined);

          // While there may be several dates given (e.g. publication in different countries),
          // only the earliest one is of interest here
          if (foundYear && (year === undefined || parseInt(date.year, 10) < parseInt(year, 10))) {
            year = date.year;
          }

          debug("found year", this, date, date.year, year, foundYear);
        });
      }

      debug("generateDescription work", lang);
      description = generateDescriptionInternal([
        [ lcGlobal.instances ],
        [ creators, language.descBy ],
        [ texters, language.descAnd ],
        [ year, language.descFromTime ]
      ], lang, itemIds);
    } else if (locationsSmall !== undefined || locationsBig !== undefined) {
      // Places (ONE location type, possibly additional country)
      debug("generateDescription location", lang);
      description = generateDescriptionInternal([
        [ lcGlobal.instances ],
        [ locationsSmall, language.descIn ],
        [ locationsBig, language.descIn ]
      ], lang, itemIds);
    }

    if (description === undefined && lcGlobal.instances !== undefined && (lcGlobal.instances.length !== 1 || lcGlobal.instances[0] !== items.human)) {
      // For everything else and for everything where the above methods failed for a missing label:
      // Try to just print the instance value(s)
      debug("generateDescription other", lang);
      description = generateDescriptionInternal([[ lcGlobal.instances ]], lang, itemIds);
    }

    return (prepareMode) ? itemIds : description;
  }

  // Generates a description from the given claim values, possibly using the given connectors
  // As soon as one part is unknown, undefined is returned
  function generateDescriptionInternal(values, lang, itemIds) {
    var connector, i, description, labels, len, value;

    for (i = 0, len = values.length; i < len; i++) {
      value = values[i][0];
      connector = values[i][1];

      if (value !== undefined) {
        if (typeof value === "string") {
          labels = value;
        } else {
          $.merge(itemIds, value);
          labels = getConcatenatedItemLabels(value, lang);
        }

        if (connector !== undefined && description !== undefined && labels !== undefined) {
          description = getText(connector, {
            "a": description,
            "b": labels
          });
        } else if (i === 0) {
          description = labels;
        } else {
          description = undefined;
        }
      }
    }

    return description;
  }

  // Extract the error message from an AJAX response
  function getAjaxErrorMessage(data) {
    debug("ajax error", data);

    if (data.error.messages !== undefined && data.error.messages.html !== undefined && data.error.messages.html["*"] !== undefined) {
      return data.error.messages.html["*"];
    } else {
      return JSON.stringify(data);
    }
  }

  // Access the cached item labels without throwing an error when undefined
  function getCachedItemLabel(item, lang) {
    if (lcGlobal.cachedItemLabels[item] !== undefined) {
      return lcGlobal.cachedItemLabels[item][lang];
    }

    return; // return undefined
  }

  // Return all values of claims with the given property (lazy load on first call)
  function getClaimValues(property) {
    var pid, prop, snak, val;

    if (property === undefined) {
      return; // return undefined
    }

    if (lcGlobal.claimValues === undefined) {
      lcGlobal.claimValues = {};

      $.each(properties, function() {
        prop = this;
        pid = "P" + prop;
        val = [];

        if (lcGlobal.entity.claims !== undefined && lcGlobal.entity.claims[pid] !== undefined) {
          $.each(lcGlobal.entity.claims[pid], function(key, value) {
            snak = value.mainsnak;

            if (snak === undefined || snak.datavalue === undefined || snak.datavalue.value === undefined) {
              return true; // continue each
            }

            if (snak.datavalue.value["numeric-id"] !== undefined) {
              val.push(snak.datavalue.value["numeric-id"]);
            } else if (snak.datavalue.time !== undefined) {
              val.push(snak.datavalue.time);
            }
          });
        }

        if (val.length > 0) {
          lcGlobal.claimValues[prop] = val;
        }
      });
    }

    return lcGlobal.claimValues[property];
  }

  // Returns the translated labels of the items (e.g. A,B,C) as a proper list ("A, B and C")
  // Returns undefined as soon as there is one undefined part (i.e. an item label missing in that language)
  function getConcatenatedItemLabels(array, lang) {
    var ret = "";
    var i, item, len;

    if (lang !== null && array !== undefined) {
      for (i = 0, len = array.length; i < len; i++) {
        item = getCachedItemLabel(array[i], lang);

        if (item === undefined || i === 0) {
          ret = item;
        } else if (i < len - 1) {
          ret += ", " + item;
        } else {
          ret = getText(lcGlobal.languages[lang].descAnd, {
            "a": ret,
            "b": item
          });
        }
      }
    }

    return ret;
  }

  // Create editor for label, description or alias
  function $getEditorLine(lang, existingValue, idSuggest, classField, idFieldExisting, idFieldNew, txtFieldExisting) {
    var $line = $("<tr />");

    var $btnSuggest = $createButton(null, null, getText(i18n.btnSuggest), getText(i18n.ttSuggestNone), idSuggest + lang, null, true);
    var $btnReset = $createButton(null, null, getText(i18n.btnReset), getText(i18n.ttReset), null, null, true);

    $btnReset.click(function() {
      // Reset "new" field to "existing" value
      setFieldValue(idFieldNew + lang, $getById(idFieldExisting + lang).val(), false);
    });

    $line.append($("<td />", { "class": classField + " " + classes.existing }).append($createInputFieldValue(true, idFieldExisting + lang, lang, txtFieldExisting, existingValue)));
    $line.append($("<td />").append($btnReset));
    $line.append($("<td />").append($btnSuggest));
    $line.append($("<td />", { "class": classField + " " + classes.newValue }).append($createInputFieldValue(true, idFieldNew + lang, lang)));

    return $line;
  }

  // Returns the values of the first of the given properties that actually has a value
  function getFirstPropertysValues(props) {
    var i, len, values;

    if (props !== undefined) {
      for (i = 0, len = props.length; i < len; i++) {
        values = getClaimValues(props[i]);

        if (values !== undefined) {
          return values;
        }
      }
    }

    return; // return undefined
  }

  // Load all languages supported by Wikidata, set user languages
  function getLanguages(onlyUserLanguages) {
    var db, inUserLanguages, lang, site;

    $.each(wikibase.sites.getSites(), function(index, value) {
      db = index;
      site = value._siteDetails;
      lang = (realLanguage[site.languageCode] === undefined) ? site.languageCode : realLanguage[site.languageCode];
      inUserLanguages = inArray(lang, lcGlobal.userLanguages);

      if ((onlyUserLanguages && ! inUserLanguages) || (! onlyUserLanguages && inUserLanguages)) {
        return true; // continue each
      }

      // Add language
      if (lcGlobal.languages[lang] === undefined) {
        lcGlobal.languages[lang] = {
          "lang": lang,
          "projects": []
        };

        // Load definitions from languageData
        if (languageData[lang] !== undefined) {
          lcGlobal.languages[lang].descAnd = (languageData[lang].descAnd !== undefined) ? languageData[lang].descAnd : languageData.defaultLang.descAnd;
          lcGlobal.languages[lang].descBy = (languageData[lang].descBy !== undefined) ? languageData[lang].descBy : languageData.defaultLang.descBy;
          lcGlobal.languages[lang].descFromPlace = (languageData[lang].descFromPlace !== undefined) ? languageData[lang].descFromPlace : languageData.defaultLang.descFromPlace;
          lcGlobal.languages[lang].descFromTime = (languageData[lang].descFromTime !== undefined) ? languageData[lang].descFromTime : languageData.defaultLang.descFromTime;
          lcGlobal.languages[lang].descIn = (languageData[lang].descIn !== undefined) ? languageData[lang].descIn : languageData.defaultLang.descIn;
          lcGlobal.languages[lang].descOf = (languageData[lang].descOf !== undefined) ? languageData[lang].descOf : languageData.defaultLang.descOf;
          lcGlobal.languages[lang].script = languageData[lang].script;
          lcGlobal.languages[lang].sisters = languageData[lang].sisters;
          lcGlobal.languages[lang].separator = (languageData[lang].separator !== undefined) ? languageData[lang].separator : languageData.defaultLang.separator;
          lcGlobal.languages[lang].suggest = languageData[lang].suggest;
        }
      }

      // Add project
      if (projectCodes[site.group] !== undefined) {
        lcGlobal.languages[lang].projects.push({
          "db": db,
          "code": (realLanguage[site.languageCode] === undefined) ? projectCodes[site.group] : (site.languageCode + "-" + projectCodes[site.group]),
          "group": site.group
        });
      }
    });

    // Sort projects: Wikipedia first
    $.each(lcGlobal.languages, function(index) {
      lang = index;

      lcGlobal.languages[lang].projects.sort(function(a, b) {
        if (a.group === "wikipedia" && b.group !== "wikipedia") {
          return -1;
        } else if (a.group !== "wikipedia" && b.group === "wikipedia") {
          return 1;
        } else {
          return a.db.localeCompare(b.db);
        }
      });
    });
  }

  // Create a preview section for a project and language
  function getPreviewLine(db, lang) {
    var $previewLine = $("<tr />");

    // Prepare display
    $previewLine.append($("<td />").append($createDiv(classes.triangle + " " + classes.triangleRight)));
    $previewLine.append($("<td />").append($createButton(null, null, getText(i18n.btnSuggest), getText(i18n.ttSuggestNone), ids.suggestDesc + db + lang, null, true)));
    $previewLine.append($("<td />").append($createDiv(classes.ellipsis, ids.preview + db + lang)));
    $getById(ids.previewArea + lang).find("table").append($previewLine);

    // Collapsing
    $previewLine.click(function(event) {
      // Don't toggle if the "Suggest" button or the text is clicked (or selected)
      if (! $(event.target).is("input[type=button],span")) {
        $getById(ids.preview + db + lang).toggleClass(classes.ellipsis);
        $previewLine.find("." + classes.triangle).toggleClass(classes.triangleRight);
      }
    });
  }

  // Extend sister languages: Sister languages get the same rules and projects than the main language
  function getSisterLanguages() {
    var lang, sisterlang, sisters;

    $.each(lcGlobal.languages, function(index) {
      sisters = this.sisters;
      lang = index;

      if (sisters !== undefined) {
        $.each(sisters, function () {
          sisterlang = this;

          if (lcGlobal.languages[sisterlang] === undefined) {
            lcGlobal.languages[sisterlang] = { "lang": sisterlang };
          }
        });

        // Override all sister languages' projects by main language projects
        $.each(sisters, function () {
          sisterlang = this;

          // Copy main language regexes and all projects to sister
          lcGlobal.languages[sisterlang].descAnd = lcGlobal.languages[lang].descAnd;
          lcGlobal.languages[sisterlang].descBy = lcGlobal.languages[lang].descBy;
          lcGlobal.languages[sisterlang].descFromPlace = lcGlobal.languages[lang].descFromPlace;
          lcGlobal.languages[sisterlang].descFromTime = lcGlobal.languages[lang].descFromTime;
          lcGlobal.languages[sisterlang].descIn = lcGlobal.languages[lang].descIn;
          lcGlobal.languages[sisterlang].descOf = lcGlobal.languages[lang].descOf;
          lcGlobal.languages[sisterlang].ignoreSitelinks = true;
          lcGlobal.languages[sisterlang].projects = lcGlobal.languages[lang].projects;
          lcGlobal.languages[sisterlang].script = lcGlobal.languages[lang].script;
          lcGlobal.languages[sisterlang].separator = lcGlobal.languages[lang].separator;
          lcGlobal.languages[sisterlang].sisters = undefined;
          lcGlobal.languages[sisterlang].suggest = lcGlobal.languages[lang].suggest;
        });
      }
    });
  }

  // Internationalisation of a text, incl. string formatting
  // Parameters may be strings or i18n object (which get translated then)
  // First parameter may contain placeholders {txt}, {year}, ... (which get replaced by the other parameters then)
  function getText(text, format) {
    var lang = mw.config.get("wgUserLanguage");
    var returnText = "";
    var textInternal;

    returnText = getTextInternal(text, lang);

    if (format !== undefined && returnText !== undefined) {
      $.each(format, function(key, value) {
        textInternal = getTextInternal(value, lang);
        // Dollar signs in existing labels etc. have to be doubled, otherwise they would disturb the template system
        textInternal = XRegExp.replace(textInternal, XRegExp.cache("\\$", "gm"), "\$$\$$");
        returnText = XRegExp.replace(returnText, XRegExp.cache("\\{" + key + "\\}", "gm"), textInternal);
      });
    }

    return returnText;
  }

  // Internationalisation (if an i18n object has been passed instead of a string)
  function getTextInternal(text, lang) {
    if (typeof text === "string") {
      return text;
    } else if (text === undefined) {
      return; // return undefined
    } else if (text[lang] === undefined && text.en === undefined) {
      return getText(i18n.intLabelMissing);
    } else if (text[lang] !== undefined) {
      return text[lang];
    } else {
      // Fallback to en
      return text.en;
    }
  }

  // Helper method to shorten jQuery's check whether a value is in an array
  function inArray(value, array) {
    return ($.inArray(value, array) > -1);
  }

  // Display the current item
  function initItem() {
    var newItemIds = [];
    var qids = "";
    var qidsText = "";
    var item, itemIds;

    lcGlobal.claimValues = undefined;
    lcGlobal.globalLabel = {};

    showStatus(getText(i18n.statusInitItem));

    window.location = mw.util.getUrl(lcConfig.pageTool) + "#" + lcGlobal.entity.id;
    setTitle(lcGlobal.entity.id);

    lcGlobal.instances = getFirstPropertysValues([
      properties.taxonRank,
      properties.instance,
      properties.subclass,
      properties.astronomicalObject,
      properties.administrativeType,
      properties.structureType,
      properties.electionType,
      properties.lakeType
    ]);

    // Load labels for items used to generate description from statements
    itemIds = generateDescription(true, null);

    $.each(itemIds, function() {
      item = this;

      if (lcGlobal.cachedItemLabels[item] === undefined && ! inArray(item, newItemIds)) {
        newItemIds.push(item);
        qids += ((qids === "") ? "" : "|") + "Q" + item;
        qidsText += ((qidsText === "") ? "" : ", ") + "Q" + item;
      }
    });

    // Clear log display
    setTitle(lcGlobal.entity.id);

    if (newItemIds.length > 0) {
      loadItemLabels(newItemIds, qids, qidsText);
    } else {
      // Generate GUI and load data
      loadData();
    }
  }

  // Initialise the GUI
  function initPage() {
    applyCSS();
    createPage();
    setTitle();
    loadBabel();
  }

  // Initialise the supported languages and projects - extend languages to all projects supported by Wikidata itself
  function initProjects() {
    debug("init projects start");

    // Call getLanguages() twice to sort user languages before all others
    getLanguages(true);
    getLanguages(false);
    getSisterLanguages();

    debug("init projects finish");

    // Load first item entity
    jumpTo(window.location.hash.substr(1));
  }

  // Startup method
  function initTool() {
    var urlAnchor = window.location.hash;
    var userIsOnToolPage = (mw.config.get("wgPageName") === lcConfig.pageTool && mw.config.get("wgAction") === "view");

    if (userIsOnToolPage && urlAnchor !== undefined && urlAnchor !== "") {
      mw.util.addCSS("." + classes.center + "{ text-align: center; }");

      mw.loader.using([ "jquery.spinner", "jquery.ui", "wikibase" ], function () {
        $getById(ids.mwBodyContent).replaceWith($createDiv(classes.center, ids.mwBodyContent).append($.createSpinner({ "size": "large" })));

        // Load XRegExp library
        loadXRegExp();
      });
    }
  }

  // Check whether a field has been changed compared to the existing value
  function isChanged(id) {
    var $field = $getById(id);
    var valueExisting = $field.parents("tr").find("input[type=text]").first().val();
    var valueNew = $field.val();

    return (valueExisting !== valueNew);
  }

  // Check whether the item is an instance of the given item (and nothing else)
  function isInstance(item) {
    return (lcGlobal.instances.length === 1 && lcGlobal.instances[0] === item);
  }

  // Go to the specified item or user's edits
  function jumpTo(target) {
    if (target.toUpperCase().substr(0, 1) === "Q") {
      // Load starting item
      loadEntity(target.toUpperCase());
    } else if (target.toUpperCase().substr(0, 5) === "USER:") {
      // Load user's edited items
      lcGlobal.userUcContinue = undefined;
      loadUsersEdits(target.substr(5));
    } else if (target.toUpperCase() === "SPECIAL:RANDOM") {
      // Load a random item
      loadRandomItem();
    } else if (target.toUpperCase().substr(0, 15) === "SPECIAL:SEARCH/") {
      // Perform a search
      lcGlobal.srOffset = undefined;
      loadSearchItems(target.substr(15));
    } else {
      showError(getText(i18n.txtFailedToReadInput, { "txt": target }));
    }
  }

  // Load user's Babel languages from user page categories by AJAX call and go on
  function loadBabel() {
    var userpage = "User:" + mw.config.get("wgUserName");
    var category, page, query;

    showStatus(getText(i18n.statusLoadBabel, { "userpage": userpage }));

    // Add user UI language
    lcGlobal.userLanguages.push(mw.config.get("wgUserLanguage"));

    // Get babel languages
    $.ajax({
      "url": mw.util.wikiScript("api"),
      "data": {
        "action": "query",
        "format": "json",
        "prop": "categories",
        "titles": userpage
      },
      "success": function(babeldata) {
        query = babeldata.query;

        if (! babeldata.hasOwnProperty("error") && query !== undefined && query.pages !== undefined && query.pages[-1] === undefined) {
          debug("loadBabel success", userpage);

          $.each(query.pages, function() {
            page = this;

            $.each(page.categories, function() {
              category = XRegExp.exec(this.title, XRegExp.cache("Category:User (?<lang>\\w+)"));

              if (category !== null && category !== undefined && category.lang !== undefined && ! inArray(category.lang, lcGlobal.userLanguages)) {
                lcGlobal.userLanguages.push(category.lang);
              }
            });
          });
        } else {
          debug("loadBabel error", getAjaxErrorMessage(data));
        }

        initProjects();
      },
      "error": function(jqXHR, textStatus) {
        showError(getText(i18n.intFailedToLoad) + textStatus);
      }
    });
  }

  // Generate GUI and load data
  function loadData() {
    var $content = $createDiv(classes.main, ids.mwBodyContent);
    var $entries, $field;
    var language, script;

    debug("loadData");
    setTitle(lcGlobal.entity.id);

    $getById(ids.mwBodyContent).replaceWith($content);

    // Load data for each language
    $.each(lcGlobal.languages, function() {
      language = this;
      addEntry(language, false);
    });

    enableButtons(true);

    // If there is only one entry in total, auto-expand it in any case
    $entries = $content.find("." + classes.entry);
    if ($entries.length === 1 && $entries.first().find("." + classes.collapsed).length > 0) {
      $entries.first().find("." + classes.collapsed).click();
    }

    // If all existing labels are the same, possibly set all other labels also
    $.each($entries.find("." + classes.newValue + "." + classes.label + " input[type=text]"), function() {
      $field = $(this);
      script = lcGlobal.languages[$field.attr("lang")].script;

      if (lcConfig.autoFill && checkGlobalLabelValidity(script)) {
        setFieldValue($field.attr("id"), lcGlobal.globalLabel[script], false);
      }
    });
  }

  // Load Wikidata entity data (claims, etc.) by AJAX call and go on
  function loadEntity(qid) {
    var entities;

    setTitle(qid);
    showStatus(getText(i18n.statusLoadEntity, { "qid": qid }));

    $.ajax({
      "url": mw.util.wikiScript("api"),
      "data": {
        "action": "wbgetentities",
        "format": "json",
        "ids": qid
      },
      "success": function(data) {
        entities = data.entities;

        if (! data.hasOwnProperty("error") && entities !== undefined && entities[qid] !== undefined && entities[qid].missing === undefined) {
          lcGlobal.entity = entities[qid];

          // Start script
          debug("loadEntity success");
          initItem();
        } else {
          debug("loadEntity error", getAjaxErrorMessage(data));
          showError(getText(i18n.txtFailedToItem, { "txt": qid }));
        }
      },
      "error": function(jqXHR, textStatus) {
        showError(getText(i18n.intFailedToLoad) + textStatus);
      }
    });
  }

  // Load the labels of items used in this item's claims and go on
  function loadItemLabels(itemIds, qids, qidsText) {
    var itemId, qid, label, sisterLang;

    showStatus(getText(i18n.statusLoadItemLabels, { "qids": qidsText }));

    $.ajax({
      "url": mw.util.wikiScript("api"),
      "data": {
        "action": "wbgetentities",
        "format": "json",
        "ids": qids,
        "props": "labels"
      },
      "success": function(data) {
        if (! data.hasOwnProperty("error") && data.entities !== undefined) {
          debug("loadItemLabels success");

          $.each(itemIds, function() {
            itemId = this;
            qid = "Q" + itemId;

            lcGlobal.cachedItemLabels[itemId] = {};

            // Set labels
            if (data.entities[qid] !== undefined) {
              $.each(data.entities[qid].labels, function () {
                label = this;

                if (label.value !== undefined) {
                  lcGlobal.cachedItemLabels[itemId][label.language] = label.value;
                }
              });
            }
          });

          // Set labels for sister languages, if needed
          $.each(lcGlobal.cachedItemLabels, function(id, labels) {
            $.each(labels, function (lang, thisLabel) {
              if (thisLabel !== undefined && languageData[lang] !== undefined && languageData[lang].sisters !== undefined) {
                $.each(languageData[lang].sisters, function() {
                  sisterLang = this;

                  if (lcGlobal.cachedItemLabels[id][sisterLang] === undefined) {
                    lcGlobal.cachedItemLabels[id][sisterLang] = thisLabel;
                  }
                });
              }
            });
          });
        } else {
          debug("loadItemLabels error", getAjaxErrorMessage(data));
        }

        // Generate GUI and load data
        loadData();
      },
      "error": function(jqXHR, textStatus) {
        showError(getText(i18n.intFailedToLoad) + textStatus);
      }
    });
  }

  // Load a list of items based on a search query
  function loadItemList(term, params, termStore, offset, searchResult, loadMsg, errMsg) {
    var newList = [];
    var result;

    showStatus(getText(loadMsg, { "term": term }));

    termStore = term;

    if (offset === undefined) {
      offset = { "continue": "" };
    }

    $.extend(params, offset);

    $.ajax({
      "url": mw.util.wikiScript("api"),
      "data": params,
      "success": function(data) {
        if (! data.hasOwnProperty("error") && data.query !== undefined && data.query[searchResult] !== undefined) {
          offset = data["continue"];
          debug("loadItemList success", offset);

          // Only add each item once
          $.each(data.query[searchResult], function () {
            result = this;

            if (! inArray(result.title, lcGlobal.itemList)) {
              lcGlobal.itemList.push(result.title);
              newList.push(result.title);
            }
          });
        } else {
          debug("loadItemList error", getAjaxErrorMessage(data));
          offset = undefined;
        }

        if (newList.length > 0) {
          loadNextItem();
        } else {
          offset = undefined;
          showError(getText(errMsg, { "term": term }));
        }
      },
      "error": function() {
        offset = undefined;
        showError(getText(errMsg, { "term": term }));
      }
    });
  }

  // Loads the item page (i.e. close the Label Collector)
  function loadItemPage(qid) {
    if (qid !== undefined) {
      window.location = mw.util.getUrl(qid);
    } else {
      showError(getText(i18n.txtFailedToItem, { "txt": qid }));
    }
  }

  // Load next item's page (on "Skip" or "Save & Next")
  function loadNextItem() {
    var ids = "";
    var loadThis = false;
    var i, id, len;

    setTitle();

    // Scroll to top, in case the browser doesn't do this on its own after the page is cleared
    $("body").animate({ "scrollTop": 0 }, 0);

    // Check next couple of items for existence
    if (lcGlobal.itemList.length === 0) {
      // Standard mode: Next items by numeric ID
      id = parseInt(lcGlobal.entity.id.substr(1), 10);
      debug("loadNextItem standard mode", id);
      showStatus(getText(i18n.statusLoadNextItem, { "qid": String(id + 1) }));

      for (i = 1; i <= lcConfig.checkNextItems; i++) {
        ids += ((ids === "") ? "" : "|") + "Q" + (id + i);
      }
    } else {
      // User contribution mode / search mode: Next items in itemList
      lcGlobal.itemPointer++;
      debug("loadNextItem user/search mode", lcGlobal.itemPointer);

      if (lcGlobal.itemList.length <= lcGlobal.itemPointer) {
        if (lcGlobal.userUcContinue !== undefined) {
          loadUsersEdits(lcGlobal.usersContribs);
        } else if (lcGlobal.srOffset !== undefined) {
          loadSearchItems(lcGlobal.searchTerm);
        } else {
          showError(getText(i18n.txtFailedToLoadNext));
        }

        return;
      }

      id = lcGlobal.itemList[lcGlobal.itemPointer].substr(1);
      showStatus(getText(i18n.statusLoadNextItem, { "qid": String(id) }));

      for (i = 0, len = lcGlobal.itemList.length; i < lcConfig.checkNextItems && lcGlobal.itemPointer < len; i++, lcGlobal.itemPointer++) {
        ids += ((ids === "") ? "" : "|") + (lcGlobal.itemList[lcGlobal.itemPointer]);
      }
    }

    debug("loadNextItem query", ids);

    // Query API to check if items exist and should be edited
    if (ids !== "") {
      $.ajax({
        "url": mw.util.wikiScript("api"),
        "data": {
          "action": "wbgetentities",
          "format": "json",
          "ids": ids,
          "props": "aliases|claims|descriptions|labels|sitelinks"
        },
        "success": function(data) {
          debug("loadNextItem success");
          lcGlobal.entity = undefined;

          // Load first existing item (remind that the API query returns results for missing items, too)
          loadThis = findNextValidItem(data.entities);

          if (lcGlobal.entity === undefined) {
            // Not a single existing item returned by query - we might have reached the end (or a gap big enough that we can't tell)
            showError(getText(i18n.txtFailedToLoadNext));
          } else if (loadThis) {
            // Found a valid item to load
            if (lcGlobal.itemList.length > 0) {
              // Reset itemPointer
              lcGlobal.itemPointer = $.inArray(lcGlobal.entity.id, lcGlobal.itemList);
            }

            initItem();
          } else {
            // There have been items, but all had to be skipped - check next bunch
            loadNextItem();
          }
        },
        "error": function() {
          // API error
          showError(getText(i18n.txtFailedToLoadNext));
        }
      });
    } else {
      // No IDs set (probably reached end of user's loaded contributions
      showError(getText(i18n.txtFailedToLoadNext));
    }
  }

  // Loads a random item (that can be edited)
  function loadRandomItem() {
    debug("loadRandomItem");

    $.ajax({
      "type": "POST",
      "url": mw.util.wikiScript("api"),
      "data": {
        "action": "query",
        "format": "json",
        "list": "random",
        "rnlimit": 1,
        "rnnamespace": "0"
      },
      "success": function(data) {
        if (! data.hasOwnProperty("error") && data.query !== undefined && data.query.random !== undefined && data.query.random[0] !== undefined) {
          debug("loadRandomItem success");

          // Get random QID
          lcGlobal.entity.id = data.query.random[0].title;
          // Search next item to be edited
          loadNextItem();
        } else {
          debug(getAjaxErrorMessage(data));
          showError(getText(i18n.txtFailedToLoadNext));
        }
      },
      "error": function() {
        showError(getText(i18n.txtFailedToLoadNext));
      }
    });
  }

  // Loads a list of search results
  function loadSearchItems(term) {
    var params = {
      "action": "query",
      "format": "json",
      "list": "search",
      "srlimit": lcConfig.checkUserContribs,
      "srnamespace": 0,
      "srprop": "title",
      "srsearch": term
    };

    loadItemList(term, params, lcGlobal.searchTerm, lcGlobal.srOffset, "search", i18n.statusLoadSearchResults, i18n.txtFailedToLoadSearchResults);
  }

  // Loads the list of the items the user most recently edited
  function loadUsersEdits(user) {
    var params = {
      "action": "query",
      "format": "json",
      "list": "usercontribs",
      "uclimit": lcConfig.checkUserContribs,
      "ucnamespace": 0,
      "ucprop": "title",
      "ucuser": user
    };

    loadItemList(user, params, lcGlobal.usersContribs, lcGlobal.userUcContinue, "usercontribs", i18n.statusLoadUserEdits, i18n.txtFailedToLoadUsersItems);
  }

  // Load the introduction of an article as a preview, create suggestions from it (only if none is existing)
  // Sequential loading: Load first defined project first, then recursively load second and so on, so the more important projects get a better chance to make suggestions
  function loadWikiContent(language, projects, aliases, desc, wasDescriptionSuggested) {
    var description = desc;
    var introhtml = "";
    var lang = language.lang;
    var separator = language.separator;
    var suggest = language.suggest;
    var wasDescSuggested = wasDescriptionSuggested;
    var actualAliases, actualNewLabel, db, extract, introtext, newAliases, newDescription, project, redirect, url;

    // Skip if no project for this language
    if (projects === undefined || projects.length === 0) {
      return;
    }

    db = projects[0].db;
    project = projects[0].code;

    // Skip if no sitelink for this project
    if (db === undefined || lcGlobal.entity.sitelinks === undefined || lcGlobal.entity.sitelinks[db] === undefined) {
      if (projects.length > 1) {
        loadWikiContent(language, projects.slice(1, projects.length), aliases, description, wasDescSuggested);
      } else {
        loadWikidataContent(lang, wasDescSuggested, description);
      }

      return;
    }

    getPreviewLine(db, lang);

    $.createSpinner({
      "size": "small",
      "type": "block"
    }).appendTo("#" + ids.preview + db + lang);

    debug("loadWikiContent");

    // Query API to get introduction
    $.getJSON(wikibase.sites.getSite(db).getApi() + "?callback=?", {
      "action": "query",
      "exchars": 1000,
      "exintro": true,
      "format": "json",
      "prop": "extracts|info",
      "titles": lcGlobal.entity.sitelinks[db].title
    },
    function(data) {
      // Get introduction
      redirect = data.query.pages[Object.keys(data.query.pages)[0]].redirect;
      extract = data.query.pages[Object.keys(data.query.pages)[0]].extract;

      debug("loadWikiContent loaded", redirect, extract);

      if (extract !== undefined) {
        introhtml = extract;

        // Remove content most likely not being a valuable part of the article text (disambiguation boxes etc.)
        introhtml = XRegExp.replace(introhtml, XRegExp.cache("<(?<tag>dd|dl)[^>]*>(?<content>.*?)<\\/\\s?\\k<tag>>", "g"), " ");

        // Remove tags (not their content) which would corrupt the preview
        introhtml = XRegExp.replace(introhtml, XRegExp.cache("<\\/?\\s?(blockquote|br|center|dl|li|ol|p|ul)[^>]*>", "g"), " ");

        // Remove line breaks, non-breaking spaces etc.
        introhtml = XRegExp.replace(introhtml, XRegExp.cache("(&(nbsp|#160|#x00A0|#8239|#x202F|#xFEFF|#65279|#x2007|#8199);)|\\u00A0|\\u202F|\\uFEFF|\\u2007", "g"), " ");

        // Remove multiple spaces
        introhtml = XRegExp.replace(introhtml, XRegExp.cache("\\s{2,}", "g"), " ");
        introhtml = introhtml.trim();
      }

      if (introhtml === "" || redirect !== undefined) {
        introhtml = (redirect !== undefined) ? getText(i18n.txtRedirect) : getText(i18n.txtNothing);
        introhtml = "<i>" + introhtml + "</i>";
        setFieldValue(ids.descNew + lang, $getById(ids.descExisting + lang).val(), false);
        wasDescSuggested = true;
      }

      // Put <span> around it again, to allow using jQuery here and the ellipsis to work
      introhtml = "<span>" + introhtml + "</span>";

      // Show preview text
      url = wikibase.sites.getSite(lcGlobal.entity.sitelinks[db].site).getUrlTo(lcGlobal.entity.sitelinks[db].title);
      $getById(ids.preview + db + lang).html($(introhtml).prepend(getText("[<a href='{url}'>{project}</a>]: ", {
        "url": url,
        "project": project
      })));

      // Remove stress marks from suggestions for aliases and description (only for Russion, only if string normalization supported by browser)
      if (lcConfig.cleanDescription && lang === "ru" && String.prototype.normalize) {
        introhtml = XRegExp.replace(introhtml.normalize("NFD"), XRegExp.cache("\\u0301", "g"), "");
      }

      // Strip all HTML tags
      introtext = $(introhtml).text();

      // Get what now really is in the new label and alias fields
      actualNewLabel = $getById(ids.labelNew + lang).val();
      actualAliases = $getById(ids.aliasesNew + lang).val();
      actualAliases = (actualAliases === "") ? aliases : actualAliases;

      if (lcConfig.autoFill) {
        newAliases = parseAliases(introhtml, actualAliases, actualNewLabel, parseLabel(lcGlobal.entity.sitelinks[db].title));
        setFieldValue(ids.aliasesNew + lang, newAliases, true);
      } else {
        setFieldValue(ids.aliasesNew + lang, actualAliases, true);
      }

      // Get suggestions
      // - Descriptions: prefill existing if available
      // - Aliases: always keep existing and add new ones as suggestion)
      newDescription = parseDescription(introtext, db, suggest, separator);

      if (newDescription !== description && newDescription !== "…") {
        // Enable project-specific "Suggest" button even if there is already a suggestion
        updateSuggestButton(ids.suggestDesc + db + lang, ids.descNew + lang, newDescription);
      }

      if (! wasDescSuggested) {
        wasDescSuggested = setExistingOrSuggestion(lang, ids.descNew, description, newDescription, ids.suggestDesc);
      }

      // Load next project
      if (projects.length > 1) {
        loadWikiContent(language, projects.slice(1, projects.length), aliases, description, wasDescSuggested);
      } else {
        loadWikidataContent(lang, wasDescSuggested, description);
      }
    });
  }

  // Generate an automatic description from the Wikidata statements
  function loadWikidataContent(lang, wasDescriptionSuggested, desc) {
    var db = "wikidata";
    var description = desc;
    var newDescription = getText(i18n.txtNothing);
    var project = "d";
    var wasDescSuggested = wasDescriptionSuggested;
    var newDesc;

    if (lcGlobal.instances !== undefined) {
      getPreviewLine(db, lang);

      newDescription = generateDescription(false, lang);

      if (description === undefined) {
        description = "";
      }

      if (newDescription !== undefined && newDescription !== "…") {
        if (newDescription !== description) {
          // Enable project-specific "Suggest" button even if there is already a suggestion
          updateSuggestButton(ids.suggestDesc + db + lang, ids.descNew + lang, newDescription);
        }

        if (! wasDescSuggested) {
          wasDescSuggested = setExistingOrSuggestion(lang, ids.descNew, description, newDescription, ids.suggestDesc);
        }
      }

      newDesc = ((newDescription === undefined) ? getText(i18n.txtNothing) : "<i>" + newDescription + "</i>");
      $getById(ids.preview + db + lang).html(getText("[{project}]: ", { "project": project }) + newDesc);
    }
  }

  // Load XRegExp library by AJAX call and go on
  // This is a requirement for some core functions but also parts of the getText() function
  function loadXRegExp() {
    var url = "https://cdnjs.cloudflare.com/ajax/libs/xregexp/2.0.0/xregexp-all-min.js";

    showStatus(getText(i18n.statusLoadXRegExp) + "<a href='" + url + "'>XRegExp</a>");

    $.ajax({
      "url": url,
      "dataType": "script",
      "cache": true,
      "timeout": lcConfig.timeout,
      "success": function() {
        debug("loadXRegExp success");
        initPage();
      },
      "error": function(jqXHR, textStatus) {
        showError(getText(i18n.intFailedToLoad) + textStatus);
      }
    });
  }

  // Make suggestions for new aliases based on what's set bold in the introduction text
  // Existing ones are always kept, new ones are only suggested if they're not in the exiting ones or in the label or in the (possibly theoretical) label proposal
  function parseAliases(text, alias, label, labelSuggestion) {
    var aliases = alias;
    var existing, isLabelIdentical, isSuggestedLabelIdentical, isTooShort, newAlias, newAliasLc, thisAlias;

    $(text).find("b").each(function() {
      newAlias = XRegExp.replace($(this).text().trim(), XRegExp.cache("^\"(.*)\"$"), "${1}");
      newAliasLc = newAlias.toLowerCase();
      existing = false;

      // Only add finding if it's not already in the aliases list, and it's not identical to the label, and it's not too short (e.g. bold letters in abbreviations)
      isLabelIdentical = (label !== undefined && newAliasLc === label.toLowerCase());
      isSuggestedLabelIdentical = (labelSuggestion !== undefined && newAliasLc === labelSuggestion.toLowerCase());
      isTooShort = (newAlias.length <= 2);

      if (isLabelIdentical || isSuggestedLabelIdentical || isTooShort) {
        existing = true;
      } else if (aliases !== "" && aliases !== undefined) {
        $.each(aliases.split("|"), function() {
          thisAlias = this;

          if (thisAlias.trim().toLowerCase() === newAliasLc) {
            existing = true;

            return false; // break each
          }
        });
      }

      if (! existing) {
        aliases += ((aliases === "") ? "" : "|") + newAlias;
      }
    });

    return aliases.trim();
  }

  // Make a suggestion for the description based on the article introduction
  // - The first sentence is scanned for "is a" definitions based on the regular expression defined for each language
  // - If nothing found, the complete first introduction sentence is proposed as a description
  function parseDescription(text, db, suggest, separator) {
    var firstSentence, fullstop, result, sentences;

    // Only examine the first sentence
    sentences = XRegExp.exec(text, XRegExp.cache(separator));

    if (sentences !== null && sentences !== undefined && sentences.sentence !== undefined && sentences.sentence !== text) {
      firstSentence = sentences.sentence;
    } else {
      fullstop = Math.max(text.indexOf("."), text.indexOf("。"));

      if (fullstop > -1) {
        firstSentence = text.substr(0, fullstop);
      }
    }

    // Try to find the relevant part in the text
    try {
      if (suggest !== undefined && firstSentence !== undefined) {
        result = XRegExp.exec(firstSentence, XRegExp.cache(suggest));

        if (result !== null && result !== undefined && result.desc !== undefined) {
          result = result.desc.trim();
        }
      }
    } catch(e) {
      showMessage(getText(i18n.txtFailedToParseIntro, {
        "db": db,
        "txt": e.toString()
      }));
    }

    if (result === undefined || result === null) {
      // Fallback: Suggest the complete first introduction sentence (or all, if no dot)
      result = (firstSentence === undefined) ? text : firstSentence;
    }

    return result.trim();
  }

  // Make a suggestion for the label based on the article title (strip of bracket parts, if existing)
  function parseLabel(text) {
    var result = XRegExp.replace(text, XRegExp.cache(" \\(.+\\)$"), "");

    if (result.length > 0) {
      return result.trim();
    }

    // Fallback: Suggest complete title as label
    return text.trim();
  }

  // Action: Reset all
  function resetAll() {
    $.each(lcGlobal.languages, function() {
      var lang = this.lang;

      // Skip if collapsed
      if (! $getById(ids.labelNew + lang).is(":visible")) {
        return true; // continue each
      }

      debug("reset", lang);

      setFieldValue(ids.labelNew + lang, $getById(ids.labelExisting + lang).val(), false);
      setFieldValue(ids.aliasesNew + lang, $getById(ids.aliasesExisting + lang).val(), false);
      setFieldValue(ids.descNew + lang, $getById(ids.descExisting + lang).val(), false);
    });
  }

  // Save changes (afterwards, reload item page or load next item page, based on parameter)
  function saveItem(loadNext) {
    var aliases = {};
    var aliasesSummary = "";
    var clearAliasLanguages = [];
    var descriptions = {};
    var descriptionsCount = 0;
    var descriptionsSummary = "";
    var json = {};
    var labels = {};
    var labelsCount = 0;
    var labelsSummary = "";
    var summary = "";
    var i, lang, len, splitValue, value;

    try {
      debug("saveItem");

      // Collect changes
      $.each(lcGlobal.languages, function() {
        lang = this.lang;
        var $fldLabel = $getById(ids.labelNew + lang);

        // Skip if collapsed - only save expanded entries
        if (! $fldLabel.is(":visible")) {
          return true; // continue each
        }

        if (isChanged(ids.labelNew + lang)) {
          value = cleanValue($fldLabel);

          labels[lang] = {
            "language": lang,
            "value": value
          };

          labelsSummary += getText("{plural}[{lang}]: {txt}", {
            "plural": ((labelsCount === 0) ? "" : ", "),
            "lang": lang,
            "txt": value
          });

          labelsCount++;
        }

        if (isChanged(ids.descNew + lang)) {
          value = cleanValue($getById(ids.descNew + lang));

          descriptions[lang] = {
            "language": lang,
            "value": value
          };

          descriptionsSummary += getText("{plural}[{lang}]: {txt}", {
            "plural": ((descriptionsCount === 0) ? "" : ", "),
            "lang": lang,
            "txt": value
          });

          descriptionsCount++;
        }

        if (isChanged(ids.aliasesNew + lang)) {
          // Split concatenated string into array
          value = cleanValue($getById(ids.aliasesNew + lang));
          splitValue = value.split("|");
          aliases[lang] = [];

          if (value.length === 0) {
            // Separate API call needed to clear aliases
            clearAliasLanguages.push(lang);
          } else {
            for (i = 0, len = splitValue.length; i < len; i++) {
              aliases[lang].push({
                "language": lang,
                "value": splitValue[i]
              });
            }

            aliasesSummary += getText("{plural}[{lang}]: {txt}", {
              "plural": ((aliasesSummary.length === 0) ? "" : ", "),
              "lang": lang,
              "txt": splitValue.join(", ")
            });
          }
        }
      });

      // Don't save if nothing changed
      if (labelsCount === 0 && descriptionsCount === 0 && aliasesSummary.length === 0 && clearAliasLanguages.length === 0) {
        showMessage(getText(i18n.txtNothingToSave));

        if (loadNext) {
          loadNextItem();
        } else {
          loadItemPage(lcGlobal.entity.id);
        }

        return;
      }

      lcGlobal.saveCounter++;

      if (loadNext && ! lcGlobal.hasMessages && lcGlobal.saveCounter % lcConfig.checkUserTalk === 0) {
        checkUserTalkPage();
      }

      // Create summary and JSON data structure
      if (labelsCount > 0) {
        json.labels = labels;
        summary = getText(lcConfig.labelSummaryTmp, {
          "plural": (labelsCount > 1) ? "s" : "",
          "txt": labelsSummary
        });
      }

      if (descriptionsCount > 0) {
        json.descriptions = descriptions;
        summary += ((labelsCount === 0) ? "" : ", ") + getText(lcConfig.descSummaryTmp, {
          "plural": (descriptionsCount > 1) ? "s" : "",
          "txt": descriptionsSummary
        });
      }

      if (aliasesSummary.length > 0) {
        json.aliases = aliases;
        summary += ((labelsCount === 0 && descriptionsCount === 0) ? "" : ", ") + getText(lcConfig.aliasesSummaryTmp, { "plural": "es", "txt": aliasesSummary });
      }

      summary += lcConfig.summaryTmp;

      if (labelsCount > 0 || descriptionsCount > 0 || aliasesSummary.length > 0) {
        debug("saveItem", lcGlobal.entity.id, json, summary);

        // Send main request to server
        $.ajax({
          "type": "POST",
          "url": mw.util.wikiScript("api"),
          "data": {
            "action": "wbeditentity",
            "assert": "user",
            "format": "json",
            "data": JSON.stringify(json),
            "id": lcGlobal.entity.id,
            "summary": summary,
            "type": "item",
            "token": mw.user.tokens.get("csrfToken")
          },
          "success": function(data) {
            if (data.hasOwnProperty("error")) {
              showMessage(getText(i18n.txtSavingFailed, { "txt": getAjaxErrorMessage(data) }));
              enableButtons(true);
            } else {
              debug("saveItem success");
            }

            // Clear aliases (per language), if needed, then load next item
            clearAliases(clearAliasLanguages, loadNext);
          },
          "error": function() {
            showMessage(getText(i18n.txtSavingFailed, { "txt": i18n.txtApiError }));
            enableButtons(true);
          }
        });
      } else if (clearAliasLanguages.length > 0) {
        clearAliases(clearAliasLanguages, loadNext);
      }
    } catch(e) {
      showMessage(getText(i18n.intError, { "txt": e.toString() }));
      enableButtons(true);
    }
  }

  // Save changes and load next item page
  function saveItemNext() {
    saveItem(true);
  }

  // Save changes and reload item page
  function saveItemReload() {
    saveItem(false);
  }

  // Fill the "new value" field with either the existing value or a suggestion
  // (if value exists but a different suggestion could be made, prefill existing value and enable "Suggest" button)
  // Returns whether a suggestion was made
  function setExistingOrSuggestion(lang, idFieldNew, existing, suggestion, idSuggestButton) {
    if (! lcConfig.autoFill) {
      // Don't suggest anything
      setFieldValue(idFieldNew + lang, existing, true);

      return false;
    } else if (existing === "" && suggestion !== "" && suggestion !== "…") {
      // No value existing -> suggest one
      setFieldValue(idFieldNew + lang, suggestion, true);

      return true;
    } else if (existing !== "") {
      // Value existing -> prefill existing
      setFieldValue(idFieldNew + lang, existing, true);

      if (existing.toLowerCase() !== suggestion.toLowerCase() && suggestion !== "" && suggestion !== "…") {
        // Suggestion would be different -> enable "Suggest" button
        updateSuggestButton(idSuggestButton + lang, idFieldNew + lang, suggestion);

        return true;
      }
    } else {
      $getById(idFieldNew + lang).attr("disabled", false);
    }

    return false;
  }

  // Sets a new value for the field and triggers change handler
  // Enable the field (on startup, they're disabled to prevent the user filling them manually before the suggestions are loaded)
  function setFieldValue(field, value, replaceHtmlEntities) {
    var $field = $getById(field);

    if (replaceHtmlEntities && lcConfig.cleanDescription) {
      // Secure way to decode HTML entities: using an oldschool textarea
      var textArea = document.createElement("textarea");
      textArea.innerHTML = value;
      value = textArea.value;
    }

    $field.attr("disabled", false);
    $field.attr("oldval", $field.val());
    $field.val(value);
    $field.change();
  }

  // Clears the page, sets the title for the new qid, displays a throbber until content is loaded
  function setTitle(qid) {
    var $history;

    if (qid !== undefined) {
      $history = $("<sup />").append("[").append($createLink(lcConfig.pageHistory + qid, getText(i18n.lnkHistory))).append("]");
      $getById(ids.pageTitle).html(getText("{title} – ", { "title": i18n.intTitleShort })).append($createLink(qid, qid)).append($history);

      document.title = getText("{title} – {qid}", {
        "title": i18n.intTitle,
        "qid": (qid !== undefined) ? qid : i18n.txtLoading
      });
    } else {
      $getById(ids.pageTitle).html(getText("{title} – {loading}", {
        "title": i18n.intTitleShort,
        "loading": i18n.txtLoading
      }));
    }

    $getById(ids.mwBodyContent).replaceWith($createDiv(classes.center, ids.mwBodyContent).append($.createSpinner({ "size": "large" })));
  }

  // Display actions menu
  function showActions() {
    var $content = $("<div />").append($createButton(addLanguage, "a", getText(i18n.btnAdd)));
    $content.append("<div />").append($createButton(toggleCollapseAll, "e", getText(i18n.btnExpandCollapseAll)));
    $content.append("<div />").append($createButton(clearAll, "c", getText(i18n.btnClearAll)));
    $content.append("<div />").append($createButton(resetAll, "r", getText(i18n.btnResetAll)));

    $content.dialog({
      "buttons": [{
        "text": getText(i18n.btnClose),
        "click": function() {
          $( this ).dialog("close");
        }
      }],
      "html": $content,
      "modal": true,
      "title": getText(i18n.btnActions)
    });
  }

  // Show item (if filled, having a sitelink or is in user's babel languages)
  function showEntry(language, aliases, forceShow, newLabel, sitelinks, $sitelinksList) {
    var lang = language.lang;
    var isInUserLanguages = inArray(lang, lcGlobal.userLanguages);
    var babelClass = (isInUserLanguages || forceShow) ? classes.babel : classes.nonBabel;
    var description = (lcGlobal.entity.descriptions === undefined || lcGlobal.entity.descriptions[lang] === undefined) ? "" : lcGlobal.entity.descriptions[lang].value;
    var label = (lcGlobal.entity.labels === undefined || lcGlobal.entity.labels[lang] === undefined) ? "" : lcGlobal.entity.labels[lang].value;
    var $entry, $field, $header, $headerDiv, $headerLine, $preview, $previewDiv, $sub, $table;
    var aliasCount, changed, cleanDescription, hasOriginalSitelinks, headerLine, isFilled, sitelinksList, value;

    // Only show items that are filled, have a sitelink or are in the user's babel languages
    isFilled = (label !== "" || description !== "" || aliases !== "");
    hasOriginalSitelinks = sitelinks.length > 0 && ! lcGlobal.languages[lang].ignoreSitelinks;

    if (forceShow || isInUserLanguages || hasOriginalSitelinks || isFilled) {
      cleanDescription = description;

      if (lcConfig.cleanDescription) {
        // Clean description (remove Wiki link syntax and multiple spaces)
        cleanDescription = XRegExp.replace(cleanDescription, XRegExp.cache("\\[\\[([^\\]]*?\\|)?(?<linktext>.+?)\\]\\]", "g"), "${linktext}");
        cleanDescription = XRegExp.replace(cleanDescription, XRegExp.cache("\\s{2,}", "g"), " ");
      }

      // Header (language code, titles, links; labels and description when collapsed)
      $entry = $createDiv(classes.entry + " " + babelClass);
      sitelinksList = (sitelinks.length > 0) ? $sitelinksList : $("<i />", { "html": getText(i18n.txtNoSitelink) });
      $headerDiv = $createDiv(classes.header + " " + classes.collapsed);
      $header = $("<table />");
      $headerLine = $("<tr />");
      $headerLine.append($("<td />").append($createDiv(classes.triangle + " " + classes.triangleRight)));
      $headerLine.append($("<td />", { "html": getText("[{lang}]: ", { "lang": lang }) }).append(sitelinksList));
      $headerLine.append($("<td />", { "class": classes.headerLabel }));
      $header.append($headerLine);
      $headerDiv.append($header);
      $entry.append($headerDiv);

      headerLine = label;

      if (description !== "") {
        headerLine += ((headerLine !== "") ? ", " : "") + description;
      }

      if (headerLine !== "") {
        headerLine = getText("({headerline})", { "headerline": $("<div />").text(headerLine.trim()).html() });
      }

      if (aliases !== "") {
        aliasCount = aliases.split("|").length;
        headerLine += getText(" (<i>{connector}{count} {aliases}</i>)", {
          "connector": ((headerLine !== "") ? "+" : ""),
          "count": String(aliasCount),
          "aliases": ((aliasCount !== 1) ? i18n.txtAliases : i18n.txtAlias)
        });
      }

      $headerLine.find("." + classes.headerLabel).html(headerLine);

      // Content (labels, descriptions, aliases, preview)
      $sub = $("<div />").css("display", "none");
      $table = $("<table />");
      $sub.append($("<div />", { "html": $table }));
      $entry.append($sub);

      $table.append($getEditorLine(lang, label, ids.suggestLabel, classes.label, ids.labelExisting, ids.labelNew, getText(i18n.ttExistingLabel, { "txt": (label === "") ? i18n.txtNothing : label })));
      $table.append($getEditorLine(lang, aliases, ids.suggestAlias, classes.alias, ids.aliasesExisting, ids.aliasesNew, getText(i18n.ttExistingAliases, { "txt": (aliases === "") ? i18n.txtNothing : aliases })));
      $table.append($getEditorLine(lang, description, ids.suggestDesc, classes.desc, ids.descExisting, ids.descNew, getText(i18n.ttExistingDescription, { "txt": (description === "") ? i18n.txtNothing : description })));

      // Article preview
      $preview = $("<table />");
      $previewDiv = $createDiv(classes.preview, ids.previewArea + lang);
      $sub.append($previewDiv);
      $previewDiv.append($preview);

      // Collapsing/expanding
      $headerDiv.click(function(event) {
        toggleCollapseInitial(language, $headerLine, $headerDiv, true, event.target, sitelinks, aliases, cleanDescription);
      });

      $getByClass(classes.main).append($entry);

      // Change handler - color changes, reset button gets enabled/disabled
      $entry.find("." + classes.newValue + " input[type=text]").on("change keypress paste focus textInput input", function() {
        $field = $(this);
        value = $field.val();

        if (value !== $field.attr("oldval")) {
          $field.attr("oldval", value);

          changed = isChanged($field.attr("id"));

          $field.toggleClass(classes.modified, changed);
          $field.parents("tr").find("input[type=button]").first().attr("disabled", ! changed);
        }
      });

      setExistingOrSuggestion(lang, ids.labelNew, label, newLabel, ids.suggestLabel);

      // Auto-expand Babel languages
      // - with sitelink and no label or description OR
      // - with sitelink and no description, but a description suggestion from Wikidata statements OR
      // - if they are the only one OR
      // - if the "always expand" option is used
      if (forceShow || (isInUserLanguages && lcConfig.alwaysExpandEditorLangs) || (isInUserLanguages && sitelinks.length > 0 && (label === "" || description === "")) || (! isInUserLanguages && lcConfig.alwaysExpandOtherLangs)) {
        toggleCollapseInitial(language, $headerLine, $headerDiv, false, null, sitelinks, aliases, cleanDescription);
      }
    }
  }

  // Displays a error message on the screen
  function showError(text) {
    showStatus(text, true);
  }

  // Display a message
  function showMessage(text) {
    if (text !== undefined) {
      $("<div />").html(text).dialog({
        "buttons": [{
          "click": function() {
            $(this).remove();
          },
          "text": getText(i18n.btnOkay)
        }],
        "title": getText(i18n.intTitle),
        "width": "auto"
      });
    }
  }

  // Add an option checkbox
  function $showOption(name, config) {
    return $("<label />").html(getText(name)).prepend($("<input />", {
      "change": function() {
        lcConfig[config] = this.checked;
      },
      "checked": lcConfig[config],
      "title": getText(name),
      "type": "checkbox"
    }));
  }

  // Display the configuration dialog
  function showOptions() {
    var $fldEditorLanguages = $("<label />").html(getText(i18n.txtEditorLanguages)).append($("<input />", {
      "change": function() {
        var inputLanguages;
        var inputValidated = true;

        if (this.value !== undefined && this.value !== "") {
          inputLanguages = this.value.toLowerCase().split(",");
        }

        if (inputLanguages === undefined || inputLanguages.length === 0) {
          return;
        }

        // Validate input
        $.each(inputLanguages, function() {
          var lang = this;
          if (lcGlobal.languages[lang] === undefined) {
            inputValidated = false;
            showMessage(getText(i18n.txtInvalidLanguage, { "code": lang }));
            return false;
          }
        });

        if (inputValidated) {
          lcGlobal.userLanguages = inputLanguages;

          // Apply changes in UI
          lcGlobal.languages = {};
          initProjects();
        }
      },
      "value": lcGlobal.userLanguages.toString(),
      "title": getText(i18n.txtEditorLanguages),
      "type": "text"
    }));

    var $content = $("<div />").append($fldEditorLanguages);
    $content.append("<div />").append($showOption(i18n.txtSkipIfEditorLangsSetOrNoLinks, "skipIfEditorLangsSetOrNoLinks"));
    $content.append("<div />").append($showOption(i18n.txtSkipIfEditorLangsSet, "skipIfEditorLangsSet"));
    $content.append("<div />").append($showOption(i18n.txtAlwaysExpandEditorLangs, "alwaysExpandEditorLangs"));
    $content.append("<div />").append($showOption(i18n.txtAlwaysExpandOtherLangs, "alwaysExpandOtherLangs"));
    $content.append("<div />").append($showOption(i18n.txtCleanDescription, "cleanDescription"));
    $content.append("<div />").append($showOption(i18n.txtAutoFill, "autoFill"));
    $content.append("<div />").append($showOption(i18n.txtDebugMode, "debugMode"));

    $content.dialog({
      "buttons": [{
        "text": getText(i18n.btnOkay),
        "click": function() {
          $( this ).dialog("close");
        }
      }],
      "html": $content,
      "modal": true,
      "title": getText(i18n.btnOptions)
    });
  }

  // Displays a status message on the screen
  function showStatus(text, error) {
    var $bodyContent = $getById(ids.mwBodyContent);

    debug(text);

    if (error) {
      $getByClass(classes.main).empty();
      $bodyContent.append("<p><b>" + text + "</b></p>");
      $bodyContent.find("." + classes.mwSpinner).remove();
      enableButtons(false);
      $("form > input").prop("disabled", false);
    } else {
      $bodyContent.append("<p>" + text + "</p>");
    }
  }

  // Collapse or expand an entry
  function toggleCollapse($headerLine, $headerDiv, slide, target) {
    // Don't toggle if the link is clicked
    if (! $(target).is("a")) {
      $headerLine.find("." + classes.triangle).toggleClass(classes.triangleRight);
      $headerDiv.toggleClass(classes.collapsed);

      if (slide) {
        $headerDiv.next().slideToggle();
      } else {
        $headerDiv.next().toggle();
      }
    }
  }

  // Collapses all entries if all are expanded, expands all collapsed entries otherwise
  function toggleCollapseAll() {
    var $collapsed = $getByClass(classes.collapsed);

    if ($collapsed.length === 0) {
      $getByClass(classes.header).click();
    } else {
      $collapsed.click();
    }
  }

  // Expand an entry for the first time, so additionally load preview (or fill existing values)
  function toggleCollapseInitial(language, $headerLine, $headerDiv, slide, target, sitelinks, aliases, cleanDescription) {
    var lang = language.lang;
    var projects = language.projects;

    // Fill fields with existing or suggested values
    if (sitelinks.length === 0) {
      setFieldValue(ids.aliasesNew + lang, aliases, true);
      setFieldValue(ids.descNew + lang, cleanDescription, true);

      loadWikidataContent(lang, false, cleanDescription);
    } else {
      loadWikiContent(language, projects, aliases, cleanDescription, false);
    }

    $headerDiv.unbind("click");
    $headerDiv.click(function(event) {
      toggleCollapse($headerLine, $headerDiv, true, event.target);
    });

    toggleCollapse($headerLine, $headerDiv, slide, target);
  }

  // Enable and activate the suggestion button
  function updateSuggestButton(btnId, fldId, suggestion) {
    var $btnSuggest = $getById(btnId);
    $btnSuggest.attr("disabled", false);
    $btnSuggest.attr("title", getText(i18n.ttSuggest, { "txt": suggestion }));

    $btnSuggest.click(function() {
      setFieldValue(fldId, suggestion, false);
    });
  }
}());
// </nowiki>