_ _    _ _____  ___   __                       
 __      _(_) | _(_)___ / ( _ ) / /_   ___ ___  _ __ ___  
 \ \ /\ / / | |/ / | |_ \ / _ \| '_ \ / __/ _ \| '_ ` _ \ 
  \ V  V /| |   <| |___) | (_) | (_) | (_| (_) | | | | | |
   \_/\_/ |_|_|\_\_|____/ \___/ \___(_)___\___/|_| |_| |_|

Utilisateur:Ltrlg/scripts/TemplateDataEditor.js

Dans le monde d'aujourd'hui, Utilisateur:Ltrlg/scripts/TemplateDataEditor.js est un sujet qui a acquis une pertinence sans précédent. Depuis son émergence, Utilisateur:Ltrlg/scripts/TemplateDataEditor.js a eu un impact sur la façon dont les gens interagissent les uns avec les autres, ainsi que sur la manière dont différents processus et activités sont menés dans la société. Ce phénomène a suscité un grand intérêt dans divers domaines, de l’éducation et de la technologie à la politique et à l’économie. Utilisateur:Ltrlg/scripts/TemplateDataEditor.js a changé la façon dont les décisions sont prises, les idées sont promues et les affaires sont menées, générant un impact significatif sur la vie quotidienne des gens. C’est pourquoi il est nécessaire d’analyser en profondeur ce phénomène et d’en comprendre l’ampleur aujourd’hui.
 /**************************************************

== Français ==
Gadget pour éditer les balises « templatedata » de l’extension « TemplateData » de MediaWiki sans avoir à manipuler le JSON.

Un lien « TDE » est ajouté dans la boîte à outils d’édition.
Il ouvre une fenêtre de modification permettant toutes les modifications autorisées.

* Auteurs : Ltrlg (TDE) & Salix alba (TDS)
* Dernière mise à jour : 19 août 2013

== English ==
Gadget to edit “templatedata” tags for the MediaWiki extension TemplateData without having to edit JSON.

A “TDE” link is added in the edition toolbox when editing a template.
It opens a window allowing all modifications to the template data.

* Authors: Ltrlg (TDE) & Salix alba (TDS)
* Last update: 2013-08-19

== Limitation ==
MediaWiki _allows_ using parameters like '{' (yes, really!)

But here, you can’t use it:
* TDS does not see these parameters
* The default name for a parameter matches /\{+\}/ and TDE does not save if if there is any parameter containing '{'.

Of course, I don’t think anybody uses a '{' parameter. So this is not blocking in many cases.

== Translations ==
* el: Xaris333, Geraki
* en: NicoV
* it: Jacopo Werther
* gl: Elisardojm
* ja: Shirayuki
* ko: Kwj2772
* nl: Wolf Lambert

== Source ==

<syntaxhighlight lang="javascript">

 **************************************************/

function TemplateDataEditor($) {

	var
		/* global objects */
		ui, // The interface (instance of Interface)
		td, // The current (or last) editor (instance of TemplateData)
		
		/* unique identifiers (see trait UniqueElement) */
		uniq = 0,
		
		/* traits */
		UniqueElement, DataForm,
		
		/* tde regexps */
		regExpTwoTags = /<templatedata*>(*)<\/templatedata>/,
		regExpOneTag = /<templatedata*\/>/,
		matchType = 0,
		
		/* indntation */
		defaultIndent = '\t',
		indent = defaultIndent,
		
		/* languages */
		userLanguage = mw.config.get('wgUserLanguage'),
		contentLanguage = mw.config.get('wgContentLanguage'),
		
		/* translations */
		messages = {
			"el": {
				"apply": "Εφαρμογή",
				"cancel": "Ακύρωση",
				"close": "Κλείσιμο",
				"collapse": "Κατάρρευση",
				"colon": ": ",
				"description": "Περιγραφή προτύπου",
				"description-placeholder": "Τοποθετείστε μια περιγραφή του προτύπου",
				"error-description": "Συνέβη κάποιο σφάλμα$2:\n\n$1",
				"error-it-lang-inexistent": "Αυτή η γλώσσα ($1) δεν μπορεί να μετακινηθεί επειδή ο TDE δεν μπορεί να την εντοπίσει", // $1 είναι ο κωδικός της γλώσσας
				"error-name-already-used": "Δεν μπορείτε να μετονομάσετε το στοιχείο επειδή το νέο όνομα χρησιμοποιείται ήδη",
				"error-name-inexistent": "Αυτό το στοιχείο ($1) δεν μπορεί να μετακινηθεί επειδή ο TDE δεν μπορεί να το εντοπίσει.", // $1 είναι η ονομασία
				"error-report": " (Αναφορά σφάλματος)",
				"error-set-inexistent": "Αυτό το σύνολο ($1) δεν μπορεί να μετακινηθεί επειδή ο TDE δεν μπορεί να το εντοπίσει", // $1 είναι το id του συνόλου
				"error-tds-not-loaded": "Η σελίδα δεν έχει φορτώσει",
				"expand": "Ανάπτυξη",
				"invalid-name": "“$1” δεν είναι κατάλληλο κλειδί",
				"it-add": "Προσθέστε μια γλώσσα",
				"it-otherlanguages-show": "Δείξτε τις γλώσσες ($1)", // $1 είναι η ονομασία της ξένης γλώσσας
				"it-otherlanguages-hide": "Αποκρύψτε τις γλώσσες", // $1 είναι ο αριθμός των ξένων γλωσσών
				"it-remove": "Αφαίρεση γλώσσας",
				"param-add": "Προσθήκη παραμέτρου",
				"param-aliases": "Άλλες ονομασίες",
				"param-default": "Προεπιλεγμένη τιμή: ",
				"param-deprecated": "Καταργημένη",
				"param-deprecated-tooltip": "Λεπτομέρειες",
				"param-description": "Περιγραφή",
				"param-inherits": "Μεταβιβάζει",
				"param-label": "Εμφανιζόμενο όνομα",
				"param-name": "Πραγματικό όνομα",
				"param-remove": "Αφαίρεση αυτής της παραμέτρου",
				"param-required": "Απαιτείται",
				"param-type": "Είδος: ",
				"param-type-number": "Αριθμός",
				"param-type-string": "Κείμενο",
				"param-type-string/line": "Κείμενο (μία γραμμή)",
				"param-type-string/wiki-page-name": "Τίτλος σελίδας",
				"param-type-string/wiki-user-name": "Όνομα χρήστη",
				"param-type-unknown": "Άγνωστο",
				"parse-error": "Το δεδομένο δεν μπορεί να αναλυθεί. Αυτό προκαλείται σε γενικές γραμμές από ένα συντακτικό σφάλμα στην JSON ή από την παρουσία των δύο συνόλων δεδομένων.",
				"preload-data": "Προσυμπληρώστε τα δεδομένα",
				"preload-load": "Εκτέλεση",
				"preload-none": "Μην προσυμπληρώσετε",
				"preload-running": "Εκτελείται…",
				"preload-select": "Προσυμπληρώστε από",
				"no-data": "Δεν έχουν βρεθεί δεδομένα. Παρακαλώ προσθέστε μια ετικέτα <templatedata /> ώστε να μπορείτε να την επεξεργαστείτε.",
				"section-description": "Περιγραφή",
				"section-params": "Παράμετροι",
				"section-sets": "Σύνολα",
				"set-add": "Πρόσθεση συνόλου",
				"set-label": "Όνομα: ",
				"set-params": "Παράμετροι",
				"set-remove": "Αφαίρεση αυτού του συνόλου",
				"title": "Τροποποίηση δεδομένων προτύπου",
				"title-documentation": "Τεκμηρίωση",
				"start-tde": "Επεξεργασία δεδομένων προτύπου",
				"use-pipes": " (να διαχωρίζονται με “|”)"
			},
			"en": {
				"apply": "Apply",
				"cancel": "Cancel",
				"close": "Close",
				"collapse": "Collapse",
				"colon": ": ",
				"description": "Description of this template",
				"description-placeholder": "Enter a description of this template here",
				"error-description": "An error happened$2:\n\n$1", // $2 is message(error-report) or '' ; $1 is the error
				"error-it-lang-inexistent": "This language ($1) can’t be removed because TDE can’t find it anymore", // $1 is the language code
				"error-name-already-used": "You can’t rename this element because the new name is already used",
				"error-name-inexistent": "This element ($1) can’t be removed because TDE can’t find it anymore", // $1 is the name
				"error-report": " (report it)",
				"error-set-inexistent": "This set ($1) can’t be removed because TDE can’t find it anymore", // $1 is the id of the set
				"error-tds-not-loaded": "The page hasn’t been loaded",
				"expand": "Expand",
				"invalid-name": "“$1” is not a valid key",
				"it-add": "Add a language",
				"it-otherlanguages-show": "Show the languages ($1)", // $1 is the number of foreign languages
				"it-otherlanguages-hide": "Hide the languages", // $1 is the number of foreign languages
				"it-remove": "Remove this language",
				"param-add": "Add a parameter",
				"param-aliases": "Other names",
				"param-default": "Default value",
				"param-deprecated": "Deprecated",
				"param-description": "Description",
				"param-description-placeholder": "Insert a description of this parameter here",
				"param-inherits": "Documentation inherits from",
				"param-label": "Displayed name",
				"param-label-placeholder": "Enter name here",
				"param-name": "Real name",
				"param-remove": "Remove this parameter",
				"param-required": "Required",
				"param-type": "Type",
				"param-type-number": "Number",
				"param-type-string": "Text",
				"param-type-string/line": "Text (one line)",
				"param-type-string/wiki-page-name": "Page title",
				"param-type-string/wiki-user-name": "User name",
				"param-type-unknown": "Unknown",
				"parse-error": "The data can’t be parsed. This is caused in general by a syntax error in the JSON or by the presence of two datasets.",
				"preload-data": "Prefill the data",
				"preload-load": "Run",
				"preload-none": "Do not prefill",
				"preload-running": "Running…",
				"preload-select": "Prefill from",
				"no-data": "No data has been found. Please, add a <templatedata /> tag to be able to edit it.",
				"section-description": "Description",
				"section-params": "Parameters",
				"section-sets": "Sets",
				"set-add": "Add a set",
				"set-label": "Name",
				"set-params": "Parameters",
				"set-remove": "Remove this set",
				"start-tde": "Modify template data",
				"title": "Modify template data",
				"title-documentation": "documentation",
				"use-pipes": " (separated by pipes “|”)"
			},
			"fr": {
				"apply": "Appliquer",
				"cancel": "Annuler",
				"close": "Fermer",
				"collapse": "Fermer",
				"colon": "\xA0: ",
				"description": "Description de ce modèle",
				"description-placeholder": "Entrez une description de ce modèle",
				"error-description": "Une erreur est survenue$2\xA0:\n\n$1",
				"error-invalid-name": "L’identifiant «\xA0$1\xA0» n’est pas valide",
				"error-it-lang-inexistent": "Cette traduction ($1) ne peut être supprimée car TDE ne la trouve plus",
				"error-name-already-used": "Vous ne pouvez pas renommer cet élément car le nom donné est déjà utilisé",
				"error-name-inexistent": "Cet élément ($1) ne peut être supprimé car TDE ne le trouve plus",
				"error-report": " (signalez-la)",
				"error-set-inexistent": "Cet ensemble ($1) ne peut être supprimé car TDE ne le trouve plus",
				"error-tds-not-loaded": "La page n’a pas pu être chargée",
				"expand": "Ouvrir",
				"it-add": "Ajouter une langue",
				"it-no-language": "Pas de code de langue ($1)",
				"it-otherlanguages-show": "Afficher les traductions ($1)",
				"it-otherlanguages-hide": "Cacher les traductions",
				"it-remove": "Retirer cette langue",
				"param-add": "Ajouter un paramètre",
				"param-aliases": "Autre noms",
				"param-default": "Valeur par défaut",
				"param-deprecated": "Obsolète",
				"param-description": "Description",
				"param-description-placeholder": "Entrez une description de ce paramètre",
				"param-inherits": "Documentation héritée de",
				"param-label": "Nom affiché",
				"param-label-placeholder": "Ajoutez un nom",
				"param-name": "Nom réel",
				"param-remove": "Retirer ce paramètre",
				"param-required": "Obligatoire",
				"param-type": "Type",
				"param-type-number": "Nombre",
				"param-type-string": "Texte",
				"param-type-string/line": "Texte (une ligne)",
				"param-type-string/wiki-page-name": "Titre de page",
				"param-type-string/wiki-user-name": "Nom d’utilisateur",
				"param-type-unknown": "Inconnu",
				"parse-error": "Les données ne peuvent pas être interprétées. Cela arrive généralement lorsqu’il y a une erreur de syntaxe dans le JSON ou lorsqu’il y a deux ensembles de données dans la page.",
				"preload-data": "Pré-remplir les données du modèle",
				"preload-load": "Exécuter",
				"preload-none": "Ne pas pré-remplir",
				"preload-running": "Chargement en cours…",
				"preload-select": "Pré-remplir depuis",
				"no-data": "Aucune donnée n’a été trouvée. Veuillez ajouter une balise <templatedata /> pour pouvoir l’éditer.",
				"section-description": "Description",
				"section-params": "Paramètres",
				"section-sets": "Ensembles",
				"set-add": "Ajouter un ensemble",
				"set-label": "Nom",
				"set-label-placeholder": "Ajoutez un nom",
				"set-params": "Paramètres",
				"set-remove": "Retirer cet ensemble",
				"start-tde": "Modifier les données du modèle",
				"title": "Modifier les données du modèle",
				"title-documentation": "documentation",
				"use-pipes": " (séparés par des tubes «\xA0|\xA0»)"
			},
			"gl": {
				"apply": "Aplicar",
				"cancel": "Cancelar",
				"close": "Pechar",
				"collapse": "Pregar",
				"colon": ": ",
				"description": "Descrición deste modelo",
				"description-placeholder": "Indique aquí unha descrición deste modelo",
				"error-description": "Ocorreu un erro$2:\n\n$1", // $2 é a mensaxe(erro-aviso) ou '' ; $1 é o erro
				"error-it-lang-inexistent": "Este idioma ($1) non pode eliminarse porque TDE non a pode atopar", // $1 é o código do idioma
				"error-name-already-used": "Non pode renomear este elemento porque o novo nome xa se está usando",
				"error-name-inexistent": "Este elemento ($1) non pode eliminarse porque TDE non o pode atopar", // $1 é o nome
				"error-report": " (informar)",
				"error-set-inexistent": "Este conxunto ($1) non pode eliminarse porque TDE non o pode atopar", // $1 é o id do conxunto
				"error-tds-not-loaded": "A páxina non foi cargada",
				"expand": "Expandir",
				"invalid-name": "“$1” non é unha chave válida",
				"it-add": "Engadir un idioma",
				"it-otherlanguages-show": "Mostrar os idiomas ($1)", // $1 é o número de idiomas estranxeiros
				"it-otherlanguages-hide": "Ocultar os idiomas", 
				"it-remove": "Eliminar este idioma",
				"param-add": "Engadir un parámetro",
				"param-aliases": "Outros nomes",
				"param-default": "Valor por defecto",
				"param-deprecated": "Desprezar",
				"param-description": "Descrición",
				"param-description-placeholder": "Inserir aquí a descrición para este parámetro",
				"param-inherits": "Documentación herdada de",
				"param-label": "Nome visualizado",
				"param-label-placeholder": "Engada o nome aquí",
				"param-name": "Nome real",
				"param-remove": "Eliminar este parámetro",
				"param-required": "Requirido",
				"param-type": "Tipo",
				"param-type-number": "Número",
				"param-type-string": "Texto",
				"param-type-string/line": "Texto (unha liña)",
				"param-type-string/wiki-page-name": "Título de páxina",
				"param-type-string/wiki-user-name": "Nome de usuario",
				"param-type-unknown": "Descoñecido",
				"parse-error": "Os datos non poden ser analizados. Normalmente isto ocorre por un erro de sintaxe no JSON ou pola presenza de dous bloques de datos.",
				"preload-data": "Cubra os datos",
				"preload-load": "Executar",
				"preload-none": "Non cubrir",
				"preload-running": "Executando…",
				"preload-select": "Cubrir dende",
				"no-data": "Non se atoparon datos. Por favor, engada unha marca <templatedata /> para poder editala.",
				"section-description": "Descrición",
				"section-params": "Parámetros",
				"section-sets": "Conxuntos",
				"set-add": "Engadir un conxunto",
				"set-label": "Nome",
				"set-params": "Parámetros",
				"set-remove": "Eliminar este conxunto",
				"start-tde": "Modificar datos do modelo",
				"title": "Modificar datos do modelo",
				"title-documentation": "documentación",
				"use-pipes": " (separados por barras “|”)"
			},
			"ja": {
				"apply": "適用",
				"close": "閉じる",
				"colon": ": ",
				"error-description": "エラーが発生しました$2:\n\n$1",
				"param-add": "引数を追加",
				"param-aliases": "その他の名前",
				"param-default": "既定値",
				"param-deprecated": "廃止予定",
				"param-deprecated-tooltip": "詳細: ",
				"param-description": "説明",
				"param-inherits": "継承",
				"param-label": "表示名",
				"param-name": "名前",
				"param-remove": "この引数を除去",
				"param-required": "必須",
				"param-type": "型: ",
				"param-type-number": "数値",
				"param-type-string": "文字列",
				"param-type-string/line": "文字列 (1 行)",
				"param-type-string/wiki-page-name": "ページ名",
				"param-type-string/wiki-user-name": "利用者名",
				"param-type-unknown": "不明",
				"no-data": "データが見つかりませんでした。編集できるようにするには、<templatedata /> タグを追加してください。",
				"section-description": "説明",
				"section-params": "引数",
				"section-sets": "集合",
				"set-add": "集合を追加",
				"set-label": "名前: ",
				"set-params": "引数",
				"set-remove": "この集合を除去",
				"title": "TemplateData の変更",
				"title-documentation": "説明文書",
				"start-tde": "TemplateData の編集",
				"use-pipe": " (パイプ記号「|」で区切る)"
			},
			"ko": {
				"apply": "적용",
				"close": "닫기",
				"colon": ": ",
				"error-description": "오류 발생$2:\n\n$1",
				"param-add": "변수 추가하기",
				"param-aliases": "다른 이름",
				"param-default": "기본값",
				"param-deprecated": "사용 중지",
				"param-deprecated-tooltip": "자세한 정보",
				"param-description": "설명",
				"param-inherits": "상속받을 변수",
				"param-label": "표시될 이름",
				"param-name": "실제 이름",
				"param-remove": "이 변수 제거",
				"param-required": "필수",
				"param-type": "Type: ",
				"param-type-number": "숫자",
				"param-type-string": "문자열",
				"param-type-string/wiki-page-name": "문서 이름",
				"param-type-string/wiki-user-name": "사용자 이름",
				"param-type-unknown": "알 수 없음",
				"no-data": "데이터가 없습니다. 편집을 가능하게 하려면 <templatedata /> 태그를 추가하십시오.",
				"section-description": "설명",
				"section-params": "변수",
				"section-sets": "집합",
				"set-add": "집합 추가하기",
				"set-label": "이름: ",
				"set-params": "변수",
				"set-remove": "이 집합 제거하기",
				"title": "틀 데이터 수정하기",
				"title-documentation": "설명 문서",
				"start-tde": "틀 데이터 편집하기",
				"use-pipes": " (파이프 “|”로 구분)"
			},
			"it": {
				"apply": "Applica (a tuo rischio e pericolo, ogni abuso sarà segnalato)",
				"colon": "\xA0: ",
				"param-add": "Aggiungi un parametro",
				"param-aliases": "Altri nomi",
				"param-default": "Valore di default",
				"param-deprecated": "Deprecato",
				"param-deprecated-tooltip": "Dettagli",
				"param-description": "Descrizione",
				"param-inherits": "Eredità",
				"param-label": "Nome visualizzato",
				"param-name": "Nome effettivo",
				"param-remove": "Rimuovi questo parametro",
				"param-required": "Richiesto",
				"no-data": "Spiacente. Nessun dato è stato trovato. Aggiungere un tag <templatedata /> per poter modificare il template data.",
				"section-description": "Descrizione",
				"section-params": "Parametri",
				"set-add": "Aggiungi un set",
				"title": "Benvenuto in Modifica TemplateData",
				"title-documentation": "tutorial per negati",
				"use-pipes": " (separati da pipes « | »)"
			},
			"nl": {
				"apply": "Oké",
				"cancel": "Annuleren",
				"close": "Sluiten",
				"collapse": "Inklappen",
				"colon": ": ",
				"description": "Beschrijving van dit sjabloon",
				"description-placeholder": "Voer hier een beschrijving van dit sjabloon in",
				"error-description": "Er deed zich een fout voor$2:\n\n$1", // $2 is message(error-report) or '' ; $1 is the error
				"error-it-lang-inexistent": "De taal $1 kan niet worden verwijderd, omdat TDE hem niet meer kan vinden.", // $1 is the language code
				"error-name-already-used": "Dit element kan niet van naam veranderd worden omdat de naam al in gebruik is.",
				"error-name-inexistent": "Het element \"$1\" kan niet worden verwijderd, omdat TDE het niet meer kan vinden.", // $1 is the name
				"error-report": " (rapporteer het)",
				"error-set-inexistent": "De set \"$1\" kan niet worden verwijderd, omdat TDE hem niet meer kan vinden.", // $1 is the id of the set
				"error-tds-not-loaded": "Deze pagina is niet geladen.",
				"expand": "Uitklappen",
				"invalid-name": "\"$1\" is geen valide sleutel",
				"it-add": "Taal toevoegen",
				"it-otherlanguages-show": "Toon de talen ($1)", // $1 is the number of foreign languages
				"it-otherlanguages-hide": "Verberg de talen", // $1 is the number of foreign languages
				"it-remove": "Verwijder deze taal",
				"param-add": "Parameter toevoegen",
				"param-aliases": "Andere namen",
				"param-default": "Standaardwaarde",
				"param-deprecated": "Verouderd",
				"param-description": "Beschrijving",
				"param-description-placeholder": "Voer hier een beschrijving van deze parameter in",
				"param-inherits": "Documentatie wordt overgenomen van",
				"param-label": "Getoonde naam",
				"param-label-placeholder": "Voer hier een naam in",
				"param-name": "Echte naam",
				"param-remove": "Verwijder deze parameter",
				"param-required": "Verplicht",
				"param-type": "Type",
				"param-type-number": "Nummer",
				"param-type-string": "Tekst",
				"param-type-string/line": "Tekst (één lijn)",
				"param-type-string/wiki-page-name": "Paginatitel",
				"param-type-string/wiki-user-name": "Gebruikersnaam",
				"param-type-unknown": "Onbekend",
				"parse-error": "De data kan niet geparsed worden. Dit is meestal de oorzaak van een fout in de JSON-syntax, of wanneer er twee datasets aanwezig zijn.",
				"preload-data": "Vul de data vooraf in",
				"preload-load": "Start",
				"preload-none": "Vul de data niet vooraf in",
				"preload-running": "Bezig…",
				"preload-select": "Vul vooraf in van",
				"no-data": "Er staat nog geen templatedata op dit sjabloon; plaats <templatedata /> onderaan het sjabloon om dit sjabloon te bewerken.",
				"section-description": "Beschrijving",
				"section-params": "Parameters",
				"section-sets": "Sets",
				"set-add": "Voeg een set toe",
				"set-label": "Naam",
				"set-params": "Parameters",
				"set-remove": "Verwijder deze set",
				"start-tde": "Pas de templatedata aan",
				"title": "Templatedata aanpassen",
				"title-documentation": "documentatie",
				"use-pipes": " (gescheiden door pipes \"|\")"
			}
		},
		documentations = { // Local pages (but full link for default)
			"default": '//en.wikipedia.orghttps://wiki386.com/fr/User:NicoV/TemplateDataEditor',
			"enwiki": 'User:NicoV/TemplateDataEditor',
			"frwiki": 'Utilisateur:Ltrlg/TemplateDataEditor',
			"itwiki": 'Wikipedia:VisualEditor/TemplateData'
		};
	
	////////// Translation //////////
	
	function messageLang(name, lang) {
		var
			res,
			i,
			T = ;
		
		if( name == '' ) {
			return '';
		}
		
		if( lang == 'qqx' ) {
			res = '(-tde-' + name;
			if( arguments.length > 2 ) {
				res += ': ';
				for(i=2; i<arguments.length; i++) {
					T.push(arguments);
				}
				res += T.join(', ');
			}
			return res + ')';
		} else {
			if( messages && messages ) {
				res = messages;
			} else if( messages.en ) {
				res = messages.en;
			} else {
				arguments = 'qqx';
				return messageLang.apply(null, arguments);
			}
		
			// Replace vars
			for(i=arguments.length-2; i>0; i--) {
				res = res.replace(new RegExp('\\$'+i, 'g'), arguments);
			}
			
			return res;
		}
	}
	
	function message(name) {
		var args = Array.prototype.slice.call(arguments);
		args.shift();
		args.unshift(name, userLanguage);
		return messageLang.apply(null, args);
	}
	
	function documentationLink() {
		var wiki = mw.config.get('wgWikiID');
		if( documentations.hasOwnProperty( wiki ) ) {
			return mw.util.getUrl( documentations );
		} else {
			return documentations;
		}
	}
	
	////////// Getting & setting text (compatibility with WikEd) //////////
	
	function getText() {
		if( window.wikEd && window.wikEd.useWikEd ) {
			WikEdUpdateTextarea();
		}
		return $('#wpTextbox1').val();
	}
	
	function setText(value) {
		$('#wpTextbox1').val(value);
		if( window.wikEd && window.wikEd.useWikEd ) {
			WikEdUpdateFrame();
		}
	}
	
	////////// Usefull functions //////////
	
	function userError() {
		var e = new Error( message.apply(null, arguments) );
		e.userError = true;
		return e;
	}
	
	function scriptError() {
		var e = new Error( message.apply(null, arguments) );
		e.userError = false;
		return e;
	}
	
	function alertError(e) {
		alert(message(
			'error-description',
			e.message,
			e.userError ? '' : message('error-report')
		));
		console.error('“' + e.message + '”\nError thrown by ' + e.fileName + ' on line ' + e.lineNumber);
	}
	
	function ucfirst(str) {
		return str.toUpperCase() + str.substring(1, str.length);
	}
	
	function norm(str) {
		return ucfirst( str.replace('_', ' ') );
	}
		
	function trim(str) {
		return str.replace(/^\s*(\S.*\S|\S)\s*$/, '$1');
	}

	function trimArray(Arr) {
		var i = 0;
		for(; i<Arr.length; i++) {
			Arr = trim(Arr);
		}
		return Arr;
	}
	
	function strToArr(str) {
		return trimArray( str.split('|') );
	}
	
	function arrToStr(arr) {
		return arr.join(' | ');
	}
	
	function $clear() {
		return $('<div>').addClass('tde-clear');
	}
	
	function selectValue(select) { // Select is a jQuery object $('<select>') or a DOM select node
		var res;
		$(select).children('option').each(function(){
			if( $(this).prop('selected') ) {
				res = $(this).val();
				return false;
			}
		});
		return res;
	}
	
	function getIndent(text) { // Should work fine with any well-indented JSON
		var
			lines = text.split('\n'),
			i,
			maxLength = Infinity,
			indent = defaultIndent,
			localIndent;
		
		for(i=0; i<lines.length; i++) {
			try {
				localIndent = /^(\s*)\S/.exec(lines);
				if( localIndent.length < maxLength && localIndent.length != 0 ) {
					indent = localIndent;
					maxLength = localIndent.length;
				}
			} catch(e) {
				// Nothing to do, just a line without \S
			}
		}
		return indent;
	}
	
	function noCurlyBraceKey(object) {
		for(var i in object) {
			if( //.test(i) ) {
				throw userError('error-invalid-name', i);
			}
		}
	}
	
	function arrayRemoveElement(array, i) {
		var
			newLength = array.length-1,
			j;
		
		for(j=i; j<array.length-1; j++) {
			array = array;
		}
		
		array.length = newLength;
	}
	
	////////// TemplateDataSkeleton (partial, adapted) //////////
	
	function TemplateDataSkeletonFromText(text) {
		
		var
			pat = /\{\{\{(+)(.)/g,  // '{{{' then any char other than {|}\n<
			matches, name, newReq, oldReq,
			params = {};
		
		while( (matches=pat.exec(text)) != null ) {
			name = trim(matches);
			newReq = ( matches== '}' );
			oldReq = ( params == null || params.required ); 
			
			params = {
				required: newReq && oldReq,
				label: norm(name)
			};
			
			pat.lastIndex--; // need to backtrack one character
		}
		
		return { params: params };
	}
	
	function TemplateDataSkeleton(page, cb) {
		
		function error() {
			alertError( scriptError('tds-not-loaded') );
			cb({});
		}
		
		$.ajax({
			url: mw.util.wikiScript('api'),
			data: {
				action: 'query',
				prop: 'revisions',
				titles: page,
				rvprop: 'content',
				format: 'json'
			},
			dataType: 'json',
			error: error,
			success: function( data ) {
				try {
					var pageId = Object.keys(data.query.pages);
					cb( TemplateDataSkeletonFromText(data.query.pages.revisions || '') ); // '' if missing page
				} catch(e) {
					error();
				}
			}
		});
		
	}
	
	////////// Actions //////////
	
	function action(_options) {
		
		var
			options = Object.assign({
				type: null,
				desc: '',
				fn: function(){},
				aClass: '',
				aId: ''
			}, _options),
			img = action.images;
		
		return $('<a>')
			.attr({
				title: options.desc,
				href: '#',
				'class': options.aClass,
				id: options.aId
			})
			.click(function(){
				options.fn.call(this);
				return false;
			})
			.append($('<img>')
				.attr({
					alt: options.desc,
					src: '//upload.wikimedia.org/wikipedia/commons/thumb/'
						+ img.hashStart + '/'
						+ img.hashStart + '/'
						+ img.name + '/'
						+ '24px-' + img.name + '.png'
				})
			);
	}
	
	action.images = {
		add: {
			hashStart: '8b',
			name: 'VisualEditor_-_Icon_-_Add-item.svg'
		},
		remove:  {
			hashStart: '0e',
			name: 'VisualEditor_-_Icon_-_Remove-item.svg'
		},
		close:  {
			hashStart: '8d',
			name: 'VisualEditor_-_Icon_-_Close.svg'
		},
		expand:  {
			hashStart: '2f',
			name: 'VisualEditor_-_Icon_-_Expand.svg'
		},
		collapse:  {
			hashStart: '32',
			name: 'VisualEditor_-_Icon_-_Collapse.svg'
		},
		expand_inline:  {
			hashStart: 'd3',
			name: 'VisualEditor_-_Icon_-_Move-ltr.svg'
		},
		collapse_inline:  {
			hashStart: '4e',
			name: 'VisualEditor_-_Icon_-_Move-rtl.svg'
		}
	};
	
	////////// class Interface //////////
	
	function Interface() {
		
		var that = this;
		
		this.$title = $('<h2>')
			.text( message('title') )
			.append(document.createTextNode(' ('))
			.append($('<a>')
				.attr('href', documentationLink() )
				.text( message('title-documentation') )
			)
			.append(document.createTextNode(')'));
		
		this.$body = $('<div>').attr('id', 'tde-body');
		
		this.$buttonContainer = $('<div>').addClass('tde-buttons');
		
		this.$cont = $('<div>')
			.attr('id', 'tde')
			.append($('<div>').attr('id', 'tde-mask'))
			.append($('<div>')
			.attr('id', 'tde-dialog')
			.append( this.$title )
			.append( action({type: 'close', desc: message('close'), fn: function() { that.close(); }, aId: 'tde-close'}) )
			.append( this.$body )
			.append( this.$buttonContainer )
			)
			.hide();
		
		$(document.body).append(this.$cont);
	}
	
	Interface.prototype = {
		clear: function() {
			this.$body.children().remove();
			this.deleteButtons();
		},
		
		close: function() {
			this.$cont.fadeOut();
		},
		
		open: function() {
			this.$cont.fadeIn();
		},
		
		addCancelButton: function() {
			var that = this;
			this.addButton('cancel', function(){
				that.close();
			});
		},
		
		addButton: function(msg, fn) {
			this.$buttonContainer.append($('<input>')
				.attr('type', 'button')
				.val( message(msg) )
				.click(fn)
			);
		},
		
		deleteButtons: function() {
			this.$buttonContainer.children().remove();
		},
		
		replaceButton: function(fn) {
			this.deleteButtons();
			this.$buttonContainer.append($('<input>')
				.attr({
					id: 'tde-apply',
					type: 'button'
				})
				.val( message('apply') )
				.click(fn)
			);
		}
	};
	
	ui = new Interface;
	
	////////// class Renamer //////////
	
	function Renamer(id, name, parent, readonly) {
		this.name = name;
		this.parent = parent;
		this.readonly = !! readonly;
		
		var that = this;
		
		this.$input = $('<input>')
			.addClass('tde-renamer-name-input')
			.attr({
				type: 'text',
				id: id
			})
			.prop('readonly', this.readonly)
			.blur(function(){
				that.exec();
			})
			.val(name);
	}
	
	Renamer.prototype = {
		getNode: function() {
			return this.$input;
		},
		
		exec: function() {
			var newName = this.$input.val();
			try {
				this.parent.renameElement(this.name, newName);
				this.name = newName;
			} catch(e) {
				alertError( e );
			}
			this.$input.val(this.name);
		}
	};
	
	////////// trait UniqueElement //////////
	
	UniqueElement = {
	
		defineUniq: function() {
			this.uniq = uniq;
			uniq++;
		}
	
	};
	
	////////// abstract class InterfaceText use UniqueElement //////////
	
	function InterfaceText() { /* Call to this.construct from subclasses */ }
	
	InterfaceText.prototype = Object.assign({}, UniqueElement, {
		construct: function(values, labelText, placeholderMessage, $cont) {
			this.defineUniq();
			if( $.type(values) != 'object' ) {
				this.data = {};
				this.data = values;
			} else {
				this.data = values;
				if( typeof this.data == 'undefined' ) {
					this.data = '';
				}
			}
			this.numberOtherLanguages = Object.keys( this.data ).length - 1;
			this.label = labelText + (this.useColon ? message('colon') : '');
			this.placeholder = placeholderMessage;
			this.$cont = $cont.addClass('tde-it');;
			this.createContent();
			this.createInputs();
			this.hideOther();
		},
		
		getClasses: function(lang) {
			return 'tde-it-lang tde-it-lang-' + (lang || contentLanguage);
		},
		
		createInputs: function() {
			var i;
			for( i in this.data ) {
				this.createInput(i, i == contentLanguage);
			}
		},
		
		onChange: function(domInput) {
			this.data[
				/(^|\s)tde-it-lang-(\S+)(\s|$)/.exec( $(domInput).closest('.tde-it-lang').attr('class') )
			] = domInput.value;
		},
		
		input: function($input, lang) {
			var that = this;
			return $input
				.val( this.data )
				.addClass('tde-it-input')
				.attr('placeholder', messageLang(this.placeholder, lang))
				.change(function(){
					that.onChange(this);
				});
		},
		
		getPlaceholder: function(lang) {
			return messageLang(this.placeholder, lang);
		},
		
		hideOther: function() {
			this.$expand.show();
			this.$collapse.hide();
			this.$cont.find('.tde-it-lang').each(function(){
				if(
					! $(this).hasClass('tde-it-lang-'+contentLanguage)
					&& ! $(this).hasClass('tde-it-lang-'+userLanguage)
				) {
					$(this).hide();
				}
			});
			this.$add.hide();
		},
		
		showOther: function() {
			this.$expand.hide();
			this.$collapse.show();
			this.$cont.find('.tde-it-lang').css('display', ''); // .show() does .css('display', 'inline'), but here we need inline-block, as declared in the stylesheet
			this.$add.show();
		},
		
		updateNumber: function() {
			this.$expand.attr('title', message('it-otherlanguages-show', this.numberOtherLanguages));
			this.$expand.find('img').attr('alt', message('it-otherlanguages-show', this.numberOtherLanguages));
			this.$collapse.attr('title', message('it-otherlanguages-hide', this.numberOtherLanguages));
			this.$collapse.find('img').attr('alt', message('it-otherlanguages-hide', this.numberOtherLanguages));
		},
		
		actionSuffix: '',
		useColon: true,
		
		createToggleLinks: function(){
			var that = this;
			
			this.$expand = action({
				type: 'expand' + this.actionSuffix,
				aClass: 'tde-it-expand',
				fn: function(){
					that.showOther();
					return false;
				}
			});
			
			this.$collapse = action({
				type: 'collapse' + this.actionSuffix,
				aClass: 'tde-it-collapse',
				fn: function(){
					that.hideOther();
					return false;
				}
			});
			
			this.updateNumber();
			return $().add(this.$collapse).add(this.$expand);
		},
		
		createAddLink: function() {
			var that = this;
			this.$add = action({
				type: 'add',
				desc: message('it-add'),
				fn: function(){
					that.addInput();
				},
				aClass: 'tde-add-language'
			});
			return this.$add;
		},
		
		getLangDiv: function(lang) {
			var res;
			this.$cont.find('.tde-it-lang').each(function(){
				if( $(this).hasClass('tde-it-lang-'+lang) ) {
					res = $(this);
					return false;
				}
			});
			return res;
		},
		
		getValueInput: function(lang) {
			return this.getLangDiv(lang).find('.tde-it-input');
		},
		
		renameElement: function(from, to) {
			if( this.data.hasOwnProperty(to) ) {
				if( to != from ) {
					throw userError('error-name-already-used');
				}
			} else if( this.data.hasOwnProperty(from) ) {
				this.getLangDiv(from).attr('class', this.getClasses(to));
				this.getValueInput(to).attr('placeholder', this.getPlaceholder(to)); // "this.getValueInput(to)": "to" because the class have been modified by the line before
				this.data = this.data;
				delete this.data;
			} else {
				throw scriptError('error-it-lang-inexistent', from);
			}
		},
		
		removeElement: function(lang) {
			if( this.data.hasOwnProperty(lang) ) {
				delete this.data;
				this.getLangDiv(lang).remove();
			} else {
				throw scriptError('error-it-lang-inexistent', lang);
			}
		},
		
		$removeLang: function(readonly) {
			var that = this;
			if( readonly ) {
				return null;
			} else {
				return action({
					type: 'remove',
					desc: message('it-remove'),
					fn: function(){
						var $lang = $(this).closest('.tde-it-lang');
						try {
							that.removeElement( $lang.find('.tde-renamer-name-input').val() );
							$lang.remove();
						} catch(e) {
							alertError(e);
						}
					}
				});
			}
		},
		
		untitledId: 0,
		
		addInput: function() {
			this.untitledId++;
			var lang = '{' + this.untitledId + '}';
			this.data = '';
			this.createInput(lang, false);
		},
		
		getData: function() {
			noCurlyBraceKey( this.data );
			if( Object.keys(this.data).length == 1 ) {
				return this.data || undefined;
			} else {
				// TODO delete empty strings
				return this.data;
			}
		},
		
		/* abstract */ createContent: function() { },
		/* abstract */ createInput: function(lang, readonly) { }
	});
	
	////////// class InterfaceTextBlock extends InterfaceText //////////
	
	function InterfaceTextBlock() {
		this.construct.apply(this, arguments);
		this.$cont.addClass('tde-it-block');
	}
	
	InterfaceTextBlock.prototype = Object.assign(new InterfaceText(), {
		createContent: function() {
			
			this.$tbody = $('<tbody>');
			
			var $caption = $('<caption>')
				.text(this.label)
				.append(this.createToggleLinks());
			
			this.$cont
				.append($('<table>')
					.append( $caption )
					.append( this.$tbody )
				)
				.append( this.createAddLink() )
				.append( $clear() );
			
		},
		
		useColon: false,
		
		createInput: function(lang, readonly) {
			this.$tbody.append($('<tr>')
				.attr('class', this.getClasses(lang))
				.append($('<th>')
					.attr('scope', 'row')
					.append( new Renamer(null, lang, this, readonly).getNode() )
				)
				.append($('<td>')
					.append( this.input($('<textarea>'), lang) )
				)
				.append($('<td>')
					.append( this.$removeLang(readonly) )
				)
			);
		}
	});
	
	////////// class InterfaceTextInline extends InterfaceText //////////
	
	function InterfaceTextInline() {
		this.construct.apply(this, arguments);
		this.$cont.addClass('tde-it-inline');
	}
	
	InterfaceTextInline.prototype = Object.assign(new InterfaceText(), {
		createContent: function() {
			
			this.$span = $('<span>');
			
			this.$cont
				.text( this.label )
				.append( this.$span )
				.append( this.createAddLink() )
				.append( this.createToggleLinks() );
			
		},
		
		actionSuffix: '_inline',
		
		createInput: function(lang, readonly) {
			this.$span.append($('<span>')
				.attr('class', this.getClasses(lang))
				.append( new Renamer(null, lang, this, readonly).getNode() )
				.append( document.createTextNode( message('colon') ) )
				.append( this.input($('<input>').attr('type', 'text'), lang) )
				.append( this.$removeLang(readonly) )
			);
		}
	});
	
	////////// trait DataForm //////////
	
	DataForm = {
		
		getCont: function( name ) {
			return this;
		},
		
		newline: function(cont) {
			this.getCont(cont).append('<br>');
			return this;
		},
		
		addInput: function(cont, type, key, label) {
			var
				$cont = this.getCont(cont),
				that = this,
				inputClass = 'tde-' + this.type + '-' + key,
				labelClass = inputClass + '-label',
				id = inputClass + '-' + this.uniq,
				$input;
			
			function addLabel(colon) {
				$cont
					.append($('<label>')
						.addClass(labelClass)
						.attr('for', id)
						.text(
							label
							+ ( type == 'array' ? message('use-pipes') : '' )
							+ ( colon ? message('colon') : '' ) )
					);
			}
			
			function addInput() {
				$cont
					.append( $input );
			}
			
			switch(type) {
				case 'text':
					$input = $('<input>')
						.addClass(inputClass)
						.attr({
							type: 'text',
							id: id
						})
						.val( this.data || '' )
						.change(function(){ that.change(key, this.value); });
					addLabel(true);
					addInput();
					break;
				case 'array':
					$input = $('<input>')
						.addClass(inputClass)
						.attr({
							type: 'text',
							id: id
						})
						.val( arrToStr( this.data ||  ) )
						.change(function(){ that.changeArray(key, this.value); });
					addLabel(true);
					addInput();
					break;
				case 'checkbox':
					$input = $('<input>')
						.addClass(inputClass)
						.attr({
							type: 'checkbox',
							id: id
						})
						.prop('checked', this.data)
						.change(function(){ that.change(key, this.checked); });
					addInput();
					addLabel(false);
					break;
				case 'typeSelect':
					$input = $('<select>')
						.addClass(inputClass)
						.attr('type', 'text')
						.attr('id', id)
						.append( Param.type('unknown', this.data.type) )
						.append( Param.type('number', this.data.type) )
						.append( Param.type('string', this.data.type) )
						.append( Param.type('string/line', this.data.type) )
						.append( Param.type('string/wiki-user-name', this.data.type) )
						.append( Param.type('string/wiki-page-name', this.data.type) )
						.change(function(){ that.changeType(this); });
					addLabel(true);
					addInput();
					break;
			}
			this = $input;
			return this;
		},
		
		addDescription: function(cont) {
			var $div = $('<div>');
			
			this.getCont(cont).append($div);
			
			this.description = new InterfaceTextBlock(
				this.data.description || '',
				message(this.type + '-description'),
				this.type + '-description-placeholder',
				$div
			);
			
			return this;
		},
		
		addLabel: function(cont) {
			var $span = $('<span>');
			
			this.getCont(cont).append($span);
			
			this.label = new InterfaceTextInline(
				this.data.label || '',
				message(this.type + '-label'),
				this.type + '-label-placeholder',
				$span
			);
			
			return this;
		},
		
		change: function(key, newValue) {
			if( key == 'deprecated' ) {
				this.data = newValue || false;
			} else {
				this.data = newValue;
			}
		},
		
		changeArray: function(key, str) {
			this.change(key, strToArr(str));
		},
		
		changeType: function(domSelect) {
			this.change('type', selectValue(domSelect));
		}
		
	};
	
	////////// class Param uses UniqueElement, DataForm //////////
	
	function Param(params, name, $list, data) {
		this.defineUniq();
		
		var
			that = this;
		
		this.params = params;
		this.name = name;
		this.$cont = $('<li>');
		this.data = data;
		
		this.$head = $('<div>');
		this.$body = $('<div>');
		
		this.$expand = action({type: 'expand', desc: message('expand'), fn: function() { that.expand(); }, aClass: 'tde-param-expand'});
		this.$collapse = action({type: 'collapse', desc: message('collapse'), fn: function() { that.collapse(); }, aClass: 'tde-param-collapse'});
		
		if(
			( data.required || data.description == '' )
			&& ! data.inherits
		) {
			this.expand();
		} else {
			this.collapse();
		}
		
		this.$cont
			.append( action({type: 'remove', desc: message('param-remove'), fn: function() { that.remove(); }, aClass: 'tde-remove-line'}) )
			.append( this.$expand )
			.append( this.$collapse )
			.append( this.$head )
			.append( this.$body )
			.append( $clear() );
		
		this.$head
			.append($('<label>')
				.attr('for', 'tde-paramName-'+this.uniq)
				.text( message('param-name') + message('colon') )
			)
			.append( (new Renamer('tde-paramName-'+this.uniq, name, params)).getNode() );
		
		this
			.addInput('head', 'checkbox', 'required', message('param-required'))
			.addInput('head', 'text', 'inherits', message('param-inherits'))
			.addLabel('body')
			.newline('body')
			.addInput('body', 'typeSelect', 'type', message('param-type'))
			.addInput('body', 'text', 'default', message('param-default'))
			.newline('body')
			.addInput('body', 'text', 'deprecated', message('param-deprecated'))
			.newline('body')
			.addInput('body', 'array', 'aliases', message('param-aliases'))
			.addDescription('body');
		
		$list.append( this.$cont );
	}
	
	Param.prototype = Object.assign({}, UniqueElement, DataForm, {
		
		type: 'param',
		
		expand: function() {
			this.$collapse.show();
			this.$expand.hide();
			this.$body.show();
		},
		
		collapse: function() {
			this.$collapse.hide();
			this.$expand.show();
			this.$body.hide();
		},
		
		remove: function() {
			try {
				this.params.removeElement(this.name);
				this.$cont.remove();
			} catch(e) {
				alertError( e );
			}
		},
		
		getCont: function( name ) {
			return this;
		},
		
		getData: function() {
			this.data.label = this.label.getData();
			this.data.description = this.description.getData();
			return this.data;
		}
	});
	
	Param.type = function(type, currentType) {
		return $('<option>')
			.val(type)
			.text( message('param-type-' + type) )
			.prop('selected', currentType == type);
	};
	
	////////// class Params //////////
	
	function Params($list, data) {
		this.$list = $list;
		this.data = data;
		this.params = {};
		
		var i, that = this;
		
		for( i in data ) {
			this.createLi(i);
		}
		
		this.$list.first().after(
			action({type: 'add', fn: function(){that.addItem();return false;}, desc: message('param-add'), aClass: 'tde-add-line'})
		);
	}
	
	Params.prototype = {
		
		untitledId: 0,
		
		addItem: function() {
			this.untitledId++;
			var name = '{' + this.untitledId + '}';
			this.data = {};
			this.createLi(name);
		},
		
		createLi: function(name) {
			this.params = new Param(this, name, this.$list, this.data);
		},
		
		renameElement: function(from, to) {
			if( this.data.hasOwnProperty(to) ) {
				if( to != from ) {
					throw userError('error-name-already-used');
				}
			} else if( this.data.hasOwnProperty(from) ) {
				this.data = this.data;
				this.params = this.params;
				delete this.data;
				delete this.params;
			} else {
				throw scriptError('error-name-inexistent', from);
			}
		},
		
		removeElement: function(name) {
			if( this.data.hasOwnProperty(name) ) {
				delete this.data;
				delete this.params;
			} else {
				throw scriptError('error-name-inexistent', name);
			}
		},
		
		getData: function() {
			for( var i in this.data ) {
				this.data = this.params.getData();
			}
			noCurlyBraceKey( this.data );
			return this.data;
		}
	};
	
	////////// class Set uses UniqueElement, DataForm //////////
	
	function Set(sets, $list, data) {
		this.defineUniq();
		
		var
			that = this;
		
		this.sets = sets;
		this.$cont = $('<li>');
		this.data = data;
		
		this.$body = $('<div>');
		
		this.$cont
			.append( action({type: 'remove', desc: message('set-remove'), fn: function() { that.remove(); }, aClass: 'tde-remove-line'}) )
			.append( this.$body )
			.append( $clear() );
		
		this
			.addLabel('body')
			.newline('body')
			.addInput('body', 'array', 'params', message('set-params'));
		
		$list.append( this.$cont );
	}
	
	Set.prototype = Object.assign({}, UniqueElement, DataForm, {
		
		type: 'set',
		
		remove: function() {
			try {
				this.sets.removeElement(this);
				this.$cont.remove();
			} catch(e) {
				alertError( e );
			}
		},
		
		getData: function() {
			this.data.label = this.label.getData();
			return this.data;
		}
	});
	
	Param.type = function(type, currentType) {
		return $('<option>')
			.val(type)
			.text( message('param-type-' + type) )
			.prop('selected', currentType == type);
	};
	
	////////// class Sets //////////
	
	function Sets($list, data) {
		this.$list = $list;
		this.data = data;
		this.sets = ;
		
		var i, that = this;
		
		for(i = 0; i<data.length; i++ ) {
			this.createLi(i);
		}
		
		this.$list.first().after(
			action({type: 'add', fn: function(){that.addItem();return false;}, desc: message('set-add'), aClass: 'tde-add-line'})
		);
	}
	
	Sets.prototype = {
		
		addItem: function() {
			this.data.push({});
			this.createLi(this.data.length-1);
		},
		
		createLi: function(i) {
			this.sets = new Set(this, this.$list, this.data);
		},
		
		removeElement: function(set) {
			var i = this.sets.indexOf(set);
			if( i >= 0 ) {
				arrayRemoveElement(this.data, i);
				arrayRemoveElement(this.sets, i);
			} else {
				throw scriptError('error-set-inexistent', name);
			}
		},
		
		getData: function() {
			for(var i=0; i<this.data.length; i++) {
				this.data = this.sets.getData();
			}
			return this.data;
		}
	};
	
	////////// class TemplateData //////////
	
	function TemplateData(data) {
		
		this.data = data;
		this.cleanData();
		
		ui.clear();
		
		ui.$title
			.text( message('title') )
			.append(document.createTextNode(' ('))
			.append($('<a>')
				.attr('href', documentationLink() )
				.text( message('title-documentation') )
			)
			.append(document.createTextNode(')'));
		
		this.dataToUi();
		
		ui.open();
	}
	
	TemplateData.regexpDouble = /<templatedata*>(*)<\/templatedata>/;
	TemplateData.regexpSimple = /<templatedata*\/>/;
	
	TemplateData.prototype = {
		
		cleanData: function() {
			if( typeof this.data.description == 'undefined' ) this.data.description = '';
			if( $.type(this.data.params) != 'object' ) this.data.params = {};
			if( $.type(this.data.sets) != 'array' ) this.data.sets = ;
		},
		
		dataToUi: function() {
			this.descriptionToUi();
			this.paramsToUi();
			this.setsToUi();
		},
		
		descriptionToUi: function() {
			var $div = $('<div>').attr('id', 'tde-desc-cont');
			
			ui.$body
				.append($('<h3>')
					.text( message('section-description') )
				)
				.append($div);
			
			this.description = new InterfaceTextBlock(this.data.description, message('description'), 'description-placeholder', $div);
		},
		
		paramsToUi: function() {
			var $list = $('<ul>');
			
			ui.$body.append($('<h3>')
					.text( message('section-params') )
				)
				.append($list);
			
			this.params = new Params($list, this.data.params);
		},
		
		setsToUi: function() {
			var $list = $('<ul>');
			
			ui.$body.append($('<h3>')
					.text( message('section-sets') )
				)
				.append($list);
			
			this.sets = new Sets($list, this.data.sets);
		},
		
		getData: function() {
			this.data.description = this.description.getData();
			this.data.params = this.params.getData();
			this.data.sets = this.sets.getData();
			if( this.data.sets.length == 0 ) {
				delete this.data.sets;
			}
			return this.data;
		}
	};
	
	///////// Starting /////////
	
	function write() {
		try {
			var
				newContent = '<templatedata>\n' + JSON.stringify(td.getData(), null, indent) + '\n</templatedata>',
				text = getText();
		
			switch(matchType) {
				case 1:
					text = text.replace(regExpOneTag, newContent);
					break;
				case 2:
					text = text.replace(regExpTwoTags, newContent);
					break;
			}
			
			setText(text);
			ui.close();
		} catch(e) {
			alertError(e);
		}
	}
	
	function startWithData(data) {
		td = new TemplateData(data);
		ui.addCancelButton();
		ui.addButton('apply', write);
	}
	
	function startWithTds(text) {
		var
			T = mw.config.get('wgPageName').replace('_', ' ').split('/'),
			i,
			page = '',
			$preload = $('<div>').attr('id', 'tde-preload'),
			$select = $('<select>').attr('id', 'tde-preload-select');
		
		function $option(val, _label) {
			var label = _label || val;
			return $('<option>')
				.val( val )
				.text( label );
		}
		
		for(i=0; i<T.length; i++) {
			page += (page ? '/' : '') + T;
			T = page;
		}
		
		$select.append( $option('', message('preload-none')) );
		
		for(i=T.length-1; i>-1; i-- ) {
			$select.append( $option( T ) );
		}
		
		ui.clear();
		
		ui.$title.text( message('preload-data') );
		
		$preload
			.append( $('<label>')
				.attr('for', 'tde-preload-select')
				.text( message('preload-select') + message('colon') )
			)
			.append($select);
		
		ui.$body.html( $preload );
		
		ui.addCancelButton();
		ui.addButton('preload-load', function(){
			
			var template = selectValue($select);
			
			if( ! template ) {
				startWithData({});
			} else if( template == T ) { // The current template
				startWithData(
					TemplateDataSkeletonFromText(text)
				);
			} else {
				$preload
					.text( message('preload-running') )
					.addClass('tde-preload-loading');
			
				ui.deleteButtons();
			
				TemplateDataSkeleton(template, startWithData);
			}
			
		});
		
		ui.open();
	}
	
	function startTDE() {
		var
			text = getText(),
			content,
			data = null;
		
		if( regExpTwoTags.test(text) ) {
			matchType = 2;
			content = regExpTwoTags.exec( text );
			indent = getIndent(content);
			
			if( /^\s*(\{\s*\})?\s*$/.test(content) ) {
				startWithTds(text);
			} else {
				try {
					data = JSON.parse( content );
				} catch(e) {
					data = null;
				}
			
				if( data != null ) {
					startWithData(data);
				} else {
					alertError(userError('parse-error'));
				}
			}
			
		} else if( regExpOneTag.test(text) ) {
			matchType = 1;
			indent = defaultIndent;
			startWithTds(text);
		} else {
			matchType = 0;
			alertError(userError('no-data'));
		}
		
	}
	
	////////// Add a link to start TDE //////////
	
	function addTdeLink() {
		var
			$img = $('<img>').attr('alt', message('start-tde')),
			$link = $('<a>')
				.attr({
					href: '#',
					title: message('start-tde')
				})
				.append($img)
				.click(function(){
					startTDE();
					return false;
				});
		
		if( mw.user.options.get('usebetatoolbar') ) {
			$img.attr('src', '//upload.wikimedia.org/wikipedia/commons/d/d8/TemplateData_-_Icon_-_Beta_toolbar.png');
			mw.loader.using('ext.wikiEditor', function(){
				$('#wikiEditor-ui-toolbar .section-main .group-insert').before($('<div>')
					.addClass('group group-tde')
					.append($link)
				);
			});
		} else {
			$img.attr('src', '//upload.wikimedia.org/wikipedia/commons/6/63/TemplateData_-_Icon_-_Old_toolbar.png');
			$('#toolbar').append($link);
		}
	}
	
	addTdeLink();
	
	/*
	$(
		mw.util.addPortletLink('p-tb', '#', 'TemplateData', 'tde-toolbox', message('toolbox-label'))
	).click(function(){
		startTDE();
		return false;
	});
	*/
}

if( .indexOf( mw.config.get('wgNamespaceNumber') ) !== -1 && .indexOf( mw.config.get('wgAction') ) !== -1 ) {
	mw.loader.load(
		'//fr.wikipedia.org/w/index.php?title=Utilisateur:Ltrlg/styles/TemplateDataEditor.css&action=raw&ctype=text/css',
		'text/css'
	);
	mw.loader.using('mediawiki.util', function () {
		$(document).ready(TemplateDataEditor);
	});
}

// </syntaxhighlight> {{catégorisation JS|TemplateDataEditor}}