public function msg( $key = 0, $options = [], $fail = null ) {
		if ( !IntuitionUtil::nonEmptyStr( $key ) ) {
			// Invalid message key
			return $this->bracketMsg( $key, $fail );
		}

		$defaultOptions = [
			'domain' => $this->getDomain(),
			'lang' => $this->getLang(),
			'variables' => [],
			'raw-variables' => false,
			'escape' => 'plain',
			'parsemag' => true,
			'externallinks' => false,
			// Set to a wiki article path for converting
			'wikilinks' => false,
		];

		// If $options was a domain string, convert it now.
		if ( IntuitionUtil::nonEmptyStr( $options ) ) {
			$options = [ 'domain' => $options ];
		}

		// If $options is still not an array, ignore it and use default
		// Otherwise merge the options with the defaults.
		if ( !is_array( $options ) ) {
			// @codeCoverageIgnoreStart
			$options = $defaultOptions;
		} else {
			// @codeCoverageIgnoreEnd
			$options = array_merge( $defaultOptions, $options );
		}

		// Normalise key. First character is case-insensitive.
		$key = lcfirst( $key );

		$msg = $this->rawMsg( $options['domain'], $options['lang'], $key );
		if ( $msg === null ) {
			$this->errTrigger(
				"Message \"$key\" for lang \"{$options['lang']}\" in domain \"{$options['domain']}\" not found",
				__METHOD__,
				E_NOTICE
			);
			return $this->bracketMsg( $key, $fail );
		}

		// Now that we've got the message, apply any post processing
		$escapeDone = false;

		// If using raw variables, escape message before replacement
		if ( $options['raw-variables'] === true ) {
			$msg = IntuitionUtil::strEscape( $msg, $options['escape'] );
			$escapeDone = true;
		}

		// Replace variables
		foreach ( $options['variables'] as $i => $val ) {
			$n = $i + 1;
			$msg = str_replace( "\$$n", $val, $msg );
		}

		if ( $options['parsemag'] === true ) {
			$msg = $this->getMessagesFunctions()->parse( $msg, $options['lang'] );
		}

		// If not using raw vars, escape the message now (after variable replacement).
		if ( !$escapeDone ) {
			$escapeDone = true;
			$msg = IntuitionUtil::strEscape( $msg, $options['escape'] );
		}

		if ( is_string( $options['wikilinks'] ) ) {
			$msg = IntuitionUtil::parseWikiLinks( $msg, $options['wikilinks'] );
		}

		if ( $options['externallinks'] ) {
			$msg = IntuitionUtil::parseExternalLinks( $msg );
		}

		return $msg;
	}