MediaWiki:Gadget-terminology.js
Appearance
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)
- Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/**
* Terminology gadget
*
* This gadget adds functionality to keep terminology consistent for a language.
* Read all about it on [[Project:Terminology gadget]].
*
* @author Jon Harald Søby
* @version 1.4.4 (2024-08-14)
*/
function initTerminology( initData ) {
const initConditions = initData.initConditions,
user = mw.user,
username = mw.user.getName(),
api = new mw.Api(),
dir = $( 'html' ).attr( 'dir' );
let terminologyLanguage = initData.terminologyLanguage,
originalTerminologyLanguage = initData.originalTerminologyLanguage,
termlist = initData.termlist,
termlistIsInvalid = validateTermlist( termlist ),
termlistParsed = initData.termlistParsed,
termlistRevision = initData.termlistRevision,
windowManager = new OO.ui.WindowManager();
$( document.body ).append( windowManager.$element );
if ( termlistIsInvalid ) {
return mw.notify( termlistIsInvalid, { title: 'Terminology gadget', type: 'error' } );
}
mw.hook( 'mw.translate.translationView.stateChange').add( function( state ) {
if ( originalTerminologyLanguage !== state.language ) {
fetchTermlist( state.language ).then( function( newlangTermlist ) {
terminologyLanguage = newlangTermlist[ 2 ];
originalTerminologyLanguage = state.language;
termlist = newlangTermlist[ 0 ];
termlistRevision = newlangTermlist[ 1 ];
return newlangTermlist;
}).then( function( newlangTermlist ) {
parseTermlist( termlist, originalTerminologyLanguage ).then( function( parsed ) {
termlistParsed = parsed;
return;
});
}).catch( function( err ) {
console.warn( 'Terminology gadget error', err );
return;
});
}
});
/**
* Return a list of terms without the aliases
* (helper function used by the edit dialog).
*
* @returns {array}
*/
function termlistWithoutAliases() {
let result = [];
for ( const term in termlist ) {
if ( !( '@alias' in termlist[ term ] ) ) {
result.push( term );
}
}
return result;
}
/**
* Find the correct term definition in the termlist, including if the
* input is an alias.
*
* @param {string} word
* @returns {(object|boolean)}
*/
function termlistLookup( word ) {
if ( word in termlist ) {
if ( '@alias' in termlist[ word ] ) {
return termlist[ termlist[ word ][ '@alias' ]];
} else {
return termlist[ word ];
}
} else {
return false;
}
}
/**
* Find the aliases of a term.
*
* @param {string} word
* @returns {array}
*/
function findAliases( word ) {
let aliasList = [];
for ( const term in termlist ) {
if ( termlist[ term ][ '@alias' ] && termlist[ term ][ '@alias' ] == word ) {
aliasList.push( term );
}
}
return aliasList;
}
/**
* Generate suggested alias values for a new term.
*
* @param {string} word
* @returns {array}
*/
function generateSuggestions( word ) {
let suggestions = [],
root = word.slice( 0, -1 );
if ( word.endsWith( 'ies' ) ) {
word = word.replace( /ies$/, 'y' );
root = word.slice( 0, -1 );
suggestions.push( word );
} else if ( word.endsWith( 's' ) && !word.endsWith( 'ss' ) ) {
word = root;
root = root.slice( 0, -1 );
suggestions.push( word );
}
if ( /[sxz]$/.test( word ) ) {
suggestions.push( word + 'es', word + 'ed', word + 'ing' );
} else if ( /[^aeiou]y$/.test( word ) ) {
suggestions.push( root + 'ies', root + 'ied', word + 'ing' );
} else if ( word.slice( -1 ) === 'e' ) {
suggestions.push( word + 's', word + 'd', root + 'ing' );
} else {
suggestions.push( word + 's', word + 'ed', word + 'ing' );
}
return suggestions;
}
/**
* Generate a button to go to Special:SearchTranslations
* when adding a new term.
*
* @param {array} words
* @returns {jQuery}
*/
function searchTranslationsButton( words ) {
if ( !words.length ) return false;
let displayWord = words[ 0 ],
searchUrl = mw.util.getUrl( 'Special:SearchTranslations', {
'filter': 'translated',
'language': originalTerminologyLanguage,
'query': words.join( '|' )
});
let button = new OO.ui.ButtonWidget( {
'label': mw.message( 'gadget-term-dialog-search-translations', displayWord ).text(),
'icon': 'search',
'target': '_blank',
'href': searchUrl,
'framed': false,
'classes': [ 'gadget-term-searchtranslationsbutton' ]
});
return button;
}
/**
* Edit the termlist.
*
* @param {object} [data] - Data about what should be changed
* @param {string} [action] - What type of edit should be performed
* @param {string} [addsummary=''] - Summary to add to the automatic summary
*/
function editTermlist( data, action, addsummary = '' ) {
let newTermlist = termlist,
word = data.word.trim(),
oldAliases = findAliases( word ),
termlistWord = termlist[ data.removeterm ] || termlist[ word ] || {},
newTerm = {},
today = new Date().toISOString().slice( 0, 10 ),
summary;
if ( action === 'delete' ) { // Delete an entire term + aliases
delete newTermlist[ word ];
for ( let alias of oldAliases ) {
delete newTermlist[ alias ];
}
summary = 'Remove term "' + word + '": ' + addsummary;
} else if ( action === 'resolveDiscussion' ) { // Mark a discussion as resolved
delete newTermlist[ word ].discussion;
if ( Object.keys( newTermlist[ word ] ).length === 0 || ( Object.keys( newTermlist[ word ] ).length === 1 && newTermlist[ word ][ '@metadata' ] ) ) {
delete newTermlist[ word ];
}
summary = '[[Portal talk:' + terminologyLanguage + '#gadget-terminology-' + word + '|Discussion]] about "' + word + '" marked as resolved: ' + addsummary;
} else if ( action === 'startDiscussion' ) { // Start a new discussion about a word
if ( newTermlist[ word ] ) {
newTermlist[ word ].discussion = true;
} else {
newTermlist[ word ] = { 'discussion': true };
}
summary = '[[Portal talk:' + terminologyLanguage + '#gadget-terminology-' + word + '|Discussion started]] about "' + word + '"';
} else { // Default action, for 'edit' or 'add'
if ( data.removeterm ) {
delete newTermlist[ data.removeterm ];
}
if ( data.isAlias ) {
newTermlist[ word ] = { '@alias': data.aliasTarget, '@metadata': { 'editors': [ username ], 'date_modified': today } };
} else {
if ( data.translation ) newTerm.translation = data.translation;
if ( data.usage_notes ) newTerm.usage_notes = data.usage_notes;
if ( termlistWord.discussion ) newTerm.discussion = true;
if ( termlistWord[ '@metadata' ] ) {
let editors = termlistWord[ '@metadata' ].editors;
editors.push( username );
newTerm[ '@metadata' ] = { 'editors': [ ...new Set( editors ) ], 'date_modified': today };
} else {
newTerm[ '@metadata' ] = { 'editors': [ username ], 'date_modified': today };
}
newTermlist[ word ] = newTerm;
// If an alias that was present before is not present in the new
// data, remove it from the termlist.
for ( let oldAlias of oldAliases ) {
if ( !( data.aliases.includes( oldAlias ) ) ) {
delete newTermlist[ oldAlias ];
}
}
// If an alias has been addded, add it to the termlist.
for ( let newAlias of data.aliases ) {
if ( !( oldAliases.includes( newAlias ) ) ) {
newTermlist[ newAlias ] = { '@alias': word, '@metadata': { 'editors': [ username ], 'date_modified': today } };
}
}
}
summary = 'Edited the term "' + word + '"';
if ( action === 'add' ) summary = 'Added the term "' + word + '"';
}
// Sort newTermlist by key (in order to have somewhat easier-to-read
// terminology.json pages)
// Credit for this goes to Mathias Bynens & wadezhan on Stack Overflow
// https://stackoverflow.com/a/31102605/8196939
// License: CC-by-SA 4.0
const newTermlistSorted = Object.keys( newTermlist ).sort().reduce(
( obj, key ) => {
obj[ key ] = newTermlist[ key ];
return obj;
}, {});
// At this point, newTermlist is finished and we can add it to the right
// page via the API
return api.postWithEditToken( {
title: 'Portal:' + terminologyLanguage + '/terminology.json',
action: 'edit',
text: JSON.stringify( newTermlistSorted ),
summary: summary,
contentformat: 'application/json',
contentmodel: 'json',
baserevid: termlistRevision,
tags: 'terminology'
}).then( function( res ) {
termlistRevision = res.edit.newrevid;
return [ parseTermlist( newTermlist, originalTerminologyLanguage ), res ];
}).then( function( res ) {
mw.notify( mw.message( 'postedit-confirmation-saved', user, res[ 1 ].edit.newrevid ).parseDom(), { type: 'success' } );
return res[ 0 ];
}).then( function( res ) {
termlistParsed = res;
return res;
}).catch( function( err ) {
mw.notify( mw.message( 'edit-error-short', err ), { type: 'error' } );
return err;
});
}
/**
* Dialog for starting a new discussion about a term on the Portal talk page.
*
* @param {string} word
* @todo Open discussion in new tab?
*/
function startDiscussion( word ) {
function StartDiscussionDialog( config ) {
StartDiscussionDialog.super.call( this, config );
}
OO.inheritClass( StartDiscussionDialog, OO.ui.ProcessDialog );
StartDiscussionDialog.static.name = 'startDiscussionDialog';
StartDiscussionDialog.static.title = mw.message( 'gadget-term-discussion-dialog-title', word ).text();
StartDiscussionDialog.static.actions = [
{ action: 'save', label: mw.message( 'gadget-term-discussion-save' ).text(), flags: [ 'primary', 'progressive' ], accessKey: mw.message( 'accesskey-save' ).text() },
{ action: 'cancel', label: mw.message( 'gadget-term-dialog-cancel' ).text(), flags: 'safe', icon: 'close', invisibleLabel: true }
];
StartDiscussionDialog.prototype.initialize = function() {
StartDiscussionDialog.super.prototype.initialize.apply( this, arguments );
this.size = 'large';
this.content = new OO.ui.PanelLayout( { padded: true, expanded: false } );
let discussionTopic = new OO.ui.TextInputWidget( {
value: mw.message( 'gadget-term-discussion-topic', word ).text(),
tabIndex: 1,
id: 'discussion-topic'
});
let discussionPost = new OO.ui.MultilineTextInputWidget( {
placeholder: mw.message( 'gadget-term-discussion-message-placeholder' ).text(),
tabIndex: 2,
rows: 5,
autosize: true,
maxRows: 8,
id: 'discussion-post'
});
let fieldset = new OO.ui.FieldsetLayout();
let discussionTopicField = new OO.ui.FieldLayout( discussionTopic, {
label: mw.message( 'gadget-term-discussion-topic-label' ).text()
}),
discussionPostField = new OO.ui.FieldLayout( discussionPost, {
label: mw.message( 'gadget-term-discussion-message', user ).text()
});
this.fields = [ discussionTopicField, discussionPostField ];
fieldset.addItems( this.fields );
this.content.$element.append( fieldset.$element );
this.$body.append( this.content.$element );
};
StartDiscussionDialog.prototype.getActionProcess = function ( action ) {
let dialog = this;
if ( action == 'save' ) {
return new OO.ui.Process( function () {
dialog.pushPending();
let data = {},
termTemplate = '{{discuss term|' + word + '|language=' + terminologyLanguage + '}}\n';
for ( let field of dialog.fields ) {
let input = field.getField();
data[ input.elementId ] = input.value;
}
api.postWithEditToken( {
action: 'edit',
title: 'Portal talk:' + terminologyLanguage,
section: 'new',
sectiontitle: data['discussion-topic'],
text: termTemplate + data['discussion-post'].replace( /(\s*)~{4}\s*/g, '$1' ) + ' ~~' + '~~',
tags: 'terminology',
format: 'json',
formatversion: 2
}).then( function() {
editTermlist( { word: word }, 'startDiscussion' ).done( function() {
$( '.term-sourcemessage' ).each( function() {
$( this ).replaceWith( processSourcemessage( $( this ) ) );
});
});
}).then( function() {
dialog.close();
mw.notify(
$( '<p>' ).append( mw.message( 'gadget-term-discussion-started-message', word, 'Portal talk:' + terminologyLanguage + '#gadget-term-' + word ).parseDom() ),
{ title: mw.message( 'gadget-term-discussion-started-title' ).text(), type: 'success' }
);
}).catch( function( err ) {
dialog.popPending();
mw.notify( 'An error occured: ' + err, { type: 'error' } );
});
} );
} else if ( action == 'cancel' ) {
dialog.close();
}
return StartDiscussionDialog.super.prototype.getActionProcess.call( this, action );
};
let dialog = new StartDiscussionDialog();
windowManager.addWindows( [ dialog ] );
windowManager.openWindow( dialog );
}
/**
* Dialog for editing a term. Most of the magic happens here.
*
* @param {string} word
* @param {boolean} [addTerm=false] - If the term doesn't exist in termlist already
*/
function editTermDialog( word, addTerm = false ) {
let term = termlistLookup( word ) || false,
showAdvanced = mw.storage.get( 'gadget-terminology-showadvanced' ) === 'true';
if ( termlist[ word ] && termlist[ word ][ '@alias' ] ) word = termlist[ word ][ '@alias' ];
function EditTermDialog( config ) {
EditTermDialog.super.call( this, config );
}
OO.inheritClass( EditTermDialog, OO.ui.ProcessDialog );
EditTermDialog.static.name = 'editTermDialog';
EditTermDialog.static.title = addTerm ? mw.message( 'gadget-term-dialog-add-term' ).text() : mw.message( 'gadget-term-dialog-edit-term' ).text();
let saveButton = new OO.ui.ActionWidget( {
action: 'save',
label: mw.message( 'gadget-term-dialog-save' ).text(),
flags: [ 'primary', 'progressive' ],
disabled: false,
framed: true,
accessKey: mw.message( 'accesskey-save' ).text()
}),
deleteButton = new OO.ui.ActionWidget( {
action: 'delete',
icon: 'trash',
label: mw.message( 'gadget-term-dialog-delete' ).text(),
flags: [ 'destructive' ],
disabled: addTerm, // Disable the delete button if we are adding a new term
classes: [ 'term-advanced' ],
framed: true
}),
helpButton = new OO.ui.ActionWidget( {
action: 'help',
icon: 'help',
label: mw.message( 'help' ).text(),
invisibleLabel: true,
framed: true,
href: '/wiki/Special:MyLanguage/Project:Terminology_gadget',
target: '_blank'
});
EditTermDialog.static.actions = [
saveButton,
{ action: 'cancel', label: mw.message( 'gadget-term-dialog-cancel' ).text(), flags: 'safe', icon: 'close', invisibleLabel: true },
deleteButton,
helpButton // Doesn't work properly, probably needs same magic later in getActionProcess
];
EditTermDialog.prototype.initialize = function () {
EditTermDialog.super.prototype.initialize.apply( this, arguments );
this.id = 'editTermDialog';
this.size = 'larger';
this.content = new OO.ui.PanelLayout( { padded: true, expanded: false } );
let editnotice = false;
if (
!mw.storage.get( 'gadget-terminology-showeditnotice-' + terminologyLanguage ) &&
termlistParsed[ '@editnotice' ] &&
termlistParsed[ '@editnotice' ].usage_notes
) {
editnotice = new OO.ui.MessageWidget( {
type: 'notice',
label: new OO.ui.HtmlSnippet( termlistParsed[ '@editnotice' ].usage_notes ),
showClose: true // FIXME: Not available yet
});
editnotice.onCloseButtonClick = function( e ) {
mw.storage.set( 'gadget-terminology-showeditnotice-' + terminologyLanguage, 'false' );
mw.storage.setExpires( 'gadget-terminology-showeditnotice-' + terminologyLanguage, 8000000 ); // ~3 months
editnotice.toggle( false );
};
}
let termInput = new OO.ui.TagMultiselectWidget( {
inputPosition: 'inline',
allowArbitrary: true,
selected: word ? [ word ].concat( findAliases( word ) ) : null,
tabIndex: 1,
dir: 'ltr',
id: 'term-terminput'
});
let isAliasCheckbox = new OO.ui.CheckboxInputWidget( {
value: 'isAlias',
id: 'term-isAlias'
});
let aliasTargetDropdown = new OO.ui.DropdownInputWidget( {
label: '',
options: termlistWithoutAliases().map( x => ( { data: x, label: x } ) ),
tabIndex: 3,
id: 'term-aliasTarget'
});
let translation = new OO.ui.MultilineTextInputWidget( {
placeholder: mw.message( 'gadget-term-dialog-translation-placeholder' ).text(),
value: term.translation || '',
tabIndex: 4,
autosize: true,
maxRows: 4,
id: 'term-translation'
});
let usage_notes = new OO.ui.MultilineTextInputWidget( {
placeholder: mw.message( 'gadget-term-dialog-usage-notes-placeholder' ).text(),
value: term.usage_notes || '',
tabIndex: 5,
autosize: true,
maxRows: 4,
id: 'term-usage_notes'
});
if ( word ) termInput.findItemFromData( word ).$element.addClass( 'term-terminput-main' );
let fieldset = new OO.ui.FieldsetLayout();
let termInputField = new OO.ui.FieldLayout( termInput, {
label: mw.message( 'gadget-term-dialog-english' ).text(),
help: mw.message( 'gadget-term-dialog-english-help' ).text()
}),
aliasCheckboxField = new OO.ui.FieldLayout( isAliasCheckbox, {
label: mw.message( 'gadget-term-dialog-is-alias' ).text(),
classes: [ 'term-advanced' ]
}),
aliasTargetField = new OO.ui.FieldLayout( aliasTargetDropdown, {
label: mw.message( 'gadget-term-dialog-alias-for' ).text(),
help: mw.message( 'gadget-term-dialog-alias-for-help', user ).text(),
classes: [ 'showWhenAliasSelected' ]
}),
translationField = new OO.ui.FieldLayout( translation, {
label: mw.message( 'gadget-term-dialog-translation' ).text(),
help: mw.message( 'gadget-term-dialog-translation-help', user ).text(),
classes: [ 'hideWhenAliasSelected' ]
}),
usage_notesField = new OO.ui.FieldLayout( usage_notes, {
label: mw.message( 'gadget-term-dialog-usage-notes' ).text(),
help: mw.message( 'gadget-term-dialog-usage-notes-help', user ).text(),
classes: [ 'hideWhenAliasSelected', 'term-advanced' ]
}),
emptyTranslationField = new OO.ui.FieldLayout( new OO.ui.MessageWidget( {
type: 'error',
inline: true,
label: mw.message( 'gadget-term-dialog-translation-un-error' ).text()
}), {
align: 'inline',
classes: [ 'term-empty-translation' ]
});
this.fields = [ termInputField, aliasCheckboxField, aliasTargetField, translationField, usage_notesField, emptyTranslationField ];
fieldset.addItems( this.fields );
if ( editnotice ) fieldset.addItems( [ editnotice ], 0 );
this.content.$element.append( fieldset.$element );
emptyTranslationField.$element.hide();
let discussionButton = new OO.ui.ButtonWidget( {
label: mw.message( 'gadget-term-dialog-start-discussion' ).text(),
icon: 'speechBubbleAdd',
disabled: !word, // Disabled by default if there is no word variable, enabled if there is
data: { 'word': word }
}).on( 'click', function() {
dialog.close();
startDiscussion( discussionButton.getData().word );
} );
let discussionLink = new OO.ui.MessageWidget( {
type: 'warning',
icon: 'speechBubbles',
label: $( '<div>' ).append( mw.message( 'gadget-term-popup-discussion', 'Portal talk:' + terminologyLanguage + '#gadget-terminology-' + word ).parseDom() ),
classes: [ 'term-add-margin' ]
});
// Check if the terms field is valid. It can not be empty, and the
// terms in it can not already be defined in the termlist.
// Display error messages for each case and disable the save button
// while the error is not resolved.
termInput.on( 'change', function() {
let newAliasList = termInput.items.map( x => x.data.toLowerCase() ),
existingAliases = [ word ].concat( findAliases( word ) ),
inputId = termInput.getInputId(),
suggestions = newAliasList.length ? generateSuggestions( newAliasList[ 0 ] ) : [],
$suggestions = $( '<div>' ).addClass( 'term-suggestions' ).html( mw.message( 'gadget-term-dialog-suggested-aliases' ).text() + ' ' ).css( 'margin-top', '6px' ),
searchTranslations = searchTranslationsButton( newAliasList ),
errorMessage = new OO.ui.MessageWidget( {
type: 'error',
inline: true,
classes: [ 'term-input-error', 'term-add-margin' ]
});
for ( const suggestion of suggestions ) {
let suggestionElement = new OO.ui.TagItemWidget( {
label: suggestion,
fixed: true,
draggable: false
});
suggestionElement.closeButton.$element.remove(); // Hacky, but it works
suggestionElement.$element.on( 'click', function() {
termInput.addTag( suggestion );
$( this ).remove();
});
suggestionElement.$label.css( 'cursor', 'inherit' );
// Override default padding that leaves space for the close button we removed
suggestionElement.$element.css( { 'padding': '0 8px', 'cursor': 'pointer' } );
if ( !newAliasList.includes( suggestion ) ) $suggestions.append( suggestionElement.$element );
}
// Add the suggestion element
$( '.term-suggestions' ).remove();
if ( suggestions.length !== 0 ) {
termInput.$element.after( $suggestions );
}
// Add button to go to Special:SearchTranslations
$( '.gadget-term-searchtranslationsbutton' ).remove();
if ( searchTranslations ) {
termInput.$element.parent().append( searchTranslations.$element );
}
// Add a dummy error element
if ( $( '.term-input-error' ).length ) {
$( '.term-input-error' ).empty().removeClass( 'term-add-margin' );
} else {
termInput.$element.after( $( '<div>' ).addClass( 'term-input-error' ) );
}
// Check for errors, replace dummy element if there are any
if ( newAliasList.length === 0 && $( '#' + inputId ).length !== 0 ) {
errorMessage.setLabel( mw.message( 'gadget-term-dialog-english-notempty' ).text() );
$( '.term-input-error' ).replaceWith( errorMessage.$element );
saveButton.setDisabled( true );
discussionButton.setDisabled( true );
} else {
discussionButton.setData( { 'word': newAliasList[ 0 ] } );
discussionButton.setDisabled( false );
for ( const alias of newAliasList ) {
if ( !( existingAliases.includes( alias ) ) && ( alias in termlist ) ) {
errorMessage.setLabel( mw.message( 'gadget-term-dialog-english-error', alias ).text() );
$( '.term-input-error' ).replaceWith( errorMessage.$element );
saveButton.setDisabled( true );
break;
} else {
$( '.term-input-error' ).empty().removeClass( 'term-add-margin' );
saveButton.setDisabled( false );
}
}
}
});
if ( termInput.items.length > 0 ) {
termInput.emit( 'change' );
}
// Check if either the translation or the usage_notes field is filled
// in. If neither is, disable the save button because there is nothing
// productive to be done.
function validationHelper() {
if ( translation.getValue().trim().length + usage_notes.getValue().trim().length === 0 ) {
emptyTranslationField.$element.slideDown();
saveButton.setDisabled( true );
} else {
emptyTranslationField.$element.slideUp();
saveButton.setDisabled( false );
}
}
translation.on( 'change', validationHelper );
usage_notes.on( 'change', validationHelper);
this.content.$element.append( '<p>' + mw.message( 'gadget-term-dialog-footnote', user ).parse() + '</p>' );
// Add a note about an existing discussion (at the top) if there is one.
// If not, add a 'start a discussion' button at the bottom.
// TODO: Add neither if dialog is initialized from a talk page.
if ( term.discussion ) {
this.content.$element.prepend( discussionLink.$element );
} else {
this.content.$element.append( $( '<div>' ).css( 'text-align', 'center' ).append( discussionButton.$element ) );
}
// Add a metadata box about who has edited the term and when it was last edited.
// TODO: Figure out how to parse the date to a language-specific human-readable format.
if ( term ) {
let editorList = term['@metadata'].editors.map( username => $( '<bdi>' ).append( $( '<a>' ).attr( 'href', mw.util.getUrl( 'User:' + username ) ).text( username ) ).prop( 'outerHTML' ) ),
editors = new OO.ui.MessageWidget( {
type: 'notice',
icon: 'userContributions',
label: new OO.ui.HtmlSnippet( mw.message( 'gadget-term-dialog-metadata', $( mw.language.listToText( editorList ) ), term['@metadata'].date_modified, editorList.length ).parseDom() ),
classes: [ 'term-metadata', 'term-advanced', 'term-add-margin' ]
});
this.content.$element.append( editors.$element.toggle( showAdvanced ) );
}
aliasTargetField.$element.hide();
if ( !addTerm ) aliasCheckboxField.$element.hide();
if ( !showAdvanced ) {
deleteButton.$element.hide();
aliasCheckboxField.$element.hide();
usage_notesField.$element.hide();
}
isAliasCheckbox.on( 'change', function() {
if ( isAliasCheckbox.isSelected() ) {
$( '.showWhenAliasSelected' ).slideDown();
$( '.hideWhenAliasSelected' ).slideUp();
} else {
$( '.showWhenAliasSelected' ).slideUp();
$( '.hideWhenAliasSelected' ).slideDown();
}
});
let showAdvancedButton = new OO.ui.ToggleSwitchWidget({
id: 'advancedToggle',
value: showAdvanced
}).on( 'change', function() {
if ( showAdvancedButton.getValue() ) {
// TODO: Alias checkbox behaviour is not quite as intended
if ( addTerm ) aliasCheckboxField.$element.slideDown();
$( '.term-advanced' ).slideDown();
mw.storage.set( 'gadget-terminology-showadvanced', true );
} else {
$( '.term-advanced' ).slideUp();
mw.storage.set( 'gadget-terminology-showadvanced', false );
}
});
let showAdvancedOption = new OO.ui.HtmlSnippet(
$( '<p>' )
.addClass( 'term-show-advanced' )
.text( mw.message( 'gadget-term-dialog-show-advanced' ).text() )
.append( showAdvancedButton.$element )
).content;
this.$body.append( this.content.$element );
this.$otherActions.append( showAdvancedOption ); // Kinda hacky, but it works.
};
EditTermDialog.prototype.getActionProcess = function( action ) {
let dialog = this;
if ( action === 'save' ) {
return new OO.ui.Process( function () {
// Probably not using this correctly, but it works, so... Maybe the OO.ui.Process can be omitted entirely.
dialog.pushPending();
let submits = {};
for ( let field of dialog.fields ) {
let input = field.getField();
switch ( input.elementId ) {
case 'term-terminput':
let inputTerms = input.items.map( x => x.data.toLowerCase() );
if ( inputTerms.includes( word ) ) {
submits.word = word;
submits.aliases = inputTerms.filter( x => x !== word );
} else {
submits.word = inputTerms[ 0 ];
submits.aliases = inputTerms.slice( 1 );
submits.removeterm = word;
}
break;
case 'term-isAlias':
submits.isAlias = input.selected;
break;
case 'term-aliasTarget':
submits.aliasTarget = input.value;
break;
case 'term-translation':
submits.translation = input.value.trim();
break;
case 'term-usage_notes':
submits.usage_notes = input.value.trim();
break;
}
}
if ( ( submits.translation.length + submits.usage_notes.length === 0 ) && !submits.isAlias ) {
dialog.close();
} else {
editTermlist( submits, addTerm ? 'add' : 'edit' ).done( function() {
if ( initConditions === 'Special:Translate' ) {
$( '.term-sourcemessage' ).each( function() {
$( this ).replaceWith( processSourcemessage( $( this ) ) );
});
dialog.close();
} else {
location.reload();
}
});
}
} );
} else if ( action === 'delete' ) {
dialog.pushPending();
OO.ui.prompt( mw.message( 'gadget-term-dialog-delete-reason', user ).text() ).done( function( summary ) {
if ( summary !== null ) {
editTermlist( { word: word }, 'delete', summary ).done( function() {
$( '.term-sourcemessage' ).each( function() {
$( this ).replaceWith( processSourcemessage( $( this ) ) );
});
});
dialog.close();
} else {
dialog.popPending();
}
});
} else if ( action === 'help' ) {
window.open( '/wiki/Special:MyLanguage/Project:Terminology_gadget', '_blank' );
} else if ( action === 'cancel' ) {
dialog.close();
}
return EditTermDialog.super.prototype.getActionProcess.call( this, action );
};
let dialog = new EditTermDialog();
windowManager.addWindows( [ dialog ] );
windowManager.openWindow( dialog );
}
/**
* Build the popup for words that have a definition.
*
* @param {string} word
* @param {object} term
* @param {jQuery} element - The <span> element to amend
* @param {boolean} [onTalkPage=false] - Are we on a talk page?
* @returns {jQuery}
*/
function popupBuilder( word, term, element, onTalkPage = false ) {
let popup, popupMenu, discussionBox, popupClose,
shouldPopupClose = true,
$popupContent = $( '<div>' ).append( '<dl>' ),
wordId = mw.util.escapeIdForAttribute( word );
if ( termlist[ word ] && termlist[ word ][ '@alias' ] ) word = termlist[ word ][ '@alias' ];
discussionBox = new OO.ui.MessageWidget( {
type: 'warning',
icon: 'speechBubbles',
label: $( '<div>' ).append( mw.message( 'gadget-term-popup-discussion', 'Portal talk:' + terminologyLanguage + '#gadget-terminology-' + wordId ).parseDom() )
});
popupMenu = new OO.ui.ButtonMenuSelectWidget( {
$overlay: true,
label: 'Menu',
invisibleLabel: true,
framed: false,
icon: 'menu',
classes: [ 'term-popup-menu' ],
menu: {
items: [
new OO.ui.MenuOptionWidget({
data: 'edit',
label: mw.message( 'gadget-term-popup-edit-term' ).text(),
icon: 'edit'
}),
new OO.ui.MenuOptionWidget({
data: 'discuss',
label: mw.message( 'gadget-term-popup-discuss-term' ).text(),
icon: 'speechBubbleAdd'
})
]
}
});
popup = new OO.ui.PopupWidget( {
$content: onTalkPage ? $popupContent : $.merge( $( popupMenu.$element ), $popupContent ),
padded: true,
position: ( initConditions === 'terminology.json' ) ? 'before' : 'above',
$container: $( '#bodyContent' ),
autoClose: true,
width: 400
});
if ( term.translation ) {
$popupContent.append( $( '<dt>' ).text( mw.message( 'gadget-term-popup-translation' ).text() ) );
$popupContent.append( $( '<dd>' ).html( termlistParsed[ word ].translation ) );
element.addClass( 'term-translation' );
}
if ( term.usage_notes ) {
$popupContent.append( $( '<dt>' ).text( mw.message( 'gadget-term-popup-usage-notes' ).text() ) );
$popupContent.append( $( '<dd>' ).html( termlistParsed[ word ].usage_notes ) );
element.addClass( 'term-usage_notes' );
}
if ( term.discussion && !onTalkPage ) {
$popupContent.append( discussionBox.$element.css( 'clear', 'both' ) );
element.addClass( 'term-discussion' );
}
// Don't hide the popup after the menu button has been clicked. This is
// necessary because the menu opens a floating div that is not a child
// of the popup element, so hovering it would cause the popup to close
// while the menu stays open.
// TODO: Could need some tweaking though, because if you click the menu
// button and then click somewhere outside of it, the popup closes, but
// the next time the popup opens, it will stay open.
popupMenu.on( 'click', () => shouldPopupClose = !shouldPopupClose );
popupMenu.getMenu().on( 'choose', function( menuOption) {
switch ( menuOption.getData() ) {
case 'edit':
editTermDialog( word );
break;
case 'discuss':
startDiscussion( word );
break;
}
});
// The next line is necessary because the popup is child of the source
// message div, which always is English and has dir=ltr
popup.$element.attr( 'dir', $( 'html' ).attr( 'dir' ) );
element.append( popup.$element ).on( 'mouseover', function() {
popup.toggle( true );
clearTimeout( popupClose );
}).on( 'mouseout', function() {
popupClose = setTimeout( function() {
popup.toggle( !shouldPopupClose );
}, 200);
});
return element;
}
/**
* Process a .sourcemessage text to add the necessary spans and elements
* to it.
*
* @param {jQuery} element
* @returns {jQuery} element
*/
function processSourcemessage( element ) {
let $sourcemessage = element;
if ( !$sourcemessage.data( 'original' ) ) $sourcemessage.data( 'original', $sourcemessage.text() ).addClass( 'term-sourcemessage' );
// Sort the termlist in descending order by key length.
// This gives us the longest keys first, which feeds into the
// regex and ensures that compound terms (i.e. terms with spaces
// in them) are treated first.
let termlistDescending = Object.keys( termlist ).sort( function( a, b ) {
return b.length - a.length;
});
// Human-friendly explanation of the following regex:
// * Find all occurences of keys from termlistDescending
// * That are surrounded by word boundaries \b, in order to avoid
// marking up substrings.
// * That are not encapsulated in <tags>
// * Finally, find all other Latin-script words (\p{L})
let regex = new RegExp( '(?<!<|<\/|&)\\b(' + termlistDescending.map( x => mw.util.escapeRegExp( x ).replace( '\\-', '-' ) ).concat(['']).join('|') + '\\p\{L\}+)\\b(?!>|;)', 'gui' );
let text = $sourcemessage.data( 'original' ) || '';
text = mw.html.escape( text );
text = text.replaceAll( regex, function( x ) {
return '<span data-term="' + x.toLowerCase() + '">' + x + '</span>';
});
$sourcemessage.html( text );
$sourcemessage.children( 'span[data-term]' ).each( function() {
let adderShow;
let $this = $( this );
const word = $( this ).attr( 'data-term' ),
term = termlistLookup( word );
if ( term ) {
let popup = popupBuilder( word, term, $this );
$this.replaceWith( popup );
} else {
const addButton = new OO.ui.ButtonWidget( {
label: mw.message( 'gadget-term-dialog-add-term-adder' ).text(),
title: mw.message( 'gadget-term-dialog-add-term-adder' ).text(),
icon: 'add',
invisibleLabel: true,
framed: false,
flags: [ 'progressive' ]
}).on( 'click', () => editTermDialog( word, true ) );
let adder = new OO.ui.PopupWidget( {
$content: addButton.$element.css( { 'margin-left': '0', 'margin-right': '0' } ),
$container: $( '#bodyContent' ),
position: 'after',
padded: false,
width: null,
anchor: false,
classes: [ 'term-adder' ]
});
$this.append( adder.$element );
let showAdder = false;
$this.on( 'mouseover', function() {
showAdder = true;
adderShow = setTimeout( function() {
adder.toggle( showAdder );
}, 300 );
}).on( 'mouseout', function() {
clearTimeout( adderShow );
showAdder = false;
adderShow = setTimeout( function() {
adder.toggle( showAdder );
}, 1);
});
}
});
return $sourcemessage;
}
if ( mw.config.get( 'wgCanonicalSpecialPageName' ) === 'Translate' ) {
mw.hook( 'mw.translate.editor.afterEditorShown' ).add( function( data ) {
let $this = $( data ).find( '.sourcemessage:not(.term-sourcemessage)' );
$this.replaceWith( processSourcemessage( $this ) );
});
} else if ( $( '.gadget-term-discuss' ).length ) {
$( '.gadget-term-discuss' ).each( function() {
let $template = $( this );
$template.removeAttr( 'style' );
if ( $template.attr( 'data-language' ) !== terminologyLanguage ) {
$template.addClass( 'gadget-term-error' ).html( mw.message( 'gadget-term-discuss-language-mismatch', 'language', user ).parse() );
} else {
let word, term;
let templateMenu = new OO.ui.ButtonMenuSelectWidget( {
icon: 'menu',
label: 'Menu',
invisibleLabel: true,
framed: false,
classes: [ 'gadget-term-discuss-menu' ],
menu: {
horizontalPosition: 'end',
items: [
new OO.ui.MenuOptionWidget({
data: 'edit',
label: mw.message( 'gadget-term-popup-edit-term' ).text(),
icon: 'edit'
}),
new OO.ui.MenuOptionWidget({
data: 'reopen',
label: mw.message( 'gadget-term-discuss-reopen' ).text(),
icon: 'speechBubble'
})
]
}
});
templateMenu.getMenu().on( 'choose', function( menuOption) {
switch ( menuOption.getData() ) {
case 'edit':
editTermDialog( word, true );
break;
case 'discuss':
startDiscussion( word );
break;
}
});
$template.find( '[data-term]' ).each( function() {
word = $( this ).attr( 'data-term' );
term = termlistLookup( word );
if ( term ) {
if ( term.translation || term.usage_notes ) popupBuilder( word, term, $( this ), true );
if ( term.discussion ) {
$template.addClass( 'gadget-term-discuss-open' );
} else {
$template.append( templateMenu.$element );
$template.addClass( 'gadget-term-discuss-resolved' );
$template.find( '.gadget-term-status' ).html( mw.message( 'gadget-term-discuss-resolved' ).parse() );
$template.find( '.gadget-term-controls' ).hide();
}
} else {
const adder = $( '<div>' ).addClass( 'term-adder' ).text( '+' ).on( 'click', () => editTermDialog( word, true ) );
$( this ).append( adder );
$template.append( templateMenu.$element );
$template.addClass( 'gadget-term-discuss-resolved' );
$template.find( '.gadget-term-status' ).html( mw.message( 'gadget-term-discuss-resolved' ).parse() );
$template.find( '.gadget-term-controls' ).hide();
}
});
$template.find( '.gadget-term-editlink' ).on( 'click', function( e ) {
e.preventDefault();
editTermDialog( word );
});
$template.find( '.gadget-term-resolvelink' ).on( 'click', function( e ) {
e.preventDefault();
OO.ui.prompt( mw.message( 'gadget-term-discuss-summary' ).text() ).done( function( summary ) {
if ( summary !== null ) {
editTermlist( { word: word }, 'resolveDiscussion', summary );
mw.notify( mw.message( 'gadget-term-discuss-marked-resolved', summary ).text(), { title: word, type: 'success' });
}
});
});
}
});
} else if ( mw.config.get( 'wgPageName' ).endsWith( '/terminology.json' ) ) {
$( 'table.mw-json' ).hide();
let $prejson = $( '<div>' ).addClass( 'gadget-term-json mw-body-content mw-content-' + dir ),
$table = $( '<table>' ),
toggleButton = new OO.ui.ButtonWidget( {
label: mw.message( 'gadget-term-json-showhide' ).text(),
icon: 'expand',
flags: [ 'progressive' ]
}).on( 'click', function() {
$( 'table.mw-json' ).toggle();
toggleButton.setIcon( $( 'table.mw-json' ).is( ':visible' ) ? 'collapse' : 'expand' );
}),
redirectMessage = new OO.ui.MessageWidget( {
label: new OO.ui.HtmlSnippet( mw.message( 'gadget-term-json-redirect', originalTerminologyLanguage, terminologyLanguage ).parse() ),
icon: 'articleRedirect',
type: 'warning'
}),
$introMessage = $( '<p>' ).append( mw.message( 'gadget-term-json-intro', user ).parseDom() ),
addTermButton = new OO.ui.ButtonWidget( {
label: mw.message( 'gadget-term-dialog-add-term' ).text(),
flags: [ 'progressive', 'primary' ],
icon: 'add'
}).on( 'click', function() {
editTermDialog( '', true ); // '' because no word is specified in this context
});
redirectMessage.$icon.css( 'background-position', '50%' );
if ( termlistWithoutAliases().length === 0 ) {
$table.append( '<tr>' ).append( '<td>' ).addClass( 'nodata' ).css( 'text-align', 'center' ).html( mw.message( 'gadget-term-json-noterms' ).parse() );
} else {
for ( const word of termlistWithoutAliases() ) {
let $tr = $( '<tr>' ),
$td1 = $( '<td>' ).addClass( 'gadget-term-term' ),
$td2 = $( '<td>' ),
$termspan = $( '<span>' ).attr( 'data-term', word ).text( word ),
term = termlist[ word ];
$td1.append( popupBuilder( word, termlistLookup( word ), $termspan ) ).append( '<br />' );
$td1.append( findAliases( word ).join( mw.message( 'comma-separator' ).text() ) );
if ( !term.translation && !term.usage_notes ) {
$td2.append( $( '<p>' ).addClass( 'nodata' ).html( mw.message( 'gadget-term-json-nodata' ).parse() ) );
}
if ( term.translation ) $td2.append( $( '<p>' ).html( '<b>' + mw.message( 'gadget-term-popup-translation' ).text() + '</b>' + mw.message( 'word-separator' ).text() + termlistParsed[ word ].translation ) );
if ( term.usage_notes ) $td2.append( $( '<p>' ).html( '<b>' + mw.message( 'gadget-term-popup-usage-notes' ).text() + '</b>' + mw.message( 'word-separator' ).text() + termlistParsed[ word ].usage_notes ) );
if ( term[ '@metadata' ] ) {
let editorList = term['@metadata'].editors.map( username => $( '<bdi>' ).append( $( '<a>' ).attr( 'href', mw.util.getUrl( 'User:' + username ) ).text( username ) ).prop( 'outerHTML' ) );
$td2.append( $( '<p>' ).addClass( 'metadata' ).append( mw.message( 'gadget-term-dialog-metadata', $( mw.language.listToText( editorList ) ), term['@metadata'].date_modified, editorList.length ).parseDom() ) );
}
$tr.append( $td1 );
$tr.append( $td2 );
$table.append( $tr );
}
}
$prejson.append( $introMessage );
if ( terminologyLanguage !== originalTerminologyLanguage ) $prejson.append( redirectMessage.$element );
$prejson.append( addTermButton.$element );
$prejson.append( $table );
$prejson.append( toggleButton.$element.css( { 'text-align': 'center', 'display': 'block' } ) );
$( '#mw-content-text' ).first().before( $prejson );
} else if ( $( '.mw-translate-edit-deftext' ).length ) {
let $def = $( '.mw-translate-edit-deftext' );
$def.html( $def.html().replace( /<br ?\/?>/g, '\n' ) );
$def.replaceWith( processSourcemessage( $def ) ).addClass( 'sourcemessage' );
}
}
/**
* Fetch the termlist page from the API.
*
* It returns an array with three members:
* 0 - the actual termlist
* 1 - the revision ID of the termlist (to help detect edit conflicts)
* 2 - the language code of the termlist (may be different from the language
* code in is translating into, in case of redirects))
*
* @param {string} language
* @returns {array}
*/
function fetchTermlist( language ) {
return new mw.Api().get( {
action: 'query',
prop: 'revisions',
titles: 'Portal:' + language + '/terminology.json',
rvprop: 'content|ids',
rvslots: 'main',
format: 'json',
maxage: 0,
smaxage: 0,
requestid: Date.now()
}).then( function( output ) {
const pageid = Object.keys( output.query.pages )[ 0 ];
if ( pageid === '-1' ) {
return [ {}, 0, language ];
} else {
const currentrev = output.query.pages[ pageid ].revisions[ 0 ].revid,
content = JSON.parse( output.query.pages[ pageid ].revisions[ 0 ].slots.main[ '*' ] );
if ( Object.keys( content ).includes( '@redirect' ) ) {
return fetchTermlist( content[ '@redirect' ] );
} else {
return [ content, currentrev, language ];
}
}
}).fail( function( err ) {
throw 'Something went wrong when fetching terminology.json';
});
}
/**
* Validate the termlist.
*
* Check that:
* * It doesn't contain any empty keys
* * All values have at least one valid (sub)key
* * All terms that are aliases point to an existing term
* * All translations and/or usage_notes have the required metadata field.
* @param {Object} termlist
* @returns {(boolean|string)} Either false (for no error) or an error string
*/
function validateTermlist( termlist ) {
for ( const [key, value] of Object.entries( termlist ) ) {
if ( key.length === 0 ) {
return 'There is an empty key in the termlist';
}
if ( !( value.translation || value.usage_notes || value.discussion || value[ '@alias' ] ) ) {
return 'The key "$1" does not have any of the required fields.'
.replace( '$1', key );
}
if ( ( value.translation || value.usage_notes ) && !value[ '@metadata' ] ) {
return 'The definition for "$1" is missing the required metadata field.'
.replace( '$1', key );
}
if ( value[ '@alias' ] && !termlist.hasOwnProperty( value[ '@alias' ] ) ) {
return 'The key "$1" is an alias for "$2", which isn\'t defined.'
.replace( '$1', key )
.replace( '$2', value[ '@alias' ] );
}
}
return false;
}
/**
* Parse the termlist.
* @param {object} termlist
* @param {string} originalTerminologyLanguage
* @returns {object}
*/
function parseTermlist( termlist, originalTerminologyLanguage ) {
let termlistToParse = [];
for ( const term in termlist ) {
for ( const deftype in termlist[ term ] ) {
if ( [ 'translation', 'usage_notes' ].includes( deftype ) ) {
termlistToParse.push( '((((' + term + '|' + deftype + '))))\n\n' + termlist[ term ][ deftype ].replaceAll( '$VARIANT', originalTerminologyLanguage ) );
}
}
}
if ( termlistToParse.length === 0 ) {
return Promise.resolve( {} );
} else {
return new mw.Api().parse( termlistToParse.join('\n\n␞\n\n'), {
wrapoutputclass: '',
disablelimitreport: true,
title: mw.config.get( 'wgPageName' )
} ).then( function( owndata ) {
let owndatasplit = owndata.split( '<p>␞\n</p>' ),
parsed = {};
for ( const term of owndatasplit ) {
const regex = /<p>\(\(\(\((.+?)\|(.+?)\)\)\)\)\r?\n<\/p>\n?/,
matches = term.match( regex ),
word = matches[ 1 ],
deftype = matches[ 2 ],
parseresult = term.replace( regex, '' );
if ( !parsed[ word ] ) parsed[ word ] = {};
parsed[ word ][ deftype ] = parseresult;
}
return parsed;
});
}
}
(function() {
const pageName = mw.config.get( 'wgPageName' );
let language, initConditions, originalLanguage;
if ( mw.config.get( 'wgCanonicalSpecialPageName' ) === 'Translate' ) {
initConditions = 'Special:Translate';
language = $( '.tux-messagelist' ).attr( 'data-targetlangcode' );
} else if ( ( $( '.gadget-term-discuss' ).length ) ) {
initConditions = 'PortalTalk';
language = $( '.gadget-term-discuss' ).attr( 'data-language' );
} else if ( ( /^Portal:[A-Za-z]{2,3}(-[A-Za-z\d]+)*\/terminology\.json$/u.test( pageName ) ) &&
( mw.config.get( 'wgAction' ) === 'view' ) &&
( !mw.config.get( 'wgDiffNewId' ) ) ) {
initConditions = 'terminology.json';
language = pageName.replace( /^Portal:(.*)\/terminology\.json/, '$1' ).toLowerCase();
} else if ( $( '.mw-translate-edit-deftext' ).length ) {
initConditions = 'legacy';
language = pageName.split( '/' ).pop();
}
originalLanguage = language;
if ( !initConditions ) return;
// Set up conditions
function startupHelper( termlist, loadMessages ) {
let initData = {};
initData.initConditions = initConditions;
initData.termlist = termlist[ 0 ];
initData.termlistRevision = termlist[ 1 ];
initData.terminologyLanguage = termlist[ 2 ];
initData.originalTerminologyLanguage = originalLanguage;
return parseTermlist( initData.termlist, originalLanguage ).then( function( output ) {
initData.termlistParsed = output;
return initData;
}).catch( function( err ) {
console.warn( 'Terminology gadget error', err );
return;
});
}
let termlist = fetchTermlist( language );
let loadMessages = new mw.Api().loadMessagesIfMissing( [
'accesskey-save', // reuse of generic message
'comma-separator', // reuse of generic message
'edit-error-short', // reuse of generic message
'help', // reuse of generic message
'postedit-confirmation-saved', // reuse of generic message
'word-separator', // reuse of generic message
'gadget-term-dialog-add-term',
'gadget-term-dialog-add-term-adder',
'gadget-term-dialog-edit-term',
'gadget-term-dialog-save',
'gadget-term-dialog-cancel',
'gadget-term-dialog-delete',
'gadget-term-dialog-english',
'gadget-term-dialog-english-help',
'gadget-term-dialog-english-error',
'gadget-term-dialog-english-notempty',
'gadget-term-dialog-suggested-aliases',
'gadget-term-dialog-search-translations',
'gadget-term-dialog-is-alias',
'gadget-term-dialog-alias-for',
'gadget-term-dialog-alias-for-help',
'gadget-term-dialog-translation',
'gadget-term-dialog-translation-help',
'gadget-term-dialog-translation-placeholder',
'gadget-term-dialog-usage-notes',
'gadget-term-dialog-usage-notes-help',
'gadget-term-dialog-usage-notes-placeholder',
'gadget-term-dialog-translation-un-error',
'gadget-term-dialog-metadata',
'gadget-term-dialog-footnote',
'gadget-term-dialog-start-discussion',
'gadget-term-dialog-show-advanced',
'gadget-term-dialog-delete-reason',
'gadget-term-popup-edit-term',
'gadget-term-popup-discuss-term',
'gadget-term-popup-translation',
'gadget-term-popup-usage-notes',
'gadget-term-popup-discussion',
'gadget-term-discussion-dialog-title',
'gadget-term-discussion-topic-label',
'gadget-term-discussion-topic',
'gadget-term-discussion-message',
'gadget-term-discussion-message-placeholder',
'gadget-term-discussion-save',
'gadget-term-discussion-started-title',
'gadget-term-discussion-started-message',
'gadget-term-started-title',
'gadget-term-started-message',
'gadget-term-discuss-language-mismatch',
'gadget-term-discuss-summary',
'gadget-term-discuss-resolved',
'gadget-term-discuss-marked-resolved',
'gadget-term-discuss-reopen',
'gadget-term-json-intro',
'gadget-term-json-redirect',
'gadget-term-json-nodata',
'gadget-term-json-noterms',
'gadget-term-json-showhide',
// The following aren't used by the script directly, but by {{discuss term}}
'gadget-term-discuss-nogadget',
'gadget-term-discuss-open',
'gadget-term-discuss-edit',
'gadget-term-discuss-mark-resolved',
'gadget-term-editnotice',
// The following aren't used by the script directly, but by {{portal}}
'gadget-term-jsonlink-label',
'gadget-term-jsonlink-title'
]);
$.when( termlist, loadMessages )
.then( startupHelper )
.then( initTerminology )
.fail( function( err ) {
console.log( 'Terminology gadget error', err );
});
})();