User:Mooeypoo/wikiArticleWizard.js

From mediawiki.org

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.
/*!
 * wikiArticleWizard v0.0.1
 *
 * This is a user script to display an article creation wizard
 * based on preset article skeletons dynamically created per wiki.
 *
 * The code for this user script is being maintained on Github:
 * https://github.com/mooeypoo/wikiArticleWizard
 *
 * ** Please do not change this local file directly **
 *
 * Any updates should be created from the official repository
 * by running `grunt build` and using
 * `dist/wikiArticleWizard.userscript.js` and
 * `dist/wikiArticleWizard.userscript.css`
 *
 * Latest update: 2018-05-28T14:04:31Z
 * (Monday, May 28th, 2018, 5:04:31 PM +0300)
 */
/* >> Starting source: src/init.waw.js << */
// Set up a global variable for the rest of the code to use.
var waw = { ui: {} }; // eslint-disable-line no-unused-vars
/* >> End source: src/init.waw.js << */
/* >> Starting source: src/waw.Config.js << */
waw.Config = {
	ARTICLE_REPO: mw.config.get( 'articlesandbox-article-repo' ) || '',
	NAME_FOR_SANDBOX: mw.config.get( 'articlesandbox-article-nameforsandbox' ) || 'sandbox'
};
/* >> End source: src/waw.Config.js << */
/* >> Starting source: src/init.loader.start.js << */
mw.loader.using( [
	'oojs-ui',
	'mediawiki.api',
	'ext.visualEditor.targetLoader'
] ).then( function () {
/* >> End source: src/init.loader.start.js << */
/* >> Starting source: src/waw.Utils.js << */

	/**
	 * Define a set of utility functions and values
	 *
	 * @return {Objects}
	 */
	waw.Utils = ( function () {
		/**
		 * Translate from the structure given by the prefixsearch API to
		 * the required hierarchical article structure and paths that
		 * the wikiArticleWizard expects
		 *
		 * @param  {Object} apiPrefixSearch API result
		 * @param  {string} repository Name of the base repository
		 * @return {Object} Article structure
		 */
		var getArticleStrctureFromPrefixSearch = function ( apiPrefixSearch, repository ) {
				var path, flatPath,
					articles = {};

				apiPrefixSearch.forEach( function ( articleData ) {
					var pieces = [],
						title = articleData.title;

					if ( title.indexOf( repository ) !== 0 ) {
						return;
					}

					pieces = title.replace( repository, '' ).split( '/' );
					pieces = pieces.filter( function ( piece ) {
						return !!piece; // Get rid of empty strings
					} );

					// Build article structure
					path = articles;
					flatPath = '';
					pieces.forEach( function ( piece ) {
						flatPath += '/' + piece;

						path[ piece ] = path[ piece ] || {
							_path: repository + flatPath,
							_articles: {}
						};

						// Shift reference
						// eslint-disable-next-line no-underscore-dangle
						path = path[ piece ]._articles;
					} );
				} );
				return articles;
			},
			/**
			 * Fetch the details from the API and build the base article
			 * structure the system is expecting, in the form of a hierarchical
			 * object representing sections and their sub-pages.
			 *
			 * @param {string} repository Path to the article repository
			 * @return {Object} Article structure
			 */
			buildArticleStructure = function ( repository ) {
				var utils = this;

				if ( !repository ) {
					return $.Deferred().reject( 'noconfig' );
				}

				return ( new mw.Api() ).get( {
					action: 'query',
					format: 'json',
					list: 'prefixsearch',
					pssearch: repository
				} ).then(
					// Success
					function ( queryData ) {
						var articles = utils.getArticleStrctureFromPrefixSearch(
							queryData.query.prefixsearch,
							repository
						);

						if ( $.isEmptyObject( articles ) ) {
							return $.Deferred().reject( 'noarticles' );
						}

						return articles;
					},
					function () {
						return $.Deferred().reject( 'noarticles' );
					}
				);
			},
			/**
			 * Fetch the HTML contents of a given wiki page from restbase
			 *
			 * @param  {string} page Page name
			 * @return {jQuery} HTML content of the page body
			 */
			getRestbasePage = function ( page ) {
				return mw.libs.ve.targetLoader.requestParsoidData( page, {} )
					.then(
						function ( response ) {
							var content = response.visualeditor.content,
								doc = new DOMParser().parseFromString( content, 'text/html' );

							// TODO: Consider re-adding the body classes so the
							// inner classes will be represented properly; otherwise
							// images and some templates are not floated correctly
							// in the preview
							return $( doc.body.childNodes );
						},
						// Failure
						function () {
							OO.ui.alert( mw.msg( 'articlesandbox-error-restbase-preview' ) );

							// Resolve with an error message
							return $.Deferred().resolve(
								$( '<span>' )
									.append( mw.msg( 'articlesandbox-error-restbase-preview-content' ) )
							);
						}
					);
			},
			/**
			 * Create an array of article items preceeded by a section item per section
			 * in the article structure.
			 *
			 * Recursively adds sections with the respective indent for representation
			 * in the end result list.
			 *
			 * @param {string} title Section title
			 * @param {Object} articles Article structure object
			 * @param {string} sectionPath A full path for the parent section
			 * @param {number} [indent=0] The indentation of this section, if it is a sub-section
			 * @return {waw.ui.ArticleItemWidget[]} An array of items and section items representing
			 *  the section and its sub sections
			 */
			buildItemsSection = function ( title, articles, sectionPath, indent ) {
				var utils = this,
					items = [],
					pages = articles ? Object.keys( articles ) : [],
					sectionTitle = new waw.ui.ArticleSectionWidget( title, indent );

				indent = indent || 0;

				// Add title
				if ( title ) {
					items.push( sectionTitle );
				}

				// Look for sub sections and items
				pages.forEach( function ( page ) {
					// eslint-disable-next-line no-underscore-dangle
					if ( !$.isEmptyObject( articles[ page ]._articles ) ) {
						// This is a sub-section, recurse
						// eslint-disable-next-line no-underscore-dangle
						items.concat( utils.buildItemsSection( page, articles[ page ]._articles, articles[ page ]._path, indent + 1 ) );
					} else {
						// This is a direct child
						items.push(
							new waw.ui.ArticleItemWidget(
								page,
								// eslint-disable-next-line no-underscore-dangle
								articles[ page ]._path,
								sectionPath,
								indent
							)
						);
					}
				} );

				return items;
			},
			/**
			 * Build the 'create page' form.
			 * This will be updated with details from the item as it is chosen
			 *
			 * @param {string} itemPath Path to the article this item represents
			 * @return {jQuery} Form jQuery object with the relevant data in the
			 *  fields.
			 */
			buildCreatePageForm = function ( itemPath ) {
				var makeHiddenInput = function ( name, value ) {
						return $( '<input>' )
							.attr( 'type', 'hidden' )
							.attr( 'name', name )
							.attr( 'value', value );
					},
					titleInput = new OO.ui.TextInputWidget( {
						name: 'visibletitle',
						// name: 'title',
						placeholder: mw.msg( 'articlesandbox-create-input-placeholder' )
					} ),
					errorLabel = new OO.ui.LabelWidget( {
						classes: [ 'articlesandbox-create-titleerror' ],
						label: mw.msg( 'articlesandbox-error-badtitle' )
					} ),
					submit = new OO.ui.ButtonInputWidget( {
						type: 'submit',
						icon: 'add',
						flags: [ 'progressive' ],
						label: mw.msg( 'articlesandbox-create-button' )
					} ),
					$hiddenTitle = makeHiddenInput( 'title' ),
					// Mock a inputbox process
					$form = $( '<form>' )
						.attr( 'action', '/w/index.php' )
						.attr( 'method', 'get' )
						.append(
							makeHiddenInput( 'veaction', 'edit' ),
							$hiddenTitle,
							makeHiddenInput( 'preload', itemPath ),
							makeHiddenInput( 'summary', mw.msg( 'articlesandbox-create-articlesummary' ) ),
							makeHiddenInput( 'prefix', 'Special:MyPage/' + waw.Config.NAME_FOR_SANDBOX + '/' )
						);

				errorLabel.toggle( false );
				submit.setDisabled( true );

				titleInput.on( 'change', function ( val ) {
					var valid = !!mw.Title.newFromText( val );

					titleInput.setValidityFlag( valid );
					submit.setDisabled( !valid );
					errorLabel.toggle( !valid );
					$hiddenTitle.attr( 'value', 'Special:MyPage/' + waw.Config.NAME_FOR_SANDBOX + '/' + titleInput.getValue() );
				} );

				$form.append(
					titleInput.$element,
					errorLabel.$element,
					submit.$element
				);

				return $form;
			};

		// Public methods
		return {
			buildArticleStructure: buildArticleStructure,
			getRestbasePage: getRestbasePage,
			buildItemsSection: buildItemsSection,
			buildCreatePageForm: buildCreatePageForm,
			getArticleStrctureFromPrefixSearch: getArticleStrctureFromPrefixSearch
		};
	}() );
/* >> End source: src/waw.Utils.js << */
/* >> Starting source: src/waw.ui.DialogPageLayout.js << */

	/**
	 * Create a custom page layout for the wizard dialog
	 *
	 * @class
	 * @extends OO.ui.PageLayout
	 *
	 * @constructor
	 * @param {string} name Page name
	 * @param {Object} [config] Optional config
	 */
	waw.ui.DialogPageLayout = function DialogPageLayout( name, config ) {
		waw.ui.DialogPageLayout.super.call( this, name, config );

		config = config || {};

		this.titleWidget = new OO.ui.LabelWidget( {
			label: config.title,
			classes: [ 'articlesandbox-page-title' ]
		} );
		this.$content = config.$content;

		// Create a panel that holds the intro
		this.$intro = $( '<div>' )
			.addClass( 'articlesandbox-page-intro-content' )
			.append( config.intro );
		this.introPanel = new OO.ui.PanelLayout( {
			$content: this.$intro,
			classes: [ 'articlesandbox-page-intro' ],
			framed: true,
			expanded: false,
			padded: true
		} );

		this.introLink = new OO.ui.ButtonWidget( {
			label: mw.msg( 'articlesandbox-moreinfo' ),
			icon: 'newWindow',
			framed: false,
			classes: [ 'articlesandbox-page-introlink' ],
			target: '_blank'
		} );

		this.$element.append(
			this.titleWidget.$element,
			this.introPanel.$element,
			this.introLink.$element,
			this.$content
		);

		// Initialize
		this.titleWidget.toggle( config.title );
		this.introPanel.toggle( config.intro );
		this.introLink.toggle( false );
	};
	OO.inheritClass( waw.ui.DialogPageLayout, OO.ui.PageLayout );

	/**
	 * Set the title for the page
	 *
	 * @param  {string} title Page title
	 */
	waw.ui.DialogPageLayout.prototype.setTitle = function ( title ) {
		this.titleWidget.setLabel( title );
		this.titleWidget.toggle( title );
	};

	/**
	 * Set or change the intro of this page.
	 * Also include an optional button to 'read more'
	 *
	 * @param  {jQuery|string} intro Page intro
	 * @param  {string} [linkToReadMore] Link to the 'read more' button.
	 *  If not given, the 'read more' button won't be displayed.
	 */
	waw.ui.DialogPageLayout.prototype.setIntro = function ( intro, linkToReadMore ) {
		this.$intro.empty().append( intro );
		this.introLink.setHref( linkToReadMore );

		this.introPanel.toggle( !!intro );
		this.introLink.toggle( !!intro && !!linkToReadMore );
	};

/* >> End source: src/waw.ui.DialogPageLayout.js << */
/* >> Starting source: src/waw.ui.ArticleItemWidget.js << */

	/**
	 * Create a special item option that represents an article item.
	 *
	 * @param {string} pageName Page name
	 * @param {string} path Page path
	 * @param {string} [parentPath=''] Page parent path
	 * @param {Number} [indent=0] Indent; this dictates how many levels
	 *  of indent this page is under, in case it is in a sub-category.
	 * @constructor
	 */
	waw.ui.ArticleItemWidget = function ArticleItemWidget( pageName, path, parentPath, indent ) {
		waw.ui.ArticleItemWidget.super.call( this, { label: pageName, data: { path: path, parentPath: parentPath }, icon: 'article' } );

		this.$element
			.addClass( 'articlesandbox-articleItemWidget' )
			.css( { left: ( indent ) + 'em' } )
			.toggleClass( 'articlesandbox-articleItemWidget-indent', indent );
	};
	OO.inheritClass( waw.ui.ArticleItemWidget, OO.ui.DecoratedOptionWidget );
/* >> End source: src/waw.ui.ArticleItemWidget.js << */
/* >> Starting source: src/waw.ui.ArticleSectionWidget.js << */

	/**
	 * Create a section title inside the article list to represent hierarchical
	 * and categorized article items
	 *
	 * @class
	 * @extends OO.ui.MenuSectionOptionWidget
	 *
	 * @constructor
	 * @param {string} pageName Page name
	 * @param {Number} [indent=0] Indent; this dictates how many levels
	 *  of indent this page is under, in case it is in a sub-category.
	 * @param {Object} [config] Optional configuration options
	 */
	waw.ui.ArticleSectionWidget = function ArticleSectionWidget( pageName, indent, config ) {
		waw.ui.ArticleSectionWidget.super.call( this, $.extend( { label: pageName }, config ) );

		this.$element
			.addClass( 'articlesandbox-articleSectionWidget' )
			.css( { left: ( indent ) + 'em' } )
			.toggleClass( 'articlesandbox-articleSectionWidget-indent', indent );
	};
	OO.inheritClass( waw.ui.ArticleSectionWidget, OO.ui.MenuSectionOptionWidget );
/* >> End source: src/waw.ui.ArticleSectionWidget.js << */
/* >> Starting source: src/waw.ui.WizardDialog.js << */

	/**
	 * Create the wizard dialog
	 *
	 * @class
	 * @extends OO.ui.ProcessDialog
	 *
	 * @constructor
	 * @param {Object} [config] Configuration options
	 */
	waw.ui.WizardDialog = function WizardDialog( config ) {
		waw.ui.WizardDialog.super.call( this, config );

		this.chosenItem = null;
		this.error = false;
	};
	OO.inheritClass( waw.ui.WizardDialog, OO.ui.ProcessDialog );
	waw.ui.WizardDialog.static.actions = [
		{ label: mw.msg( 'articlesandbox-dismiss' ), modes: [ 'articles', 'error', 'create' ], flags: 'safe' },
		{ action: 'back', label: mw.msg( 'articlesandbox-goback' ), modes: 'create', flags: 'safe', icon: 'arrowPrevious' }
	];
	waw.ui.WizardDialog.static.title = mw.msg( 'articlesandbox-title' );
	waw.ui.WizardDialog.static.name = 'articlehelperdialog';
	waw.ui.WizardDialog.static.size = 'large';

	/**
	 * Set error state for the dialog
	 *
	 * @param {string} [type='noarticles'] Error type; 'noconfig' or 'noarticles'
	 */
	waw.ui.WizardDialog.prototype.setError = function ( type ) {
		var label = type === 'noconfig' ?
			mw.msg( 'articlesandbox-error-missing-repoconfig' ) :
			$( '<span>' )
				.append(
					mw.msg( 'articlesandbox-error-missing-articles' ),
					new OO.ui.ButtonWidget( {
						flags: [ 'progressive' ],
						icon: 'newWindow',
						label: mw.msg( 'articlesandbox-error-missing-articles-gotorepo' ),
						href: mw.config.get( 'wgArticlePath' ).replace( '$1', waw.Config.ARTICLE_REPO ),
						target: '_blank'
					} ).$element
				);

		this.error = true;
		this.errorLabel.setLabel( label );
		this.showPage( 'error' );
	};

	/**
	 * @inheritdoc
	 */
	waw.ui.WizardDialog.prototype.initialize = function () {
		waw.ui.WizardDialog.super.prototype.initialize.apply( this, arguments );

		this.$articlesPage = $( '<div>' )
			.addClass( 'articlesandbox-page-articles' );
		this.$createPageForm = $( '<div>' )
			.addClass( 'articlesandbox-page-create-form' );

		this.errorLabel = new OO.ui.LabelWidget( {
			classes: [ 'articlesandbox-error' ]
		} );

		this.bookletLayout = new OO.ui.BookletLayout();
		this.bookletLayout.addPages( [
			new waw.ui.DialogPageLayout( 'error', {
				title: mw.msg( 'articlesandbox-title-error' ),
				$content: this.errorLabel.$element
			} ),
			new waw.ui.DialogPageLayout( 'articles', {
				title: mw.msg( 'articlesandbox-create-article' ),
				$content: this.$articlesPage
			} ),
			new waw.ui.DialogPageLayout( 'create', {
				$content: $( '<div>' )
					.addClass( 'articlesandbox-page-create' )
					.append( this.$createPageForm )
			} )
		] );

		this.$body.append( this.bookletLayout.$element );
	};

	/**
	 * @inheritdoc
	 */
	waw.ui.WizardDialog.prototype.getActionProcess = function ( action ) {
		if ( action === 'back' ) {
			this.showPage( 'articles' );
		}
		return waw.ui.WizardDialog.super.prototype.getActionProcess.call( this, action );
	};

	/**
	 * @inheritdoc
	 */
	waw.ui.WizardDialog.prototype.getSetupProcess = function ( data ) {
		return waw.ui.WizardDialog.super.prototype.getSetupProcess.call( this, data )
			.next( function () {
				if ( this.error ) {
					this.showPage( 'error' );
				} else {
					this.showPage( 'articles' );
				}
			}, this );
	};

	/**
	 * Show a specific page in the dialog
	 *
	 * @param {string} [name='articles'] Page name; 'articles', 'create' or 'error'
	 */
	waw.ui.WizardDialog.prototype.showPage = function ( name ) {
		name = name || 'articles';

		this.bookletLayout.setPage( name );
		this.getActions().setMode( name );
	};

	/**
	 * Respond to 'choose' event from any of the list widgets.
	 *
	 * @param {OO.ui.OptionWidget} item Chosen item
	 */
	waw.ui.WizardDialog.prototype.onArticleListChoose = function ( item ) {
		var promise,
			dialog = this,
			data = item.getData();

		this.chosenItem = item;

		// Get the data from parent section
		if ( data.parentPath ) {
			promise = waw.Utils.getRestbasePage( data.path );
		} else {
			promise = $.Deferred().resolve( '' ).promise();
		}

		this.pushPending();
		promise.then( function ( $intro ) {
			var currPage;

			dialog.$createPageForm.empty().append( waw.Utils.buildCreatePageForm( data.path ) );
			dialog.showPage( 'create' );

			currPage = dialog.bookletLayout.getCurrentPage();
			currPage.setTitle( mw.msg( 'articlesandbox-create-article-for', item.getLabel() ) );
			currPage.setIntro( $intro );

			dialog.popPending();
		} );
	};

	/**
	 * Build the content based on the article structure given
	 *
	 * @param {Object} articles Article structure with paths
	 * @param {jQuery} [$introContent] A jQuery object for the intro
	 */
	waw.ui.WizardDialog.prototype.buildContent = function ( articles, $introContent ) {
		var introLink = new OO.ui.ButtonWidget( {
				label: mw.msg( 'articlesandbox-moreinfo' ),
				icon: 'newWindow',
				framed: false,
				href: mw.config.get( 'wgArticlePath' ).replace( '$1', waw.Config.ARTICLE_REPO ),
				target: '_blank'
			} ),
			list = new OO.ui.SelectWidget( {
				classes: [ 'articlesandbox-articles-section-list' ]
			} );

		if ( $introContent ) {
			this.$articlesPage.append(
				new OO.ui.PanelLayout( {
					$content: $introContent,
					classes: [ 'articlesandbox-page-intro' ],
					framed: true,
					expanded: false,
					padded: true
				} ).$element,
				introLink.$element
			);
		}

		// Events
		list.on( 'choose', this.onArticleListChoose.bind( this ) );

		// Create the list
		Object.keys( articles ).forEach( function ( page ) {
			// eslint-disable-next-line no-underscore-dangle
			var items = waw.Utils.buildItemsSection( page, articles[ page ]._articles, articles[ page ]._path, 0 );
			list.addItems( items );
		} );

		// Append to articles page
		this.$articlesPage.append( list.$element );
	};

	/**
	 * @inheritdoc
	 */
	waw.ui.WizardDialog.prototype.getBodyHeight = function () {
		// 50% height of window
		// TODO: Figure out how to make the height variable
		// every time a page is switched
		return $( window ).height() * 0.7;
	};

/* >> End source: src/waw.ui.WizardDialog.js << */
/* >> Starting source: src/waw.init.DOM.js << */

	var mainDialog = new waw.ui.WizardDialog(),
		windowManager = new OO.ui.WindowManager(),
		mainButton = new OO.ui.ButtonWidget( {
			label: mw.msg( 'articlesandbox-button' ),
			icon: 'article',
			flags: [ 'progressive' ]
		} );

	mainButton.setDisabled( true );
	windowManager.addWindows( [ mainDialog ] );

	// Attach events
	mainButton.on( 'click', function () {
		windowManager.openWindow( mainDialog );
	} );

	if ( waw.Config.ARTICLE_REPO ) {
		// Get the data
		$.when(
			// Get the data for the articles
			waw.Utils.buildArticleStructure( waw.Config.ARTICLE_REPO ),
			// Get the content for the introduction page
			waw.Utils.getRestbasePage( waw.Config.ARTICLE_REPO )
		).then(
			// Success
			mainDialog.buildContent.bind( mainDialog ),
			// Failure
			mainDialog.setError.bind( mainDialog )
		).always( function () {
			mainButton.setDisabled( false );
		} );
	} else {
		// If ARTICLE_REPO is undefined, show an error
		mainDialog.setError( 'noconfig' );
		mainButton.setDisabled( false );
	}

	// Load the stylesheet and wait for document ready to attach the
	// main button to the DOM
	$.when(
		// Stylesheet
		mw.loader.load( 'https://www.mediawiki.org/w/index.php?title=User:Mooeypoo/articlesandbox.css&action=raw&ctype=text/css', 'text/css' ),
		// Document ready
		$.ready
	).then( function () {
		// Attach to DOM
		$( '#right-navigation' ).append( mainButton.$element );
		$( 'body' ).append( windowManager.$element );
	} );
/* >> End source: src/waw.init.DOM.js << */
/* >> Starting source: src/init.loader.end.js << */
} ); // End mw.loader.using
/* >> End source: src/init.loader.end.js << */
/* >> Starting source: src/init.language.js << */
/**/( function () {
	/**
	 * Define translation messages and initialize by user language
	 * This happens early, without waiting for modules to load.
	 */
	var userLang = mw.config.get( 'wgUserLanguage' ),
		messages = {
			he: {
				'articlesandbox-title': 'אשף ערכים',
				'articlesandbox-button': 'אשף ערכים',
				'articlesandbox-dismiss': 'ביטול',
				'articlesandbox-moreinfo': 'מידע נוסף',
				'articlesandbox-goback': 'חזרה למסך הקודם',
				'articlesandbox-create-article': '{{GENDER:|צור|צרי}} ערך',
				'articlesandbox-create-article-for': '{{GENDER:|צור|צרי}} ערך עבור $1',
				'articlesandbox-sections': 'רשימת ערכים',
				'articlesandbox-create-button': '{{GENDER:|צור|צרי}} ערך',
				'articlesandbox-create-input-placeholder': 'כותרת הערך',
				'articlesandbox-create-articlesummary': 'נוצר באמצעות [https://www.mediawiki.org/wiki/User:Mooeypoo/articlesandbox.js|אשף הערכים]',
				'articlesandbox-error-badtitle': 'כותרת הערך שבחרת אינה תקינה. אנא {{GENDER:|נסה|נסי}} שוב.'
			},
			en: {
				'articlesandbox-title': 'Article wizard',
				'articlesandbox-button': 'Article wizard',
				'articlesandbox-dismiss': 'Dismiss',
				'articlesandbox-goback': 'Go back',
				'articlesandbox-moreinfo': 'More info',
				'articlesandbox-title-error': 'Error displaying contents',
				'articlesandbox-create-article': 'Create article',
				'articlesandbox-create-article-for': 'Create article for $1',
				'articlesandbox-sections': 'Available article templates',
				'articlesandbox-create-button': 'Create article',
				'articlesandbox-create-input-placeholder': 'Article title',
				'articlesandbox-create-articlesummary': 'Created by [https://www.mediawiki.org/wiki/User:Mooeypoo/articlesandbox.js|ArticleSandbox] user script',
				'articlesandbox-error-missing-repoconfig': 'Cannot load article templates, since there is no repository configured. Please configure a base repository for all article templates through setting the mw.config entry \'articlesandbox-article-repo\' where you load this script, and try again.',
				'articlesandbox-error-missing-articles': 'Cannot find any defined article templates. Please add articles to the repository, and try again.',
				'articlesandbox-error-missing-articles-gotorepo': 'Go to the article repository',
				'articlesandbox-error-badtitle': 'Invalid title: The title you chose contains invalid characters. Please retry.',
				'articlesandbox-error-restbase-preview': 'There was a problem fetching the preview. You can still add the article!',
				'articlesandbox-error-restbase-preview-content': 'Preview could not be displayed.'
			}
		};

	// Set language, with default 'English'
	mw.messages.set( messages.en );
	if ( userLang && userLang !== 'en' && userLang in messages ) {
		mw.messages.set( messages[ userLang ] );
	}
}() );
/* >> End source: src/init.language.js << */